#!/usr/bin/env python3
"""Cyberjunky's 3Commas bot helpers."""
import argparse
import configparser
import json
import os
import sqlite3
import sys
import time
from pathlib import Path
from constants.pair import PAIREXCLUDE_EXT

from helpers.logging import Logger, NotificationHandler
from helpers.misc import check_deal, wait_time_interval
from helpers.threecommas import init_threecommas_api, set_threecommas_bot_pairs

def load_config():
    """Create default or load existing config file."""

    cfg = configparser.ConfigParser()
    if cfg.read(f"{datadir}/{program}.ini"):
        return cfg

    cfg["settings"] = {
        "timezone": "Europe/Amsterdam",
        "timeinterval": 86400,
        "debug": False,
        "logrotate": 7,
        "3c-apikey": "Your 3Commas API Key",
        "3c-apisecret": "Your 3Commas API Secret",
        "notifications": False,
        "notify-urls": ["notify-url1"],
    }

    cfg["cluster_default"] = {
        "botids": [12345, 67890],
        "max-same-deals": "1",
    }

    with open(f"{datadir}/{program}.ini", "w") as cfgfile:
        cfg.write(cfgfile)

    return None

def upgrade_config(thelogger, cfg):
    """Upgrade config file if needed."""

    thelogger.info("Config does not require any upgrade at this moment")

    return cfg

def init_cluster_db():
    """Create or open database to store cluster and coin data."""
    try:
        dbname = f"{program}.sqlite3"
        dbpath = f"file:{datadir}/{dbname}?mode=rw"
        dbconnection = sqlite3.connect(dbpath, uri=True)
        dbconnection.row_factory = sqlite3.Row

        logger.info(f"Database '{datadir}/{dbname}' opened successfully")

    except sqlite3.OperationalError:
        dbconnection = sqlite3.connect(f"{datadir}/{dbname}")
        dbconnection.row_factory = sqlite3.Row
        dbcursor = dbconnection.cursor()
        logger.info(f"Database '{datadir}/{dbname}' created successfully")

        dbcursor.execute(
            "CREATE TABLE IF NOT EXISTS deals ("
            "dealid INT Primary Key, "
            "coin STRING, "
            "clusterid STRING, "
            "botid INT, "
            "active BIT"
            ")"
        )

        dbcursor.execute(
            "CREATE TABLE IF NOT EXISTS cluster_coins ("
            "clusterid STRING, "
            "coin STRING, "
            "number_active INT, "
            "PRIMARY KEY(clusterid, coin)"
            ")"
        )

        dbcursor.execute(
            "CREATE TABLE IF NOT EXISTS bot_pairs ("
            "clusterid STRING, "
            "botid INT, "
            "botname STRING, "
            "pair STRING, "
            "coin STRING, "
            "enabled BIT, "
            "PRIMARY KEY(clusterid, botid, pair)"
            ")"
        )

        logger.info("Database tables created successfully")

    return dbconnection

def upgrade_cluster_db():
    """Upgrade database if needed."""

    try:
        try:
            # DROP column supported from sqlite 3.35.0 (2021.03.12)
            cursor.execute("ALTER TABLE deals DROP COLUMN pair")
        except sqlite3.OperationalError:
            logger.debug("Older SQLite version; not used columns not removed")

        cursor.execute("ALTER TABLE deals ADD COLUMN coin STRING")

        cursor.execute("DROP TABLE cluster_pairs")
        cursor.execute(
            "CREATE TABLE IF NOT EXISTS cluster_coins ("
            "clusterid STRING, "
            "coin STRING, "
            "number_active INT, "
            "PRIMARY KEY(clusterid, coin)"
            ")"
        )

        cursor.execute("ALTER TABLE bot_pairs ADD COLUMN coin STRING")

        logger.info("Database schema upgraded (pair to coins)")
    except sqlite3.OperationalError:
        logger.debug("Database schema is up-to-date")

def clean_bot_db_data(thebot):
    """Cleanup old bot data in the db."""

    bot_id = thebot["id"]

    logger.info(f"Cleaning previous data for bot {bot_id}")
    db.execute(
        f"DELETE FROM deals WHERE botid = {bot_id} AND active = {0}"
    )
    db.execute(
        f"DELETE FROM bot_pairs WHERE botid = {bot_id} and enabled = {1}"
    )

    db.commit()

def process_bot_deals(cluster_id, thebot):
    """Check deals from bot and update deals in db."""

    bot_id = thebot["id"]

    # Process current deals and deals which have been closed since the last processing time
    current_deals = []
    deals = thebot["active_deals"]
    if deals:
        for deal in deals:
            deal_id = deal["id"]

            current_deals.append(deal_id)

            existing_deal = check_deal(cursor, deal_id)
            if not existing_deal:
                deal_coin = deal['pair'].split("_")[1]

                db.execute(
                    f"INSERT INTO deals (dealid, coin, clusterid, botid, active) "
                    f"VALUES ({deal_id}, '{deal_coin}', '{cluster_id}', {bot_id}, {1})"
                )
                logger.info(
                    f"New deal found {deal_id}/{deal['pair']} on bot \"{thebot['name']}",
                    True
                )
            else:
                logger.info(
                    f"Deal {deal_id} already registered and still active"
                )

        # Mark all other deals as not active anymore. Required later in the process and
        # will be cleaned during next processing round
        if current_deals:
            # Remove start and end square bracket so we can properly use it
            current_deals_str = str(current_deals)[1:-1]

            logger.info(f"Mark all deals from {bot_id} as inactive except {current_deals_str}")

            db.execute(
                f"UPDATE deals SET active = {0} "
                f"WHERE botid = {bot_id} AND dealid NOT IN ({current_deals_str})"
            )

    # No deals for this bot anymore, so mark them all (if any) as inactive
    if not current_deals:
        logger.info(f"Mark all deals from {bot_id} as inactive")
        db.execute(
            f"UPDATE deals SET active = {0} "
            f"WHERE botid = {bot_id}"
        )

    # Save all pairs used by the bot currently. Make sure not to overwrite disabled pairs
    botpairs = thebot["pairs"]
    if botpairs:
        botname = thebot["name"]

        logger.info(
            f"Saving {len(botpairs)} pairs for bot {botname} ({bot_id})"
        )

        for pair in botpairs:
            db.execute(
                f"INSERT OR IGNORE INTO bot_pairs ("
                f"clusterid, botid, botname, pair, coin, enabled"
                f") "
                f"VALUES ("
                f"'{cluster_id}', {bot_id},'{botname}', '{pair}', '{pair.split('_')[1]}', {1}"
                f")"
            )

    db.commit()

def log_deals(cluster_id):
    """Log the deals within this cluster"""

    dealdata = cursor.execute(
        f"SELECT dealid, pair, coin, botid, active FROM deals "
        f"WHERE clusterid = '{cluster_id}'"
    ).fetchall()

    if dealdata:
        logger.info(f"Printing deals for '{cluster_id}':")
        for entry in dealdata:
            logger.info(
                f"{entry[0]}: {entry[1]} ({entry[2]}), {entry[3]} => {entry[4]}"
            )
    else:
        logger.info(
            f"No deals data for '{cluster_id}'"
        )

def aggregrate_cluster(cluster_id):
    """Aggregate deals within cluster."""

    logger.info(f"Cleaning and aggregating data for '{cluster_id}'")

    # Remove old data
    db.execute(
        f"DELETE from cluster_coins WHERE clusterid = '{cluster_id}'"
    )

    # Create the cluster data, how many of the same coins are active within the
    # cluster based on the active deals
    db.execute(
        f"INSERT INTO cluster_coins (clusterid, coin, number_active) "
        f"SELECT clusterid, coin, "
        f"SUM ( CASE WHEN active = {1} THEN 1 ELSE 0 END) AS number_active "
        f"FROM deals "
        f"WHERE clusterid = '{cluster_id}' "
        f"GROUP BY coin"
    )

    db.commit()

def log_cluster_data(cluster_id):
    """Log the aggregated data based on the deals for this cluster"""

    clusterdata = cursor.execute(
        f"SELECT clusterid, coin, number_active FROM cluster_coins "
        f"WHERE clusterid = '{cluster_id}'"
    ).fetchall()

    if clusterdata:
        logger.info(f"Printing cluster_coins for '{cluster_id}':")
        for entry in clusterdata:
            logger.info(
                f"{entry[1]}: {entry[2]}"
            )
    else:
        logger.info(
            f"No cluster_pair data for '{cluster_id}'"
        )

def process_cluster_deals(cluster_id):
    """Process deals within cluster."""

    # Get the cluster data and pair numbers
    clusterdata = db.execute(
        f"SELECT clusterid, coin, number_active FROM cluster_coins "
        f"WHERE clusterid = '{cluster_id}'"
    ).fetchall()

    if clusterdata:
        logger.info(f"Processing cluster_coins for '{cluster_id}':")

        enablecoins = []
        disablecoins = []

        for entry in clusterdata:
            coin = str(entry[1])
            numberofdeals = int(entry[2])

            logger.info(
                f"Coin {coin} has {numberofdeals} deal(s) active"
            )

            if numberofdeals >= int(config.get(cluster_id, "max-same-deals")):
                # Found a pair which should be suspended
                disablecoins.append(coin)

                log_disable_enable_coin(cluster_id, coin, 0)
            else:
                # Found a pair which can be activated again
                enablecoins.append(coin)

                log_disable_enable_coin(cluster_id, coin, 1)

        # Enable the found coins (caused by finished deals)
        if enablecoins:
            # Remove start and end square bracket so we can properly use it
            enablecoins_str = str(enablecoins)[1:-1]

            logger.info(
                f"Enabling coins for {cluster_id}: {enablecoins_str}"
            )
            db.execute(
                f"UPDATE bot_pairs SET enabled = {1} "
                f"WHERE clusterid = '{cluster_id}' AND coin IN ({enablecoins_str})"
            )

        # Disable the found coins (caused by new deals)
        if disablecoins:
            # Remove start and end square bracket so we can properly use it
            disablecoins_str = str(disablecoins)[1:-1]

            logger.info(
                f"Disabling coins for {cluster_id}: {disablecoins_str}"
            )
            db.execute(
                f"UPDATE bot_pairs SET enabled = {0} "
                f"WHERE clusterid = '{cluster_id}' AND coin IN ({disablecoins_str})"
            )

        db.commit()
    else:
        logger.info(
            f"No cluster_pair data for '{cluster_id}'"
        )

def log_disable_enable_coin(cluster_id, coin, new_enabled_value):
    """Log the pairs which will be enabled or disabled"""

    pairdata = cursor.execute(
        f"SELECT botid, botname, enabled FROM bot_pairs "
        f"WHERE clusterid = '{cluster_id}' AND coin = '{coin}'"
    ).fetchall()

    if pairdata:
        changedbots = ""

        for entry in pairdata:
            if entry[2] != new_enabled_value:
                if changedbots:
                    changedbots += ", "
                changedbots += str(entry[1]) + " (" + str(entry[0]) + ")"

        if changedbots:
            if new_enabled_value:
                logger.info(
                    f"Enabling coin {coin} for the following bot(s): {changedbots}",
                    True
                )
            else:
                logger.info(
                    f"Disabling coin {coin} for the following bot(s): {changedbots}",
                    True
                )

def update_bot_config(cluster_id, thebot):
    """Update bots at 3C"""

    bot_id = thebot["id"]

    # Here rowid is used which is not defined in the init() function. Rowid
    # is a default column in SQLite when using an INT as primary key
    botenabledpairs = cursor.execute(
        f"SELECT pair FROM bot_pairs "
        f"WHERE clusterid = '{cluster_id}' AND botid = {bot_id} AND enabled = {1} "
        f"ORDER BY rowid ASC"
    ).fetchall()

    if botenabledpairs:
        enabledpairlist = [row[0] for row in botenabledpairs]
        set_threecommas_bot_pairs(logger, api, thebot, enabledpairlist, False, False, False)
    else:
        logger.warning(
            f"Failed to get enabled pairs for bot {bot_id}"
        )

