# import gym
import numpy as np
import scipy as sp

seed = 123
np.random.seed(seed)

from matplotlib import pyplot as plt
from scipy.integrate import solve_ivp, odeint
from scipy.special import comb

import sys
sys.path.append('../')
from tensor import KoopmanTensor
sys.path.append('../../')
import observables

#%% System dynamics
state_dim = 2
action_dim = 1

state_order = 2
action_order = 2

state_column_shape = [state_dim, 1]
action_column_shape = [action_dim, 1]
phi_dim = int( comb( state_order+state_dim, state_order ) )
psi_dim = int( comb( action_order+action_dim, action_order ) )

action_range = np.array([-50, 50])
# action_range = np.array([-75, 75])
step_size = 0.1
all_actions = np.arange(action_range[0], action_range[1]+step_size, step_size)
all_actions = np.round(all_actions, decimals=2)

gamma = -0.8
delta = -0.7
t_span = np.arange(0, 0.01, 0.001)

def continuous_f(action=None):
    """
        INPUTS:
        action - action vector. If left as None, then random policy is used
    """

    def f_u(t, input):
        """
            INPUTS:
            input - state vector
            t - timestep
        """
        x_1, x_2 = input

        x_1_dot = gamma * x_1
        x_2_dot = delta * (x_2 - x_1**2)

        u = action
        if u is None:
            u = np.random.choice(all_actions, size=action_column_shape)

        return [ x_1_dot, x_2_dot ]

    return f_u

def f(state, action):
    """
        INPUTS:
        state - state column vector
        action - action column vector

        OUTPUTS:
        state column vector pushed forward in time
    """
    u = action[:,0]

    soln = solve_ivp(fun=continuous_f(u), t_span=[t_span[0], t_span[-1]], y0=state[:,0], method='RK45')

    return np.vstack(soln.y[:,-1])

#%% Generate data
num_episodes = 500
num_steps_per_episode = 1000
N = num_episodes*num_steps_per_episode # Number of datapoints
X = np.zeros([state_dim,N])
Y = np.zeros([state_dim,N])
U = np.zeros([action_dim,N])

# initial_xs = np.zeros([num_episodes, state_dim])
# for episode in range(num_episodes):
#     x = np.random.random(state_column_shape) * 0.5 * np.random.choice([-1,1], size=state_column_shape)
#     u = np.array([[0]])
#     soln = solve_ivp(fun=continuous_f(u), t_span=[0.0, 10.0], y0=x[:,0], method='RK45')
#     initial_xs[episode] = soln.y[:,-1]

initial_xs = np.random.rand(num_episodes,state_dim) * 2 * np.random.choice([-1,1], size=[num_episodes,state_dim])

for episode in range(num_episodes):
    x = np.vstack(initial_xs[episode])
    for step in range(num_steps_per_episode):
        X[:,(episode*num_steps_per_episode)+step] = x[:,0]
        # u = np.random.choice(all_actions, size=action_column_shape)
        u = np.array([[0]])
        U[:,(episode*num_steps_per_episode)+step] = u[:,0]
        x = f(x, u)
        Y[:,(episode*num_steps_per_episode)+step] = x[:,0]

#%% Estimate Koopman tensor
tensor = KoopmanTensor(
    X,
    Y,
    U,
    phi=observables.monomials(state_order),
    psi=observables.monomials(action_order),
    is_generator=True,
    regressor='ols'
)

#%% Policy function(s)
def zero_policy(x):
    return np.array([[0]])

def random_policy(x):
    return np.random.choice(action_range, size=action_column_shape)

#%% Compute true and learned dynamics over time
initial_state = np.random.rand(*state_column_shape) * 2 * np.random.choice([-1,1], size=state_column_shape)
tspan = np.arange(0, 25, 0.001)
u = zero_policy(initial_state)

true_xs = odeint(continuous_f(u), initial_state[:,0], tspan, tfirst=True)
# learned_xs = odeint(continuous_tensor_f(u), tensor.phi(initial_state)[:,0], tspan, tfirst=True)
learned_xs = np.zeros([25000,phi_dim])
phi_state = tensor.phi(initial_state)
for i in range(learned_xs.shape[0]):
    # learned_xs[i] = (sp.linalg.expm((0.1*(i+1)) * tensor.K_(u)) @ tensor.phi(initial_state))[:,0]
    learned_xs[i] = (sp.linalg.expm(0.1 * tensor.K_(u)) @ phi_state)[:,0]
    phi_state = np.vstack(learned_xs[i])
learned_xs = (tensor.B.T @ learned_xs.T).T

#%% Plot true vs. learned dynamics over time
fig, axs = plt.subplots(2)
fig.suptitle('Dynamics Over Time')

axs[0].set_title('True dynamics')
axs[0].set(xlabel='Step', ylabel='State value')

axs[1].set_title('Learned dynamics')
axs[1].set(xlabel='Step', ylabel='State value')

labels = np.array(['x_1', 'x_2'])
for i in range(state_dim):
    axs[0].plot(true_xs[:,i], label=labels[i])
    axs[1].plot(learned_xs[:,i], label=labels[i])
lines_labels = [axs[0].get_legend_handles_labels()]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
fig.legend(lines, labels)

plt.tight_layout()
plt.show()


# This is Linear SVM

# array handling with numpy
import numpy as np

# matplotlib lib is optional
import matplotlib.pyplot as plt
from matplotlib import style

style.use('ggplot')

class Linear_SVM(object):

    def __init__(self, visualization=True):
        # visualization is set to True by default, define it as False
        # to disable it
        self.visualization = visualization

        # the positive class is red
        # the negative is blue
        self.colors = {1:'r', -1:'b'}

        if self.visualization:
            self.fig = plt.figure()
            self.ax = self.fig.add_subplot(1, 1, 1)

    def fit(self, data):
        self.data = data

        # opt_dict is the magnitude of W vector {|| W ||}
        # and it is essentially dictionary holding [w, b] as key: value
        opt_dict = {}

        # iterate through transforms and get the product of each value
        # with W passing through all possible values
        transforms = [[1, 1],
                      [-1, 1],
                      [-1,-1],
                      [1, -1]]

        # Get the maximum and minimum possible values
        # for optimizing
        all_data = []

        for yi in self.data:
            for featureset in self.data[yi]:
                for feature in featureset:
                    all_data.append(feature)

        self.max_feature_value = max(all_data)
        self.min_feature_value = min(all_data)
        all_data = None

        # svm = yi(xi.w+b) = 1
        # Defining the step sizes for the convex optimization
        # starting from bigger (0.1) to smaller (0.001) steps
        # so we don't take too much small steps
        # TODO make step_sizes dynamic
        step_sizes = [self.max_feature_value * 0.1,
                      self.max_feature_value * 0.01,
                      self.max_feature_value * 0.001]  # point of expense

        # point of expense: high
        # b does not need as smaller steps to be efficient
        # the smaller it gets the bigger time and cost
        b_range_multiple = 5 # very expensive

        # no need of bigger steps with b
        b_multiple = 5
        latest_optimum = self.max_feature_value * 10

        # start the stepping process
        for step in step_sizes:
            w = np.array([latest_optimum, latest_optimum])
            # Set optimized as false by default
            # until there is no more steps to be taken
            # in the convex
            optimized = False

            while not optimized:
                for b in np.arange(-1 * (self.max_feature_value *
                                         b_range_multiple),
                                   self.max_feature_value * b_range_multiple,
                                   step * b_multiple):
                    for transformation in transforms:
                        w_t = w * transformation
                        found_option = True

                        # yi(xi.w+b) >= 1
                        for i in self.data:
                            for xi in self.data[i]:
                                yi = i
                                if not yi * (np.dot(w_t, xi) + b) >= 1:
                                    found_option = False
                                    break # TODO
                                    # print(yi*(np.dot(w_t, xi) + b))

                        if found_option:
                            # the magnitude of the vector
                            opt_dict[np.linalg.norm(w_t)] = [w_t, b]
                if w[0] < 0:
                    optimized = True
                    print('Optimizing a step') # TODO remove after test is done
                else:
                    w = w - step
            # sorting the list of magnitudes in opt_dict
            norms = sorted([n for n in opt_dict])
            # get the lowest magnitude in opt_dict
            # ||w|| : [w,b]
            opt_choice = opt_dict[norms[0]]
            self.w = opt_choice[0]
            self.b = opt_choice[1]
            latest_optimum = opt_choice[0][0] + step * 2

    def predict(self, features):
        # classification is the sign(Xi.W + b)
        # b = bias
        classification = np.sign(np.dot(np.array(features), self.w) + self.b)

        # this step is optional and its for visualizing the classification
        # checks if classification is not empty and
        # there is self.visualization
        if classification is not 0 and self.visualization:
            self.ax.scatter(features[0], features[1], s=200,
                            marker='*', c=self.colors[classification])
        # either way, it returns the classification
        return classification

    def visualize(self, data_dict):
        # this functions is all about visualizing
        # not vital for calculating the actual svm algorithm
        # can be removed/not used if plotting is not needed
        # or you want to use your own function and/or plotting method
        [[self.ax.scatter(x[0], x[1], s=100, color=self.colors[i])
          for x in data_dict[i]] for i in data_dict]

        def hyperplane(x, w, b, v):
            # embeded function
            # hyperplane is x.w+b
            # v = x.w+b
            # positive_sv = 1
            # negative_sv = -1
            # decision_boundry = 0
            return (-w[0] * x - b + v) / w[1]

        datarange = (self.min_feature_value * 0.9,
                     self.max_feature_value * 1.1)

        hyp_x_min = datarange[0]
        hyp_x_max = datarange[1]

        # (w.x + b) = 1
        # positive support vector hyperplane
        psv1 = hyperplane(hyp_x_min, self.w, self.b, 1)
        psv2 = hyperplane(hyp_x_max, self.w, self.b, 1)
        self.ax.plot([hyp_x_min, hyp_x_max], [psv1, psv2], 'k')

        # (w.x + b) = -1
        # negative support vector hyperplane
        nsv1 = hyperplane(hyp_x_min, self.w, self.b, -1)
        nsv2 = hyperplane(hyp_x_max, self.w, self.b, -1)
        self.ax.plot([hyp_x_min, hyp_x_max], [nsv1, nsv2], 'k')

        # (w.x + b) = 0
        # decision boundry support vector hyperplane
        db1 = hyperplane(hyp_x_min, self.w, self.b, 0)
        db2 = hyperplane(hyp_x_max, self.w, self.b, 0)
        self.ax.plot([hyp_x_min, hyp_x_max], [db1, db2], 'y--')

        plt.show()


import lane_detect_CNN as cnn
import motor as m
import eyes as E
import controller as c
import ultrasonic_distance as dist
from threading import Thread, current_thread, Event
from multiprocessing import current_process
import time
import os

stop_dist = 10    # cm
stop_event = Event()
drive_auto_event = Event()

def io_thread(controller: c, driver: m):
    timeout = 0.001  # ms timeout (range: ~1-300ms)
    pid = os.getpid()
    threadName = current_thread().name
    processName = current_process().name
    print(f"{pid}/{processName}/{threadName} ---> Starting IO Thread...")
    last_event = time.time()
    while not stop_event.is_set():
        if drive_auto_event.is_set():
            if (time.time()-last_event) > timeout:
                command = controller.read_command()
                if command is not None:
                    parse_command(driver, command, True)
                last_event = time.time()
            else:
                time.sleep(timeout)
        else:
            command = controller.read_command()
            if command is not None:
                parse_command(driver, command, True)

    print(f"{pid}/{processName}/{threadName} ---> Finished IO Thread...")

def parse_command(driver, command, print_activity=False):
    # print(command)                # DEBUG
    if command == "START":          # stop program
        driver.loop = False
        stop_event.set()
        print("Exiting...")
    elif command == "X":            # Toggle motor
        driver.toggle_motor()
        if print_activity:
            print(command, '--->', driver.get_drive_state_label(),
                  f'---> Motors: {"On" if driver.motor_state else "Off"}')
    elif driver.auto:               # CNN is controlling:
        if command == "SELECT":     # switch driving modes
            driver.motor_off()
            driver.toggle_auto()
            drive_auto_event.clear()
            if print_activity:
                print(command, '--->', f'Self Driving: {driver.auto}')
    else:                               # Manual control
        if command == "UP":             # go forward
            driver.forward()
        if command == "DOWN":           # go backward
            driver.backward()
        if command == "LEFT":           # turn left
            driver.turn_left()
        if command == "RIGHT":          # turn right
            driver.turn_right()
        if command == "A":              # default speed
            driver.set_speed(1)
        if command == "Y":              # increase speed
            driver.speed_up()
        if command == "B":              # decrease speed
            driver.speed_down()
        if command == "LEFT TRIGGER":   # rotate left
            driver.rotate_left()
        if command == "RIGHT TRIGGER":  # rotate right
            driver.rotate_right()
        if command == "SELECT":         # switch driving modes
            E.cv2.destroyAllWindows()
            drive_auto_event.set()
            driver.motor_on()
            driver.set_speed(1)         # Default speed
            driver.toggle_auto()

        if print_activity:
            if driver.motor_state:
                if command == 'Y':
                    print(command, '--->', f'Speed UP: {driver.current_duty}')
                elif command == 'B':
                    print(command, '--->', f'Speed DOWN: {driver.current_duty}')
                elif command == 'A':
                    print(command, '--->', f'Speed DEFAULT: {driver.current_duty}')
                else:
                    print(command, '--->', driver.get_drive_state_label())
            else:
                print(command, '--->', driver.get_drive_state_label(),
                      f'---> Motors: {"On" if driver.motor_state else "Off"}')

def auto_drive_handler(driver, command):
    if command == "forward":        # go forward
        driver.forward()
    if command == "left":           # turn left
        driver.turn_left()
    if command == "right":          # turn right
        driver.turn_right()
    if command == "rotate_left":    # rotate left
        driver.rotate_left()
    if command == "rotate_right":   # rotate right
        driver.rotate_right()

if __name__ == '__main__':
    try:    # Eeeee... va?
        with open('vars/eva_ascii') as f:
            print(f.read())
    except Exception as e:
        print(e)
    except FileExistsError as e:
        print("Eee... VA?")

    print("Preparing Launch Procedures...")
    controller = c.Controller(3)
    driver = m.MotorControl()
    driver.set_speed(1)
    eye = E.Eyes()
    auto_motor = cnn.DriveDetection()

    # Start gamepad i/o thread
    t1_io = Thread(target=io_thread, args=(controller, driver))
    t1_io.start()
    pid = os.getpid()
    threadName = current_thread().name
    processName = current_process().name
    print(f"{pid}/{processName}/{threadName} ---> Starting Main...")

    print("Ready For Takeoff!\n")
    print(f'{"Auto" if driver.auto else "Manual"} Mode',
          f'---> {driver.get_drive_state_label()}',
          f'---> Motors: {"On" if driver.motor_state else "Off"}')
    try:
        while driver.loop:
            if driver.auto:
                obj_dist = dist.distance()
                if (obj_dist > 0) and (obj_dist < stop_dist):
                    print(f'OBSTRUCTION DETECTED: {obj_dist:.2f}/{stop_dist}cm')
                    driver.motor_off()
                    while obj_dist < stop_dist:
                        obj_dist = dist.distance()
                    print("Continuing...")
                    driver.motor_on()
                else:
                    img = eye.get_thresh_img()
                    motor_pred, cert = auto_motor.drive_predict(img)
                    print(motor_pred[1])
                    auto_drive_handler(driver, motor_pred[1])
            else:
                img = eye.get_img()
                img_thresh = eye.get_thresh_img(img)
                img_horiz_stack = E.stack_images(1, ([img, img_thresh]))
                eye.show_img(img_horiz_stack, "Camera View", 1)

    except Exception as e:
        print(e)
    except KeyboardInterrupt as e:
        print(e)
        print("Shutting Down...")
    finally:
        if not stop_event.is_set():
            stop_event.set()
        t1_io.join()
        try:
            m.destroy()
        except Exception as e:
            print(e)
        try:
            controller.kill_device()
        except Exception as e:
            print(e)
        try:
            eye.destroy()
        except Exception as e:
            print(e)
        print('Done.')


"""SQLAlchemy models for storing reference genomes and taxonomy information."""

from typing import Sequence, Union, Dict, List, Any, Optional, Tuple, Iterable, Collection

import sqlalchemy as sa
from sqlalchemy import Column, Integer, String, Boolean, Float
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship, backref, deferred
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm.attributes import InstrumentedAttribute

from .sqla import JsonString

__all__ = [
	'Genome',
	'ReferenceGenomeSet',
	'AnnotatedGenome',
	'Taxon',
]

# Naming convention for constraints and indices, used by SQLAlchemy when creating schema.
# Important for Alembic migration scripts, see https://alembic.sqlalchemy.org/en/latest/naming.html
NAMING_CONVENTION = {
  "ix": "ix_%(column_0_label)s",
  "uq": "uq_%(table_name)s_%(column_0_name)s",
  "ck": "ck_%(table_name)s_%(constraint_name)s",
  "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
  "pk": "pk_%(table_name)s",
}

# SqlAlchemy metadata object and declarative base
metadata = sa.MetaData(naming_convention=NAMING_CONVENTION)
Base = declarative_base(metadata=metadata)

class Genome(Base):
	"""Base model for a reference genome that can be compared to query.

	Corresponds to a single assembly (one or more contigs, but at least partially assembled) from
	what should be a single sequencing run. The same organism or strain may have several genome
	entries for it. Typically this will correspond directly to a record in Genbank (assembly
	database).

	The data on this model should primarily pertain to the sample and sequencing run itself. It
	would be updated if for example a better assembly was produced from the original raw data,
	however more advanced interpretation such as taxonomy assignments belong on an attached
	:class:`AnnotatedGenome` object.

	Attributes
	----------
	id : int
		Integer column (primary key).
	key : str
		String column (unique). Unique "external id" used to reference the genome from outside the
		SQL database, e.g. from a file containing K-mer signatures.
	description : Optional[str]
		String column (optional). Short one-line description. Recommended to be unique but this is
		not enforced.
	ncbi_db : Optional[str]
		String column (optional). If the genome corresponds to a record downloaded from an NCBI
		database this column should be the database name (e.g. ``'assembly'``) and ``ncbi_id``
		should be the entry's UID. Unique along with ``ncbi_id``.
	ncbi_id : Optional[int]
		Integer column (optional). See previous.
	genbank_acc : Optional[str]
		String column (optional, unique). GenBank accession number for this genome, if any.
	refseq_acc : Optional[str]
		String column (optional, unique). RefSeq accession number for this genome, if any.
	extra : Optional[dict]
		JSON column (optional). Additional arbitrary metadata.
	annotations : Collection[.AnnotatedGenome]
		One-to-many relationship to :class:`.AnnotatedGenome`.
	"""

	__tablename__ = 'genomes'

	@declared_attr
	def __table_args__(cls):
		return (
			UniqueConstraint('ncbi_db', 'ncbi_id'),
		)

	id = Column(Integer(), primary_key=True)
	key = Column(String(), unique=True, nullable=False)
	description = Column(String(), nullable=False)
	ncbi_db = Column(String())
	ncbi_id = Column(Integer())
	genbank_acc = Column(String(), unique=True)
	refseq_acc = Column(String(), unique=True)
	extra = deferred(Column(JsonString()))

	annotations = relationship('AnnotatedGenome', lazy=True, cascade='all, delete-orphan')

	def __repr__(self):
		return f'<{type(self).__name__}:{self.id} {self.key!r}>'

class ReferenceGenomeSet(Base):
	"""
	A collection of reference genomes along with additional annotations and data. A full MIDAS
	database which can be used for queries consists of a genome set plus a set of k-mer signatures
	for those genomes (stored separately).

	Membership of :class:`.Genome`s in the set is determined by the presence of an associated
	:class:`.AnnotatedGenomes` object, which also holds additional annotation data for the genome.
	The genome set also includes a set of associated :class:`.Taxon` entries, which form a taxonomy
	tree under which all its genomes are categorized.

	This schema technically allows for multiple genome sets within the same database (which can
	share :class:`.Genome`\\ s but with different annotations), but the MIDAS application generally
	expects that genome sets are stored in their own SQLite files.

	Attributes
	----------
	id : int
		Integer primary key.
	key : str
		String column. An "external id"  used to uniquely identify this genome set. Unique along
		with ``version``.
	version : str
		Optional version string, an updated version of a previous genome set should have the same key
		with a later version number.
		Should be in the format defined by `PEP 440 <https://www.python.org/dev/peps/pep-0440/>`_.
	name : str
		String column. Unique name.
	description : Optional[str]
		Text column. Optional description.
	extra : Optional[dict]
		JSON column. Additional arbitrary data.
	genomes : Collection[.AnnotatedGenome]
		Many-to-many relationship with :class:`.AnnotatedGenome`, annotated versions of genomes in
		this set.
	base_genomes : Collection[.Genome]
		Unannotated :class:`Genome`\\ s in this set. Association proxy to the ``genome``
		relationship of members of :attr:`genome`.
	taxa : Collection[.Taxon]
		One-to-many relationship to :class:`.Taxon`. The taxa that form the classification system
		for this genome set.
	"""
	__tablename__ = 'genome_sets'

	@declared_attr
	def __table_args__(cls):
		return (
			UniqueConstraint('key', 'version'),
		)

	id = Column(Integer(), primary_key=True)
	key = Column(String(), index=True, nullable=False)
	version = Column(String())
	name = Column(String(), nullable=False)
	description = Column(String())
	extra = Column(JsonString())

	genomes = relationship('AnnotatedGenome', lazy='dynamic', cascade='all, delete-orphan')
	base_genomes = relationship('Genome', secondary='genome_annotations', lazy='dynamic', viewonly=True)

	def __repr__(self):
		return f'<{type(self).__name__}:{self.id} {self.key!r}:{self.version!r}>'

	def root_taxa(self) -> Collection['Taxon']:
		"""Query for root taxa belonging to the set.

		Returns
		-------
		sqlalchemy.orm.query.Query
		"""
		return self.taxa.filter_by(parent=None)