def write_cluster_exclude_files(cluster_id, bot_list):
    """Write exclude files for each bot in the specified cluster"""

    if sharedir is not None:
        clusterdisabledcoins = cursor.execute(
            f"SELECT coin FROM cluster_coins "
            f"WHERE clusterid = '{cluster_id}' AND "
            f"number_active >= {int(config.get(cluster_id, 'max-same-deals'))}"
        ).fetchall()

        if clusterdisabledcoins:
            disabledcoinlist = [row[0] for row in clusterdisabledcoins]

            for botid in bot_list:
                write_bot_exclude_file(botid, disabledcoinlist)

def write_bot_exclude_file(bot_id, coins):
    """Write the exclude file for the specified bot"""

    excludefilename = f"{sharedir}/{bot_id}.{PAIREXCLUDE_EXT}"

    logger.info(
        f"Writing coins {coins} to file {excludefilename}"
    )

    with open(excludefilename, 'w') as filehandle:
        for coin in coins:
            filehandle.write('%s\n' % coin)

# Start application
program = Path(__file__).stem

# Parse and interpret options.
parser = argparse.ArgumentParser(description="Cyberjunky's 3Commas bot helper.")
parser.add_argument(
    "-d", "--datadir", help="directory to use for config and logs files", type=str
)
parser.add_argument(
    "-s", "--sharedir", help="directory to use for shared files", type=str
)
parser.add_argument(
    "-b", "--blacklist", help="local blacklist to use instead of 3Commas's", type=str
)

args = parser.parse_args()
if args.datadir:
    datadir = args.datadir
else:
    datadir = os.getcwd()

# pylint: disable-msg=C0103
if args.sharedir:
    sharedir = args.sharedir
else:
    sharedir = None

# pylint: disable-msg=C0103
if args.blacklist:
    blacklistfile = f"{datadir}/{args.blacklist}"
else:
    blacklistfile = None

# Create or load configuration file
config = load_config()
if not config:
    # Initialise temp logging
    logger = Logger(datadir, program, None, 7, False, False)
    logger.info(
        f"Created example config file '{datadir}/{program}.ini', edit it and restart the program"
    )
    sys.exit(0)
else:
    # Handle timezone
    if hasattr(time, "tzset"):
        os.environ["TZ"] = config.get(
            "settings", "timezone", fallback="Europe/Amsterdam"
        )
        time.tzset()

    # Init notification handler
    notification = NotificationHandler(
        program,
        config.getboolean("settings", "notifications"),
        config.get("settings", "notify-urls"),
    )

    # Initialise logging
    logger = Logger(
        datadir,
        program,
        notification,
        int(config.get("settings", "logrotate", fallback=7)),
        config.getboolean("settings", "debug"),
        config.getboolean("settings", "notifications"),
    )

    # Upgrade config file if needed
    config = upgrade_config(logger, config)

    logger.info(f"Loaded configuration from '{datadir}/{program}.ini'")

# Initialize 3Commas API
api = init_threecommas_api(config)

# Initialize or open the database
db = init_cluster_db()
cursor = db.cursor()

# Upgrade the database if needed
upgrade_cluster_db()

# DCA Deal Cluster
while True:

    # Reload config files and refetch data to catch changes
    config = load_config()
    logger.info(f"Reloaded configuration from '{datadir}/{program}.ini'")

    # Configuration settings
    timeint = int(config.get("settings", "timeinterval"))
    debug = config.getboolean("settings", "debug")

    for section in config.sections():
        if section.startswith("cluster_"):
            # Bot configuration for section
            botids = json.loads(config.get(section, "botids"))

            # Walk through all bots configured
            for bot in botids:
                boterror, botdata = api.request(
                    entity="bots",
                    action="show",
                    action_id=str(bot),
                )
                if botdata:
                    clean_bot_db_data(botdata)
                    process_bot_deals(section, botdata)
                else:
                    if boterror and "msg" in boterror:
                        logger.error("Error occurred reading bots: %s" % boterror["msg"])
                    else:
                        logger.error("Error occurred reading bots")

            # Aggregate deals to cluster level
            if debug:
                log_deals(section)

            aggregrate_cluster(section)

            if debug:
                log_cluster_data(section)

            # Enable and disable coins
            process_cluster_deals(section)

            # Update the bots in this cluster with the enabled/disabled pairs
            for bot in botids:
                boterror, botdata = api.request(
                    entity="bots",
                    action="show",
                    action_id=str(bot),
                )
                if botdata:
                    update_bot_config(section, botdata)
                else:
                    if boterror and "msg" in boterror:
                        logger.error("Error occurred updating bots: %s" % boterror["msg"])
                    else:
                        logger.error("Error occurred updating bots")

            # Some coins could not be active in a bot, but we should inform other scripts about
            # those excluded coins (prevent adding those coins)
            write_cluster_exclude_files(section, botids)

            # Send notifications for the processed cluster, otherwise the message can
            # become too long resulting in a bad request
            notification.send_notification()
        elif section not in ("settings"):
            logger.warning(
                f"Section '{section}' not processed (prefix 'cluster_' missing)!",
                False
            )

    if not wait_time_interval(logger, notification, timeint, False):
        break


from collections import defaultdict

with open("day24.txt") as input_file:
    lines = input_file.readlines()

directions = {
    "e": -2 + 0j,
    "w": +2 + 0j,
    "sr": -1 - 1j,
    "nr": -1 + 1j,
    "sl": +1 - 1j,
    "nl": +1 + 1j,
}

replacements = {"se": "sr", "ne": "nr", "sw": "sl", "nw": "nl"}

def displacement(line: str) -> complex:
    for old, new in replacements.items():
        line = line.replace(old, new)
    return sum(d * line.count(k) for k, d in directions.items())

tiles = defaultdict(bool)
for line in lines:
    tiles[displacement(line)] = not tiles[displacement(line)]

print(sum(tiles.values()))

def expand(grid: defaultdict) -> None:
    for p, tile in list(grid.items()):
        if not tile:
            continue
        for d in directions.values():
            if p + d not in grid:
                grid[p + d] = False

def step(old_grid: defaultdict) -> defaultdict:
    new_grid = old_grid.copy()
    expand(new_grid)
    for p, tile in new_grid.items():
        neigh = sum(old_grid[p + d] for d in directions.values())
        if tile and (neigh == 0 or neigh > 2):
            new_grid[p] = False
        elif (not tile) and (neigh == 2):
            new_grid[p] = True
    return new_grid

for _ in range(100):
    tiles = step(tiles)

print(sum(tiles.values()))


from app.funcs.network_graph import NetworkEdgeSchema, NetworkNodeSchema
from app.utils.meta_graph import gwas_label_formatter

node_schemas = [
    NetworkNodeSchema(
        meta_node="Gwas",
        id_col="gwas.id",
        label_formatter=gwas_label_formatter(),
    ),
    NetworkNodeSchema(
        meta_node="Variant", id_col="variant.name", label_col="variant.name"
    ),
    NetworkNodeSchema(
        meta_node="Gene",
        id_col="gene.name",
        label_col="gene.name",
        match_node_by="name",
    ),
    NetworkNodeSchema(
        meta_node="Protein",
        id_col="protein.uniprot_id",
        label_col="protein.uniprot_id",
    ),
    NetworkNodeSchema(
        meta_node="Pathway",
        id_col="pathway.name",
        label_col="pathway.name",
        match_node_by="name",
    ),
]
edge_schemas = [
    NetworkEdgeSchema(
        to_col="variant.name",
        from_col="gwas.id",
        from_meta_node="Gwas",
        hover_title_formatter=lambda row: """
                    gwas.id: {gwas_id},
                    variant.name: {variant_name}

                    beta: {beta:.2E},
                    se: {se:.2E},
                    pval: {pval:.2E}
                    """.format(
            gwas_id=row["gwas.id"],
            variant_name=row["variant.name"],
            beta=row["gwas_to_variant.beta"],
            se=row["gwas_to_variant.se"],
            pval=row["gwas_to_variant.pval"],
        ),
    ),
    NetworkEdgeSchema(
        from_col="variant.name", to_col="gene.name", from_meta_node="Variant"
    ),
    NetworkEdgeSchema(
        from_col="gene.name",
        to_col="protein.uniprot_id",
        from_meta_node="Gene",
    ),
    NetworkEdgeSchema(
        from_col="protein.uniprot_id",
        to_col="pathway.name",
        from_meta_node="Protein",
    ),
]


import pandas as pd
import numpy as np
import laspy
import json
import pptk
from math import *
from matplotlib import pyplot as plt
from matplotlib import path
from mpl_toolkits.mplot3d import Axes3D
from sklearn.neighbors import KDTree
import alphashape
from shapely.geometry import Point
from scipy.spatial import ConvexHull
from pysolar.solar import *
import datetime
import csv
import pytz
import time
import logging
import re
import os
from pyproj import Transformer
from datetime import timedelta

# from ipywidgets import IntProgress
# from IPython.display import display

from multiprocessing import Pool

#HARDCODED VALUES

COLOR_DICT = {
    "WHITE" : [255,255,255],
    "RED" : [1,0,0],
    "GREEN" : [0,1,0],
    "BLUE" : [0,0,1],
    "YELLOW" : [255,255,0],
    "LIGHT_BLUE" : [0,255,255],
    "PINK" : [255,0,255]
}

# DATE_DICT = {
#     "YEAR" : 2021,
#     "MONTH" : 6,
#     "DAY" : 21
# }

def Create_Dataframe_fromLas(lasFilePath):
    las = laspy.read(lasFilePath)
    #point_format = las.point_format
    lidar_points = np.array((las.X,las.Y,las.Z,las.intensity,las.classification, las.return_number, las.number_of_returns)).transpose()
    lidar_df = pd.DataFrame(lidar_points)
    lidar_df[0] = lidar_df[0]/100
    lidar_df[1] = lidar_df[1]/100
    lidar_df[2] = lidar_df[2]/100
    lidar_df.columns = ['X', 'Y', 'Z', 'intens', 'class', 'return_number', 'number_of_returns']

    return lidar_df

def Extract_SRdata(SR_JSONarr):
    #Store all Raw Treepoints
    SR_RawTreePoints = []
    #Store all Tree points mapped to a Tree ID
    SR_TreeClusterDict = {}
    #Store Tree Heights
    TreeHeights_ClusterHashmap = {}
    #Store GroundZ Elevation
    GroundElevation_ClusterHashmap = {}
    #Store All Tree Ids
    JSONClusterID_SR = []

    for i in range(len(SR_JSONarr)):
        #get points and cluster ID
        points = SR_JSONarr[i]["SRpointsInfo"]["SpecificClusterSRpoints"]
        cluster_id = SR_JSONarr[i]['SRpointsInfo']['SRpointsTreeCluster']
        #Storing with Cluster ID hashmap prevents storing multiple points from the same tree, if Tree present
        if cluster_id not in SR_TreeClusterDict:
            SR_TreeClusterDict[cluster_id] = points
        else:
            for p in points:
                SR_TreeClusterDict[cluster_id].append(p)
        #Tree Heights
        if cluster_id not in TreeHeights_ClusterHashmap:
            TreeHeights_ClusterHashmap[cluster_id] = SR_JSONarr[i]['TreeFoliageHeight']
        #GroundZvalue
        if cluster_id not in GroundElevation_ClusterHashmap:
            GroundElevation_ClusterHashmap[cluster_id] = SR_JSONarr[i]['GroundZValue']

        #Raw Points
        for j in points:
            SR_RawTreePoints.append(j)

    return SR_RawTreePoints, SR_TreeClusterDict, TreeHeights_ClusterHashmap, GroundElevation_ClusterHashmap

def Extract_MRdata(MR_JSONarr):
    ##Store all Raw Treepoints
    MR_RawTreePoints = []
    #Store all Tree points mapped to a Tree ID
    MR_TreeClusterDict = {}
    #Store Cluster Location
    TreeLocation_ClusterHashmap = {}

    for i in range(len(MR_JSONarr)):
        #get points and cluster ID
        points = MR_JSONarr[i]["ConvexHullDict"]["ClusterPoints"]
        cluster_id = MR_JSONarr[i]['ClusterID']
        #Storing with Cluster ID hashmap prevents storing multiple points from the same tree, if Tree present
        if cluster_id not in MR_TreeClusterDict:
            MR_TreeClusterDict[cluster_id] = points
        else:
            for p_mr in points:
                MR_TreeClusterDict[cluster_id].append(p_mr)

        Tree_lat = MR_JSONarr[i]["PredictedTreeLocation"]["Latitude"]
        Tree_long = MR_JSONarr[i]["PredictedTreeLocation"]["Longitude"]

        if cluster_id not in TreeLocation_ClusterHashmap:
            TreeLocation_ClusterHashmap[cluster_id] = [Tree_lat,Tree_long]

        #Raw points
        for k in points:
            MR_RawTreePoints.append(k)

    return MR_RawTreePoints, MR_TreeClusterDict, TreeLocation_ClusterHashmap