class AnnotatedGenome(Base):
	"""A genome with additional annotations as part of a genome set.

	This object serves to attach a genome to a :class:`.ReferenceGenomeSet`, and to assign a
	taxonomy classification to that genome. Hybrid attributes mirroring the attributes of the
	connected genome effectively make this behave as an extended ``Genome`` object.

	Attributes
	----------
	genome_id : int
		Integer column, part of composite primary key. ID of :class:`.Genome` the annotations are
		for.
	genome_set_id : int
		Integer column, part of composite primary key. ID of the :class:`.ReferenceGenomeSet` the
		annotations are under.
	organism : str
		String column. Single string describing the organism. May be "Genus species [strain]" but
		could contain more specific information. Intended to be human-readable and shouldn't have
		any semantic meaning for the application (in contrast to the :attr:`taxa` relationship).
	taxon_id : int
		Integer column. ID of the :class:`Taxon` this genome is classified as.
	genome : .Genome
		Many-to-one relationship to :class:`.Genome`.
	genome_set : .ReferenceGenomeSet
		Many-to-one relationship to :class:`.ReferenceGenomeSet`.
	taxon : .Taxon
		Many-to-one relationship to :class:`.Taxon`. The primary taxon this genome is classified as
		under the associated ``ReferenceGenomeSet``. Should be the most specific and "regular"
		(ideally defined on NCBI) taxon this genome belongs to.
	key : str
		Hybrid property connected to attribute on :attr:`genome`.
	description : Optional[str]
		Hybrid property connected to attribute on :attr:`genome`.
	ncbi_db : Optional[str]
		Hybrid property connected to attribute on :attr:`genome`.
	ncbi_id : Optional[int]
		Hybrid property connected to attribute on :attr:`genome`.
	genbank_acc : Optional[str]
		Hybrid property connected to attribute on :attr:`genome`.
	refseq_acc : Optional[str]
		Hybrid property connected to attribute on :attr:`genome`.
	"""
	__tablename__ = 'genome_annotations'

	genome_id = Column(ForeignKey('genomes.id', ondelete='CASCADE'), primary_key=True)
	genome_set_id = Column(ForeignKey('genome_sets.id', ondelete='CASCADE'), primary_key=True)
	taxon_id = Column(ForeignKey('taxa.id', ondelete='SET NULL'), index=True)
	organism = Column(String())

	genome = relationship('Genome', back_populates='annotations')
	genome_set = relationship('ReferenceGenomeSet', back_populates='genomes')
	taxon = relationship('Taxon', backref=backref('genomes', lazy='dynamic'))

	key = hybrid_property(lambda self: self.genome.key)
	description = hybrid_property(lambda self: self.genome.description)
	ncbi_db = hybrid_property(lambda self: self.genome.ncbi_db)
	ncbi_id = hybrid_property(lambda self: self.genome.ncbi_id)
	genbank_acc = hybrid_property(lambda self: self.genome.genbank_acc)
	refseq_acc = hybrid_property(lambda self: self.genome.refseq_acc)

	def __repr__(self):
		return '<{}:{}:{} {!r}/{!r}>'.format(
			type(self).__name__,
			self.genome_set_id,
			self.genome_id,
			self.genome_set.key,
			self.genome.key,
		)

class Taxon(Base):
	"""A taxon used for classifying genomes.

	Taxa are specific to a :class:`.ReferenceGenomeSet` and form a tree/forest structure through the
	:attr:`parent` and :attr`children` relationships.

	Attributes
	----------
	id : int
		Integer column (primary key).
	key : str
		String column (unique). An "external id"  used to uniquely identify this taxon.
	name : str
		String column. Human-readable name for the taxon, typically the standard scientific name.
	rank : Optional[str]
		String column (optional). Taxonomic rank, if any. Species, genus, family, etc.
	description : Optional[str]
		String column (optional). Optional description of taxon.
	distance_threshold : Optional[float]
		Float column (optional). Query genomes within this distance of one of the taxon's reference
		genomes will be classified as that taxon. If NULL the taxon is just used establish the tree
		structure and is not used directly in classification.
	report : Bool
		Boolean column. Whether to report this taxon directly as a match when producing a
		human-readable query result. Some custom taxa might need to be "hidden" from the user,
		in which case the value should be false. The application should then ascend the taxon's
		lineage and choose the first ancestor where this field is true. Defaults to true.
	extra : Optional[dict]
		JSON column (optional). Additional arbitrary data.
	genome_set_id : int
		Integer column. ID of :class:`.ReferenceGenomeSet` the taxon belongs to.
	parent_id : Optional[int]
		Integer column. ID of Taxon that is the direct parent of this one.
	ncbi_id : Optional[int]
		Integer column (optional). ID of the entry in the NCBI taxonomy database this taxon
		corresponds to, if any.
	parent : Optional[.Taxon]
		Many-to-one relationship with :class:`.Taxon`, the parent of this taxon (if any).
	children : Collection[.Taxon]
		One-to-many relationship with :class:`.Taxon`, the children of this taxon.
	genome_set : .ReferenceGenomeSet
		Many-to-one relationship to :class:`.ReferenceGenomeSet`.
	genomes : Collection[.AnnotatedGenome]
		One-to-many relationship with :class:`.AnnotatedGenome`, genomes which are assigned to this
		taxon.
	"""

	__tablename__ = 'taxa'

	id = Column(Integer(), primary_key=True)
	key = Column(String(), unique=True, nullable=False)
	name = Column(String(), index=True, nullable=False)
	rank = Column(String(), index=True)
	description = Column(String())
	distance_threshold = Column(Float())
	report = Column(Boolean(), nullable=False, default=True, server_default=sa.true())

	genome_set_id = Column(ForeignKey('genome_sets.id', ondelete='CASCADE'), nullable=False, index=True)
	parent_id = Column(ForeignKey('taxa.id', ondelete='SET NULL'), index=True)
	ncbi_id = Column(Integer(), index=True)
	extra = deferred(Column(JsonString()))

	genome_set = relationship(
		'ReferenceGenomeSet',
		backref=backref('taxa', lazy='dynamic', cascade='all, delete-orphan')
	)
	parent = relationship('Taxon', remote_side=[id], backref=backref('children', lazy=True))

	def ancestors(self, incself=False) -> Iterable['Taxon']:
		"""Iterate through the taxon's ancestors from bottom to top.

		Parameters
		----------
		incself : bool
			If True start with self, otherwise start with parent.
		"""
		taxon = self if incself else self.parent
		while taxon is not None:
			yield taxon
			taxon = taxon.parent

	def lineage(self) -> List['Taxon']:
		"""Get a sorted list of the taxon's ancestors from top to bottom, including itself."""
		l = list(self.ancestors(incself=True))
		l.reverse()
		return l

	def root(self) -> 'Taxon':
		"""Get the root taxon of this taxon's tree.

		The set of taxa in a :class:`.ReferenceGenomeSet` will generally form
		a forest instead of a single tree, so there can be multiple root taxa.

		Returns self if the taxon has no parent.
		"""
		if self.parent is None:
			return self
		else:
			return self.parent.root()

	def isleaf(self) -> bool:
		"""Check if the taxon is a leaf (has no children)."""
		return not self.children

	def descendants(self, incself=False) -> Iterable['Taxon']:
		"""Iterate through taxa all of the taxon's descendants (pre-order depth-first).

		Parameters
		----------
		incself : bool
			Yield self first.
		"""
		if incself:
			yield self
		for child in self.children:
			yield from child.descendants(incself=True)

	def leaves(self) -> Iterable['Taxon']:
		"""Iterate through all leaves in the taxon's subtree.

		For leaf taxa this will just yield the taxon itself.
		"""
		if self.isleaf():
			yield self
		else:
			for child in self.children:
				yield from child.leaves()

	def print_tree(self, indent='  ', *, _depth=0):
		"""Print the taxon's subtree for debugging.

		Parameters
		---------
		indent : str
			String used to indent each level of descendants.
		"""
		print(indent * _depth + self.name)
		for child in sorted(self.children, key=lambda c: c.name):
			child.print_tree(indent=indent, _depth=_depth + 1)

	def __repr__(self):
		return f'<{type(self).__name__}:{self.id} {self.name!r}>'

#: Attributes of :class:`midas.db.models.Genome` which serve as unique IDs.
GENOME_ID_ATTRS = {
	'key': Genome.key,
	'genbank_acc': Genome.genbank_acc,
	'refseq_acc': Genome.refseq_acc,
	'ncbi_id': Genome.ncbi_id,
}

# Type alias for argument specifying genome id attribute
GenomeAttr = Union[str, InstrumentedAttribute]

def _check_genome_id_attr(attr: GenomeAttr) -> InstrumentedAttribute:
	"""Check that Genome ID attribute is valid, and convert from string argument.
	"""
	if isinstance(attr, str):
		try:
			return GENOME_ID_ATTRS[attr]
		except KeyError:
			pass

	elif isinstance(attr, InstrumentedAttribute):
		for allowed in GENOME_ID_ATTRS.values():
			if attr is allowed:
				return attr

	raise ValueError('Genome ID attribute must be one of the following: ' + ', '.join(GENOME_ID_ATTRS))

def _get_genome_id(genome: Union[Genome, AnnotatedGenome], attr: InstrumentedAttribute):
	"""Get value of ID attribute for genome."""
	if isinstance(genome, AnnotatedGenome):
		genome = genome.genome
	return attr.__get__(genome, Genome)

def _check_genomes_have_ids(genomeset: ReferenceGenomeSet, id_attr: InstrumentedAttribute):
	"""Check all genomes in ReferenceGenomeSet have values for the given ID attribute or raise a ``RuntimeError``."""
	c = genomeset.genomes \
		.join(AnnotatedGenome.genome) \
		.filter(id_attr == None) \
		.count()

	if c > 0:
		raise RuntimeError(f'{c} genomes missing value for ID attribute {id_attr.key}')

def _map_ids_to_genomes(genomeset: ReferenceGenomeSet, id_attr: Union[str, InstrumentedAttribute]) -> Dict[AnnotatedGenome, Any]:
	"""Get dict mapping ID values to AnnotatedGenome."""
	q = genomeset.genomes.join(AnnotatedGenome.genome).add_columns(id_attr)
	return {id_: g for g, id_ in q}

def genomes_by_id(genomeset: ReferenceGenomeSet, id_attr: GenomeAttr, ids: Sequence, strict: bool = True) -> List[Optional[AnnotatedGenome]]:
	"""Match a :class:`ReferenceGenomeSet`'s genomes to a set of ID values.

	This is primarily used to match genomes to signatures based on the ID values stored in a
	signature file. It is expected that the signature file may contain signatures for more genomes
	than are present in the genome set, see also :func:`.genomes_by_id_subset` for that condition.

	Parameters
	----------
	genomeset
	id_attr
		ID attribute of :class:`midas.db.models.Genome` to use for lookup. Can be used as the
		attribute itself (e.g. ``Genome.refseq_acc``) or just the name (``'refsec_acc'``).
		See :data:`.GENOME_IDS` for the set of allowed values.
	ids
		Sequence of ID values (strings or integers, matching type of attribute).
	strict
		Raise an exception if a matching genome cannot be found for any ID value.

	Returns
	-------
	List[Optional[AnnotatedGenome]]
		List of genomes of same length as ``ids``. If ``strict=False`` and a genome cannot be found
		for a given ID the list will contain ``None`` at the corresponding position.

	Raises
	------
	KeyError
		If ``strict=True`` and any ID value cannot be found.
	"""
	id_attr = _check_genome_id_attr(id_attr)
	_check_genomes_have_ids(genomeset, id_attr)
	d = _map_ids_to_genomes(genomeset, id_attr)
	if strict:
		return [d[id_] for id_ in ids]
	else:
		return [d.get(id_) for id_ in ids]

def genomes_by_id_subset(genomeset: ReferenceGenomeSet,
                         id_attr: GenomeAttr,
                         ids: Sequence,
                         ) -> Tuple[List[AnnotatedGenome], List[int]]:
	"""Match a :class:`ReferenceGenomeSet`'s genomes to a set of ID values, allowing missing genomes.

	This calls :func:`.genomes_by_id` with ``strict=False`` and filters any ``None`` values from the
	output. The filtered list is returned along with the indices of all values in ``ids`` which were
	not filtered out. The indices can be used to load only those signatures which have a matched
	genome from a signature file.

	Note that it is not checked that every genome in ``genomeset`` is matched by an ID. Check the
	size of the returned lists for this.

	Parameters
	----------
	genomeset
	id_attr
		ID attribute of :class:`midas.db.models.Genome` to use for lookup. Can be used as the
		attribute itself (e.g. ``Genome.refseq_acc``) or just the name (``'refsec_acc'``).
		See :data:`.GENOME_IDS` for the set of allowed values.
	ids
		Sequence of ID values (strings or integers, matching type of attribute).

	Returns
	-------
	Tuple[List[AnnotatedGenome], List[int]]
	"""
	genomes = genomes_by_id(genomeset, id_attr, ids, strict=False)
	genomes_out = []
	idxs_out = []

	for i, g in enumerate(genomes):
		if g is not None:
			genomes_out.append(g)
			idxs_out.append(i)

	return genomes_out, idxs_out


'''
Created on 10. 10. 2018

@author: esner
'''
from kbc.env_handler import KBCEnvHandler
from gemius.extractor_service import ExtractorService
from gemius.client import Client
import logging
import collections

import ast
import csv

from datetime import datetime

KEY_USER = 'user'
KEY_PASS = '#pass'
KEY_PERIOD_FROM = 'period_from'
KEY_PERIOD_TO = 'period_to'
KEY_RELATIVE_PERIOD = 'relative_period'
KEY_DATASETS = 'datasets'

KEY_MAND_PERIOD_GROUP = [KEY_PERIOD_FROM, KEY_PERIOD_TO]
KEY_MAND_DATE_GROUP = [KEY_RELATIVE_PERIOD, KEY_MAND_PERIOD_GROUP]

MANDATORY_PARS = [KEY_USER, KEY_PASS, KEY_DATASETS, KEY_MAND_DATE_GROUP]

APP_VERSION = '0.2.3'
class Component(KBCEnvHandler):

    def __init__(self):
        KBCEnvHandler.__init__(self, MANDATORY_PARS)

    def run(self, debug=False):
        '''
        Main execution code
        '''
        self.set_default_logger('DEBUG' if debug else 'INFO')
        logging.info('Running version %s', APP_VERSION)
        logging.info('Loading configuration...')
        self.validateConfig()

        params = self.cfg_params
        # get periods
        if not params.get(KEY_RELATIVE_PERIOD):
            from_date = params.get(KEY_PERIOD_FROM)
            to_date = params.get(KEY_PERIOD_TO)
        else:
            from_date = super().get_past_date(params.get(KEY_RELATIVE_PERIOD))
            to_date = datetime.utcnow()

        gemius_srv = ExtractorService(
            Client(params.get(KEY_USER), params.get(KEY_PASS)))

        datasets = params.get(KEY_DATASETS)

        result_files = []
        index = 0
        for dataset in datasets:
            p_type = dataset.get('period_type')
            logging.info(
                'Downloading dataset %s in period %s - %s [%s]', dataset["dataset_type"], from_date, to_date, p_type)
            index += 1
            periods = gemius_srv.get_periods_in_interval(
                from_date, to_date, p_type)

            countries_no_period = [
                c for c in periods.keys() if len(periods[c]) == 0]

            if not periods or len(countries_no_period) == len(periods.values()):
                logging.warning(
                    'No periods [from:%s,to:%s] type:%s', from_date, to_date, p_type)
                continue
            elif len(countries_no_period) > 0:
                logging.warning(
                    'Some countries contain no specified periods [from:%s,to:%s] type:%s', from_date, to_date, p_type)

            res = self.retrieve_n_save_dataset(
                dataset, periods, index, gemius_srv)

            result_files.extend(res)

        logging.info('Building manifest files..')
        self._process_results(result_files, self.cfg_params.get('bucket'))

        logging.info('Extraction finished sucessfully!')

    def _process_results(self, res_files, output_bucket):
        for res in res_files:
            dest_bucket = 'in.c-esnerda-ex-gemius-' + str(self.kbc_config_id)
            if output_bucket:
                suffix = '-' + output_bucket
            else:
                suffix = ''

            # build manifest
            self.configuration.write_table_manifest(
                file_name=res['full_path'],
                destination=dest_bucket + suffix + '.' + res['type'],
                primary_key=res['pkey'],
                incremental=True)

    def retrieve_n_save_dataset(self, dataset, periods, index, gemius_srv):
        dataset_type = dataset.get('dataset_type')
        if dataset_type == 'stats':
            res = self.retrieve_n_save_stats(
                dataset, periods, index, gemius_srv)
        else:
            res = gemius_srv.get_n_save_dataset_in_available_periods(
                dataset_type, self.tables_out_path, index, periods)

        return res

    def retrieve_n_save_stats(self, dataset, periods, index, service):
        filters = dataset.get('filters')
        filter_dict = collections.OrderedDict()
        met_filter = None
        for f in filters:
            filter_dict.update(self._build_filter(f))
            if f['filter'] == 'metric':
                met_filter = f

        if not met_filter:
            raise ValueError('Metrics must be defined for Stats dataset!!')

        metrics = self._get_metrics(met_filter)
        #all_periods = service.get_periods_in_interval(begin = None, end = None, period_type =None)

        #res = service.get_unique_available_metrics_in_periods(all_periods, filter_dict.get('metric'))

        return service.get_n_save_stats_in_available_periods(self.tables_out_path, index, periods, metrics, **filter_dict)

    def _get_metrics(self, met_filter):
        src = met_filter.get('source_table')

        if src.startswith('['):
            # is manually entered
            values = [{'id':v, 'name': 'N/A'} for v in ast.literal_eval(src)]

        else:
            # is from table
            table = super().get_input_table_by_name(src)

            with open(table['full_path'], 'r') as input_:
                reader = csv.reader(input_)
                next(reader)
                values = [{'id':row[0], 'name':row[1]} for row in reader]

        return values

    def _build_filter(self, filter_):
        key = filter_.get('filter')
        src = filter_.get('source_table')

        if src.startswith('['):
            # is manually entered
            values = ast.literal_eval(src)
        else:
            # is from table
            values = self._get_values_from_table(src)

        return {key: values}

    def _get_values_from_table(self, input_table_name):
        table = super().get_input_table_by_name(input_table_name)

        with open(table['full_path'], 'r') as input_:
            reader = csv.reader(input_)
            next(reader)
            values = [row[0] for row in reader]

        return values

"""
        Main entrypoint
"""
if __name__ == "__main__":
    comp = Component()
    comp.run()


import pytest
from hueb.apps.hueb20.models.utils import (
    close_range,
    open_range,
    timerange_serialization,
)
from psycopg2.extras import NumericRange

@pytest.mark.parametrize(
    "test_input,expected",
    [
        # Convert infinite ranges into bounded ranges
        (NumericRange(1, None), NumericRange(1, 2)),
        (NumericRange(2, None), NumericRange(2, 3)),
        (NumericRange(None, 2), NumericRange(2, 3)),
        (NumericRange(None, 1), NumericRange(1, 2)),
        # Convert non-ranges (shouldn't occur because postgres creates empty ranges out of them)
        (NumericRange(1, 1), NumericRange(1, 2)),
        (NumericRange(2, 2), NumericRange(2, 3)),
        # Empty
        (NumericRange(empty=True), NumericRange(empty=True)),
        # None
        (None, None),
    ],
)
def test_close_range(test_input, expected):
    output = close_range(test_input)
    assert output == expected

@pytest.mark.parametrize(
    "test_input,expected",
    [
        # Convert infinite ranges into bounded ranges
        (NumericRange(1, 2), NumericRange(1, None)),
        (NumericRange(2, 3), NumericRange(2, None)),
        # Convert closed ranges with a greater delta than one from a range with excluded upper bound to one with an included upper bound
        (NumericRange(2, 4), NumericRange(2, 3)),
        (NumericRange(2, 5), NumericRange(2, 4)),
    ],
)
def test_open_range(test_input, expected):
    output = open_range(test_input)
    assert output == expected

@pytest.mark.parametrize(
    "timerange,expected",
    [
        # delta 1 range, return only single value
        (NumericRange(2, 3), "2"),
        (NumericRange(3, 4), "3"),
        (NumericRange(4, 5), "4"),
        # default range, reduce upper end by one
        (NumericRange(1, 3), "1 - 2"),
        (NumericRange(1, 4), "1 - 3"),
        (NumericRange(1, 5), "1 - 4"),
        # upper end unknown
        (NumericRange(1, None), "1 - ?"),
        (NumericRange(1, None), "1 - ?"),
        (NumericRange(1, None), "1 - ?"),
        # lower end unknown
        (NumericRange(None, 3), "? - 2"),
        (NumericRange(None, 4), "? - 3"),
        (NumericRange(None, 5), "? - 4"),
        # unbound range
        (NumericRange(None, None), "? - ?"),
        # empty range
        (NumericRange(empty=True), "-"),
        # None
        (None, "-"),
    ],
)
def test_timerange_serialization(timerange, expected):
    output = timerange_serialization(timerange)
    assert output == expected


from rest_framework import serializers

from ids.models import Id

class IdListSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Id
        fields = [
            "id",
            "url",
            "type",
            "verifiable_id",
            "verified_at",
            "created_at",
        ]
        extra_kwargs  = {
            "url": {
                "view_name": "ids:api:id-detail"
            }
        }


"""
Fixtures for floating IPs.

@author: schipiga@mirantis.com
"""

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from horizon_autotests.steps import FloatingIPsSteps

from ._utils import AttrDict

__all__ = [
    'allocate_floating_ip',
    'floating_ip',
    'floating_ips_steps'
]

@pytest.fixture
def floating_ips_steps(login, horizon):
    """Fixture to get floating IPs steps."""
    return FloatingIPsSteps(horizon)