def Get_SR_Treepoints_from_Hashmap(TreeClusterID, SR_TreeClusterDict, las_filename='25192'):
    return SR_TreeClusterDict[las_filename+"_"+str(TreeClusterID)]

def Get_MR_Treepoints_from_Hashmap(TreeClusterID, MR_TreeClusterDict, las_filename='25192'):
    return MR_TreeClusterDict[las_filename+"_"+str(TreeClusterID)]

def Get_FullTreeCentroid(Full_Tree_points):
    centroid = np.mean(Full_Tree_points,axis=0)
    centroid[2] = 0

    return centroid

def Get_Shadow(points, az, amp):
    projected_points = []
    for point in points:
        sinAz = sin( radians( az + 180.0 ) )
        cosAz = cos( radians( az + 180.0 ) )
        tanAmp = tan( radians(amp) )
        pointGroundX = point[0] + ( ( point[2] / tanAmp ) *sinAz )
        pointGroundY = point[1] + ( ( point[2] / tanAmp ) *cosAz )
        pointGroundZ =  point[2] * 0

        projected_points.append([pointGroundX,pointGroundY,pointGroundZ])

    return np.array(projected_points)

def Get_ShadowCharacteristics(Full_Tree, TreeShadow):

    F_distance, F_point = Get_FurthestPointFromTreeCentroid(Full_Tree, TreeShadow)
    centroid = Get_FullTreeCentroid(Full_Tree)
    hull = ConvexHull(TreeShadow[:,:2])

    Shadow_Area = hull.area
    Shadow_length = np.linalg.norm(centroid - F_point) # Tree Centroid - Furthest point from Tree centroid on Tree shadow
    shadow_breadth = Shadow_Area/Shadow_length

    return Shadow_length, shadow_breadth, Shadow_Area

def Get_FurthestPointFromTreeCentroid(Full_Tree, TreeShadow):

    Shadow_KDTree = KDTree(TreeShadow)
    centroid = Get_FullTreeCentroid(Full_Tree)
    dist,ind = Shadow_KDTree.query(centroid.reshape(1,3),len(TreeShadow))

    Furtherst_Point_Distance = dist[0][-1]
    Furthest_Point_Vector = TreeShadow[ind[0][-1]]

    return Furtherst_Point_Distance, Furthest_Point_Vector

def Get_ShadowAlphaShape(ShadowPoints):
    return alphashape.alphashape(ShadowPoints, alpha=0)

def Get_GroundElevation(BuildingLidarDict):
    arr = (np.concatenate(list(BuildingLidarDict.values())))
    #Get ground elevation from building points min(list(BuildingLidarDict.values())[0][:,2])
    return min(arr[:,2])

#Code to automate Obtaining Az and Amp
def Get_SunData(Tree_Latitude, Tree_Longitude, year, month, day, hour, minute):
    date = datetime.datetime(year, month, day, hour, minute, 0, 0, tzinfo=datetime.timezone.utc)
    date_est = date.astimezone(pytz.timezone('US/Eastern')).isoformat()
    az = get_azimuth(Tree_Latitude, Tree_Longitude, date)
    amp = get_altitude(Tree_Latitude, Tree_Longitude, date)

    return date_est, az, amp

def pptk_wrapperSimplePlot(PointsArr, Color):
    v = pptk.viewer(PointsArr, [Color]*len(PointsArr))
    v.set(show_grid=False)
    v.set(show_axis=False)
    v.set(bg_color = [0,0,0,0])
    v.set(point_size = 0.04)

def sample_polygon(V, eps=0.25):
    # samples polygon V s.t. consecutive samples are no greater than eps apart
    # assumes last vertex in V is a duplicate of the first
    M = np.ceil(np.sqrt(np.sum(np.diff(V, axis=0) ** 2, axis = 1)) / eps)
    Q = []
    for (m, v1, v2) in zip(M, V[: -1], V[1:]):
        Q.append(np.vstack([ \
            np.linspace(v1[0], v2[0], endpoint = False), \
            np.linspace(v1[1], v2[1], endpoint = False)]).T)
    Q = np.vstack(Q)

    return Q

def Read_GeoJSON(filepath):
    with open(filepath, 'rb') as fd:
        data = json.load(fd)

    return data

def Get_SampledBuildingFootprints(Buildingdata):
    BuildingFeature_Coords = [np.array(F['geometry']['coordinates'][0][0]) for F in Buildingdata['features']]
    SampledFootprint = np.vstack([sample_polygon(W) for W in BuildingFeature_Coords])
    SampledFootprint = np.c_[SampledFootprint, np.zeros(len(SampledFootprint))]

    return SampledFootprint

def Get_BuildingFootprint(GeoJSON_Filepath):
    BuidlingFootprintData = Read_GeoJSON(GeoJSON_Filepath)

    return Get_SampledBuildingFootprints(BuidlingFootprintData)

def Get_BuildingDataDict(BuildingFilePath, f, year=2017):
    with open(BuildingFilePath, 'rb') as fd:
        NYC_building_Footprint = json.load(fd)

    Vs_25192 = [np.array(F['geometry']['coordinates'][0][0]) for F in NYC_building_Footprint['features']]
    ### **Note** : NYC points already projected to state plane, No need for pyproj
    Ws_25192 = Vs_25192
    Total_Building_Count = len(NYC_building_Footprint['features'])
    #Building_Data_dict
    Building_coords_dict = {}
    B_ID = 0
    Building_Key = f[:-4] + "_" + str(year) + "_Building_" + str(B_ID) # initialized to 25192_2015_Building_0

    for building in range(Total_Building_Count):

        B_ID += 1
        Building_Key = f[:-4] + "_" + str(year) + "_Building_" + str(B_ID) # initialized to 25192_2015_Building_0
        building_coords = sample_polygon(Ws_25192[building]) #NYC_building_Footprint['features'][building]['geometry']['coordinates'][0][0]
        Building_coords_dict[Building_Key] = building_coords

    return Building_coords_dict

#NOTE: Function takes most time
def Get_BuildingsUnderEffectOfShadow(Building_coords_dict, TreeShadowPoints):
    ShadowAlphaShape = Get_ShadowAlphaShape(TreeShadowPoints)
    Affected_Building_IDarr = [] #Buildings Under the Direct Affect of the Shadow
    for B_ID, Sampled_B_coords in Building_coords_dict.items():
        bool_temp_list = []
        for point in Sampled_B_coords:
            tp = Point(point)
            bool_temp_list.append(tp.within(ShadowAlphaShape))
        #check if any points in the sampled footprint fall within the area of shadow
        if len(Sampled_B_coords[bool_temp_list]) > 0:
            Affected_Building_IDarr.append(B_ID)

    return Affected_Building_IDarr

def Get_BuildingsUnderEffectOfShadow_Rapid(Building_coords_dict, TreeShadowPoints):

    Affected_Building_IDarr = [] #Buildings Under the Direct Affect of the Shadow
    for B_ID, Sampled_B_coords in Building_coords_dict.items():
        bool_temp_list = []

        p = path.Path(TreeShadowPoints[:,:2])
        bool_temp_list = p.contains_points(Sampled_B_coords)
        # #check if any points in the sampled footprint fall within the area of shadow
        if len(Sampled_B_coords[bool_temp_list]) > 0:
            Affected_Building_IDarr.append(B_ID)

    return Affected_Building_IDarr

def Get_BuildingFootprintSampledCoords(BIDs_Arr, Building_coords_dict):

    labelled_BF_points = []
    for B_ID in BIDs_Arr:

        b_points = Building_coords_dict[B_ID]

        for p in b_points:
            labelled_BF_points.append(p)

    labelled_BF_points = np.array(labelled_BF_points) #shape : (Nx2)

    #reformat for plotting purposes
    LBF_arr = []
    for cord in range(len(labelled_BF_points)):
        x = labelled_BF_points[cord][0]
        y = labelled_BF_points[cord][1]
        z = 0
        LBF_arr.append([x,y,z])

    LBF_arr = np.array(LBF_arr)

    return LBF_arr

def Get_BuildingLidarPoints(BIDs_Arr, Building_coords_dict, lasdf):

    lidarPointsRaw = lasdf.iloc[:,:3].to_numpy()
    # Selecting Lidar Points within Building footprint

    X_max , X_min = lasdf.X.max(), lasdf.X.min()
    Y_max , Y_min = lasdf.Y.max(), lasdf.Y.min()

    X_plane_tile_divisor = 50 #in m - indicates the number of tiles you want to divide the tiles into
    Y_plane_tile_divisor = 50 #in m

    X_diff = X_max - X_min
    Y_diff = Y_max - Y_min

    X_div_len = X_diff/X_plane_tile_divisor
    Y_div_len = Y_diff/Y_plane_tile_divisor

    # print(X_max , X_min)
    # print(Y_max , Y_min)

    #NOTE : Iterating over all the points takes too long, need a more optimized way

    lidar_BpointsDict = {}

    #For every building under the effect of the shadow
    for B_ID in BIDs_Arr:

        #List to append lidar points into for a specifc building
        B_ID_LidarPointsTempList = []

        #get the sampled building footprint points
        bf_points = Building_coords_dict[B_ID]

        #create a 2D convex Hull
        bf_shape = ConvexHull(bf_points[:,:2])

        #Get an area of points to look at (No need to iterate through all points in the tile)
        #get bounding subset to look at
        bf_shape_X_max = bf_shape.max_bound[0] + X_div_len
        bf_shape_Y_max = bf_shape.max_bound[1] + Y_div_len

        bf_shape_X_min = bf_shape.min_bound[0] - X_div_len
        bf_shape_Y_min = bf_shape.min_bound[1] - Y_div_len

        # print(bf_shape_X_max,bf_shape_Y_max,bf_shape_X_min,bf_shape_Y_min)

        #Bounded_Vertices_BF = bf_points[bf_shape.vertices]

        lidar_subset_df = lasdf[
            (lasdf['X'].between(bf_shape_X_min, bf_shape_X_max, inclusive=False) &
        lasdf['Y'].between(bf_shape_Y_min, bf_shape_Y_max, inclusive=False))
        ]

        # print("subset shape : ",lidar_subset_df.shape)

        #Get only points from BF
        lidar_BFMasked_points = lidar_subset_df.iloc[:,:3].to_numpy()

        #Check if a lidar point is within a building Footprint

        #Add a z-coordinate to BF ,  needed for alphashape
        bf_points_with_Z = np.c_[bf_points, np.zeros(len(bf_points))]
        #Create a shape of the building footprint
        bf_shape_alpha = alphashape.alphashape(bf_points_with_Z, alpha=0)

        for p in lidar_BFMasked_points:

            tp = Point(p)
            if(tp.within(bf_shape_alpha)):
                B_ID_LidarPointsTempList.append(p)

        if B_ID not in lidar_BpointsDict:
            lidar_BpointsDict[B_ID] = np.array(B_ID_LidarPointsTempList)

    return lidar_BpointsDict

def Set_BuildingLidarHeightAdjustment(BuidlingDataDict, height_Adjustment):

    for B_ID, B_Lidarpoints in BuidlingDataDict.items():

        bpArr = B_Lidarpoints
        #adjust height of buildings
        lidar_Bpoints_Hadj = []
        for bp in bpArr:
            lidar_Bpoints_Hadj.append(bp - [0,0,height_Adjustment])

        BuidlingDataDict[B_ID] = np.array(lidar_Bpoints_Hadj)

    return BuidlingDataDict

#https://github.com/ulikoehler/UliEngineering/blob/master/UliEngineering/Math/Coordinates.py
# -*- coding: utf-8 -*-
"""
Estimating BOunding Box
"""
__all__ = ["BoundingBox"]