@pytest.yield_fixture
def allocate_floating_ip(floating_ips_steps):
    """Fixture to create floating IP with options.

    Can be called several times during test.
    """
    floating_ips = []

    def _allocate_floating_ip():
        ip = floating_ips_steps.allocate_floating_ip()
        floating_ip = AttrDict(ip=ip)
        floating_ips.append(floating_ip)
        return floating_ip

    yield _allocate_floating_ip

    for floating_ip in floating_ips:
        floating_ips_steps.release_floating_ip(floating_ip.ip)

@pytest.fixture
def floating_ip(allocate_floating_ip):
    """Fixture to create floating IP with default options before test."""
    return allocate_floating_ip()


import pygame
import sys
import settings
import spacecraft
import game_functions as gf

def run_game():
  pygame.init()
  rocket_settings = settings.Settings()
  screen = pygame.display.set_mode((rocket_settings.height, rocket_settings.width))
  pygame.display.set_caption('Rocket Game')
  rocket = spacecraft.Spacecraft(screen)

  while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.quit
        gf.deal_with_up_events(event, rocket)
        gf.deal_with_down_events(event, rocket)
    rocket.moving_rocket()
    gf.update_screen(rocket_settings, screen, rocket)

run_game()


from itertools import permutations

from edge import Edge
from node import Node

import networkx as nx
from matplotlib import pyplot as plt

class Graph():

    def __init__(self):#construtor da classe Grafo
        self.edges: list[Edge] = [] #lista de arestas do grafo
        self.nmNodes : int = None #numero de vertices do grafo
        self.G = nx.DiGraph() #formação do grafo direcionado pela lib NetworkX
        self.nodes: list[Node] = [] #lista de vertices do grafo
        self.visitedEdges: list[Edge] = [] #lista de arestas já visitadas no grafo
        self.visitedNodes: list[Node] = [] #lista de vertices já visitados no grafo
        self.workload: int = 0 #carga horária
        self.gain: int = 0 #ganho total

    def addEdge(self, edge : Edge): #adiciona uma aresta no grafo
        self.edges.append(edge)
        self.G.add_edge(edge.begin.id, edge.end.id, length = edge.gas_cost, weight = edge.distance)

    def addNode(self, node : Node): #adiciona um vertice no grafo
        replica = False
        for i in self.nodes:
            if (node.id == i.id):
                replica = True
                break

        if (not replica):
            self.nodes.append(node)

    def printGraph(self, nodes): #printa e plota o grafo, utilizando as libs: networkX e matplot
        color_map = []
        pos = { #posições dos nós no plano cartesiano
            '0':([0.0, 0.0]),
            '1':([-0.2, 4.0]),
            '2':([5.5, 8.0]),
            '3':([8.0, 3.0]),
            '4':([10.0, -4.0]),
            '5':([1.0, -4.0]),
            '6':([0.3, -7.0]),
            '7':([-4.0, -3.8]),
            '8':([-4.5, 2.5]),
            '9':([-2.0, 6.0])
        }

        for i in self.nodes:#loop que colorirá o grafo, verde os vértices já visitados e azul os não visitados
            if (self.existNode(i, nodes)):
                color_map.append('green')
            else:
                color_map.append('blue')

        nx.draw_networkx_edges(self.G, pos, edge_color= '#000000')
        nx.draw_networkx_nodes(self.G, pos, node_color=color_map)
        nx.draw_networkx_labels(self.G, pos)
        plt.show()

    def setNmNodes(self, nmNodes : int): #define o número de vértices do grafo
        self.nmNodes = nmNodes

    def verifyNode(self, node_id): #recebe um id de um vértice e verifica se este existe no grafo
        for i in self.nodes:
            if (i.id == node_id):
                return i

    def existNode(self, node, visitedNodes): #recebe um id de um vértice e verifica se este existe já foi visitado no grafo
        for i in visitedNodes:
            if (i == node):
                return True

        return False

    def return_VisitedNodeList(self, str): #retorna uma lista de vértices visitados no grafo
        split = str.split(' ')
        list = []
        for i in split:
            if (i.isalnum() and i != "home"):
                list.append(self.verifyNode(i))
        return list

    def verifyEdge(self, edge): #recebe uma aresta e verifica se esta ja foi percorrida no grafo
        for i in self.visitedEdges:
            if (i == edge):
                return True
        return False

    def find_BestStep(self, node): #função de caminhamento guloso
        max_bounty = 0
        canContinue = False

        for i in node.edges: #recebe um vértice 'node' e faz-se a seguinte verificação: o ganho do vértice da outra ponta da aresta possui um ganho maior que o já armazenado em 'max_bounty'? Com a carga-horária atual, é possível caminhar para este vértice em observação e voltar para divinópolis? A aresta a ser caminhada ja foi utilizada?

            if (i.end.bounty > max_bounty and i.end.time_spent < (self.workload - self.timeToReturn(i.end) - i.distance) and (not self.verifyEdge(i))):
                max_bounty = i.end.bounty
                best_node = i.end
                edge_passed = i
                canContinue = True

        if(canContinue): #se foi possível dar um passo, adiciona o vértice na lista de visitados e atualiza o ganho e a carga-horária do grafo
            self.visitedNodes.append(best_node)
            node = best_node
            if (edge_passed.begin == self.nodes[0]):
                self.addEdge_NodeEdgeList(node)
            self.workload = self.workload - (best_node.time_spent + edge_passed.distance)
            self.gain = self.gain + (best_node.bounty - edge_passed.gas_cost)

        else: #se não foi possível dar o passo, volta para Divinópolis e atualiza o ganho e a carga-horária
            self.visitedNodes.append(self.nodes[0])
            self.workload = self.workload - self.timeToReturn(node)
            self.gain = self.gain - self.costToReturn(node)

        return node

    def gulosoMaxValue(self, workload): #recebe a carga horária semanal e roda o algoritmo guloso olhando todas as possibildades de caminho, fazendo um intermédio entre programação-dinâmica e guloso
        results_dict = {} #dicionário que armazenará o resultado final do algoritmo
        results_dict["Nodes"] = ""
        results_dict["Gain"] = self.gain

        for i in range (len(self.nodes[0].edges)): #enquando houver arestas saindo de divinópolis, rodará a função find_BestStep()
            self.cleanVariables(workload)
            no = self.nodes[0]
            self.visitedNodes.append(no)
            if (self.can_ChooseEdge(no)): #observa se há carga-horária suficiente para andar no grafo, pois há casos que há algumas arestas que não são possíveis de percorrer
                while (no != self.find_BestStep(no)):
                    no = self.visitedNodes[-1]
            else:
                break

            if (self.gain > results_dict["Gain"]):
                results_dict["Nodes"] = self.returnVisitedNodes()
                results_dict["Gain"] = self.gain

        return results_dict

    def timeToReturn(self, node): #recebe um vértice e retorna o tempo necessário para voltar desse vertice para divinopolis
        for i in node.edges:
            if (i.end == self.nodes[0]):
                return i.distance

    def costToReturn(self, node): #recebe um vértice e retorna o custo com gasolina necessário para voltar desse vertice para divinopolis
        for i in node.edges:
            if (i.end == self.nodes[0]):
                return i.gas_cost

    def returnVisitedNodes(self): #retorna uma string apontando os vértices que foram caminhados
        str = ""
        for i in self.visitedNodes:
            str = str + i.id + " -> "

        str = str + "home"
        return str

    def printVisitedEdges(self): #printa as arestas visitadas
        for i in self.visitedEdges:
            print(i.begin.id," -> ", i.end.id)

    def addEdge_NodeEdgeList(self, node): #adiciona uma aresta na lista de arestas visitadas de um vértice específico
        for i in self.edges:
            if (node.id == i.end.id and i.begin == self.nodes[0]):
                self.visitedEdges.append(i)

    def cleanVariables(self, workload): #limpa todas as variáveis utilizadas no loop guloso-dinâmico
        self.workload = workload
        self.visitedNodes.clear()
        self.gain = 0

    def can_ChooseEdge(self, node): #retorna se é possível percorrer por alguma aresta no grafo
        for i in node.edges:
            if (i.end.time_spent < (self.workload - self.timeToReturn(i.end) - i.distance) and (not self.verifyEdge(i))):
                return True

        return False


"""Builder for CVs."""
from copy import deepcopy
from datetime import  date

from regolith.builders.basebuilder import LatexBuilderBase
from regolith.fsclient import _id_key
from regolith.sorters import ene_date_key, position_key, doc_date_key
from regolith.stylers import sentencecase, month_fullnames
from regolith.tools import (
    all_docs_from_collection,
    filter_publications,
    filter_projects,
    filter_grants,
    awards_grants_honors,
    make_bibtex_file,
    filter_employment_for_advisees,
    dereference_institution,
    merge_collections_superior,
    filter_presentations, remove_duplicate_docs, fuzzy_retrieval,
)

class CVBuilder(LatexBuilderBase):
    """Build CV from database entries"""

    btype = "cv"
    needed_colls = ['institutions', 'people', 'grants', 'citations', 'projects',
                  'proposals', 'presentations']

    def construct_global_ctx(self):
        """Constructs the global context"""
        super().construct_global_ctx()
        gtx = self.gtx
        rc = self.rc
        gtx["people"] = sorted(
            all_docs_from_collection(rc.client, "people"),
            key=position_key,
            reverse=True,
        )
        gtx["presentations"] = sorted(
            all_docs_from_collection(rc.client, "presentations"), key=_id_key
        )
        gtx["institutions"] = sorted(
            all_docs_from_collection(rc.client, "institutions"), key=_id_key
        )
        gtx["all_docs_from_collection"] = all_docs_from_collection
        gtx["float"] = float
        gtx["str"] = str
        gtx["zip"] = zip

    def latex(self):
        """Render latex template"""
        rc = self.rc
        gtx = self.gtx
        if rc.people:
            people = [fuzzy_retrieval(gtx["people"], ["aka", "_id", "name"], rc.people[0])]
        else:
            people = gtx["people"]

        for p in people:
            # so we don't modify the dbs when de-referencing
            names = frozenset(p.get("aka", []) + [p["name"]] + [p["_id"]])
            begin_period = date(1650, 1, 1)

            pubs = filter_publications(
                all_docs_from_collection(rc.client, "citations"),
                names,
                reverse=True,
            )
            bibfile = make_bibtex_file(
                pubs, pid=p["_id"], person_dir=self.bldir
            )
            emps = p.get("employment", [])
            emps = [em for em in emps
                    if not em.get("not_in_cv", False)]
            for e in emps:
                e['position'] = e.get('position_full', e.get('position').title())
            emps.sort(key=ene_date_key, reverse=True)
            edu = p.get("education", [])
            edu.sort(key=ene_date_key, reverse=True)
            teach = p.get("teaching", [])
            for t in teach:
                t['position'] = t.get('position').title()

            projs = filter_projects(
                all_docs_from_collection(rc.client, "projects"), names
            )
            for proj in projs:
                for member in proj.get('team', []):
                    member.get('role', '').replace('pi', 'PI')
                    member['role'] = member.get('role', "").title()
            just_grants = list(all_docs_from_collection(rc.client, "grants"))
            just_proposals = list(all_docs_from_collection(rc.client, "proposals"))
            grants = merge_collections_superior(just_proposals,
                                                just_grants,
                                                "proposal_id")
            presentations = filter_presentations(self.gtx["people"],
                               self.gtx["presentations"],
                               self.gtx["institutions"],
                               p.get("_id"),
                               statuses=["accepted"])

            for grant in grants:
                for member in grant.get("team"):
                    dereference_institution(member, self.gtx["institutions"])

            pi_grants, pi_amount, _ = filter_grants(grants, names, pi=True)
            coi_grants, coi_amount, coi_sub_amount = filter_grants(
                grants, names, pi=False
            )
            for grant in coi_grants:
                format_pi = grant['me'].get('position', '').replace('copi', 'Co-PI')
                format_role = format_pi.replace('pi', 'PI')
                grant['me']['position'] = format_role

            aghs = awards_grants_honors(p, "honors")
            service = awards_grants_honors(p, "service", funding=False)
            # TODO: pull this out so we can use it everywhere
            for ee in [emps, edu]:
                for e in ee:
                    dereference_institution(e, self.gtx["institutions"])

            undergrads = filter_employment_for_advisees(self.gtx["people"],
                                                        begin_period,
                                                        "undergrad", p["_id"])
            for undergrad in undergrads:
                undergrad['role'] = undergrad['role'].title()
            masters = filter_employment_for_advisees(self.gtx["people"],
                                                     begin_period,
                                                     "ms", p["_id"])
            for master in masters:
                master['role'] = master['role'].title()
            currents = filter_employment_for_advisees(self.gtx["people"],
                                                      begin_period,
                                                      "phd", p["_id"])
            graduateds = filter_employment_for_advisees(self.gtx["people"],
                                                        begin_period,
                                                        "phd", p["_id"])
            postdocs = filter_employment_for_advisees(self.gtx["people"],
                                                      begin_period,
                                                      "postdoc", p["_id"])
            postdocs = remove_duplicate_docs(postdocs, "name")
            visitors = filter_employment_for_advisees(self.gtx["people"],
                                                      begin_period,
                                                      "visitor-unsupported",
                                                      p["_id"])
            visitors = remove_duplicate_docs(visitors, "name")
            for visitor in visitors:
                visitor['role'] = visitor['role'].title()

            iter = deepcopy(graduateds)
            for g in iter:
                if g.get("active"):
                    graduateds.remove(g)
            iter = deepcopy(currents)
            for g in iter:
                if not g.get("active"):
                    currents.remove(g)
            #############
            # hindex
            #############
            if p.get("hindex"):
                hindex = sorted(p.get("hindex"), key=doc_date_key).pop()
            else:
                hindex = {}

            self.render(
                "cv.tex",
                p["_id"] + ".tex",
                p=p,
                title=p.get("name", ""),
                aghs=aghs,
                hindex=hindex,
                service=service,
                undergrads=undergrads,
                masters=masters,
                currentphds=currents,
                graduatedphds=graduateds,
                postdocs=postdocs,
                visitors=visitors,
                pubs=pubs,
                names=names,
                bibfile=bibfile,
                education=edu,
                employment=emps,
                presentations=presentations,
                sentencecase=sentencecase,
                monthstyle=month_fullnames,
                projects=projs,
                pi_grants=pi_grants,
                pi_amount=pi_amount,
                coi_grants=coi_grants,
                coi_amount=coi_amount,
                coi_sub_amount=coi_sub_amount,
            )
            self.pdf(p["_id"])


"""
Base module and utils for the mean curvature based-equation learning problem
"""

import torch
from torch.utils.data import DataLoader

from nnpf.utils import get_default_args
from nnpf.problems import Problem
from nnpf.domain import Domain
from nnpf.functional import norm
import nnpf.shapes as shapes
from .datasets import MCSphereDataset, MCSphereLazyDataset
from .utils import check_sphere_mass

__all__ = [
    "MeanCurvatureProblem",
]

class MeanCurvatureProblem(Problem):
    """
    Base class for the mean curvature problem

    Features the train and validation data (MCSphereDataset), and the metric.

    Parameters
    ----------
    bounds: iterable of pairs of float
        Bounds of the domain
    N: int or iterable of int
        Number of discretization points
    epsilon: float
        Interface sharpness in phase field model
    dt: float
        Time step. If None, set to the maximum time step that guarantee the scheme stability.
    batch_size: int
        Size of the batch during training and validation steps. Full data if None.
    batch_shuffle: bool
        Shuffle batch content.
    lr: float
        Learning rate of the optimizer
    loss_norms: list of pair (p, weight)
        Compose loss as sum of weight * (output - target).norm(p).pow(e).
        Default to l2 norm.
        Exponent e is defined with loss_power parameter.
    loss_power: float
        Power applied to each loss term (for regularization purpose).
    radius: list of 2 floats
        Bounds on sphere radius (ratio of domain bounds) used for training and validation datasets.
    lp: int or float
        Power of the lp-norm used to defined the spheres in training and validation datasets.
    train_N, val_N: int
        Size of the training and validation datasets
    train_steps, val_steps: int
        Number of evolution steps applied to each input
    train_reverse, val_reverse: float
        Probability of having a sample with reversed inside and outside
    kwargs: dict
        Parameters passed to Problem (see doc)
    """

    def __init__(self, bounds=[[0., 1.], [0., 1.]], N=256, epsilon=2/256, dt=None,
                 batch_size=10, batch_shuffle=True, lr=1e-4,
                 loss_norms=None, loss_power=2.,
                 radius=[0.05, 0.45], lp=2,
                 train_N=100, train_steps=1, train_reverse=0.,
                 val_N=200, val_steps=5, val_reverse=0.,
                 use_lazy_datasets=False, num_workers=0,
                 **kwargs):

        super().__init__(**kwargs)

        # Default values
        dt = dt or self.stability_dt(epsilon)
        loss_norms = loss_norms or [[2, 1.]]

        # Hyper-parameters (used for saving/loading the module)
        self.save_hyperparameters(
            'bounds', 'N', 'dt', 'epsilon',
            'batch_size', 'batch_shuffle', 'lr', 'loss_norms', 'loss_power',
            'radius', 'lp', 'train_N', 'val_N', 'train_steps', 'val_steps',
            'train_reverse', 'val_reverse', 'use_lazy_datasets',
            'num_workers',
        )

    @staticmethod
    def stability_dt(epsilon):
        """ Maximum allowed time step that guarantee stability of the scheme """
        ...

    @staticmethod
    def profil(dist, epsilon):
        """ Solution profil from distance field """
        ...

    @staticmethod
    def iprofil(dist, epsilon):
        """ Distance field from solution profil """
        ...

    @staticmethod
    def sphere_radius(r0, t):
        """ Time evolution of the radius of the sphere """
        ...

    def shape(self, radius, center, p=2):
        """ Reference shape (sphere by design or any shape with same signature) """
        return shapes.sphere(radius, center, p)

    @property
    def domain(self):
        return Domain(self.hparams.bounds, self.hparams.N, device=self.device)

    @property
    def example_input_array(self):
        """ Example of input (for graph generation) """
        return torch.rand(self.hparams.batch_size or self.hparams.train_N, 1, *self.domain.N)

    def loss(self, output, target):
        """ Default loss function """
        dim = tuple(range(2, 2 + self.domain.dim))
        error = target - output
        return sum(
            w * norm(error, p, dim).pow(self.hparams.loss_power)
            for p, w in self.hparams.loss_norms).mean() / torch.tensor(self.domain.N).prod()

        # Below, loss with rescale depending on the solution norm (i.e. the circle size)
        # The idea was to compensate the less importance of small circle in the final loss
        # Need a lit more work on the scaling to work good
        """
        return sum(
            w * (norm(error, p, dim) / norm(target, p, dim)).pow(self.hparams.loss_power)
            for p, w in self.hparams.loss_norms).mean()
        """

    def check_sphere_mass(self, radius=0.45, num_steps=100, center=None, progress_bar=False):
        """
        Check a mean curvature model by measuring the decreasing of the mass of the profil associated to a sphere.

        Parameters
        ----------
        radius: float or list of float
            List of sphere radius (ratio of domain diameter) used as initial condition.
        num_steps: int or list of int
            Number of time steps (possibly per radius)
        center: list of float
            Sphere center (domain center if None)
        progress_bar: bool
            True to display a progress bar

        Returns
        -------
        model_mass: torch.tensor
            Sphere mass evolution for the given model
        exact_mass: torch.tensor
            Sphere mass evolution of the solution
        """

        def as_list(v):
            try: iter(v)
            except TypeError: return [v]
            else: return list(v)

        radius = as_list(radius)
        num_steps = as_list(num_steps)
        num_steps += [num_steps[-1]] * (len(num_steps) - len(radius))

        domain_diameter = min(b[1] - b[0] for b in self.domain.bounds)
        model_mass, exact_mass = [], []

        for r, n in zip(radius, num_steps):
            r = r * domain_diameter

            with torch.no_grad():
                mm, em = check_sphere_mass(
                    self.sphere_radius,
                    self.profil,
                    self, self.domain, r, self.hparams.epsilon, self.hparams.dt,
                    n, center, p=self.hparams.lp, progress_bar=progress_bar,
                    shape=self.shape)
                model_mass.append(mm)
                exact_mass.append(em)

        return torch.cat(model_mass), torch.cat(exact_mass)

    def training_step(self, batch, batch_idx):
        """ Default training step with custom loss function """
        data, *targets = batch
        loss = data.new_zeros([])
        for target in targets:
            data = self.forward(data)
            loss += self.loss(data, target)
        loss /= len(targets)

        return self.dispatch_metrics({'loss': loss})

    def validation_step(self, batch, batch_idx):
        """ Called at each batch of the validation data """
        data, *targets = batch
        loss = data.new_zeros([])
        for target in targets:
            data = self(data)
            loss += self.loss(data, target)
        loss /= len(targets)

        return {'val_loss': loss}

    def validation_epoch_end(self, outputs):
        """ Called at epoch end of the validation step (after all batches) """
        avg_loss = torch.stack([x['val_loss'] for x in outputs]).mean()

        # Metric calculation
        model_mass, exact_mass = self.check_sphere_mass()
        mass_error = ((model_mass - exact_mass) / exact_mass).norm() / model_mass.numel()

        self.dispatch_metrics({'val_loss': avg_loss, 'metric': mass_error})

    def configure_optimizers(self):
        """ Default optimizer """
        return torch.optim.Adam(self.parameters(), lr=self.hparams.lr)

    def prepare_data(self):
        """ Prepare training and validation data """

        # Additionnal dimensions for appropriate broadcasting
        sup_dims = (1,) + self.domain.dim * (1,)

        # Shape properties
        domain_diameter = min(b[1] - b[0] for b in self.domain.bounds)
        radius = [domain_diameter * r for r in self.hparams.radius]
        center = [0.5 * sum(b) for b in self.domain.bounds]

        # Datasets
        Dataset = MCSphereLazyDataset if self.hparams.use_lazy_datasets else MCSphereDataset
        def generate_data(num_samples, steps, reverse):
            return Dataset(
                self.sphere_radius,
                self.profil,
                self.domain.X,
                radius=torch.linspace(radius[0], radius[1], num_samples),
                center=center,
                epsilon=self.hparams.epsilon,
                dt=self.hparams.dt,
                lp=self.hparams.lp,
                steps=steps,
                reverse=torch.rand(num_samples) < reverse,
                shape=self.shape,
            )

        self.train_dataset = generate_data(self.hparams.train_N, self.hparams.train_steps, self.hparams.train_reverse)
        self.val_dataset = generate_data(self.hparams.val_N, self.hparams.val_steps, self.hparams.val_reverse)

    def train_dataloader(self):
        """ Returns the training data loader """
        return DataLoader(
            self.train_dataset,
            batch_size=self.hparams.batch_size or len(self.train_dataset),
            shuffle=self.hparams.batch_shuffle,
            num_workers=self.hparams.num_workers
        )

    def val_dataloader(self):
        """ Returns the validation data loader """
        return DataLoader(
            self.val_dataset,
            batch_size=self.hparams.batch_size or len(self.val_dataset),
            num_workers=self.hparams.num_workers
        )

    @staticmethod
    def add_model_specific_args(parent_parser, defaults={}):

        # Parser for the domain bounds
        import re
        def bounds_parser(s):
            bounds = []
            per_dim = s.split('x')
            for dim_spec in per_dim:
                match = re.fullmatch(r'\s*\[([^\]]*)\]\s*', dim_spec)
                if not match:
                    raise ValueError(f"Invalid bound specification {dim_spec}")
                bounds.append([float(b) for b in match.group(1).split(',')])
            return bounds

        # Parser for loss definition
        def float_or_str(v):
            try:
                return float(v)
            except ValueError:
                return v

        # Parser for lp norm
        def int_or_float(v):
            try:
                return int(v)
            except ValueError:
                return float(v)

        from distutils.util import strtobool

        parser = Problem.add_model_specific_args(parent_parser, defaults)
        group = parser.add_argument_group("Mean curvature problem", "Options common to all models of mean curvature based equation.")
        group.add_argument('--bounds', type=bounds_parser, help="Domain bounds in format like '[0, 1]x[1, 2.5]'")
        group.add_argument('--N', type=int, nargs='+', help="Domain discretization")
        group.add_argument('--epsilon', type=float, help="Interface sharpness")
        group.add_argument('--dt', type=float, help="Time step (epsilon**2 if None)")
        group.add_argument('--train_N', type=int, help="Number of initial conditions in the training dataset")
        group.add_argument('--val_N', type=int, help="Number of initial conditions in the validation dataset")
        group.add_argument('--train_steps', type=int, help="Number of evolution steps in the training dataset")
        group.add_argument('--val_steps', type=int, help="Number of evolution steps in the validation dataset")
        group.add_argument('--train_reverse', type=float, help="Probability of having a sample with reversed inside and outside in training dataset")
        group.add_argument('--val_reverse', type=float, help="Probability of having a sample with reversed inside and outside in validation dataset")
        group.add_argument('--radius', type=float, nargs=2, help="Bounds on sphere radius (ratio of domain bounds) used for training and validation dataset.")
        group.add_argument('--lp', type=int_or_float, help="Power of the lp-norm used to define the spheres for training and validation dataset.")
        group.add_argument('--batch_size', type=int, help="Size of batch")
        group.add_argument('--batch_shuffle', type=lambda v: bool(strtobool(v)), nargs='?', const=True, help="Shuffle batch")
        group.add_argument('--lr', type=float, help="Learning rate")
        group.add_argument('--loss_norms', type=float_or_str, nargs=2, action='append', help="List of (p, weight). Compose loss as sum of weight * (output - target).norm(p).pow(e). Default to l2 norm. Exponent e is defined with loss_power parameter.")
        group.add_argument('--loss_power', type=float, help="Power applied to each loss term (for regularization purpose)")
        group.add_argument('--use_lazy_datasets', type=lambda v: bool(strtobool(v)), nargs='?', const=True, help="Use lazy-evaluation datasets")
        group.add_argument('--num_workers', type=int, help="Number of subprocesses used for data loading")
        group.set_defaults(**{**get_default_args(MeanCurvatureProblem), **defaults})

        return parser