class BoundingBox(object):
    """
    A 2D bounding box
    """
    def __init__(self, points):
        """
        Compute the upright 2D bounding box for a set of
        2D coordinates in a (n,2) numpy array.

        You can access the bbox using the
        (minx, maxx, miny, maxy) members.
        """
        if len(points.shape) != 2 or points.shape[1] != 2:
            raise ValueError("Points must be a (n,2), array but it has shape {}".format(
                points.shape))
        if points.shape[0] < 1:
            raise ValueError("Can't compute bounding box for empty coordinates")
        self.minx, self.miny = np.min(points, axis=0)
        self.maxx, self.maxy = np.max(points, axis=0)

    @property
    def width(self):
        """X-axis extent of the bounding box"""
        return self.maxx - self.minx

    @property
    def height(self):
        """Y-axis extent of the bounding box"""
        return self.maxy - self.miny

    @property
    def area(self):
        """width * height"""
        return self.width * self.height

    @property
    def aspect_ratio(self):
        """width / height"""
        return self.width / self.height

    @property
    def center(self):
        """(x,y) center point of the bounding box"""
        return (self.minx + self.width / 2, self.miny + self.height / 2)

    @property
    def max_dim(self):
        """The larger dimension: max(width, height)"""
        return max(self.width, self.height)

    @property
    def min_dim(self):
        """The larger dimension: max(width, height)"""
        return min(self.width, self.height)

    def __repr__(self):
        return "BoundingBox({}, {}, {}, {})".format(
            self.minx, self.maxx, self.miny, self.maxy)
    #NOTE: Added by me
    def Get_Coords(self):
        return [self.minx, self.maxx, self.miny, self.maxy]

def Get_TreeShadowBoundingBoxLimits(TreeShadow):
    Box_obj = BoundingBox(TreeShadow[:,:2])
    Box_Bounds = np.array(Box_obj.Get_Coords())

    B_minx = Box_Bounds[0]
    B_maxx = Box_Bounds[1]
    B_miny = Box_Bounds[2]
    B_maxy = Box_Bounds[3]

    return B_minx, B_maxx, B_miny, B_maxy

def Get_TreeShadowBoundingBoxesCoords(TreeShadow, Distance):

    B_minx, B_maxx, B_miny, B_maxy = Get_TreeShadowBoundingBoxLimits(TreeShadow)

    Tree_ShadowBox_Coords = [
        [B_minx, B_miny, 0],
        [B_minx, B_maxy, 0],
        [B_maxx, B_maxy, 0],
        [B_maxx, B_miny, 0],
        [B_minx, B_miny, 0] # final one is to complete the box used for plotting purposes only
    ]

    Extend_dist = Distance #in m
    Tree_ShadowBoxExtend_Coords = [
        [B_minx, B_miny - Extend_dist, 0],
        [B_minx, B_maxy, 0],
        [B_maxx, B_maxy, 0],
        [B_maxx, B_miny - Extend_dist, 0],
        [B_minx, B_miny - Extend_dist, 0] # final one is to complete the box used for plotting purposes only
    ]

    return Tree_ShadowBox_Coords, Tree_ShadowBoxExtend_Coords

#Facade shade
def Get_FacadeShadePoints(TreeShadow, PrimaryBuilding_IdArr, Building_coords_dict):

    FacadeShade_Points = []

    RemainingShadow_Points = TreeShadow

    for B_ID in PrimaryBuilding_IdArr:

        #get the sampled building footprint points
        bf_points = Building_coords_dict[B_ID]

        #Get only points from BF
        BuildingFootprint_shape = alphashape.alphashape(bf_points, alpha=0)

        curr_BF_bool_list = []
        #find Tree Shadow Points in StreetShade
        for tsp in RemainingShadow_Points:
            tp = Point(tsp)
            curr_BF_bool_list.append(tp.within(BuildingFootprint_shape))

        for fp in RemainingShadow_Points[curr_BF_bool_list]: #only inside the BF shape
            FacadeShade_Points.append(fp)

        RemainingShadow_Points = RemainingShadow_Points[~np.array(curr_BF_bool_list)]

    return RemainingShadow_Points, FacadeShade_Points

def Get_InShadePoints(TreeShadow, All_Building_LidarPointsDict_HAdj, Az, Amp):

    InShade_Points = []
    RemainingShadow_Points = TreeShadow

    for B_ID, B_lidarpoints in All_Building_LidarPointsDict_HAdj.items():

        B_Shadow = Get_Shadow(B_lidarpoints, Az, Amp)

        #Get only points from BS
        BuildingShadow_shape = alphashape.alphashape(B_Shadow, alpha=0)

        curr_BS_bool_list = []
        #find Tree Shadow Points in InShade
        for tsp in RemainingShadow_Points:
            tp = Point(tsp)
            curr_BS_bool_list.append(tp.within(BuildingShadow_shape))

        for Ip in RemainingShadow_Points[curr_BS_bool_list]: #only inside the BS shape
            InShade_Points.append(Ip)

        if(len(curr_BS_bool_list) > 0):
            RemainingShadow_Points = RemainingShadow_Points[~np.array(curr_BS_bool_list)]

    return RemainingShadow_Points, InShade_Points

def InitiateShadingLogger(filename:str,year:int)-> None:

    #Where the logger file will be created
    LoggerPath = "Datasets/"+"Package_Generated/"+filename[:-4]+"/"+str(year)+"/Shading_Logs_"+filename[:-4]+"/"

    print("Logger Folder Path : ",LoggerPath)
    # Check whether the specified LoggerPath exists or not
    isExist = os.path.exists(LoggerPath)

    if not isExist:
    # Create a new directory because it does not exist
        os.makedirs(LoggerPath)

    logfilename = LoggerPath + 'ShadingScript_'+filename[:-4]+'.log'
    logger = logging.getLogger()
    fhandler = logging.FileHandler(filename=logfilename, mode='a')
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    fhandler.setFormatter(formatter)
    logger.addHandler(fhandler)
    logger.setLevel(logging.INFO)

def parse_log_Completedfiles(log_file_path):
    """_summary_

    Args:
        log_file_path (_type_): _description_

    Returns:
        _type_: _description_

    Usage:
    log_file_path = 'TEST_log.log'
    filenames = parse_log_Completedfiles(log_file_path)
    print(filenames)
    """
    filenames = []
    with open(log_file_path, 'r') as log_file:
        for line in log_file:
            match = re.search(r'(?<=Completed file: )\S+', line)
            if match:
                filenames.append(match.group())
    return filenames

#Functions modified from v1
def Get_dirnames(parent_path:str)->list:
    dirnames = [d for d in os.listdir(parent_path) if os.path.isdir(os.path.join(parent_path, d))]
    return dirnames

# Reading JSON tree data
def Get_JSONfilenames(folder_path:str, year:int)->list:
    filenames = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
    return filenames

def Get_TreeLocation(json_dataBuffer):
    lat = json_dataBuffer['PredictedTreeLocation']['Latitude']
    lon = json_dataBuffer['PredictedTreeLocation']['Longitude']
    return lat,lon

def Get_TreeGroundElevation(json_dataBuffer):
    return json_dataBuffer['GroundZValue']

def Get_TreeHeight(json_dataBuffer):
    return json_dataBuffer['TreeFoliageHeight']

def Get_FullTree_points(json_dataBuffer, Ground_Elevation = 0, Tree_Height = 0):
    Full_Tree = []

    #TODO : might be doing repetitive conversion here
    All_SingleTree_points = np.array(json_dataBuffer['Tree_Points'])*3.28 # Convert m to ft
    for p in All_SingleTree_points:
        Full_Tree.append(p - [0,0,Ground_Elevation+Tree_Height]) #Height Adjusted, default = 0

    return np.array(Full_Tree)

def Get_TreesNotPark(json_filenames, JSON_TreeData_path):
    JsonFiles_notInPark = []
    for jf in json_filenames:
        json_filobj = open(JSON_TreeData_path + jf)
        json_SingleTreeDataBuffer = json.load(json_filobj)
        if not json_SingleTreeDataBuffer['InPark']:
            JsonFiles_notInPark.append(jf)

    return JsonFiles_notInPark

def Get_AllTreePoints_from_JSONfiles(json_filenames,JSON_TreeData_path):
    tree_p = []
    for jf in json_filenames:
        json_filobj = open(JSON_TreeData_path + jf)
        json_SingleTreeDataBuffer = json.load(json_filobj)

        singleTreePoints = json_SingleTreeDataBuffer['Tree_Points']

        for p in singleTreePoints:
            tree_p.append(p)

    return tree_p

def footprintPointsFromGeoJSON_v2(feature):
    points = []

    try:
        height = feature["properties"]["heightroof"]

        if type(height) == float:
            height = height
        else:
            height = 0
    except:
        height = 0

    if feature['geometry']['type'] == 'Point':
        point = [feature["geometry"]["coordinates"][0],feature["geometry"]["coordinates"][1],height]
        points.append(point)
        point = [feature["geometry"]["coordinates"][0],feature["geometry"]["coordinates"][1],0]
        points.append(point)
    elif feature['geometry']['type'] == 'LineString':
        for polygonPart in feature["geometry"]["coordinates"]:
            for polygonSubPart in polygonPart:
                point = [polygonSubPart[0],polygonSubPart[1],height]
                points.append(point)
                point = [polygonSubPart[0],polygonSubPart[1],0]
                points.append(point)
    # elif feature['geometry']['type'] == 'Polygon':
    #     for polygonPart in feature["geometry"]["coordinates"]:
    #         for polygonSubPart in polygonPart:
    #             for coordinates in polygonSubPart:
    #                 point = [coordinates[0],coordinates[1],height]
    #                 points.append(point)
    #                 point = [coordinates[0],coordinates[1],0]
    #                 points.append(point)
    elif feature['geometry']['type'] == 'MultiPolygon':
        for MpolygonPart in feature["geometry"]["coordinates"]:
            for polygonPart in MpolygonPart:
                for polygonSubPart in polygonPart:
                    #print(polygonSubPart)
                    #for coordinates in polygonSubPart:
                    point = [polygonSubPart[0],polygonSubPart[1],height]
                    points.append(point)
                    point = [polygonSubPart[0],polygonSubPart[1],0]
                    points.append(point)
    else:
        pass
        #print(feature['geometry']['type'])
        #print(feature["geometry"]["coordinates"])

    return points, height

#Functions to trim geojson
def findCentroid(buildingPoints):
    xs = []
    ys = []
    for buildingPoint in buildingPoints:
        xs.append(buildingPoint[0])
        ys.append(buildingPoint[1])
    xCenter = sum(xs)/len(xs)
    yCenter = sum(ys)/len(ys)
    return xCenter, yCenter

def convertLatLon(lat,lon):
    #translate from geojson CRS (NAD 1983) to .las CRS (UTM Zone 18N (meters))
    transformer = Transformer.from_crs( "epsg:4326", "epsg:2263" )
    x, y = transformer.transform(lat, lon)
    return x, y

def convertXY(x,y):
    #translate from .las CRS to geojson CRS (NAD 1983) (UTM Zone 18N (meters))
    transformer = Transformer.from_crs( "epsg:2263", "epsg:4326" )
    lat, lon = transformer.transform(x, y)
    return lat, lon

def lasdf_bounds(lasdf):

    xMin = min(lasdf.X)
    xMax = max(lasdf.X)
    yMin = min(lasdf.Y)
    yMax = max(lasdf.Y)

    return xMin,yMin,xMax,yMax

def trimGeoJSON_v2(features,lasdf):

    #print(len(features))

    xMin,yMin,xMax,yMax = lasdf_bounds(lasdf)

    Min_lat,Min_lon  = convertXY(xMin, yMin)
    Max_lat,Max_lon  = convertXY(xMax, yMax)

    features2 = []

    for feature in features: #NOTE: modified from - for feature in features[:]:
        buildingPoints,height = footprintPointsFromGeoJSON_v2(feature)

        xCenter, yCenter = findCentroid(buildingPoints)

        #lat varies in y direction
        #lon varies in x direction

        if yCenter > Min_lat and yCenter < Max_lat and xCenter > Min_lon and xCenter < Max_lon:
            features2.append(feature)

        else:
            continue

    return features2

def convert_dict_of_arrays(data):
    transformer = Transformer.from_crs("epsg:4326", "epsg:2263" )
    converted_data = {}
    for key, value in data.items():
        x, y = transformer.transform(value[:,1], value[:,0])
        converted_data[key] = np.column_stack((x, y))
    return converted_data

def ProcessShading(f,year,fpath):

    try:

        stime = time.time()

        InitiateShadingLogger(f,year)

        ShadeYear = 2021
        start = datetime.datetime(ShadeYear, 1, 8) #8 am
        end = datetime.datetime(ShadeYear, 6, 30, 17, 59) #6 pm
        delta = timedelta(minutes=15)

        logging.info("Year : %d",ShadeYear)
        logging.info("Start date : %s",start)
        logging.info("End date : %s",end)
        logging.info("Time Interval : 15")

        logging.info("TerraVide lidar Shading Metrics Estimation Initated")

        las_file_path = fpath+f[:-4]+'/'+str(year)+'/LasClassified_'+f[:-4]+'/lasFile_Reconstructed_'+f[:-4]+'.las'

        logging.info("Reading Reconstructed lasfile from : %s",las_file_path)

        #Create a dataframe from las file
        lasdf = Create_Dataframe_fromLas(las_file_path)

        #in ft
        lasdf.X = lasdf.X*3.28
        lasdf.Y = lasdf.Y*3.28
        lasdf.Z = lasdf.Z*3.28

        #Buidling footprint of all NYC buildings - 2015 data
        NYC_GeoJSON_Filepath = 'Datasets/BuildingTileSet/building_BL.geojson'
        BuidlingFootprintData = Read_GeoJSON(NYC_GeoJSON_Filepath)

        Filtered_FeatureList = trimGeoJSON_v2(BuidlingFootprintData['features'],lasdf)

        #Save to Geojson

        # Serialize features into a GeoJSON string
        features_geojson = {
            "type": "FeatureCollection",
            "features": Filtered_FeatureList
        }
        features_str = json.dumps(features_geojson)

        geojson_folderpath = 'Datasets/Package_Generated/'+f[:-4]+'/'+str(year)+'/GeoJSON_BuildingFootprint_'+f[:-4]+'/'
        geojson_filename = 'Filtered_buildingsTile_'+f[:-4]+'.geojson'

        # Check whether the specified geojson_folderpath exists or not
        isExist = os.path.exists(geojson_folderpath)

        if not isExist:
        # Create a new directory because it does not exist
            os.makedirs(geojson_folderpath)
        # Write GeoJSON string to a file
        with open(geojson_folderpath+geojson_filename, "w") as file:
            file.write(features_str)
            logging.info("Building Footprint for tileset created")

        Building_coords_dict_latlong_coords = Get_BuildingDataDict(geojson_folderpath+geojson_filename, f, year=2017)
        Building_coords_dict = convert_dict_of_arrays(Building_coords_dict_latlong_coords)

        logging.info("TerraVide lidar Shading Metrics Estimation Initated")

        #Path to where each lasfiles JSON tree data is located (generated by Process_lasFiles.py)
        JSON_TreeData_path = fpath+f[:-4]+'/'+str(year)+'/JSON_TreeData_'+f[:-4]+'/'
        #Get filenames(1 json file = 1 tree cluster) of each json file
        JSON_filenames = Get_JSONfilenames(JSON_TreeData_path,year)
        #Get trees not present in park areas
        JSON_filenamesNotPark = Get_TreesNotPark(JSON_filenames, JSON_TreeData_path)

        logging.info("Total number of Trees : %d",len(JSON_filenames))
        logging.info("Total number of Trees not in Park: %d",len(JSON_filenamesNotPark))

        #Used for debugging with pptk viewer
        Raw_TreePointsNotPark = Get_AllTreePoints_from_JSONfiles(JSON_filenamesNotPark,JSON_TreeData_path)

        # CSV header
        header = ['DateTime_ISO', 'Year', 'Month', 'Day', 'hour', 'minute', 'Sun_Azimuth', 'Sun_Amplitude',
            'Tree_Number', 'Tree_Latitude', 'Tree_Longitude',
            'Shadow_Length','Shadow_Breadth','Shadow_Area','TreeShadow_PointCount',
            'Perc_Canopy_StreetShade', 'Perc_Canopy_FacadeShade', 'Perc_Canopy_InShade',
            'ShadowArea_Ground', 'ShadowArea_OnBuilding', 'ShadowArea_InBuildingShadow']

        for jf in JSON_filenamesNotPark:
            #Reading a single file
            TreeId = jf.split('_')[3] #25192_2017_ID_1_TreeCluster.json

            logging.info("Shading Metrics Calculation started for tree : %s",TreeId)

            json_filobj = open(JSON_TreeData_path + jf)
            json_SingleTreeDataBuffer = json.load(json_filobj)

            shadingCSV_folderpath =fpath+f[:-4]+'/'+str(year)+'/ShadingMetrics'+f[:-4]+'/'
            shadingCSV_filename = 'ShadingMetric_'+f[:-4]+'_Tree_ID_'+TreeId+'.csv'

            # Check whether the specified geojson_folderpath exists or not
            isExist = os.path.exists(shadingCSV_folderpath)

            if not isExist:
            # Create a new directory because it does not exist
                os.makedirs(shadingCSV_folderpath)

            with open(shadingCSV_folderpath+shadingCSV_filename, 'w', encoding='UTF8') as csv_f:
                writer = csv.writer(csv_f)

                # write the header
                writer.writerow(header)

                current = start
                while current <= end:
                    day = current.day
                    hour = current.hour
                    minute = current.minute
                    month = current.month
                    # Use the values as needed
                    current += delta

                    #print(ShadeYear, month, day, hour, minute)

                    #Tree Data
                    Tree_Lat , Tree_Long = Get_TreeLocation(json_SingleTreeDataBuffer)
                    Tree_GroundZ = Get_TreeGroundElevation(json_SingleTreeDataBuffer) #Convert to ft, JSON file has m stored
                    Tree_Height = Get_TreeHeight(json_SingleTreeDataBuffer)

                    # GET AZ and Amp
                    date, Az, Amp = Get_SunData(Tree_Lat, Tree_Long, ShadeYear, month, day, hour, minute)

                    if Amp > 0:
                        #Get Tree Points
                        Full_Tree = Get_FullTree_points(json_SingleTreeDataBuffer,Tree_GroundZ)

                        #Proj_Tree = get_projection(Full_Tree, 45, True)
                        TreeShadow = Get_Shadow(Full_Tree, Az, Amp) #(points, az, amp)

                        L,B,A = Get_ShadowCharacteristics(Full_Tree, TreeShadow)

                        #Get Primary Building IDs
                        #PrimaryBuilding_IdArr = Get_BuildingsUnderEffectOfShadow(Building_coords_dict, TreeShadow)
                        PrimaryBuilding_IdArr = Get_BuildingsUnderEffectOfShadow_Rapid(Building_coords_dict, TreeShadow) #Function is 50% faster
                        #Get Primary Building Coords
                        PrimaryBuilding_FootprintCoords = Get_BuildingFootprintSampledCoords(PrimaryBuilding_IdArr, Building_coords_dict)

                        #Get Primary Building Lidar Points
                        PrimaryBuilding_LidarPointsDict = Get_BuildingLidarPoints(PrimaryBuilding_IdArr, Building_coords_dict, lasdf)

                        #Get ground elevation from building points
                        G_height = Tree_GroundZ #NOTE : Same as Tree_GroundZ
                        #Adjusting height of Buildings
                        PrimaryBuilding_LidarPointsDict_HAdj = Set_BuildingLidarHeightAdjustment(PrimaryBuilding_LidarPointsDict, G_height)

                        #Bounding Box
                        Distance = 200
                        Tree_ShadowBox_Coords, Tree_ShadowBoxExtend_Coords = Get_TreeShadowBoundingBoxesCoords(TreeShadow, Distance)
                        TSBox = sample_polygon(Tree_ShadowBox_Coords)
                        TSBox = np.c_[TSBox, np.zeros(len(TSBox))]
                        TSBoxExtend = sample_polygon(Tree_ShadowBoxExtend_Coords)
                        TSBoxExtend = np.c_[TSBoxExtend, np.zeros(len(TSBoxExtend))]

                        #Get All buildings Falling under Extended Tree ShadowBox
                        All_Building_IdArr = Get_BuildingsUnderEffectOfShadow_Rapid(Building_coords_dict, TSBoxExtend)
                        #Get Secondary Building Lidar Points
                        All_Building_LidarPointsDict = Get_BuildingLidarPoints(All_Building_IdArr, Building_coords_dict, lasdf)
                        #Get ground elevation from building points
                        G_height = Tree_GroundZ #NOTE : Same as Tree_GroundZ
                        #Adjusting height of Buildings
                        All_Building_LidarPointsDict_HAdj = Set_BuildingLidarHeightAdjustment(All_Building_LidarPointsDict, G_height)

                        #Get Secondary Buildings
                        SecondaryBuilding_IdArr = [b for b in All_Building_IdArr if b not in PrimaryBuilding_IdArr]
                        #Get Secondary Building Coords
                        SecondaryBuilding_FootprintCoords = Get_BuildingFootprintSampledCoords(SecondaryBuilding_IdArr, Building_coords_dict)
                        #Get Secondary Building Lidar Points
                        SecondaryBuilding_LidarPointsDict = Get_BuildingLidarPoints(SecondaryBuilding_IdArr, Building_coords_dict, lasdf)
                        #Get ground elevation from building points
                        G_height = Tree_GroundZ #NOTE : Same as Tree_GroundZ
                        #Adjusting height of Buildings
                        SecondaryBuilding_LidarPointsDict_HAdj = Set_BuildingLidarHeightAdjustment(SecondaryBuilding_LidarPointsDict, G_height)

                        # Shade Metrics

                        # 1. Facade Shade - Points falling on the building footprint
                        # 2. In Shade - Points Fall within the shadow of Buildings not in the building footprint
                        # 3. Street Shade - Tree Shadow Points not overcasted by Anything
                        RemainingShadow_Points, FacadeShade_Points = Get_FacadeShadePoints(TreeShadow, PrimaryBuilding_IdArr, Building_coords_dict)
                        RemainingShadow_Points, InShade_Points = Get_InShadePoints(RemainingShadow_Points, All_Building_LidarPointsDict_HAdj, Az, Amp)
                        StreetShade_Points = RemainingShadow_Points

                        #print(len(FacadeShade_Points)/len(TreeShadow) , len(InShade_Points)/len(TreeShadow) ,len(StreetShade_Points)/len(TreeShadow))

                        Perc_Canopy_StreetShade = round(len(StreetShade_Points)/len(TreeShadow),3)
                        Perc_Canopy_FacadeShade = round(len(FacadeShade_Points)/len(TreeShadow),3)
                        Perc_Canopy_InShade = round(len(InShade_Points)/len(TreeShadow),3)

                        data = [date,ShadeYear, month, day, hour+4, minute, Az, Amp,
                                TreeId, round(Tree_Lat,6), round(Tree_Long,6), round(L,3), round(B,3), round(A,3), len(TreeShadow),
                                Perc_Canopy_StreetShade*100, Perc_Canopy_FacadeShade*100, Perc_Canopy_InShade*100,
                                A*Perc_Canopy_StreetShade, A*Perc_Canopy_FacadeShade, A*Perc_Canopy_InShade]
                    else:
                        data = [date,ShadeYear, month, day, hour+4, minute, Az, Amp,
                                TreeId, round(Tree_Lat,6), round(Tree_Long,6), 0, 0, 0, 0,
                                0, 0, 0,
                                0, 0, 0]

                    print(data)
                    writer.writerow(data)

        etime = time.time()

        logging.info("Shade Estimation Completed (in min): %d", (etime - stime)/60)

    except Exception as e:
        logging.error("ERROR : %s",str(e))

if __name__ == "__main__":
    #TEST SCRIPT - LOGS in TEST Directory - modify to Dataset paths with original files for deployment

    script_start_time = time.time()

    #Get las directory names from parent root folder
    p_rootpath = 'Datasets/Package_Generated/' #'/Volumes/Elements/TerraVide/Datasets/FTP_files/LiDAR/'
    year = 2017

    LAS_dirnames = Get_dirnames(p_rootpath)

    print("FileCount : "+str(len(LAS_dirnames))+" .las files found in path = "+p_rootpath )

    args = [(i, year,p_rootpath) for i in LAS_dirnames[:2]]#testing for first 2 files

    for arg_item in args:
        print(arg_item)

    with Pool(2) as p:

         p.starmap(ProcessShading,args)

    script_end_time = time.time()

    print("SCRIPT TOTAL TIME (in min): ",(script_end_time - script_start_time)/60)