import dash
import plotly.express as px
import plotly.graph_objects as go

from dash import dcc, html, Input, Output
from data import *

import os
from dotenv import load_dotenv

load_dotenv()

MAPBOX_ACCESS_TOKEN = os.getenv('MAPBOX_ACCESS_TOKEN')

# -----------------------------------------------------------------------------------------------------------------------
#MAPBOX_ACCESS_TOKEN: str = open('mapbox_token').read()
MAPBOX_STYLE = 'mapbox://styles/plotlymapbox/cjyivwt3i014a1dpejm5r7dwr'

countries = ['Austria', 'Belgium', 'Bulgaria', 'Croatia', 'Cyprus', 'Czech Republic', 'Denmark', 'Estonia', 'Finland',
             'France', 'Germany', 'Greece', 'Hungary', 'Iceland', 'Ireland', 'Italy', 'Latvia', 'Lithuania',
             'Luxembourg', 'Malta', 'Netherlands', 'Norway', 'Poland', 'Portugal', 'Romania', 'Slovakia', 'Slovenia',
             'Spain', 'Sweden', 'Switzerland', 'United Kingdom']

sectors = ['All', 'National total for the entire territory', 'Energy production and distribution',
           'Energy use in industry', 'Road transport', 'Non-road transport', 'Commercial, institutional and households',
           'Industrial processes and product use', 'Agriculture', 'Waste', 'Other']

pollutants = ['All', 'CO', 'NH3', 'NMVOC', 'NOx', 'PM10', 'PM2.5', 'SOx', 'TSP']
years = [2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020]

models = ['Random Forest Regressor', 'Decision Tree Regressor', 'Linear Regression']
# -----------------------------------------------------------------------------------------------------------------------

app = dash.Dash(__name__, meta_tags=[{'name': 'viewport', 'content': 'width=device-width, initial-scale=1.0'}])

# This is for gunicorn
server = app.server
suppress_callback_exceptions = True

# Side panel

side_panel_layout = html.Div(
    id='panel-side',
    children=[
        html.P(id='country-dropdown-text', children=['Air Pollution', html.Br(), ' Dashboard']),
        html.Div(id='country-dropdown',
                 children=dcc.Dropdown(id='dropdown-component', options=countries, clearable=False, value='Germany')),
        html.Br(),
        html.Div(id='city-dropdown',
                 children=dcc.Dropdown(
                     id='city-dropdown-component',
                     options=[],
                     clearable=False,
                     value='Hamburg')),
        html.Br(),
        html.Div(id='sector-dropdown',
                 children=dcc.Dropdown(
                     id='sector-dropdown-component',
                     options=[],
                     clearable=False,
                     value='All')),
        html.Br(),
        html.Div(id='panel-side-text', children=[
            html.H1(id='country-name', children=''),
            html.P(className='country-description', id='country-description', children=[''])
        ]),
    ],
)

# Histogram

histogram = html.Div(
    id='histogram-container',
    children=[
        html.Div(
            id='histogram-header',
            children=[
                html.H1(id='histogram-title', children=['Gothenburg Protocol, LRTAP Convention']),
                html.H1(
                    id='histogram-dropdown-sector',
                    children=[dcc.Dropdown(
                        id='dropdown-component-sector',
                        options=sectors,
                        clearable=False,
                        value='All'
                    )]
                ),
                html.H1(
                    id='histogram-dropdown',
                    children=[dcc.Dropdown(
                        id='dropdown-component-second',
                        options=pollutants,
                        clearable=False,
                        value='All'
                    )]
                )
            ],
        ),
        dcc.Graph(id='histogram-graph', config={'displayModeBar': False}, animate=True),
    ],
)

# Map

map_graph = html.Div(
    id='world-map-wrapper',
    children=[dcc.Graph(id='world-map', config={'displayModeBar': False, 'scrollZoom': True})]
)
slider = html.H1(dcc.Slider(
    2007,
    2020,
    value=2007,
    step=None,
    marks={str(year): str(year) for year in years},
    id='year-slider'
))

@app.callback(Output('world-map', 'figure'),
              Output('city-dropdown-component', 'options'),
              Output('sector-dropdown-component', 'options'),
              Input('dropdown-component', 'value'),
              Input('city-dropdown-component', 'value'),
              Input('sector-dropdown-component', 'value'),
              Input('year-slider', 'value')
              )
def show_initial_elements(country, city, sector, year):
    df = country_df_map(country, city, sector)
    filtered_df = df[df.Year == year]

    figure = px.scatter_mapbox(filtered_df,
                               lat='Latitude', lon='Longitude', hover_name='Country',
                               hover_data=['eprtrSectorName', 'Emissions', 'EPRTRAnnexIMainActivityCode', 'City'],
                               color_discrete_sequence=['fuchsia'], zoom=5, height=655)

    figure.update_layout(mapbox_accesstoken=MAPBOX_ACCESS_TOKEN,
                         mapbox_style=MAPBOX_STYLE,
                         margin={'r': 0, 't': 0, 'l': 0, 'b': 0},
                         autosize=True,
                         paper_bgcolor='#1e1e1e',
                         plot_bgcolor='#1e1e1e')

    # figure = go.Figure(go.Scattermapbox(
    #     lat=df['Latitude'],
    #     lon=df['Longitude'],
    #     mode='markers',
    #     marker=go.scattermapbox.Marker(
    #         autocolorscale=False,
    #         showscale=True,
    #         size=filtered_df['Emissions'],
    #         opacity=0.8,
    #         color=filtered_df['Emissions'],
    #         colorscale='blues',
    #         colorbar=dict(
    #             title='',
    #             thickness=20,
    #             titleside='top',
    #             outlinecolor='rgba(68,68,68,0)',
    #             ticks='outside',
    #             ticklen=3)
    #     ),
    # ))
    #
    # figure.update_layout(
    #     autosize=True,
    #      height=655,
    #      plot_bgcolor='#1e1e1e',
    #     paper_bgcolor='#1e1e1e',
    #
    #      mapbox=dict(
    #         accesstoken=MAPBOX_ACCESS_TOKEN,
    #         style=MAPBOX_STYLE,
    #           zoom=5
    #     ),
    # )
    city_options = np.append('All', filtered_df['City'].unique())
    sector_options = np.append('All', filtered_df['eprtrSectorName'].unique())

    return figure, city_options, sector_options

#--------- predictions--------------------------------------------------------------------------------------------------

predictions = html.Div(
    id='predictions-container',
    children=[
        html.Div(
            id='predictions-header',
            children=[
                html.H1(id='predictions-title', children=['Predictions for Emissions']),
                html.H1(
                    id='predictions-dropdown-sector',
                    children=[dcc.Dropdown(
                        id='predictions-component-sector',
                        options=models,
                        clearable=False,
                        value='Decision Tree Regressor'
                    )]
                ),
                html.H1(
                    id='predictions-dropdown',
                    children=[dcc.Dropdown(
                        id='predictions-component-second',
                        options=pollutants,
                        clearable=False,
                        value='All'
                    )]
                )
            ],
        ),
        dcc.Graph(id='predictions-graph', config={'displayModeBar': False}, animate=True),
    ],
)

#--------- predictions--------------------------------------------------------------------------------------------------