"""
PyTorch implementation of NVDM paper:
Neural Variational Inference for Text Processing. Miao et al. ICML 2016.

https://arxiv.org/pdf/1511.06038.pdf
"""
import torch
import torch.nn as nn

def kl_div(mu, log_sigma):
    """ KL divergence as defined in the paper, given the mu and log(sigma)
    parameters of the Gaussian variational distribution. """
    # In the paper they have an undefined "K" variable, but is set to one
    # in the author's code.
    K = 1
    # logdet(diag(s^2)) = log(prod_i s_i^2) = sum_i log s_i^2 = 2 sum_i log s_i
    return -0.5 * torch.sum(K - mu.pow(2) + 2 * log_sigma - torch.exp(2 * log_sigma), dim=1)

class NVDM(nn.Module):
    """ Neural Variational Document Model -- BOW VAE. """
    def __init__(self, vocab_size, n_hidden, n_topic, n_sample, device, learn_embeddings=False):
        super().__init__()
        self.vocab_size = vocab_size
        self.n_hidden = n_hidden
        self.n_topic = n_topic

        # Not used for now, just 1 sample on each call to self.decode()
        self.n_sample = n_sample

        # Inference network
        # See paper appendix B for details

        # Embedded Bag-of-Words
        # This converts variable-length sentences into BOW representations
        # Disable learning word embeddings for now.
        self.embed_bow = nn.EmbeddingBag(vocab_size, vocab_size, mode="sum")

        if not learn_embeddings:
            # Convert "embeddings" into non-trainable identity matrix
            # (i.e. word embeddings are always one-hot)
            # So this effectively becomes a CountVectorizer.
            self.embed_bow.requires_grad_(False)
            assert not self.embed_bow.weight.requires_grad
            assert self.embed_bow.weight.size() == (vocab_size, vocab_size)

            self.embed_bow.weight.zero_()
            self.embed_bow.weight.fill_diagonal_(1.0)

        # Encoder: takes the bag-of-words representation of a document
        # and generates an intermediate/hidden representation.
        # Original paper uses ReLU, but that results in values being too large
        # (many NaNs)
        self.encoder = nn.Sequential(
            nn.Linear(vocab_size, n_hidden),
            nn.Tanh(),
            nn.Linear(n_hidden, n_hidden),
            nn.Tanh()
        )

        # Transform the hidden representation to get the
        # Gaussian mu and log-standard deviation variational parameters.
        self.mu = nn.Linear(n_hidden, n_topic)
        self.log_sigma = nn.Linear(n_hidden, n_topic)
        # Author's code initializes weights and biases of log sigma to 0
        # torch.nn.init.zeros_(self.log_sigma.bias)
        # torch.nn.init.zeros_(self.log_sigma.weight)
        # h ~ N(mu, sigma^2)

        # Generative model
        # This linearly transforms the hidden representation
        # before computing softmax probabilities over vocabulary
        # This becomes the "learned embedding"
        self.decoder = nn.Sequential(
            nn.Linear(n_topic, vocab_size)
        )

        # Torch device (GPU/CPU)
        self.device = device

    def encode(self, text, offsets=None):
        """ If offsets is none, the we assume text is already a BoW representation. """
        # Convert text into BoW representation if required.
        if offsets is not None:
            X_bow = self.embed_bow(text, offsets)
        else:
            X_bow = text

        # Compute Gaussian parameters
        pi = self.encoder(X_bow)
        mu = self.mu(pi)
        log_sigma = self.log_sigma(pi)
        return X_bow, mu, log_sigma

    def decode(self, X_bow, mu, log_sigma):
        # Take samples from transformed standard Normal
        batch_size = X_bow.size(0)

        # Only 1 sample
        eps = torch.randn((batch_size, self.n_topic), device=self.device)
        doc_vec = torch.exp(log_sigma) * eps + mu

        # Logit probabilities over vocabulary
        logits = torch.log_softmax(self.decoder(doc_vec), dim=1)
        return logits

    def forward(self, text, offsets=None, kl_weight=1.0, n_sample=None):
        """ Here we compute both the logits and total loss. """

        if n_sample is None:
            n_sample = self.n_sample

        # Encoding step
        X_bow, mu, log_sigma = self.encode(text, offsets)

        # Reconstruction loss: estimate by sampling.
        loss_rec = 0.0
        for _ in range(n_sample):
            # decode() randomly samples from the Gaussian.
            logits = self.decode(X_bow, mu, log_sigma)

            # Log likelihood. Each logit p(x_i) is weighted by its
            # frequency in the document (retrieved from BoW)
            loss_rec_sample = (X_bow * logits).sum(dim=1)
            loss_rec += loss_rec_sample

        # Negate (important!) and sample mean (i.e. negative log likelihood)
        loss_rec = -loss_rec / n_sample

        # KL divergence loss
        # Computed once only unlike reconstruction loss.
        loss_kl = kl_div(mu, log_sigma)

        # Total loss = L_rec + L_kl
        # During training, we might want to weight the KL more as a form of
        # regularization, so multiply it by kl_weight.
        # But for the variational lower bound/estimating perplexity etc.,
        # kl_weight must be 1.
        loss_total = loss_rec + kl_weight * loss_kl
        return {"rec": loss_rec, "kl": loss_kl, "total": loss_total}


def selectitem(windowTitle, object, value, handle=None):
    log("{} :not implemented yet".format(sys._getframe().f_code.co_name))
    options = pyacc.comboboxListOptions(reverse_ldtp_me(windowTitle),reverse_ldtp_me(object),handle)
    # get position of the wanted item
    for each in range(0,len(options)):
        if options[each] == value:
            break
    # get position of the selected item
    selected = pyacc.comboboxGetSelected(reverse_ldtp_me(windowTitle),reverse_ldtp_me(object),handle)
    selected_index = options.index(selected)
    # calculate how many keypress needed
    diff = each - selected_index
    key = ""
    if diff == 0:
        # no need to move anything
        return 1
    elif diff > 0:
        #positive. move down
        key = "<down>"
    else:
        #negative. move up
        key = "<up>"
        diff *= -1
    # apply changes
    ldtp_extend_mouse_click_here()
    for each in range(0,diff):
        keypress(key)
        keyrelease(key)
        time.sleep(0.1)
    keypress("<enter>")
    keyrelease("<enter>")


import profile
from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.views import View
from newsfeed.models import Post, Comment
from .models import UserProfile
from multiprocessing import context
from django.views.generic.edit import UpdateView, DeleteView
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin

# Create your views here.

class ProfileView(View):
    def get(self, request, pk, *args, **kwargs):
        profile = UserProfile.objects.get(pk=pk)
        user = profile.user
        posts = Post.objects.filter(author=user).order_by('-created_on')

        followers = profile.followers.all()

        if len(followers) == 0:
            is_following = False

        for follower in followers:
            if follower == request.user:
                is_following = True
                break
            else:
                is_following = False

        num_of_followers = len(followers)

        context = {
            'user': user,
            'profile': profile,
            'posts': posts,
            'num_of_followers': num_of_followers,
            'is_following': is_following,
        }

        return render(request, 'profile/profile.html', context)

class ProfileEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = UserProfile
    fields = ['name', 'bio', 'birth_date', 'location', 'picture']
    template_name = 'profile/profile_edit.html'

    def get_success_url(self):
        pk = self.kwargs['pk']
        return reverse_lazy('profile', kwargs={'pk': pk})

    def test_func(self):
        profile = self.get_object()
        return self.request.user == profile.user

class AddFollower(LoginRequiredMixin, View):
    def post(self, request, pk, *args, **kwargs):
        profile = UserProfile.objects.get(pk=pk)
        profile.followers.add(request.user)

        return redirect('profile', pk=profile.pk)

class RemoveFollower(LoginRequiredMixin, View):
    def post(self, request, pk, *args, **kwargs):
        profile = UserProfile.objects.get(pk=pk)
        profile.followers.remove(request.user)

        return redirect('profile', pk=profile.pk)

class ListFollowers(View):
    def get(self, request, pk, *args, **kwargs):
        profile = UserProfile.objects.get(pk=pk)
        followers = profile.followers.all()

        context = {
            'profile': profile,
            'followers': followers,
        }

        return render(request, 'profile/followers_list.html', context)


from typing import Dict, List, Union, Tuple
from enum import Enum
from queue import PriorityQueue
from copy import deepcopy
import random
import math

class MoveType(Enum):
    TUNNEL = 0
    OPEN = 1

class Path:

    def __init__(self, minutes: int = 30, start: str = 'AA', num_agents: int = 1):
        self.minutes = minutes
        self.start = start
        self.id = random.random()
        self.move_type = []
        self.move_vertex = []
        self.move_cost = []
        self.total_cost = []
        self.num_agents = num_agents
        for i in range(num_agents):
            self.move_type.append([])
            self.move_vertex.append([])
            self.move_cost.append([])
            self.total_cost.append(0)
        self.opened = set()

    def set_move(self, agent_idx: int, type: MoveType, vertex: str, minute_cost: int) -> None:
        self.move_type[agent_idx].append(type)
        self.move_vertex[agent_idx].append(vertex)
        self.move_cost[agent_idx].append(minute_cost)
        self.total_cost[agent_idx] = self.total_cost[agent_idx] + minute_cost
        if type == MoveType.OPEN:
            self.opened.add(vertex)

    def set_move_tuple(self, agent_type_vertex_cost: Tuple[int, MoveType, str, int]) -> None:
        self.set_move(agent_type_vertex_cost[0], agent_type_vertex_cost[1],
                      agent_type_vertex_cost[2], agent_type_vertex_cost[3])

    def is_open(self, vertex: str) -> bool:
        return vertex in self.opened

    def get_current_vertex(self, agent_idx: int) -> str:
        for i in range(len(self.move_type[agent_idx])-1, -1, -1):
            if self.move_type[agent_idx][i] == MoveType.TUNNEL:
                return self.move_vertex[agent_idx][i]
        return self.start

    def path_cost(self, agent_idx: int) -> int:
        return self.total_cost[agent_idx]

    def path_reward(self, g: 'Graph'):
        acc = 0
        for agent_idx in range(self.num_agents):
            time_taken = 0
            for idx, move in enumerate(self.move_type[agent_idx]):
                time_taken += self.move_cost[agent_idx][idx]
                if move == MoveType.OPEN:
                    flow_rate = g.get_flow_rate(self.move_vertex[agent_idx][idx])
                    time_remaining = self.minutes - time_taken
                    acc += flow_rate * time_remaining
        return acc

    def path_bound(self, g: 'Graph'):
        # Simple bound by checking max if we open all remaining valves in the next step after taking account time to get there
        acc = 0
        for v in g.get_vertices():
            if not self.is_open(v):
                min_time_to_reach = min([g.get_distance(self.get_current_vertex(agent), v) + self.path_cost(agent) for agent in range(self.num_agents)])
                acc += g.get_flow_rate(v) * (self.minutes - min(self.minutes, (min_time_to_reach + 1)))
        total = acc+self.path_reward(g)
        return total

    def copy(self) -> 'Path':
        new = Path(minutes=self.minutes, start=self.start, num_agents=self.num_agents)
        new.move_type = deepcopy(self.move_type)
        new.move_vertex = deepcopy(self.move_vertex)
        new.move_cost = deepcopy(self.move_cost)
        new.opened = deepcopy(self.opened)
        new.total_cost = deepcopy(self.total_cost)
        return new

    def __lt__(self, other):
        return self.id < other.id
    def __le__(self, other):
        return self.id <= other.id
    def __eq__(self, other):
        return self.id == other.id
    def __ne__(self, other):
        return self.id != other.id
    def __gt__(self, other):
        return self.id > other.id
    def __ge__(self, other):
        return self.id >= other.id

    def __str__(self):
        string = ""
        moves = []
        for agent in range(self.num_agents):
            cost = 0
            for i in range(len(self.move_type[agent])):
                cost += self.move_cost[agent][i]
                moves.append((cost, agent, self.move_type[agent][i], self.move_vertex[agent][i]))
        moves = sorted(moves, key=lambda x: x[0])
        for move in moves:
            minute, agent, move_type, vertex = move
            string += f"Minute {minute}: Agent {agent} is {'moving to' if move_type == MoveType.TUNNEL else 'opening'} valve {vertex}\n"
        return string[:-1]

class Vertex:

    def __init__(self, id: str, flow_rate: int):
        self.id = id
        self.flow_rate = flow_rate

class Graph:

    def __init__(self):
        self.edges: Dict[str, List[str]] = {}
        self.vertices: Dict[str, Vertex] = {}

    def add_vertex(self, v: Vertex):
        self.vertices[v.id] = v
        self.edges[v.id] = []

    def add_edge(self, u_id: str, v_id: str, weight: int = 1):
        self.edges[u_id].append((v_id, weight))

    def get_edges(self, u_id: str):
        return self.edges[u_id]

    def get_flow_rate(self, u_id: str):
        return self.vertices[u_id].flow_rate

    def get_vertices(self) -> List[str]:
        return list(self.vertices.keys())

    def path_valid_next_move(self, path: Path, prioritize_opening=False, prevent_visited=False):
        all_valid_moves = []
        best_agent = 0
        for agent in range(path.num_agents):
            if path.path_cost(agent) <= path.path_cost(best_agent):
                best_agent = agent
        for agent in range(path.num_agents):
            #print(f"Agent: {agent}")
            if agent == best_agent:
                u = path.get_current_vertex(agent)
                valid_moves = [(agent, MoveType.TUNNEL, v[0], v[1]) for v in list(filter(lambda e: not path.is_open(e[0]), self.get_edges(u)))]
                if self.get_flow_rate(u) > 0:
                    if not path.is_open(u):
                        if prioritize_opening:
                            valid_moves = [(agent, MoveType.OPEN, u, 1)]
                        else:
                            valid_moves.append((agent, MoveType.OPEN, u, 1))
                all_valid_moves.extend(valid_moves)
        return all_valid_moves

    def floyd_warshall(self):
        distances = {src: {dst: math.inf for dst in self.get_vertices()} for src in self.get_vertices()}
        for u in self.get_vertices():
            for edge in self.get_edges(u):
                v, weight = edge
                distances[u][v] = weight
            distances[u][u] = 0
        for k in self.get_vertices():
            for i in self.get_vertices():
                for j in self.get_vertices():
                    potential_shorter = distances[i][k] + distances[k][j]
                    if distances[i][j] > potential_shorter:
                        distances[i][j] = potential_shorter
        return distances

    def calculate_distances(self):
        self.distances = self.floyd_warshall()

    def get_distance(self, u, v) -> int:
        return self.distances[u][v]

    def shrink_graph(self, start: str = 'AA') -> 'Graph':
        vertices_to_keep = []
        for v in self.get_vertices():
            if self.get_flow_rate(v) > 0:
                vertices_to_keep.append(v)
        distances = self.floyd_warshall()
        complete_graph = Graph()
        complete_graph.add_vertex(Vertex(start, self.get_flow_rate(start)))
        for v in vertices_to_keep:
            complete_graph.add_vertex(Vertex(v, self.get_flow_rate(v)))
            complete_graph.add_edge(start, v, distances[start][v])
        for u in vertices_to_keep:
            for v in vertices_to_keep:
                if u != v:
                    complete_graph.add_edge(u, v, distances[u][v])
        return complete_graph

    def best_first_branch_and_bound(self, start: str = 'AA', minutes: int = 30, num_agents: int = 1,
                                    prioritize_opening: bool=True, prevent_visited: bool=True) -> Path:
        queue = PriorityQueue()
        path = Path(start=start, minutes=minutes, num_agents=num_agents)
        queue.put((-1*path.path_bound(self), path))
        best_path, best_reward = None, None
        count = 0
        while not queue.empty():
            count += 1

            bound, path = queue.get(block=False)
            if count % 1000 == 0:
                print(f"Iteration: {count} Queue Size: {queue.qsize()} Path Length: {len(path.move_type[0])} Reward: {best_reward}")
                #print(path)
                #if count > 4:
                #    break
            bound = bound * -1
            path_reward = path.path_reward(self)
            if best_reward is None or path_reward > best_reward:
                best_path = path
                best_reward = path_reward
            possible_moves = self.path_valid_next_move(path, prioritize_opening=prioritize_opening, prevent_visited=prevent_visited)
            #print(possible_moves)
            new_paths = [path.copy() for _ in range(len(possible_moves))]
            for p, move in zip(new_paths, possible_moves):
                #print("NEW PATH")
                #print(p)
                p.set_move_tuple(move)
            for p in new_paths:
                path_bound = p.path_bound(self)
                if path_bound > best_reward:
                    queue.put((-1*path_bound, p))
        print(best_path)
        return best_reward

with open("input.txt", "rb") as file:
    g = Graph()
    for line in file:
        line = line.decode("utf-8")[:-1].split("; ")
        id, flow_rate = line[0][6:8], int(line[0].split("=")[-1])
        g.add_vertex(Vertex(id, flow_rate))
        target_ids = []
        if "," in line[1]:
            target_ids = line[1].split("valves ")[-1].split(", ")
        else:
            target_ids = [line[1][-2:]]
        for target in target_ids:
            g.add_edge(id, target)
    g_shrunk = g.shrink_graph()
    g_shrunk.calculate_distances()
    best_pressure = g_shrunk.best_first_branch_and_bound(minutes=26, num_agents=2)
    print(best_pressure)


"""
https://open.kattis.com/problems/tgif
Author: https://github.com/smh997/
"""
mon = {'JAN': 0, 'FEB': 1, 'MAR': 2, 'APR': 3, 'MAY': 4, 'JUN': 5, 'JUL': 6, 'AUG': 7, 'SEP': 8, 'OCT': 9, 'NOV': 10, 'DEC': 11}
day = {'MON': 0, 'TUE': 1, 'WED': 2, 'THU': 3, 'FRI': 4, 'SAT': 5, 'SUN': 6}
sumdays = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
d, m = map(str, input().split())
fj = input()
dd = int(d)
mm = mon[m]
ff = day[fj]
if mm <= 1:
    if mm == 0:
        if (dd + ff) % 7 == 5:
            print('TGIF')
        else:
            print(':(')
    else:
        if (sumdays[0] + dd + ff) % 7 == 5:
            print('TGIF')
        else:
            print(':(')
else:
    if (sumdays[mm-1] + dd + ff) % 7 == 5 or (sumdays[mm-1] + dd + ff + 1) % 7 == 5:
        print('not sure')
    else:
        print(':(')


import heapq

def dijkstra():
    distances = [[float('inf') for _ in range(max(price)+1)] for _ in range(N+1)]
    heap = [(0,price[1],1)]
    distances[1][price[1]] = 0
    while heap:
        now_weight, now_price, now = heapq.heappop(heap)
        if now == N:
            return now_weight
        if now_weight <= distances[now][now_price]:
            for next, weight in graph[now]:
                next_price = min(now_price, price[next])
                next_weight = now_weight + (weight * now_price)
                if distances[next][now_price] > next_weight:
                    distances[next][now_price] = next_weight
                    heapq.heappush(heap, (next_weight, next_price, next))

N, M = map(int, input().split())
price = [0] + list(map(int, input().split()))
graph = [[] for _ in range(N+1)]
for i in range(M):
    a, b, w = map(int, input().split())
    graph[a].append((b,w))
    graph[b].append((a,w))
print(dijkstra())


constelations = []
points = []

with open("input.txt", "r") as ins:
    for line in ins:
        points.append(tuple(int(n) for n in line.strip().split(',')))

def distance(a, b):
    return abs(a[0]-b[0]) + abs(a[1]-b[1]) + abs(a[2]-b[2]) + abs(a[3]-b[3])

def is_in_constelation(constelation, point):
    for cpoint in constelation:
        if distance(cpoint, point) <=3:
            return True

    return False

working_points = points.copy()

while True:
    new_const = [working_points.pop()]
    constelations.append(new_const)

    old_num = None
    new_num = None
    while old_num is None or old_num != new_num:
        old_num = len(new_const)

        for p in working_points.copy():
            if is_in_constelation(new_const, p):
                new_const.append(p)
                working_points.remove(p)
        new_num = len(new_const)
    if len(working_points) == 0:
        break

print(len(constelations))


# Project:      Lab 9 (TrinhAnhLab09Sec03.py)
# Name:         Anh Trinh
# Date:         02/02/16
# Description:  The program shows off a students's scores imported
#               from a file. (bar chart form)

from graphics import *
import random

#Function creating a scale score
def scale(x,y,win):

    for i in range(0,110,10):

        # Between scores distance = 1 pixel
        x += 1

        scaleScore = Text(Point(x,y),i)
        scaleScore.setSize(10)
        scaleScore.setTextColor('Grey')
        scaleScore.draw(win)

##------------------------------------------------------##

#Function creating bars score
def bar(inFile,win):

    # Set Colors
    lstColors = ['light blue','turquoise','misty rose','green yellow',
                 'yellow green','tomato','gold','medium orchid',
                 'purple','black','tan','chocolate','pale violet red',
                 'light steel blue','salmon','orange','pale turquoise']

    # Width of bar scores is 1 pixel
    # Distance between bars score is 0.5
    barWidth = 1
    barDistance = 0.5
    # Set y coordinate
    y = 0

    # Get scores from the file
    for scores in inFile:
        lstScores = scores.split()
        # Set the scores to fit in the win.setCoords()
        intScores = (int(lstScores[0]))/10 + 1

        # Starting from the lowest bar's position (1,1)
        barScore = Rectangle(Point(1,1),Point(intScores, 1 + barWidth))

        # y coordinate for each new bar is old y += barWidth + barDistance
        y += barWidth + barDistance
        barScore = Rectangle(Point(1,y),Point(intScores, y + barWidth))

        barScore.setFill(random.choice(lstColors))
        barScore.setOutline('white')
        barScore.draw(win)

        # Displaying score text
        scoreTxt = Text(Point(intScores - 0.3, y + 0.5),int(lstScores[0]))
        scoreTxt.setTextColor('white')
        scoreTxt.setStyle('bold')
        scoreTxt.setSize(10)
        scoreTxt.draw(win)

##------------------------------------------------------##

def main():

    # Get user file input
    strFile = str(input("Enter file name: "))
    inFile = open(strFile + ".txt",'r')         # no need to type .txt at the end of the file's name

    # Creat win - set new coordinates
    win = GraphWin("Test Scores",500,400)
    win.setCoords(0.0, 0.0, 12.0, 12.0)
    win.setBackground('white')

    # The title
    txtTitle = Text(Point(6,11),"5 Test Scores")
    txtTitle.setSize(20)
    txtTitle.setTextColor('dim gray')
    txtTitle.setStyle('bold')
    txtTitle.draw(win)

    # Displaying...
    # Scale start at Point(0,0.5)
    scaleScore = scale(0,0.5,win)
    bar(inFile,win)

main()


import pandas as pd

def clean_athletes():
    """
    Remove all rows with any null values.
    Null values are only present in columns weight and height

    Output the cleaned version of athletes dataframe

    @:return a dataframe that contains deleted rows and the cleaned dataframe of athletes.csv
    """
    athletes = pd.read_csv("../data/athletes.csv")

    # first store all the deleted rows
    is_null_table = athletes.isnull()
    deleted_rows = athletes[is_null_table.any(axis=1)]

    # reference:
    # https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.dropna.html
    athletes = athletes.dropna(axis=0, how='any')

    # export to csv without index
    athletes.to_csv("../data/athletes_cleaned.csv", index=False)

    return deleted_rows, athletes

def clean_countries(deleted_rows, cleaned_athletes):
    """
    Remove all the countries that no longer exist in the athletes table after clean_athletes()
    or are not in athletes[nationality]
    :return: A list of removed countries
    """
    countries = pd.read_csv('../data/countries_full.csv')
    countries_del = set(deleted_rows['nationality'].tolist())
    countries_present = set(cleaned_athletes['nationality'].tolist())

    to_be_deleted = []
    # countries no longer present in athletes dataset after cleaning
    for c in countries_del:
        if c not in countries_present:
            to_be_deleted.append(c)

    # countries that are not in the athletes dataset after cleaning
    for c in set(countries['code'].tolist()):
        if c not in countries_present:
            to_be_deleted.append(c)

    # perform deletion on countries dataframe
    for c in to_be_deleted:
        countries = countries[countries['code'] != c]

    # output csv
    countries.to_csv("../data/countries_full_cleaned.csv", index=False)
    return to_be_deleted