predictions2 = html.Div(
    id='predictions-container2',
    children=[
        html.Div(
            id='predictions-header2',
            children=[
                html.H1(id='predictions-title2', children=['Predictions for Emissions']),
                html.H1(
                    id='predictions-dropdown-sector2',
                    children=[dcc.Dropdown(
                        id='predictions-component-sector2',
                        options=models,
                        clearable=False,
                        value='Decision Tree Regressor'
                    )]
                ),
                html.H1(
                    id='predictions-dropdown2',
                    children=[dcc.Dropdown(
                        id='predictions-component-second2',
                        options=years,
                        clearable=False,
                        value=2014
                    )]
                )
            ],
        ),
        dcc.Graph(id='predictions-graph2', config={'displayModeBar': False}, animate=True),
    ],
)

# Show histogram with predictions --------------------------------------------------------------------------------------

@app.callback(Output('predictions-graph', 'figure'),
              Input('city-dropdown-component', 'value'),
              Input('predictions-component-sector', 'value'))
def show_initial_elements(city, model):

    predictions_, expectations_ = get_predictions_with_model(city, model)

    fig1 = px.line(predictions_)
    fig2 = px.line(expectations_)

    fig1.update_traces(line_color='#fec036', line_width=2)
    fig2.update_traces(line_color='#FF00FF', line_width=2)

    fig3 = go.Figure(data=fig1.data + fig2.data,
                     layout=go.Layout(
                         margin={'t': 30, 'r': 35, 'b': 40, 'l': 50},
                         legend=dict(title=None, orientation='v', y=0.7, yanchor='bottom', x=1, xanchor='right'),
                         font=dict(color='gray'),
                         paper_bgcolor='#2b2b2b',
                         plot_bgcolor='#2b2b2b',
                         autosize=True,
                         bargap=0.1,
                         xaxis={'gridcolor': '#636363', 'showline': False},
                         yaxis={'showgrid': False, 'showline': False}
                     )
                     )
    return fig3

@app.callback(Output('predictions-graph2', 'figure'),
              Input('city-dropdown-component', 'value'),
              Input('predictions-component-second2', 'value'),
              Input('predictions-component-sector2', 'value'))
def show_initial_elements(city, year, model):

    y_pred, y_test = get_scatter(city, year, model)

    #fig1 = px.scatter(df, x="sepal_width", y="sepal_length", color='petal_length')
    fig = go.Figure()

    fig.add_trace(go.Scatter(x=np.arange(1, len(y_pred) + 1), y=y_pred,

                             name='ŷ'))
    fig.add_trace(go.Scatter(x=np.arange(1, len(y_test) + 1), y=y_test,

                             name='y'))

    fig.update_layout(margin={'t': 30, 'r': 35, 'b': 40, 'l': 50},
                       legend=dict(title=None, orientation='v', y=0.7, yanchor='bottom', x=1, xanchor='right'),
                      font=dict(color='gray'),
                      paper_bgcolor='#2b2b2b',
                      plot_bgcolor='#2b2b2b',
                      autosize=True,
                      bargap=0.1,
                      xaxis={'dtick': 5, 'gridcolor': '#636363', 'showline': False},
                      yaxis={'showgrid': False})
    return fig

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

# Show histogram related to the CLRTAP Dataset -------------------------------------------------------------------------

@app.callback(Output('histogram-graph', 'figure'),
              Input('dropdown-component', 'value'),
              Input('dropdown-component-second', 'value'),
              Input('dropdown-component-sector', 'value'))
def show_initial_elements(country, pollutant, sector):
    hist = px.bar(sector_emissions_per_country(country, pollutant, sector),
                  x='Year', y='Emissions', color='Sector_label_EEA', barmode='overlay',
                  hover_data=['Sector_name'],
                  height=750, color_discrete_sequence=px.colors.qualitative.Vivid)
    hist.update_layout(margin={'t': 30, 'r': 35, 'b': 40, 'l': 50},
                       legend=dict(title=None, orientation='v', y=0.7, yanchor='bottom', x=1, xanchor='right'),
                       font=dict(color='gray'),
                       paper_bgcolor='#2b2b2b',
                       plot_bgcolor='#2b2b2b',
                       autosize=True,
                       bargap=0.1,
                       xaxis={'dtick': 5, 'gridcolor': '#636363', 'showline': False},
                       yaxis={'showgrid': False},
                       height=650)
    return hist

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

# Control panel + map
main_panel_layout = html.Div(
    id='panel-upper-lower',
    children=[
        map_graph,
        slider,
        html.Div(
            id='panel',
            children=[
                histogram,
                html.Div(),
            ],
        ),
        html.H1(),
        html.Div(
            children=[
                predictions,
                html.Div(),
            ],
        ),
        html.H1(),
        html.Div(
            children=[
                predictions2,
                html.Div(id='panel-lower' ),
            ],
        ),
    ],
)

# Root
root_layout = html.Div(
    id='root',
    children=[
        dcc.Store(id='store-placeholder'),
        # For the case no components were clicked, we need to know what type of graph to preserve
        dcc.Store(id='store-data-config', data={'info_type': '', 'satellite_type': 0}),
        side_panel_layout,
        main_panel_layout,
    ],
)

app.layout = root_layout

@app.callback(
     Output('country-name', 'children'),
     [Input('dropdown-component', 'value')],
)
def update_name(val):
     return 'Predictions are in yellow'
#
#
# @app.callback(
#     Output('country-description', 'children'),
#     [Input('dropdown-component', 'value')],
# )
# def update_description(val):
#     text = ' '
#
#     if val == "France":
#         text = ( ' ' )
#
#     elif val == "Germany":
#         text = (  ' '  )
#     return text
#
if __name__ == "__main__":
    app.run_server(debug=True)


# -*- coding: utf-8 -*-

import arcade
import arcade.gui

from twisted.internet import reactor

from game import FONT, FONT_THIN
from game import app

class MenuView(arcade.View):

    def __init__(self):
        super().__init__()

        # --- Required for all code that uses UI element,
        # a UIManager to handle the UI.
        self.manager = arcade.gui.UIManager()

        # Create a vertical BoxGroup to align buttons
        self.v_box = arcade.gui.UIBoxLayout()

        ui_text_label = arcade.gui.UITextArea(text="Menu",
                                              width=450,
                                              height=100,
                                              font_size=50,
                                              font_name="Kenney Future")
        self.v_box.add(ui_text_label.with_space_around(bottom=20))

        # Create the buttons
        start_button = arcade.gui.UIFlatButton(text="Start Game", width=200, style={'font_name': FONT})
        self.v_box.add(start_button.with_space_around(bottom=20))

        settings_button = arcade.gui.UIFlatButton(text="Settings", width=200, style={'font_name': FONT})
        self.v_box.add(settings_button.with_space_around(bottom=20))

        quit_button = arcade.gui.UIFlatButton(text="Quit", width=200, style={'font_name': FONT})
        self.v_box.add(quit_button.with_space_around(bottom=20))

        @start_button.event("on_click")
        def on_click_start(event):
            print("Start:", event)
            self.window.set_state(app.ArcadeState.ready)

        @settings_button.event("on_click")
        def on_click_settings(event):
            print("Settings:", event)
            self.window.set_state(app.ArcadeState.settings)

        @quit_button.event("on_click")
        def on_click_quit(event):
            print("quit:", event)
            arcade.exit()
            reactor.callFromThread(reactor.stop)

        # Create a widget to hold the v_box widget, that will center the buttons
        self.manager.add(
            arcade.gui.UIAnchorWidget(
                anchor_x="center_x",
                anchor_y="center_y",
                child=self.v_box)
        )

    def on_show_view(self):
        self.manager.enable()
        arcade.set_background_color(arcade.csscolor.CHOCOLATE)
        arcade.set_viewport(0, self.window.width, 0, self.window.height)

    def on_hide_view(self):
        self.manager.disable()

    def on_draw(self):
        self.clear()
        self.manager.draw()


import torch.nn as nn
import torch

class ResBlock(nn.Module):
    def __init__(self, in_channel = 32, out_channel_1 = 128, out_channel_2 = 32, dilation = 1):
        super(ResBlock, self).__init__()
        # conv
        self.add_module("resblock_conv_1", nn.Conv2d(in_channel, out_channel_1, (3,3), dilation = dilation, padding='same'))
        self.add_module('resblock_act', nn.ReLU(inplace=True))
        self.add_module("resblock_conv_2", nn.Conv2d(out_channel_1, out_channel_2, (3,3), padding='same'))

    def forward(self, x):
        out = x
        for layer in self.children():
            out = layer(out)
        out += x
        return out

class DWAN_network(nn.Module):
    def __init__(self, img_channel=1):
        super(DWAN_network, self).__init__()
        self.in_channels = img_channel
        self.add_module("conv0", nn.Conv2d(self.in_channels, 32, (3,3), padding='same'))
        self.local_pathway = self.make_local_pathway()
        self.global_pathway = self.make_global_pathway()
        self.add_module("conv_final", nn.Conv2d(64, 1, (3,3), padding='same'))
        self.add_module("direct_conv",nn.Conv2d(self.in_channels,1,(3,3), padding = 'same'))

    def make_local_pathway(self):

        layers = []
        # 4 layers of resblock in local pathway
        for i in range(4):
            layers.append(ResBlock(in_channel = 32, out_channel_1 = 128, out_channel_2=32,dilation=1))
        return nn.Sequential(*layers)

    def make_global_pathway(self):
        layers = []
        for i in range(4):
            layers.append(ResBlock(in_channel = 32, out_channel_1 = 128, out_channel_2=32,dilation=2**i))
        return nn.Sequential(*layers)

    def forward(self, x):
        # for test purpose only
        out0 = self.direct_conv(x)
        x1 = self.conv0(x)
        # local_pathway

        local_out = self.local_pathway(x1)
        # global pathway
        global_out = self.global_pathway(x1)
        # concatenation
        x2 = torch.cat((local_out, global_out),1) # need to check the concatenation dimension
        # conv final
        out1 = self.conv_final(x2)
        out = out0 + out1

        return out

# test
'''
my_DWAN = DWAN_network(img_channel=3)
print(sum(p.numel() for p in my_DWAN.parameters()))
'''


import time

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager

chrome_opt = webdriver.ChromeOptions()
prefs = {"profile.default_content_Setting_values.notification":2}
chrome_opt.add_experimental_option("prefs",prefs)
chrome_opt.headless = False

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()),options=chrome_opt)
driver.get("https://www.espn.in/soccer/table/_/league/eng.1")
driver.implicitly_wait(2)
driver.maximize_window()
print(driver.title)
time.sleep(5)
r = driver.find_elements(By.XPATH,'//tbody/tr')
c= driver.find_elements(By.XPATH,'//tbody/tr[1]/td')
rows = len(r)
cols = len(c)
print(rows, ' & ', cols)

for row in range(1, rows):
    for col in range(1,cols):
        print(driver.find_element(By.XPATH,f'//tbody/tr[{row}]/td[{col}]').text, end=' ')
    print()

S = lambda X: driver.execute_script('return document.body.parentNode.scroll' +X)
driver.set_window_size(S('Width'),S('Height'))
driver.find_element(By.TAG_NAME,'body').screenshot('./screenshots/fullpage.png')
driver.close()