def clean_events():
    """
    Convert the events dataframe to support latin-1 encoding,
    as some strings in the venues column contain unsupported characters.
    """
    events = pd.read_csv("../data/events.csv", encoding='latin-1')

    # export to csv without index
    events.to_csv("../data/events_cleaned.csv", index=False)

if __name__ == "__main__":
    del_rows, cleaned_athletes = clean_athletes()
    deleted_countries = clean_countries(del_rows, cleaned_athletes)
    print(deleted_countries)
    clean_events()


import pyautogui

#Confirmar se uma automação deve continuar rodando
resposta = pyautogui.confirm(text='Continuar rodando nossa automação?',title='Confirmação ', buttons=['sim','não','cancelar'])

if resposta == 'sim':
    print('sim')
elif resposta == 'não':
    print('não')
else:
    print('Operação cancelada')


import argparse
import cv2

parser = argparse.ArgumentParser()
parser.add_argument('--path', default='./data/Lena.png',
                    help='input image path')
parser.add_argument('--out_png', default='./data/Lena_compressed.png',
                    help='output image path for lossless result')
parser.add_argument('--out_jpg', default='./data/Lena_compressed.jpg',
                    help='output image path for lossy result')
params = parser.parse_args()
img = cv2.imread(params.path)
print(img.shape)
# 화질 손실 없이 PNG 형식으로 이미지를 저장한 다음 다시 읽어 들여
# 디스크에 저장하는 동안 모든 정보가 보존됐는지 확인

# 낮은 압축률로 이미지를 저장, 파일 크기는 크지만 디코딩이 빠름
cv2.imwrite('data/Lena_compressed.png', img, [cv2.IMWRITE_PNG_COMPRESSION, 5])

# 이미지를 저장하고 다시 불러들여 원본과 동일한지 비교한다
saved_img = cv2.imread(params.out_png)
print(saved_img.shape)
assert saved_img.all() ==img.all()

# 이미지를 낮은 화질로 저장한다. - 크기가 더 작아진다
cv2.imwrite('data/Lena_compressed.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 0])
print(cv2.imread(params.out_jpg).shape)

# 이미지를 저장하기 위해 cv2.imwrite 함수를 사용.
# cv2.imwrite 함수는 출력파일의 경로, 이미지, 저장 매개변수의 세가지 인자를 받는다
# png 형식으로 이미지를 저장할때는 압축 수준을 지정할 수 있다. (0~9)
# 숫자가 클수록 디스크의 파일 크기는 작지만 디코딩 프로세스가 느려진다
# jpeg 형식은 0에서 100사이의 값으로 지정가능 (더 큰값이 좋은 품질의미)

orig = cv2.imread(params.path)
orig_size = orig.shape[0:2]
print(orig_size)

cv2.imshow('Original image', orig)
# 첫번째 매개변수는 창의 이름, 두번째는 표시할 이미지
cv2.waitKey(2000)
# waitKey는 창의 표시 시간을 제어

# !/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# import argparse
# import cv2
#
# parser = argparse.ArgumentParser()
# parser.add_argument( '--path', default='./data/Lena.png', help='Image path.' )
# parser.add_argument( '--iter', default=50, help='Downsampling-upsampling iterations number.' )
# params = parser.parse_args()
# orig = cv2.imread( params.path )
# orig_size = orig.shape[0:2]
#
# cv2.imshow( "Original image", orig )
# cv2.waitKey( 2000 )
#
# resized = orig
#
# for i in range( params.iter ):
#     resized = cv2.resize( cv2.resize( resized, (256, 256) ), orig_size )
#     cv2.imshow( "downsized&restored", resized )
#     cv2.waitKey( 100 )
#
# cv2.destroyWindow( "downsized&restored" )
#
# cv2.namedWindow( "original", cv2.WINDOW_NORMAL )
# cv2.namedWindow( "result" )
# cv2.imshow( "original", orig )
# cv2.imshow( "result", resized )
# cv2.waitKey( 0 )
#
# cv2.destroyAllWindows()


#!/usr/bin/python
# -*- coding: utf-8 -*-

def main():
    argument_spec = get_connection_argument_spec()
    operation_choices = ['poweron', 'poweroff', 'reboot', 'suspend']
    argument_spec.update(
        dict(
            vm=dict(required=True),
            wait=dict(type='bool', required=False, default=True),
            guest=dict(type='bool', required=False, default=False), # required only for poweron and poweroff tasks
            operation=dict(required=True, choices=operation_choices)
        )
    )

    module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False)

    if not HAS_PYVMOMI:
        module.fail_json(msg='pyvmomi is required for this module')

    vm_name = module.params['vm']
    operation = module.params['operation']
    guest = module.params['guest']
    wait = module.params['wait']

    si = get_service_instance(module=module)
    vm = get_entity(si=si, name=vm_name)

    if not vm:
        module.fail_json(msg='could not find virtual machine named "{0}"'.format(vm_name))

    if operation == 'poweron':
        try:
            if vm.runtime.powerState != vim.VirtualMachinePowerState.poweredOn:
                WaitForTask(vm.PowerOn(), si=si)
            if wait:
                while not vm.guest.interactiveGuestOperationsReady:
                    time.sleep(1)

            module.exit_json(msg='power on operation successful for "{0}"'.format(vm_name))
        except Exception as e:
            module.fail_json(msg=e.message, exception=traceback.format_exc())

    if operation == 'poweroff':
        try:
            if vm.runtime.powerState != vim.VirtualMachinePowerState.poweredOff:
                if guest:
                    vm.ShutdownGuest()
                    if wait:
                        while vm.runtime.powerState != vim.VirtualMachinePowerState.poweredOff:
                            time.sleep(1)
                else:
                    WaitForTask(vm.PowerOff(), si=si)

            module.exit_json(msg='power off operation successful for "{0}"'.format(vm_name))
        except Exception as e:
            module.fail_json(msg=e.message, exception=traceback.format_exc())

    if operation == 'reboot':
        try:
            if guest:
                vm.RebootGuest()
                if wait:
                    while vm.guest.interactiveGuestOperationsReady:
                        time.sleep(1)
                    while not vm.guest.interactiveGuestOperationsReady:
                        time.sleep(1)
            else:
                WaitForTask(vm.ResetVM_Task(), si=si)
                if wait:
                    while not vm.guest.interactiveGuestOperationsReady:
                        time.sleep(1)

            module.exit_json(msg='reboot operation successful for "{0}"'.format(vm_name))
        except Exception as e:
            module.fail_json(msg=e.message, exception=traceback.format_exc())

    if operation == 'suspend':
        try:
            if vm.runtime.powerState != vim.VirtualMachinePowerState.suspended:
                WaitForTask(vm.Suspend(), si=si)

            module.exit_json(msg='suspend operation successful for "{0}"'.format(vm_name))
        except Exception as e:
            module.fail_json(msg=e.message)

    module.fail_json(msg='specified operation parameter "{0}" has invalid or not implemented'.format(operation))

import sys
import time
import traceback
sys.path.append('/tmp/vsphere_utils')
reload(sys)
sys.setdefaultencoding('utf8')
from vsphere_utils import *
from pyVim.task import WaitForTask
from pyVmomi import vim, vmodl
from ansible.module_utils.basic import *

if __name__ == '__main__':
    main()


from typing import ContextManager
import discord
from discord.ext import commands
import os
import time
import youtube_dl
import asyncio
from speech.speechSynt import makeSpeech

ytdl_format_options = {
    'format': 'bestaudio/best',
    'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
    'restrictfilenames': True,
    'noplaylist': True,
    'nocheckcertificate': True,
    'ignoreerrors': False,
    'logtostderr': False,
    'quiet': True,
    'no_warnings': True,
    'default_search': 'auto',
    'source_address': '0.0.0.0' # bind to ipv4 since ipv6 addresses cause issues sometimes
}

ffmpeg_options = {
    'options': '-vn'
}

ytdl = youtube_dl.YoutubeDL(ytdl_format_options)

intents = discord.Intents.default()
intents.typing = False
intents.presences = True
intents.members = True
intents.voice_states = True

bot = commands.Bot(command_prefix='$',  intents=intents)

@bot.event
async def on_ready():
    print('Logged in as {0} ({0.id})'.format(bot.user))
    for g in bot.guilds:
        print("guild =", g)
    print('------')

@bot.command()
async def play(ctx : commands.Context, *args):
    print("play command !")
    if len(args) != 1:
        await ctx.send("provide one of the following songs : avengers or deja-vu")
        return
    if args[0] == "avengers":
        query = "avengers-suite-theme.mp3"
    elif args[0] == "deja-vu":
        query = "initial-d-deja-vu.mp3"
    else:
        await ctx.send("provide one of the following songs : avengers or deja-vu")
        return

    if ctx.author.voice == None:
        await ctx.send("You are not in a channel !")
        return

    voice_channel = ctx.author.voice.channel
    voice = ctx.channel.guild.voice_client
    if voice is None:
        voice = await voice_channel.connect()
    elif voice.channel != voice_channel:
        await voice.move_to(voice_channel)
        time.sleep(3)

    voice = ctx.channel.guild.voice_client
    source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(query))
    #source.volume(0.5)
    voice.play(source, after=lambda e: print('Player error: %s' % e) if e else None)
    await ctx.send('Now playing: {}'.format(query))

@bot.command()
async def stop(ctx :commands.Context):
    print("stop command !")
    voice = ctx.channel.guild.voice_client
    if voice != None:
        voice.stop()
    await bot.logout()

@bot.command()
async def goAram(ctx : commands.Context):
    print("goAram command !")
    channel = discord.utils.get(ctx.guild.voice_channels, name="АRАМ")
    if channel == None:
        channel = discord.utils.get(ctx.guild.voice_channels, name="ARAM") # try with latin keyboard
    if (channel != None):
        allMembers = list(bot.get_all_members())
        await ctx.send("all users in channels move to channel ARAM!")
        for member in allMembers:
            if (member.voice != None):
                print(member, "moves to", channel)
                await member.move_to(channel)

async def from_url(url, *, loop=None, stream=False):
    loop = loop or asyncio.get_event_loop()
    data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))

    if 'entries' in data:
        # take first item from a playlist
        data = data['entries'][0]

    filename = data['url'] if stream else ytdl.prepare_filename(data)
    return discord.FFmpegPCMAudio(filename, **ffmpeg_options), data['title']

@bot.command()
async def join(self, ctx, *, channel: discord.VoiceChannel):
    """Joins a voice channel"""

    if ctx.voice_client is not None:
        return await ctx.voice_client.move_to(channel)

    await channel.connect()

@bot.command()
async def yt(ctx :commands.Context, url):
    print("yt command !")
    if ctx.author.voice == None:
        await ctx.send("You are not in a channel !")
        return

    voice_channel = ctx.author.voice.channel
    voice = ctx.channel.guild.voice_client
    if voice is None:
        voice = await voice_channel.connect()
    elif voice.channel != voice_channel:
        await voice.move_to(voice_channel)

    """Plays from a url (almost anything youtube_dl supports)"""

    async with ctx.typing():

        player, title = await from_url(url, loop=bot.loop, stream = True)
        ctx.voice_client.play(player, after=lambda e: print('Player error: %s' % e) if e else None)

    await ctx.send('Now playing: {}'.format(title))

@bot.command()
async def say(ctx : commands.Context, *args):
    print("say command !")
    query = "poryadok.mp3"

    if len(args) != 1:
        #await ctx.send("provide a number")
        #return
        pass
    if args[0] == "1":
        query = "report.mp3"
    elif args[0] == "2":
        query = "giguli.mp3"
    elif args[0] == "3":
        query = "poryadok.mp3"
    else:
        await ctx.send("I will say that")
        words = ''.join(args)
        fileName = makeSpeech(words)
        query = fileName

    if ctx.author.voice == None:
        await ctx.send("You are not in a channel !")
        return

    voice_channel = ctx.author.voice.channel
    voice = ctx.channel.guild.voice_client
    if voice is None:
        voice = await voice_channel.connect()
    elif voice.channel != voice_channel:
        await voice.move_to(voice_channel)
        time.sleep(3)

    voice = ctx.channel.guild.voice_client
    source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(query))
    #source.volume(0.5)
    voice.play(source, after=lambda e: print('Player error: %s' % e) if e else None)
    #await ctx.send('Now playing: {}'.format(query))

token = os.getenv("DiscordBot")
bot.run(token)