import os, re, time, sqlite3, json, logging

logger = logging.getLogger(__name__)

# Regexp implementation for sqlite
# Example code from https://stackoverflow.com/questions/5365451/problem-with-regexp-python-and-sqlite
def regexp(expr, item):
  if item:
    reg = re.compile(expr)
    if reg.search(item):
      return True
  return False

def bssid2int(bssid):
  if not bssid:
    return None
  return int(bssid.translate(str.maketrans('', '', ":.-, ")), 16)

class CredentialsStore():
  __instance = None
  credentialsStoreDB = None

  @staticmethod
  def getStore():
    if not CredentialsStore.__instance:
      CredentialsStore()
    return CredentialsStore.__instance

  def __init__(self):
    if CredentialsStore.__instance != None:
      raise Exception("This class is a singleton!")
    else:
      CredentialsStore.__instance = self

    logger.info("Starting Credentials Store")

    credentialsStoreDBFile = "/etc/WLANderlust/WLANderlust.credentials.db"

    if not os.path.isfile(credentialsStoreDBFile):
      logger.info("Creating Credentials Store database %s" % credentialsStoreDBFile)
    logger.info("Opening Credentials Store database %s" % credentialsStoreDBFile)

    try:
      self.credentialsStoreDB = sqlite3.connect(credentialsStoreDBFile, check_same_thread=False)
      self.credentialsStoreDB.create_function("REGEXP", 2, regexp)
      self.credentialsStoreDB.execute("CREATE TABLE IF NOT EXISTS credentialsStore (bssid INTEGER, ssid TEXT, domain TEXT, timestamp INTEGER NOT NULL, share BOOLEAN NOT NULL, credentials TEXT NOT NULL)")
      self.credentialsStoreDB.execute("CREATE UNIQUE INDEX IF NOT EXISTS bssid ON credentialsStore(bssid)")
      self.credentialsStoreDB.execute("CREATE UNIQUE INDEX IF NOT EXISTS ssid_domain ON credentialsStore(ifnull(ssid, 0), domain)")

      if logger.isEnabledFor(logging.DEBUG):
        cur = self.credentialsStoreDB.cursor()
        cur.execute("SELECT count(*) FROM credentialsStore")
        n = cur.fetchone()
        logger.debug("Nr of Credentials stored: %s" % n)
    except sqlite3.Error as e:
      logger.error(e)

    logger.info("Started Credentials Store")

  def __del__(self):
    logger.info("Stopping Credentials Store")

    if self.credentialsStoreDB:
      try:
        if logger.isEnabledFor(logging.DEBUG):
          cur = self.credentialsStoreDB.cursor()
          cur.execute("SELECT count(*) FROM credentialsStore")
          n = cur.fetchone()
          logger.debug("Nr of Credentials stored: %s" % n)

        logger.info("Closing Credentials Store database")
        self.credentialsStoreDB.close()
      except sqlite3.Error as e:
        logger.error(e)

    logger.info("Stopped Credentials Store")

  def stop(self):
    ''' '''

  def getCredentials(self, bssid, ssid, domain):
    credentials = None

    if not self.credentialsStoreDB:
      logger.error("Credentials Store not available")
      return None
    if not bssid and not ssid and not domain:
      logger.error("At least one of BSSID, SSID or domain needs to be given")
      return None
    if bssid and (ssid or domain):
      logger.error("if BSSID is given no SSID or domain can be given")
      return None

    logger.debug("Getting credentials for ('%s', '%s', '%s')" % (bssid, ssid, domain))

    if bssid:
      try:
        cur = self.credentialsStoreDB.cursor()
        cur.execute("SELECT credentials FROM credentialsStore WHERE bssid = ? AND ssid IS NULL AND domain IS NULL ORDER BY timestamp", (bssid2int(bssid),))
        credentials = cur.fetchone()
        logger.debug("Found credentials: %s" % credentials)
      except sqlite3.Error as e:
        logger.error(e)

    if ssid or domain:
      try:
        cur = self.credentialsStoreDB.cursor()
        cur.execute("SELECT credentials FROM credentialsStore WHERE bssid IS NULL AND (ssid = ? OR domain = ?) ORDER BY timestamp", (ssid, domain))
        credentials = cur.fetchone()
        logger.debug("Found credentials: %s" % credentials)
      except sqlite3.Error as e:
        logger.error(e)

    if not credentials:
      logger.info("No credentials found")
      return None

    credentials = json.loads(credentials[0])
    logger.debug("Found credentials: %s" % credentials)

    return credentials

  def setCredentials(self, bssid, ssid, domain, share, credentials):
    if not credentials:
      logger.error("No credentials given")
      return False

    if not self.credentialsStoreDB:
      logger.error("Credentials Store not available")
      return False
    if not bssid and not ssid and not domain:
      logger.error("At least one of BSSID, SSID or domain needs to be given")
      return False
    if bssid and (ssid or domain):
      logger.error("If BSSID is given no SSID or domain can be given")
      return False

    logger.debug("Adding credentials for ('%s', '%s', '%s')" % (bssid, ssid, domain))
    logger.debug("Credentials: '%s'" % credentials)
    logger.debug("Share: '%s'" % share)

    try:
      cur = self.credentialsStoreDB.cursor()
      cur.execute("REPLACE INTO credentialsStore(bssid, ssid, domain, timestamp, share, credentials) values(?, ?, ?, ?, ?, ?)",
        (bssid2int(bssid), ssid, domain, time.time(), share, json.dumps(credentials)))
      self.credentialsStoreDB.commit()
      logger.debug("Credentials added")
    except sqlite3.Error as e:
      logger.error(e)

  def deleteCredentials(self, bssid, ssid, domain):
    if not self.credentialsStoreDB:
      logger.error("Credentials Store not available")
      return False
    if not bssid and not ssid and not domain:
      logger.error("At least one of BSSID, SSID or domain needs to be given")
      return False
    if bssid and (ssid or domain):
      logger.error("if BSSID is given no SSID or domain can be given")
      return False

    logger.debug("Deleting credentials for ('%s', '%s', '%s')" % (bssid, ssid, domain))

    try:
      cur = self.credentialsStoreDB.cursor()
      cur.execute("DELETE FROM credentialsStore WHERE bssid IS ? AND ssid IS ? AND domain IS ?", (bssid2int(bssid), ssid, domain))
      self.credentialsStoreDB.commit()
      logger.debug("Credentials deleted")
    except sqlite3.Error as e:
      logger.error(e)


import numpy as np
import cv2
import os
import datetime

class KNN_FM:

    def __init__(self, f1, kp1, f2, kp2, good):
        self.f1 = f1
        self.f2 = f2
        self.kp1 = kp1
        self.kp2 = kp2
        self.good = good

    def score(self):
        return len(self.good)

    def save_img(self, img_path, img_name):
        i = cv2.drawMatchesKnn(self.f1, self.kp1, \
                self.f2, self.kp2, self.good, None, flags=2)
        save_img(img_path, img_name +"_"+ str(self.score()) + ".png", i)

    def set_info(self, video_1, idx_1, video_2, idx_2):
        self.idx_1 = idx_1
        self.idx_2 = idx_2
        self.video_1 = video_1
        self.video_2 = video_2

    def p(self, s):
        s = s.split("/")
        s = "-".join(s[-2:])
        s = s.replace("/", "-")
        s = s.replace(".mp4", "")
        return s

    def get_title(self):
        return "_".join([
            self.p(self.video_1),
            self.p(self.video_2),
        ])

def save_img(img_path, img_name, image):
    """
    @param image: frame of video
    """
    cv2.imwrite(os.path.join(img_path, img_name), image)

def save_cmp_log(cmp_iter):
    i = 0
    for c in cmp_iter:
        if i == 0:
            o = open("result/"+c.get_title() + ".txt", "w")
        o.write("\t".join([
            str(c.idx_1),
            str(c.idx_2),
            str(c.score()),
        ]) + "\n")
        i += 1

    o.close()

def compare_video(video_1, video_2, cmp_func):
    idx_1 = -1

    cap_1 = cv2.VideoCapture(video_1)
    while(cap_1.isOpened()):
        idx_1 += 1
        idx_2 = 0
        ret_1, frame_1 = cap_1.read()

        if ret_1:
            cap_2 = cv2.VideoCapture(video_2)
            while(cap_2.isOpened()):
                idx_2 += 1
                ret_2, frame_2 = cap_2.read()

                if ret_2:
                    if (idx_1 % 30) == 0 and (idx_2 % 30) == 0:
                        knn_fm = cmp_func(frame_1, frame_2)
                        knn_fm.set_info(video_1, idx_1, video_2, idx_2)
                        yield(knn_fm)
                else:
                    break
        else:
            break

    cap_1.release()
    cap_2.release()

def compare_fm(frame_1, frame_2):
    sift = sift=cv2.xfeatures2d.SIFT_create()
    kp1, des1 = sift.detectAndCompute(frame_1, None)
    kp2, des2 = sift.detectAndCompute(frame_2, None)

    bf = cv2.BFMatcher()
    matches = bf.knnMatch(des1, des2, k=2)

    good = []
    for m,n in matches:
        if m.distance < 0.75 * n.distance:
            good.append([m])

    return KNN_FM(frame_1, kp1, frame_2, kp2, good)

def camp(c, cv):
    return "video/camp/" + c + "/" + cv

def news(n, nv):
    return "video/news/" + n + "/" + nv

if __name__ == "__main__":
    camp_list = ["ssj"]
    news_list = ["mbc", "sbs", "kbs"]

    now = str(datetime.datetime.now())
    err = open("error_{}.txt".format(now), "w")
    acc = open("access_{}.txt".format(now), "w")

    for c in camp_list:
        for cv in os.listdir("video/camp/" + c):
            for n in news_list:
                for nv in os.listdir("video/news/" + n):
                    try:
                        if cv.startswith(".") or nv.startswith("."):
                            continue

                        log = "\t".join([
                            cv,
                            nv,
                        ]) + "\n"
                        acc.write(log)
                        print(log)
                        cmp_iter = compare_video(camp(c, cv), news(n, nv), compare_fm)
                        save_cmp_log(cmp_iter)
                    except:
                        err.write("\t".join([
                            cv,
                            nv,
                        ]) + "\n")
    err.close()
    acc.close()


#file where urls will be mapped to View functions
from django.urls import path
from . import views

# URLConfiguration module (every app in your project needs this)
# This needs to be imported into the main URLConfig of the project
urlpatterns = [
    path('', views.loadNsearchFunc, name="loadNsearchFunc"),
    path('save', views.saveFunc, name="saveFunc"),
    path('delete', views.deleteFunc, name="deleteFunc"),
    path('regauthor', views.regAuthorFunc, name="regAuthorFunc"),

]


# -*- coding: utf-8 -*-
"""
Created on Fri Nov 16 12:01:07 2018

@author: Alexandre
"""

###############################################################################
import numpy as np
import matplotlib.pyplot as plt
###############################################################################
from pyro.dynamic import vehicle
###############################################################################
import advanced_vehicles
import test_vehicle_controllers
###############################################################################

# "Fake" controller - Varying inputs (delta, T_f, T_r) throughout time (change in linear.py)
ctl = test_vehicle_controllers.kinematicInputs()
# Vehicule dynamical system
sys = vehicle.KinematicBicyleModel()

# Set default wheel velocity and steering angle
cl_sys = ctl+sys

# Plot open-loop behavior (ex: np.array[intial_conditions], time_of_simulation)
cl_sys.x0 = np.array([0,0,0])
cl_sys.plot_trajectory()

# Rebuild x,u and t from simulation
x = cl_sys.traj.x
u = cl_sys.traj.u
t = cl_sys.traj.t

# Plot the vehicle's trajectory
figsize   = (7, 4)
dpi       = 100
plt.figure(2,figsize=figsize, dpi=dpi)
plt.axes(xlim=(-20,150), ylim=(-5,200))
plt.plot(x[:,0],x[:,1],'--g', label = 'Kinematic Model')
plt.legend(fontsize ='15')
plt.legend(fontsize ='15')
plt.title("Vehicle's cartesian position", fontsize=20)
plt.xlabel('X position (m)')
plt.ylabel('Y position (m)')
plt.show()

# Animate the simulation
cl_sys.animate_simulation()


import json
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple

import requests

def query_sheet(
    document_id: str,
    sheet_name: str,
    tq: str = "SELECT *",
    cell_range: Optional[str] = None,
) -> Tuple[List[str], Iterable[Iterable[Any]]]:

    headers = {"X-DataSource-Auth": ""}
    params = {"tqx": "out:json", "tq": tq, "sheet": sheet_name}
    if cell_range is not None:
        params["range"] = cell_range
    data_resp = requests.get(
        f"https://docs.google.com/spreadsheets/d/{document_id}/gviz/tq",
        params=params,
        headers=headers,
    )

    sheet_data = json.loads(data_resp.text.split("\n", maxsplit=1)[1])
    cols = [c["label"] for c in sheet_data["table"]["cols"]]
    sheet_rows = sheet_data["table"]["rows"]

    return cols, ((v["v"] if v else None for v in record["c"]) for record in sheet_rows)

def query_sheet_tuples(*args: Any, **kwargs: Any) -> Iterator[Tuple[Any, ...]]:

    _, sheet_rows = query_sheet(*args, **kwargs)

    return (tuple(r) for r in sheet_rows)

def query_sheet_dicts(*args: Any, **kwargs: Any) -> Iterator[Dict[str, Any]]:

    cols, sheet_rows = query_sheet(*args, **kwargs)
    for record in sheet_rows:
        r_dict = dict(
            zip(
                cols,
                record,
            )
        )
        try:
            del r_dict[""]
        except KeyError:
            pass
        yield r_dict


#  type: ignore
import wrapt

@wrapt.when_imported("pydantic.schema")
def hook_pydantic_schema(schema):
    # This method is called by fastapi to get the schema of a model.
    # We need to replace it with a wrapper that will return the schema
    # of the assembled class instead of the base class to make the extendable
    # models work with fastapi.
    def _model_process_schema_wrapper(wrapped, instance, args, kwargs):
        model = args[0]
        if hasattr(model, "_get_assembled_cls"):
            model = model._get_assembled_cls()
            args = (model,) + args[1:]
            from pydantic import schema as schema_module

            # we must also extend the list ok know model name to the
            # assembled classes since fastapi by default restrict this list
            # to the classes defined in the service definition
            flat_models = schema_module.get_flat_models_from_models([model])
            model_name_map = schema_module.get_model_name_map(flat_models)
            kwargs["model_name_map"] = model_name_map
        return wrapped(*args, **kwargs)

    wrapt.wrap_function_wrapper(
        schema, "model_process_schema", _model_process_schema_wrapper
    )

@wrapt.when_imported("fastapi.routing")
def hook_fastapi_routing(routing):
    # This method is called by fastapi to serialize the response of a route.
    # We need to replace it with a wrapper that will use the extended class
    # instead of the base class to serialize the response.
    def _serialize_response_wrapper(wrapped, instance, args, kwargs):
        field = kwargs["field"]
        if hasattr(field, "_get_assembled_cls"):
            field = field._get_assembled_cls()
            kwargs["field"] = field
        return wrapped(**kwargs)

    wrapt.wrap_function_wrapper(
        routing, "serialize_response", _serialize_response_wrapper
    )

@wrapt.when_imported("fastapi.utils")
def hook_fastapi_utils(utils):
    # This method is called by fastapi to create a new field from an existing
    # field when the field is used as a response model. The motivation is to
    # ensure that the response model is not modified by the route handler.
    # We need to replace it with a wrapper that will use the extended class
    # as new field instead of a clone of the base class. Semantically, we
    # still ensure that the response model is not modified by the route handler.
    # since we enforce the use of the expected response model.
    def _create_cloned_field_wrapper(wrapped, instance, args, kwargs):
        field = args[0]
        if hasattr(field.type_, "_get_assembled_cls"):
            return field
        return wrapped(*args, **kwargs)

    wrapt.wrap_function_wrapper(
        utils, "create_cloned_field", _create_cloned_field_wrapper
    )


from __future__ import annotations
from pathpy.models.temporal_network import TemporalNetwork
from typing import Any, List, Union, Optional, Tuple
from functools import singledispatch
from collections import defaultdict, deque
import itertools as it
import functools as ft
from collections import Counter

from pathpy import logger, tqdm

from pathpy.core.api import NodeCollection, Node
from pathpy.core.api import EdgeCollection
from pathpy.core.api import PathCollection
from pathpy.models.classes import BaseTemporalNetwork
from pathpy.models.models import ABCDirectedAcyclicGraph

# create logger
LOG = logger(__name__)

def _remove_repetitions(path):
    """
    Remove repeated nodes in the path

    Parameters
    ----------
    path

    Returns
    -------

    Examples
    -------
    >>> remove_repetitions((1, 2, 2, 3, 4, 1))
    (1, 2, 3, 4, 1)
    >>> remove_repetitions((1, 2, 2, 2, 3)) == remove_repetitions((1, 2, 2, 3, 3))
    True
    """
    return tuple(p[0] for p in it.groupby(path))

def _expand_set_paths(set_path):
    """returns all possible paths which are consistent with the sequence of sets

    Parameters
    ----------
    set_path: list
        a list of sets or other iterable

    Examples
    -------
    >>> node_path = [{1, 2}, {2, 5}, {1, 2}]
    >>> list(expand_set_paths(node_path))
    [(1, 2, 1), (2, 2, 1), (1, 5, 1), (2, 5, 1), (1, 2, 2), (2, 2, 2), (1, 5, 2), (2, 5, 2)]
    >>> node_path = [{1, 2}, {5}, {2, 5}]
    >>> list(expand_set_paths(node_path))
    [(1, 5, 2), (2, 5, 2), (1, 5, 5), (2, 5, 5)]

    Yields
    ------
    tuple
        a possible path
    """
    # how many possible combinations are there
    node_sizes = [len(n) for n in set_path]
    num_possibilities = ft.reduce(lambda x, y: x * y, node_sizes, 1)

    # create a list of lists such that each iterator is repeated the number of times
    # his predecessors have completed their cycle
    all_periodics = []
    current_length = 1
    for node_set in set_path:
        periodic_num = []
        for num in node_set:
            periodic_num.extend([num] * current_length)
        current_length *= len(node_set)
        all_periodics.append(periodic_num)

    iterator = [it.cycle(periodic) for periodic in all_periodics]
    for i, elements in enumerate(zip(*iterator)):
        if i >= num_possibilities:
            break
        yield elements

def all_paths_from_dag(dag: ABCDirectedAcyclicGraph, node_mapping=None, max_subpath_length=None, repetitions=True, unique=False) -> PathCollection:
    """
    Calculates path statistics in a directed acyclic graph.
    All paths between all roots (nodes with zero indegree)
    and all leafs (nodes with zero outdegree) are generated.

    Parameters
    ----------
    dag: DAG
        the directed acyclic graph instance for which paths are calculated
    node_mapping: dict
        can be a simple mapping (1-to-1) or a 1-to-many (a dict with sets as values)
    max_subpath_length: int
        This can be used to limit the calculation of sub path statistics to a given
        maximum length. This is useful, as the statistics of sub paths of length k
        are only needed to fit a higher-order model with order k. Hence, if we know
        that the model selection is limited to a given maximum order K, we can safely
        set the maximum sub path length to K. By default, sub paths of any length
        will be calculated. Note that, independent of the sub path calculation
        longest path of any length will be considered in the likelihood calculation!
    repetitions: bool
        whether or not to remove repeated nodes in paths. Repeated nodes can occur
        if a non-injective node_mapping is applied. If set to True, a path a,a,b,b,c,c,d is
        returned as a,b,c,d.
    unique: bool
        whether or not multiple identical mapped paths should be counted separately. For
        DAG representations of temporal networks with delta > 1, where nodes are temporal copies,
        we do not want to count multiple paths from the same root that pass through different
        temporal copies of the same physical node. For instance with delta=2, time-stamped edges
        (a,b;1), (b,c;3) are transformed into a DAG a1->b2, a1->b3, b3->c4. With the mapping to
        physical nodes we would find two different paths a->b->c of length two, which only differ
        in terms of WHEN they arrive in node c

    Returns
    -------
    Paths

    """
    # Check whether we are doing a one-to-many mapping
    if node_mapping is None:
        node_mapping = { v.uid: v.uid for v in dag.nodes }

    if node_mapping:
        first_key = list(node_mapping.keys())[0]
        ONE_TO_MANY = isinstance(node_mapping[first_key], set)
    else:
        ONE_TO_MANY = False

    # Try to topologically sort the graph if not already sorted
    if dag.acyclic is None:
        dag.topological_sorting()
    if not dag.acyclic:
        LOG.error('Cannot extract statistics from a cyclic graph')
        raise ValueError
    else:
        # PathCollection that stores detected (projected) paths
        paths = PathCollection()

        LOG.info('Creating paths from directed acyclic graph')

        # construct all paths originating from root nodes for 1 to 1
        if not ONE_TO_MANY:
            for root in dag.roots:
                print('Processing root node ', root.uid)
                extracted_paths = dag.routes_from(root.uid, node_mapping)

                # if unique:
                #     extracted_paths = set(tuple(x) for x in extracted_paths)
                # for path in extracted_paths:   # add detected paths to paths object
                #     if repetitions:
                #         p = path
                #     else:
                #         p = _remove_repetitions(path)
                for p in extracted_paths:
                    if repetitions:
                        path = tuple([x for x in p])
                    else:
                        path = tuple(set([x for x in p]))
                    if unique:
                        paths.add(list(path), count=1, uid='-'.join(x.uid for x in path))
                    else:
                        paths.add(list(path), count=extracted_paths.counter[path], uid='-'.join([x.uid for x in path]))
        else:
            for root in dag.roots:
                for set_path in dag.routes_from_node(root.uid, node_mapping):
                    for blown_up_path in _expand_set_paths(set_path):
                        paths[blown_up_path] += 1

            # for path, count in path_counter.items():
            #     if repetitions:
            #         p = path
            #     else:
            #         p = _remove_repetitions(path)
            #     paths.add(p, count=count, uid='-'.join(p))

        #LOG.info('Expanding Subpaths')
        # p.expand_subpaths()
        #LOG.info('finished.')
        return paths

def all_paths_from_temporal_network(tempnet: TemporalNetwork, delta: int=1, max_subpath_length: int=-1) -> PathCollection:
    """
    Calculates the frequency of causal paths in a temporal network assuming a
    maximum temporal distance of delta between consecutive
    time-stamped links on a path. This method first creates a directed and acyclic
    time-unfolded graph based on the given parameter delta. This directed acyclic
    graph is used to calculate all time-respecting paths for a given delta.
    I.e., for time-stamped links (a,b,1), (b,c,5), (b,d,7) and delta = 5 the
    time-respecting path (a,b,c) will be found.

    Parameters
    ----------
    tempnet : pathpy.TemporalNetwork
        TemporalNetwork to extract the time-respecting paths from
    delta : int
        Indicates the maximum temporal distance up to which time-stamped
        links will be considered to contribute to a causal path.
        For (u,v;3) and (v,w;7) a causal path (u,v,w) is generated
        for 0 < delta <= 4, while no causal path is generated for
        delta > 4. Every time-stamped edge is a causal path of
        length one. Default value is 1.
    max_subpath_length : int
        Can be used to limit the calculation of sub path statistics to a given
        maximum length. This is useful as statistics of sub paths of length k
        are only needed to fit higher-order model with order k and larger. If model
        selection is limited to a maximum order K, we can set the maximum sub path length
        to K. Default is None, which means all subpaths are calculated.

    Returns
    -------
    Paths
        An instance of the class Paths, which can be used to generate higher- and multi-order
        models of causal paths in temporal networks.

    Examples
    ---------
    >>> t = pp.TemporalNetwork()
    >>> t.add_edge('a', 'b', 1)
    >>> t.add_edge('b', 'a', 3)
    >>> t.add_edge('b', 'c', 3)
    >>> t.add_edge('d', 'c', 4)
    >>> t.add_edge('c', 'd', 5)
    >>> t.add_edge('c', 'b', 6)

    >>> >>>causal_paths = pp.path_extraction.paths_from_temporal_network_dag(t, delta=2)
    >>> [Severity.INFO]	Constructing time-unfolded DAG ...
    >>> [Severity.INFO]	finished.
    >>> [Severity.INFO]	Generating causal trees for 2 root nodes ...
    >>> [Severity.INFO]	finished.
    >>> print(causal_paths)
    >>> Total path count: 		4.0
    >>> [Unique / Sub paths / Total]: 	[4.0 / 24.0 / 28.0]
    >>> Nodes:				    4
    >>> Edges:				    6
    >>> Max. path length:		3
    >>> Avg path length:		2.25
    >>> Paths of length k = 0		0.0 [ 0.0 / 13.0 / 13.0 ]
    >>> Paths of length k = 1		0.0 [ 0.0 / 9.0 / 9.0 ]
    >>> Paths of length k = 2		3.0 [ 3.0 / 2.0 / 5.0 ]
    >>> Paths of length k = 3		1.0 [ 1.0 / 0.0 / 1.0 ]

    >>> The calculated (longest) causal paths in this example are:
    >>> (a, b, c, d), (d, c, b), (d, c, d), (a, b, a)
    """
    from pathpy.models.directed_acyclic_graph import DirectedAcyclicGraph

    # generate a single time-unfolded DAG
    LOG.info('Constructing time-unfolded DAG ...')
    dag = DirectedAcyclicGraph.from_temporal_network(tempnet, delta)
    node_map = { v.uid: v['original'].uid for v in dag.nodes }
    LOG.info('finished.')

    # path statistics
    causal_paths = PathCollection()

    # For each root in the time-unfolded DAG, we generate a
    # causal tree and use it to count all causal paths
    # that originate at this root
    num_roots = len(dag.roots)
    current_root = 1

    LOG.info('Generating causal trees for {0} root nodes ...'.format(num_roots))
    for root in tqdm(dag.roots):

        # generate the causal tree from this root node
        causal_tree, causal_mapping = generate_causal_tree(dag, root, node_map)

        # output
        if num_roots > 10:
            step = num_roots/10
            if current_root % step == 0:
                LOG.info('Analyzing tree {0}/{1} ...'.format(current_root, num_roots))

        # calculate all unique longest path in the causal tree
        # TODO: replace by add operator
        paths = all_paths_from_dag(causal_tree, causal_mapping, repetitions=False, max_subpath_length=max_subpath_length)
        for p in paths:
            causal_paths.add(p, count=paths.counter[p.uid], uid=p.uid)
        current_root += 1

    LOG.info('finished.')

    return causal_paths

def generate_causal_tree(dag, root, node_map) -> Tuple(ABCDirectedAcyclicGraph, defaultdict):
    """
    For a directed acyclic graph and a non-injective mapping of nodes,
    this method creates a *causal tree* for a given root node.
    This is useful for the extraction of causal paths in time-unfolded DAG
    representations of temporal networks. The nodes "{v}_{d}" in the resulting
    causal tree capture that - starting from the root node at step 0 - there is
    a causal path to node v at distance d from the root. Note that the same node
    can be represented by multiple nodes in the causal tree (at different distances d).
    """
    from pathpy.models.directed_acyclic_graph import DirectedAcyclicGraph
    causal_tree = DirectedAcyclicGraph()

    causal_mapping = {}
    visited = defaultdict(lambda: False)
    queue = deque()

    # launch breadth-first-search at root of tree
    # root nodes are necessarily at depth 0
    queue.append((root.uid, 0))
    edges = []
    while queue:
        # take out left-most element from FIFO queue
        v, depth = queue.popleft()

        # x is the node ID of the node in the causal tree
        # the second component captures the distance from
        # the root of the causal tree. These IDs ensure
        # that the same physical nodes can occur at different
        # distances from the root
        x = '{0}_{1}'.format(node_map[v], depth)
        causal_mapping[x] = node_map[v]

        # process nodes at next level
        for w in dag.successors[v]:
            if (w.uid, depth+1) not in queue:
                queue.append((w.uid, depth+1))
                # only consider nodes that have not already
                # been added to this level
                if not visited[node_map[w.uid], depth+1]:
                    # add edge to causal tree
                    y = '{0}_{1}'.format(node_map[w.uid], depth+1)
                    edges.append((x, y))

                    visited[node_map[w.uid], depth+1] = True
                    causal_mapping[y] = node_map[w.uid]

    # Adding all edges at once is more efficient!
    for e in edges:
        causal_tree.add_edges(e)

    return causal_tree, causal_mapping

def PaCo(
    tn: BaseTemporalNetwork,
    delta: float,
    skip_first: int = 0,
    up_to_k: int = 10 ) -> PathCollection:
    """
    Path counting algorithm PaCo.
    Published at TempWeb 2021 workshop.
    in:
        tn : BaseTemporalNetwork,
            temporal network in which we count paths.

        delta : float
            maximal time difference that between two links that can form a path.

        skip_first = 0 : int,
            paths computed in the first `skip_first' temporal links are not counted towards the total path count. This feature is for parallel computing.

        up_to_k = 10 : int
            maximal lengt of paths that we count.
    """
    # all the entries that are at max distance delta away from the current entry
    delta_window = []

    path_collection = PathCollection()

    # current_path_stack[i][p] is the number of paths p that go from
    # current_edge of index i.
    current_path_stack = defaultdict(lambda: defaultdict(int))

    # for e, current_edge in enumerate(D):
    for e, edge in enumerate(tn.edges[:]):
        current_edge = (edge.v.uid, edge.w.uid, edge.start)
        # since we go in forward direction, delta window is back in time.
        # not every entry from delta window is important for the current
        # considered current_edge some are happening at the same time.

        # check if there is anything to update about delta window
        # and current stack
        if len(delta_window) > 0:
            # I throw away the ones in delta_window that are too far away
            # (in temporal sense)
            i = 0
            didnt_find_one_yet = True
            # since the data is sorted, I just need to find the one that
            # is inside the delta time window.
            # This while loop identifies the first entry saved in delta window,
            # that is inside the new delta window so it only loops through the
            # ones that are outside the new delta window.
            while i < len(delta_window) and didnt_find_one_yet:
                # i'th entry [i], then I need the entry,
                # and not the ennumerator [1],
                # and then I need time, which is the third column[2]
                if delta_window[i][1][2] >= current_edge[2]-delta:
                    didnt_find_one_yet = False
                else:
                    # i didn't find yet one which is inside the new delta window.
                    i += 1

            # keep all starting from the first that is inside the
            # new delta window.
            if i < len(delta_window):
                first_index_in_delta_window = delta_window[i][0]
                old_inx = [
                    e for (e, current_edge) in delta_window if e < first_index_in_delta_window]
                delta_window = delta_window[i:]
            else:
                # all from delta window should be removed.
                # asign current index
                first_index_in_delta_window = e
                old_inx = [
                    e for (e, current_edge) in delta_window if e < first_index_in_delta_window]
                delta_window = []
            # delta_window is now corresponding to the current_edge that I am
            # considering.
            # go through outdated indices
            for j in old_inx:
                # remove them from the current stack.
                del current_path_stack[j]
        # all stacks updated now.

        # compute for the next
        for enu, past_edge in delta_window:
            # if target of the link in delta_window is the same as the
            # source in considered entry
            if past_edge[1] == current_edge[0]:
                # the interactions should not happen at the same time!
                # the considered one should happen AFTER the one from the
                # delta window.
                if current_edge[2] > past_edge[2]:
                    # i know that the link happened in the appropriate time,
                    # and that the link continues the link I consider.
                    # so I can add the paths to the current_path_stack
                    for path in current_path_stack[enu]:
                        p = (*path, current_edge[1])
                        if len(p)-1 <= up_to_k:
                            current_path_stack[e][p] += current_path_stack[enu][path]

                            if e >= skip_first:
                                # path_collection[p] += current_path_stack[enu][path]
                                path_collection.add(p, uid='-'.join(p), count=current_path_stack[enu][path])
        current_path_stack[e][(current_edge[0], current_edge[1])] += 1

        if e >= skip_first:
            p = (current_edge[0], current_edge[1])
            path_collection.add(p[0], p[1], uid='-'.join(p), count=1)
            # path_collection[p] += 1

        # add this current_edge at the end of delta_window,
        # so that the next current_edge can have all the entries it needs.
        delta_window.append((e, current_edge))

    return path_collection


"""empty message

Revision ID: 55feb17be620
Revises: 1ce7e0de1096
Create Date: 2022-05-23 11:12:56.176505

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '55feb17be620'
down_revision = '1ce7e0de1096'
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('group_memberships', sa.Column('created_at', sa.DateTime(), nullable=False))
    op.add_column('groups', sa.Column('created_at', sa.DateTime(), nullable=False))
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('groups', 'created_at')
    op.drop_column('group_memberships', 'created_at')
    # ### end Alembic commands ###


# coding: utf-8
"""
Logic module handling communication with other components in a SampleDB federation
"""
import typing
import datetime
import io

import requests
import flask

from .utils import _get_dict, _get_list, _get_bool
from .users import import_user, parse_user
from .components import import_component_info, parse_component_info
from .location_types import import_location_type, parse_location_type
from .instruments import import_instrument, parse_instrument
from .action_types import import_action_type, parse_action_type
from .locations import import_location, parse_location, locations_check_for_cyclic_dependencies
from .markdown_images import parse_markdown_image, import_markdown_image
from .actions import import_action, parse_action
from .objects import import_object, parse_object
from ..components import Component, set_component_discoverable
from ..component_authentication import get_own_authentication
from .. import errors
from ...models import ComponentAuthenticationType

PROTOCOL_VERSION_MAJOR = 0
PROTOCOL_VERSION_MINOR = 1

FEDERATION_TIMEOUT = 60

def _send_request(
        method: typing.Union[typing.Literal['get'], typing.Literal['post'], typing.Literal['put']],
        endpoint: str,
        component: Component,
        headers: typing.Optional[typing.Dict[str, str]] = None,
        **kwargs: typing.Any
) -> requests.Response:
    if component.address is None:
        raise errors.MissingComponentAddressError()
    if headers is None:
        headers = {}
    auth = get_own_authentication(component.id, ComponentAuthenticationType.TOKEN)

    if auth:
        headers['Authorization'] = 'Bearer ' + auth.login['token']

    url = component.address.rstrip('/') + endpoint

    method_callable = {
        'get': requests.get,
        'post': requests.post,
        'put': requests.put,
    }[method]
    return method_callable(  # type: ignore
        url,
        headers=headers,
        timeout=FEDERATION_TIMEOUT,
        **kwargs
    )

def post(
        endpoint: str,
        component: Component,
        payload: typing.Optional[typing.Dict[str, typing.Any]] = None,
        headers: typing.Optional[typing.Dict[str, str]] = None
) -> None:
    _send_request('post', endpoint, component, headers, data=payload)

def put(
        endpoint: str,
        component: Component,
        headers: typing.Optional[typing.Dict[str, str]] = None,
        **kwargs: typing.Any
) -> None:
    _send_request('put', endpoint, component, headers, **kwargs)

def get_binary(
    endpoint: str,
    component: Component,
    headers: typing.Optional[typing.Dict[str, str]] = None
) -> typing.BinaryIO:
    response = _send_request('get', endpoint, component, headers)
    if response.status_code == 401:
        # 401 Unauthorized
        raise errors.UnauthorizedRequestError()
    if response.status_code in [500, 501, 502, 503, 504]:
        raise errors.RequestServerError()
    if response.status_code != 200:
        raise errors.RequestError()
    return io.BytesIO(response.content)

def get(
        endpoint: str,
        component: Component,
        headers: typing.Optional[typing.Dict[str, str]] = None,
        *,
        ignore_last_sync_time: bool = False
) -> typing.Dict[str, typing.Any]:
    if component.address is None:
        raise errors.MissingComponentAddressError()
    if headers is None:
        headers = {}
    auth = get_own_authentication(component.id, ComponentAuthenticationType.TOKEN)

    if auth:
        headers['Authorization'] = 'Bearer ' + auth.login['token']

    parameters = {}
    if component.last_sync_timestamp is not None and not ignore_last_sync_time:
        parameters['last_sync_timestamp'] = component.last_sync_timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')
    req = requests.get(
        component.address.rstrip('/') + endpoint,
        headers=headers,
        params=parameters,
        timeout=FEDERATION_TIMEOUT
    )
    if req.status_code == 401:
        # 401 Unauthorized
        raise errors.UnauthorizedRequestError()
    if req.status_code in [500, 501, 502, 503, 504]:
        raise errors.RequestServerError()
    if req.status_code != 200:
        raise errors.RequestError()
    try:
        return req.json()  # type: ignore
    except ValueError:
        raise errors.InvalidJSONError()

def update_poke_component(
        component: Component
) -> None:
    post('/federation/v1/hooks/update/', component)

def _validate_header(
        header: typing.Optional[typing.Dict[str, typing.Any]],
        component: Component
) -> None:
    if header is not None:
        if header.get('db_uuid') != component.uuid:
            raise errors.InvalidDataExportError(f'UUID of exporting database ({header.get("db_uuid")}) does not match expected UUID ({component.uuid}).')
        protocol_version = header.get('protocol_version')
        if protocol_version is not None:
            if 'major' not in protocol_version or 'minor' not in protocol_version:
                raise errors.InvalidDataExportError(f'Invalid protocol version \'{header.get("protocol_version")}\'')
            try:
                major, minor = int(protocol_version['major']), int(protocol_version['minor'])
                if major > PROTOCOL_VERSION_MAJOR or (major <= PROTOCOL_VERSION_MAJOR and minor > PROTOCOL_VERSION_MINOR):
                    raise errors.InvalidDataExportError(f'Unsupported protocol version {header.get("protocol_version")}')
            except ValueError:
                raise errors.InvalidDataExportError(f'Invalid protocol version {header.get("protocol_version")}')
        else:
            raise errors.InvalidDataExportError('Missing protocol_version.')

def import_updates(
        component: Component,
        *,
        ignore_last_sync_time: bool = False
) -> None:
    if flask.current_app.config['FEDERATION_UUID'] is None:
        raise errors.ComponentNotConfiguredForFederationError()
    timestamp = datetime.datetime.now(datetime.timezone.utc)
    components = None
    try:
        components = get('/federation/v1/shares/components/', component, ignore_last_sync_time=ignore_last_sync_time)
    except errors.InvalidJSONError:
        raise errors.InvalidDataExportError('Received an invalid JSON string.')
    except errors.RequestServerError:
        pass
    except errors.UnauthorizedRequestError:
        pass
    if components:
        update_components(component, components)
    users = None
    try:
        users = get('/federation/v1/shares/users/', component, ignore_last_sync_time=ignore_last_sync_time)
    except errors.InvalidJSONError:
        raise errors.InvalidDataExportError('Received an invalid JSON string.')
    except errors.RequestServerError:
        pass
    except errors.UnauthorizedRequestError:
        pass
    if users:
        update_users(component, users)
    try:
        updates = get('/federation/v1/shares/objects/', component, ignore_last_sync_time=ignore_last_sync_time)
    except errors.InvalidJSONError:
        raise errors.InvalidDataExportError('Received an invalid JSON string.')
    if updates:
        update_shares(component, updates)
    component.update_last_sync_timestamp(timestamp)

def update_components(
        component: Component,
        updates: typing.Dict[str, typing.Any]
) -> None:
    _get_dict(updates, mandatory=True)
    _validate_header(updates.get('header'), component)
    discoverable = _get_bool(updates.get('discoverable'), mandatory=True)
    set_component_discoverable(component.id, discoverable=discoverable)
    if discoverable:
        components = []
        component_data_list = _get_list(updates.get('components'), default=[])
        for component_data in component_data_list:
            components.append(parse_component_info(component_data, component))
        for component_data in components:
            if flask.current_app.config['FEDERATION_UUID'] in (component_data['uuid'], component_data['source_uuid']):
                # skip component info from or about this instance
                continue
            import_component_info(component_data, component)

def update_users(
        component: Component,
        updates: typing.Dict[str, typing.Any]
) -> None:
    _get_dict(updates, mandatory=True)
    _validate_header(updates.get('header'), component)

    users = []
    user_data_list = _get_list(updates.get('users'), default=[])
    for user_data in user_data_list:
        users.append(parse_user(user_data, component))
    for user_data in users:
        import_user(user_data, component)

def update_shares(
        component: Component,
        updates: typing.Dict[str, typing.Any]
) -> None:
    # parse and validate
    _get_dict(updates, mandatory=True)
    _validate_header(updates.get('header'), component)

    markdown_images = []
    markdown_images_data_dict = _get_dict(updates.get('markdown_images'), default={})
    for markdown_image_data in markdown_images_data_dict.items():
        markdown_images.append(parse_markdown_image(markdown_image_data, component))

    users = []
    user_data_list = _get_list(updates.get('users'), default=[])
    for user_data in user_data_list:
        users.append(parse_user(user_data, component))

    instruments = []
    instrument_data_list = _get_list(updates.get('instruments'), default=[])
    for instrument_data in instrument_data_list:
        instruments.append(parse_instrument(instrument_data))

    action_types = []
    action_type_data_list = _get_list(updates.get('action_types'), default=[])
    for action_type_data in action_type_data_list:
        action_types.append(parse_action_type(action_type_data))

    actions = []
    action_data_list = _get_list(updates.get('actions'), default=[])
    for action_data in action_data_list:
        actions.append(parse_action(action_data))

    location_types = []
    location_type_data_list = _get_list(updates.get('location_types'), default=[])
    for location_type_data in location_type_data_list:
        location_types.append(parse_location_type(location_type_data))

    locations = {}
    location_data_list = _get_list(updates.get('locations'), default=[])
    for location_data in location_data_list:
        location = parse_location(location_data)
        locations[(location['fed_id'], location['component_uuid'])] = location

    locations_check_for_cyclic_dependencies(locations)

    objects = []
    object_data_list = _get_list(updates.get('objects'), default=[])
    for object_data in object_data_list:
        objects.append(parse_object(object_data, component))

    # apply
    for markdown_image_data in markdown_images:
        import_markdown_image(markdown_image_data, component)

    for user_data in users:
        import_user(user_data, component)

    for instrument_data in instruments:
        import_instrument(instrument_data, component)

    for action_type_data in action_types:
        import_action_type(action_type_data, component)

    for action_data in actions:
        import_action(action_data, component)

    for location_type_data in location_types:
        import_location_type(location_type_data, component)

    while len(locations) > 0:
        key = list(locations)[0]
        import_location(locations[key], component, locations, users)
        if key in locations:
            locations.pop(key)

    import_status_by_object_id = {}
    for object_data in objects:
        import_status: typing.Dict[str, typing.Any] = {}
        import_object(object_data, component, import_status=import_status)
        import_status_by_object_id[object_data['fed_object_id']] = import_status

    if component.address:
        for object_id, import_status in import_status_by_object_id.items():
            if import_status:
                send_object_share_import_status(
                    object_id=object_id,
                    component=component,
                    import_status=import_status
                )

def send_object_share_import_status(
        object_id: int,
        component: Component,
        import_status: typing.Dict[str, typing.Any]
) -> None:
    put(
        endpoint=f'/federation/v1/shares/objects/{object_id}/import_status',
        component=component,
        json=import_status
    )


from fastapi import FastAPI, Body
from sqlalchemy.testing.suite.test_reflection import users

from auth_bearer import JWTBearer
app = FastAPI()
from models import  UserSchema, UserLoginSchema
from auth_handler import signJWT

@app.post("/user/signup", tags=["user"])
async def create_user(user: UserSchema = Body(...)):
    user.append(user)
    return signJWT(user.email)

def check_user(data: UserLoginSchema):
    for user in users:
        if user.email == data.email and user.password == data.password:
            return True
    return False

@app.post("/user/login", tags=["user"])
async def user_login(user: UserLoginSchema = Body(...)):
    if check_user(user):
        return signJWT(user.email)
    return {
        "error": "Wrong login details!"
    }


# 연산자 끼워넣기 210928
"""
입력
N 첫줄 수의 개수
둘째 줄에는 A1....An 의 배열이 주어진다
셋째 줄에는 합이 N-1인 정수 4개가 주어지는데 차례대로 +, -, *, /의 개수이다. (나눗셈은 몫만 취한다.)

샛째 줄의 연산자들을 활용해서 최댓값, 최솟값을 만들어서 출력하자

* 알고리즘, 주의사항
나눗셈의 경우 몫만 취한다고 했는데 음수의 경우도 생각해야 한다.
이로인해 // 연산자를 직접 사용하는것 보다 int(a/b) 함수를 사용하는게 더 낫다
"""
# 블로그에서 본 재귀 함수
from itertools import permutations
def cal(num, idx, add, sub, multi, division):
    global n, maxv, minv
    if idx == n:
        maxv = max(num, maxv)
        minv = min(num, minv)
        return
    else:
        if add:
            cal(num + num_list[idx], idx + 1, add-1, sub, multi, division)
        if sub:
            cal(num - num_list[idx], idx + 1, add, sub-1, multi, division)
        if multi:
            cal(num * num_list[idx], idx + 1, add, sub, multi-1, division)
        if division:
            cal(int(num/num_list[idx]), idx + 1, add, sub, multi, division-1)

if __name__ == "__main__":
    maxv = -float('inf')
    minv = float('inf')

    n = int(input())
    num_list = list(map(int, input().split()))
    add, sub, mul, div = map(int, input().split())
    cal(num_list[0], 1, add, sub, mul, div)
    print(maxv)
    print(minv)

# 답지에 있던 dfs 함수 (비슷한데 약간 달라서 복붙)
n = int(input())
data = list(map(int, input().split()))
add, sub, mul, div = map(int, input().split())

min_v = 1e9
max_v = -1e9

def dfs(i, now):
    global min_v, max_v, add, sub, mul, div
    if i == n:
        min_v = min(min_v, now)
        max_v = max(max_v, now)
    else:
        if add > 0:
            add -= 1
            dfs(i+1, now+data[i])
            add += 1
        if sub > 0:
            sub -= 1
            dfs(i+1, now - data[i])
            sub += 1
        if mul > 0:
            mul -= 1
            dfs(i+1, int(now * data[i]))
            mul += 1
        if div > 0:
            div -= 1
            dfs(i+1, int(now / data[i]))
            div += 1

dfs(1, data[0])

print(max_v)
print(min_v)

# permutation을 활용한 답지, 숏코딩
N = int(input())
A = list(map(int, input().split()))
a, s, m, d = map(int, input().split())

rs = list()
for p in set(permutations('+'*a+'-'*s+'*'*m+'/'*d)):
    r = A[0]
    for i in range(1, N):
        r = {'+': r+A[i], '-': r-A[i], '*': r*A[i], '/': int(r/A[i])}[p[i-1]]

    rs.append(r)

print(max(rs), min(rs))


import requests
from helpers import GenericImages as GI
from api import Champions as Champs
from helpers import UserID as UID

# Queue IDs --------------------------------------------------------------------------------------------

# Solo Ranked         : 420
# Flex Ranked         : 440

# Normal Blind        : 430
# Normal Draft        : 400

# ARAM                : 450

# Co-op [Intro]       : 830
# Co-op [Beginner]    : 840
# Co-op [Intermediate]: 850

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

def getLiveGameInfo(api, region, version, summonerID):
    endpoint = f"https://{region}.api.riotgames.com/lol/spectator/v4/active-games/by-summoner/{summonerID}"
    response = requests.get(endpoint, params={'api_key': api})

    if response.status_code != 200:
        print("The summoner is not currently in a live game.")
        return []

    data = response.json()

    players_info = []

    for player in data['participants']:
        player_data = {}

        player_data['PlayerName'] = player['summonerName']
        player_data['PlayerTeam'] = 'Blue' if player['teamId'] == 100 else 'Red'

        player_data['ChampionID'] = player['championId']
        champList = []
        champList.append(player_data['ChampionID'])
        champInfo = Champs.getChampsInfo(version, champList)
        player_data['ChampionName'] = champInfo[0]['ChampionName']
        player_data['ChampionImage'] = GI.getChampImage(version, player_data['ChampionName'])

        player_data['Keystone'] = GI.getRuneImage(version, player["perks"]["perkIds"][0])
        player_data['SecondaryRune'] = GI.getRuneImage(version, player["perks"]["perkSubStyle"])

        player_data['Summoner1'] = GI.getSummonerSpellImage(version, player['spell1Id'])
        player_data['Summoner2'] = GI.getSummonerSpellImage(version, player['spell2Id'])

        players_info.append(player_data)

    return players_info

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


import math
import random
import copy
import ast
from functools import reduce

import numpy as np
import cv2

def get_rotation_matrix(rotation_angles):

    rotation_angles = list(map(lambda x : np.deg2rad(x), rotation_angles))

    phi = rotation_angles[0] # around x
    gamma = rotation_angles[1] # around y
    theta = rotation_angles[2] # around z

    # X rotation matrix
    rot_matrix_phi = np.eye(4,4)
    sp = np.sin(phi)
    cp = np.cos(phi)
    rot_matrix_phi[1,1] = cp
    rot_matrix_phi[2,2] = rot_matrix_phi[1,1]
    rot_matrix_phi[1,2] = -sp
    rot_matrix_phi[2,1] = sp

    # Y rotation matrix
    rot_matrix_gamma = np.eye(4,4)
    sg = np.sin(gamma)
    cg = np.cos(gamma)
    rot_matrix_gamma[0,0] = cg
    rot_matrix_gamma[2,2] = rot_matrix_gamma[0,0]
    rot_matrix_gamma[0,2] = sg
    rot_matrix_gamma[2,0] = -sg

    # Z rotation matrix (in-image-plane)
    rot_matrix_theta = np.eye(4,4)
    st = np.sin(theta)
    ct = np.cos(theta)
    rot_matrix_theta[0,0] = ct
    rot_matrix_theta[1,1] = rot_matrix_theta[0,0]
    rot_matrix_theta[0,1] = -st
    rot_matrix_theta[1,0] = st

    rotation_matrix = reduce(lambda x,y : np.matmul(x,y), [rot_matrix_phi, rot_matrix_gamma, rot_matrix_theta])

    return rotation_matrix

def get_perspective_transform_estimation(pts_in, pts_out, width, height, side_length):
    pts_in_2D = pts_in[0,:]
    pts_out_2D = pts_out[0,:]

    pts_out_2D_list = []
    pts_in_2D_list = []
    for i in range(0,4): # TODO: use len
        pts_out_2D_list.append([pts_out_2D[i,0], pts_out_2D[i,1]])
        pts_in_2D_list.append([pts_in_2D[i,0], pts_in_2D[i,1]])

    pin  =  np.array(pts_in_2D_list) + [width/2., height/2.]
    pout = (np.array(pts_out_2D_list) + [1., 1.]) * (0.5*side_length)
    pin  = pin.astype(np.float32)
    pout = pout.astype(np.float32)

    return pin, pout

def get_warp_matrix(width, height, theta, phi, gamma, scale, field_vision):

    fv_half = np.deg2rad(field_vision / 2.)
    d = np.sqrt(width*width + height*height)
    side_length = scale * (d / np.cos(fv_half))
    h = d / (2.0*np.sin(fv_half))
    n = h - (d/2.0)
    f = h + (d/2.0)

    # Translation along Z-axis by -h
    T = np.eye(4,4)
    T[2,3] = -h

    # Rotation matrices around x,y,z
    rotation_matrix = get_rotation_matrix([phi, gamma, theta])

    # Projection Matrix
    P = np.eye(4,4)
    P[0,0] = 1.0/np.tan(fv_half)
    P[1,1] = P[0,0]
    P[2,2] = -(f+n)/(f-n)
    P[2,3] = -(2.0*f*n)/(f-n)
    P[3,2] = -1.0

    # pythonic matrix multiplication
    F = reduce(lambda x,y : np.matmul(x,y), [P, T, rotation_matrix])

    # shape should be 1,4,3 for ptsIn and ptsOut since perspectiveTransform() expects data in this way.
    # In C++, this can be achieved by Mat ptsIn(1,4,CV_64FC3);
    pts_in = np.array([[
                 [-width/2., height/2., 0.],[ width/2., height/2., 0.],[ width/2.,-height/2., 0.],[-width/2.,-height/2., 0.]
                 ]])
    pts_out  = np.array(np.zeros((pts_in.shape), dtype=pts_in.dtype))
    pts_out  = cv2.perspectiveTransform(pts_in, F)

    pts_in_pt2f, pts_out_pt2f = get_perspective_transform_estimation(pts_in, pts_out, width, height, side_length)

    # check float32 otherwise OpenCV throws an error
    assert(pts_in_pt2f.dtype  == np.float32)
    assert(pts_out_pt2f.dtype == np.float32)
    M33 = cv2.getPerspectiveTransform(pts_in_pt2f, pts_out_pt2f)

    return M33, int(side_length)

def get_random_angles(theta_range, phi_range, gamma_range, step):
    theta = random.randrange(theta_range[0], theta_range[1], step)
    phi = random.randrange(phi_range[0], phi_range[1], step)
    gamma = random.randrange(gamma_range[0], gamma_range[1], step)

    return (theta, phi, gamma)

def cut_warped_image(warped_image, source_width, source_height, matrix):
    """Returns a cropped version of the image, containing the minimum size required to contain the image"""
    # Find coordinates position of warped image
    image_coords = np.array([[
                    [0,0],
                    [source_width, 0],
                    [0, source_height],
                    [source_width, source_height]]], dtype='float32')

    warped_coords = cv2.perspectiveTransform(image_coords, matrix)[0]

    # Find the minimum rect bouding box that fits the warped image. p = [x, y]
    p1 = np.min(warped_coords, axis=0).astype(int)
    p2 = np.max(warped_coords, axis=0).astype(int)

    # Crop image
    result = warped_image[p1[1]:p2[1], p1[0]:p2[0]]

    return result, [p1, p2]

def warp_bboxes(bboxes, matrix, crop_points=None, rotate_bboxes=False):
    """Re-calculates new bounding boxes based on transformation matrix used for an image"""
    warped_bboxes = []
    for bbox in bboxes:
        bbox_coords = bbox_to_coords(bbox)
        bbox_points = np.array([[
            [bbox_coords[0], bbox_coords[1]],
            [bbox_coords[2], bbox_coords[1]],
            [bbox_coords[0], bbox_coords[3]],
            [bbox_coords[2], bbox_coords[3]]
        ]], dtype='float32')

        # Find the new rectangle bbox that fits the warped bbox
        warped_coords = cv2.perspectiveTransform(bbox_points, matrix)[0]
        new_bbox = copy.copy(bbox)
        if rotate_bboxes:
            # minAreaRect = (cx,cy),(w,h),angle
            bbox_rect = cv2.minAreaRect(warped_coords)
            new_bbox['cx'], new_bbox['cy'] = bbox_rect[0][0], bbox_rect[0][1]
            new_bbox['w'], new_bbox['h'] = bbox_rect[1][0], bbox_rect[1][1]
            new_bbox['angle'] = bbox_rect[2]
        else:
            # boundingRect = (x1,y1),(w,h)
            bbox_rect = cv2.boundingRect(warped_coords)
            new_bbox['w'], new_bbox['h'] = bbox_rect[2], bbox_rect[3]
            # cx = x1 + w/2, cy = x2 + h/2
            new_bbox['cx'], new_bbox['cy'] = bbox_rect[0] + (bbox_rect[2]/2), bbox_rect[1] + (bbox_rect[3]/2)
            new_bbox['angle'] = 0

        # Apply cropping to bbox if points provided
        if crop_points:
            new_bbox['cx'] -= crop_points[0][0]
            new_bbox['cy'] -= crop_points[0][1]

        warped_bboxes.append(new_bbox)

    return warped_bboxes

def warp_image(image, theta, phi, gamma, scale, fovy, bboxes=None, rotate_bboxes=False):
    """Changes the perspective of an image according to x,y,z angles"""
    height, width, _ = image.shape
    matrix, side_length = get_warp_matrix(width, height, theta, phi, gamma, scale, fovy) # Compute warp matrix
    transparent_bg = (0,0,0,0)
    result_image = image
    if image.shape[2] == 3:
        result_image = cv2.cvtColor(image, cv2.COLOR_RGB2RGBA)
    result_image = cv2.warpPerspective(result_image, matrix, (side_length, side_length), borderValue=transparent_bg) # Do actual image warp
    result_image, crop_points = cut_warped_image(result_image, image.shape[1], image.shape[0], matrix)
    result_bboxes = None
    if bboxes:
        result_bboxes = warp_bboxes(bboxes, matrix, crop_points=crop_points, rotate_bboxes=rotate_bboxes)

    return result_image, result_bboxes

def warp_image_random(image, bboxes, context):
    """Changes the perspective viewing angles of an image by a random number"""
    theta_range = ast.literal_eval(context.getConfig("Perspective", "theta_range"))
    phi_range   = ast.literal_eval(context.getConfig("Perspective", "phi_range"))
    gamma_range = ast.literal_eval(context.getConfig("Perspective", "gamma_range"))
    step = int(context.getConfig("Perspective", "rotation_step"))
    fov = int(context.getConfig("Perspective", "field_of_view"))
    scale = float(context.getConfig("Perspective", "scale"))
    rotate_bboxes = context.getBoolean("Image", "rotate_bboxes")
    theta, phi, gamma = get_random_angles(theta_range, phi_range, gamma_range, step)

    result_image, result_bboxes = warp_image(image, theta, phi, gamma, scale, fov, bboxes, rotate_bboxes)
    return result_image, result_bboxes

#region bounding box operations
# TODO: Move bbox functions to utility?

def coords_to_bbox(coords):
    """Converts (x1,y1,x2,y2) rectangular coordinates to bounding box representation (cx,xy,w,h)"""
    width = (coords[2] - coords[0]) # w = x2 - x1
    height = (coords[3] - coords[1]) # h = y2 - y1
    center_x = coords[0] + (width / 2)
    center_y = coords[1] + (height / 2)
    return (center_x, center_y, width, height)

def bbox_to_coords(bbox):
    """Converts bounding box representation (cx,xy,w,h) to (x1,y1,x2,y2) coordinates"""
    cx, cy, w, h = bbox['cx'], bbox['cy'], bbox['w'], bbox['h']
    x1 = cx - w/2
    y1 = cy - h/2
    x2 = x1 + w
    y2 = y1 + h

    return (int(x1), int(y1), int(x2), int(y2))

def get_bbox_vertices(bbox):
    """Returns a clock-wise ordered list of vertices of a bbox, accounting for rotation"""
    bbox_coords = bbox_to_coords(bbox)
    bbox_vertices = [
        [bbox_coords[0], bbox_coords[1]],
        [bbox_coords[2], bbox_coords[1]],
        [bbox_coords[2], bbox_coords[3]],
        [bbox_coords[0], bbox_coords[3]]
    ]
    cx, cy, w, h = bbox['cx'], bbox['cy'], bbox['w'], bbox['h']
    angle = np.deg2rad(bbox['angle']) # Calculations need to be done in radians
    if angle != 0: # Calculate new coordinates if bbox is rotated
        for vertex in bbox_vertices:
            temp_x = vertex[0] - cx
            temp_y = vertex[1] - cy
            rot_x = (temp_x * math.cos(angle)) - (temp_y * math.sin(angle)) + cx
            rot_y = (temp_x * math.sin(angle)) + (temp_y * math.cos(angle)) + cy
            vertex[0] = rot_x
            vertex[1] = rot_y

    return bbox_vertices

def get_centroid(vertices):
    """Calculates polygon centroid from a list of vertices"""
    verts_x = [v[0] for v in vertices]
    verts_y = [v[1] for v in vertices]
    centroid_x = sum(verts_x) / len(vertices)
    centroid_y = sum(verts_y) / len(vertices)

    return [centroid_x, centroid_y]

#endregion


#
# 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
#
# https://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 os
from flask import Flask, request, render_template
from cloudant.account import Cloudant
from cloudant.result import Result
import hashlib
import time

# Connectivity to cloudant
USERNAME = ''
PASSWORD = ''
ACCOUNT_URL = ''

client = Cloudant(USERNAME, PASSWORD, url=ACCOUNT_URL)
client.connect()
session = client.session()

# Open the database
my_database = client['files']

app = Flask(__name__)

@app.route('/')
def Welcome():
    return app.send_static_file('index.html')

################ UPLOAD LOGIC #################
@app.route('/upload', methods=['POST', 'GET'])
def upload():
    if request.method == 'POST':
        version = 1
        file = request.files['file']
        hashcode = hashlib.md5(file.filename).hexdigest()
        content = file.read()

        for document in my_database:
            if document['fileName'] == file.filename:
                if document['hashValue'] == hashcode:
                    return 'File not Uploaded'
                else:
                    version = document['versionNo'] + 1

        # Create a document to be pushed cloudant
        data = {
        'id': file.filename+hashcode,
        'fileName': file.filename,
        'hashValue': hashcode,
        'versionNo':  version,
        'fileContent': content,
        'lastModifiedDate': time.ctime(os.path.getmtime(file.filename))
        }

        #Push the document to cloudant
        my_document = my_database.create_document(data)

        return 'File Uploaded'

################ FILE LIST LOGIC ################
@app.route('/list', methods=['POST', 'GET'])
def list():
    list = ''
    for document in my_database:
        list = list + document['fileName'] + '_' + str(document['versionNo'])
        list = list + "       Download       "
        list = list + "Delete
"
    return '''<!DOCTYPE html>

    <title>Python Flask Application</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="static/stylesheets/style.css">

    ''' + list + '''

'''

################ DOWNLOAD LOGIC #################
@app.route('/download', methods=['POST', 'GET'])
def download():
    docID = request.args.get('id')
    document = my_database[docID]
    fname = document['fileName'] + '_' + str(document['versionNo'])
    downloadFile = open(fname,'w')
    downloadFile.write(document['fileContent'])
    return 'File Download Successful'

################ DELETE LOGIC #################
@app.route('/delete', methods=['POST', 'GET'])
def delete():
    docID = request.args.get('id')
    document = my_database[docID]
    document.delete()
    return 'File Delete Successful'

port = os.getenv('VCAP_APP_PORT', '5000')
if __name__ == "__main__":
	app.run(host='0.0.0.0', port=int(port), debug=True)


"""
This file stores hepler functions to enable PDF highlight scripts run smoothly
    without the main code being polluted with unnecessary logic.
"""

from typing import Tuple

import pyautogui
from PIL import Image

ColorValue = Tuple[int, int, int]

class Helpers:
    @staticmethod
    def _get_position_of_object(
        object_image_location: str,
        confidence_of_locating: float = 1,
        coords_and_square_size: dict = None,
    ) -> dict:
        """
        Determines the position of a certain object on the screen.
        When we suspect the object will be around a certain location,
            we can input dictionary with these data, and it will
            first investigate this smaller location - thus making
            the search possibly quicker that searching in the whole screen

        Using None as a default argument, because using empty dictionary is bad:
        - https://docs.quantifiedcode.com/python-anti-patterns/correctness/mutable_default_value_as_argument.html
        """

        # Trying to locate the object in the vicinity of current position (if wanted)
        if coords_and_square_size is not None:
            # Getting the coordination of the region
            region_where_to_look = Helpers.__safely_create_square_region_on_the_screen(
                x_coord=coords_and_square_size["x_coord"],
                y_coord=coords_and_square_size["y_coord"],
                square_size=coords_and_square_size["square_size"],
                screen_size=pyautogui.size(),
            )

            # Trying to locate the object in a small region
            position_of_object = pyautogui.locateOnScreen(
                object_image_location, region=region_where_to_look
            )

            # If we managed to find the object in the smaller region, return it
            # Otherwise the object will be searched for on the whole screen
            if position_of_object:
                return {"found": True, "coords": position_of_object}

        # Locating the object on the whole screen (after first trial failed
        #   or was not even wanted)
        position_of_object = pyautogui.locateOnScreen(object_image_location)

        return {"found": position_of_object is not None, "coords": position_of_object}

    @staticmethod
    def __safely_create_square_region_on_the_screen(
        x_coord: int,
        y_coord: int,
        square_size: int,
        screen_size: Tuple[int, int],
        region_type: str = "pyautogui",
    ) -> Tuple[int, int, int, int]:
        """
        Determines a square region on the screen, that has a defined square size
            and is surrounding the point with given x and y coordinates.
        It tries to put the point in the middle of the square, but when the
            point is close to screen boundary, it is not possible - in this case
            it returns still the same-sized square, but fully located on
            the screen.
        """

        # There are two possibilities of returning the result:
        # Pyautogui needs format (x_upper_left_corner, y_upper_left_corner,
        #   x_rectangle_length, y_rectangle_length), whereas PIL demands
        #   (x_upper_left_corner, y_upper_left_corner, x_bottom_right_corner,
        #   y_bottom_right_corner)
        assert region_type in ["pyautogui", "PIL"]

        # Finding out the resolution of the screen, to righly construct the square
        x_screen_size, y_screen_size = screen_size

        # Guard against square sizes bigger than the whole screen
        assert square_size <= x_screen_size and square_size <= y_screen_size

        # Calculating the coordinates of ideal top-left square corner
        x_corner = int(x_coord - square_size / 2)
        y_corner = int(y_coord - square_size / 2)

        # Transforming the possibly negative coordinates to non-negative,
        #   as negative coordinations do not exist
        x_corner = x_corner if x_corner > -1 else 0
        y_corner = y_corner if y_corner > -1 else 0

        # Identifying if the square would be outside the screen, and if so,
        #   adjust the corner further from the border, to fix this
        if x_corner + square_size >= x_screen_size:
            x_corner = x_screen_size - square_size - 1
        if y_corner + square_size >= y_screen_size:
            y_corner = y_screen_size - square_size - 1

        # Last sanity check before returning
        assert 0 <= x_corner < x_screen_size - square_size
        assert 0 <= y_corner < y_screen_size - square_size

        if region_type == "pyautogui":
            return (x_corner, y_corner, square_size, square_size)
        elif region_type == "PIL":
            return (x_corner, y_corner, x_corner + square_size, y_corner + square_size)
        else:
            raise RuntimeError(f"Unknown region_type: {region_type}")

    @staticmethod
    def _locate_pixel_in_the_image(
        PIL_image,
        colour_to_match: ColorValue,
        x_coord: int,
        y_coord: int,
        square_size: int,
        pixels_to_skip_on_margin_if_possible: int,
        grid_size_when_searching: int,
    ) -> dict:
        """
        Searching the image for a certain pixel, according to supplied
            colour.
        At the beginning smaller area around a certain point is searched,
            as there is the highest chance of locating what we want.
        """

        # First checking if the pixel colour to match really exists in the image
        # In the negative case immediately return that there is nothing
        is_there_my_colour = Helpers.__is_there_a_colour_in_a_PIL_image(
            PIL_image=PIL_image, colour_to_locate=colour_to_match
        )

        if not is_there_my_colour["is_there"]:
            return {"should_i_click": False, "coords": None}

        # There is a high chance the mouse will be already pointing at
        #   our desired pixel, so before investigating bigger picture
        #   try to see if we are not already there
        if PIL_image.getpixel((x_coord, y_coord)) == colour_to_match:
            return {"should_i_click": True, "coords": (x_coord, y_coord)}

        # If we are not so lucky, we have to examine the image more thoroughly
        # Getting the coordination of our region in vicinity of cursor
        region_where_to_look = Helpers.__safely_create_square_region_on_the_screen(
            x_coord=x_coord,
            y_coord=y_coord,
            square_size=square_size,
            screen_size=PIL_image.size,
            region_type="PIL",
        )

        # Trying to locate our colour in our region
        is_there_colour_in_region = Helpers.__is_there_a_colour_in_a_PIL_image(
            PIL_image=PIL_image.crop(box=region_where_to_look),
            colour_to_locate=colour_to_match,
        )

        # If we manage to locate it in our smaller region, locate it there
        if is_there_colour_in_region["is_there"]:
            x_values_list = Helpers.__generate_list_from_range_starting_in_the_middle(
                lower_end=region_where_to_look[0],
                higher_end=region_where_to_look[2],
                step=grid_size_when_searching,
            )

            for x_value in x_values_list:
                analyze_y_axis = Helpers._search_the_y_axis_for_pixel(
                    PIL_image=PIL_image,
                    colour_to_match=colour_to_match,
                    x_coord=x_value,
                    y_coord_current=y_coord,
                    distance_to_search=square_size,
                    pixels_to_skip_on_margin_if_possible=pixels_to_skip_on_margin_if_possible,
                )

                if analyze_y_axis["success"]:
                    return {"should_i_click": True, "coords": analyze_y_axis["coords"]}
        # If it is not in out smaller region, we must find it in the whole image
        else:
            # Starting with the current grid size, and if we are not successful
            #   divide it by two, so at the end we really examine all the pixels
            while grid_size_when_searching // 2 > 0:
                x_values_list = Helpers.__generate_list_from_range_starting_in_the_middle(
                    lower_end=0,
                    higher_end=PIL_image.size[0] - 1,
                    step=grid_size_when_searching,
                )

                y_coord_in_the_middle = PIL_image.size[1] // 2

                for x_value in x_values_list:
                    analyze_y_axis = Helpers._search_the_y_axis_for_pixel(
                        PIL_image=PIL_image,
                        colour_to_match=colour_to_match,
                        x_coord=x_value,
                        y_coord_current=y_coord_in_the_middle,
                        distance_to_search=PIL_image.size[1],
                        pixels_to_skip_on_margin_if_possible=pixels_to_skip_on_margin_if_possible,
                    )

                    if analyze_y_axis["success"]:
                        return {
                            "should_i_click": True,
                            "coords": analyze_y_axis["coords"],
                        }

                grid_size_when_searching = grid_size_when_searching // 2

        # It should never reach this assert, because the colour was identified
        #   in the image, and in the end we searched it pixel by pixel, so
        #   something must have gone wrong
        assert False

    @staticmethod
    def __generate_list_from_range_starting_in_the_middle(
        lower_end: int,
        higher_end: int,
        step: int,
        first_direction_from_middle: str = None,
    ) -> list:
        """
        Generates a list of numbers starting at the middle of the range
            and continuing gradually to the edges
        """

        assert lower_end < higher_end
        assert step > 0
        assert first_direction_from_middle in [None, "up", "down", "mixed"]

        middle_point = int((lower_end + higher_end) / 2)

        bottom_part = []
        middle_part = [middle_point]
        upper_part = []

        # Filling the bottom part
        current_point = middle_point
        while current_point > lower_end:
            current_point -= step
            if current_point > lower_end:
                bottom_part.append(current_point)
            else:
                bottom_part.append(lower_end)

        # Filling the upper part
        current_point = middle_point
        while current_point < higher_end:
            current_point += step
            if current_point < higher_end:
                upper_part.append(current_point)
            else:
                upper_part.append(higher_end)

        # Starting from the middle position, and serving other parts
        #   according to the wanted direction
        # First going through the bottom part
        if first_direction_from_middle in [None, "down"]:
            resulting_list = middle_part + bottom_part + upper_part
        # First going through the upper part
        elif first_direction_from_middle == "up":
            resulting_list = middle_part + upper_part + bottom_part
        # When there should be mixed direction, taking gradually elements
        #   from both the bottom and upper part, until they are both
        #   exhausted
        elif first_direction_from_middle == "mixed":
            mixed_list = []
            max_length = max(len(bottom_part), len(upper_part))
            for index in range(max_length):
                try:
                    mixed_list.append(bottom_part[index])
                except IndexError:
                    pass
                try:
                    mixed_list.append(upper_part[index])
                except IndexError:
                    pass
            resulting_list = middle_part + mixed_list
        else:
            raise RuntimeError(
                f"Unknown first_direction_from_middle - {first_direction_from_middle}"
            )

        return resulting_list

    @staticmethod
    def _search_the_y_axis_for_pixel(
        PIL_image: Image.Image,
        colour_to_match: ColorValue,
        x_coord: int,
        y_coord_current: int,
        distance_to_search: int,
        pixels_to_skip_on_margin_if_possible: int = 0,
    ) -> dict:
        """
        Examining the given image for a certain pixel on a specific "x"
            coordination.
        Starting at the certain point and explore both directions, with
            the upward (decreasing "y" coordination) direction at first
        """

        screen_x_size, screen_y_size = PIL_image.size

        # Determining the ideal boundaries in which to locate the pixel
        y_low_boundary = int(y_coord_current - distance_to_search / 2)
        y_high_boundary = int(y_coord_current + distance_to_search / 2)

        # Transforming the possibly negative y_low_boundary to non-negative,
        #   as negative coordinations do not exist
        if y_low_boundary < 0:
            y_low_boundary = 0

        # Identifying if the y_high_boundary would be outside the screen,
        #    and if so, adjust it
        if y_high_boundary >= screen_y_size:
            y_high_boundary = screen_y_size - 1

        # We are "guessing" the most probable position of match is upwards
        #   on the screen, so create the interval to search accordingly
        upward_direction = list(range(y_coord_current, y_low_boundary, -1))
        downward_direction = list(range(y_coord_current, y_high_boundary))
        y_coords = upward_direction + downward_direction

        # Running the searches - depending on whether we want to skip the
        #   first n pixels on the margin, not to conflict with anything
        # EXAMPLE: When there is already some highlighted text between
        #   the newly highlighted text and the direction we are approaching this,
        #   the already highlighted one is activated when we click on the
        #   very margin - and we do not want that
        # Therefore we will continue for couple pixels in the same direction
        #   before searching again, and hopefully click there
        if pixels_to_skip_on_margin_if_possible < 1:
            for y_coord in y_coords:
                pixel = (x_coord, y_coord)
                if PIL_image.getpixel(pixel) == colour_to_match:
                    return {"success": True, "coords": pixel}
        else:
            margin_pixel_position = None
            counter_of_skipped_pixels = 0
            margin_found = False

            for y_coord in y_coords:
                pixel = (x_coord, y_coord)
                # If we have not located anything, search for the margin
                if not margin_found:
                    if PIL_image.getpixel(pixel) == colour_to_match:
                        margin_pixel_position = pixel
                        margin_found = True
                # When the margin is already located, skip next n pixels
                else:
                    if counter_of_skipped_pixels < pixels_to_skip_on_margin_if_possible:
                        counter_of_skipped_pixels += 1
                    else:
                        if PIL_image.getpixel(pixel) == colour_to_match:
                            return {"success": True, "coords": pixel}

            # If we found margin, but nothing else after those pixels to skip,
            #   return the margin at least
            if margin_pixel_position is not None:
                return {"success": True, "coords": margin_pixel_position}

        # If nothing was ever found, return negative results
        return {"success": False, "coords": None}

    @staticmethod
    def __is_there_a_colour_in_a_PIL_image(
        PIL_image: Image.Image, colour_to_locate: ColorValue
    ) -> dict:
        """
        Determining whether colour is located in a PIL image
            (whether at least one pixel has this specific colour)
        It is surprisingly quick, 1920*1080 image with 12,000
            colours can identify arbitrary colour there in 0.02 seconds
        """

        # Getting the list of all colours in that image
        ocurrences_and_colours = PIL_image.getcolors(maxcolors=66666)

        # Trying to locate our wanted colour there
        for ocurrence, colour in ocurrences_and_colours:
            if colour == colour_to_locate:
                return {"is_there": True, "ocurrences": ocurrence}

        return {"is_there": False, "ocurrences": None}


from rest_framework import serializers
from api.models import Client, Contract, Event
from core.models import Employee
from core.serializers import EmployeeSerializer

class ClientSerializer(serializers.ModelSerializer):
    """
    ModelSerializer that serializes Client instances.
    """
    sales_contact = EmployeeSerializer(read_only=True)

    class Meta:
        model = Client
        fields = ['id',
                  'first_name',
                  'last_name',
                  'email',
                  'phone',
                  'mobile',
                  'company_name',
                  'sales_contact',
                  'is_prospect',
                  'created_at',
                  'updated_at',
                  'date_revokated']

    def create(self, validated_data):
        sales_contact = self.context.get("request", None).user

        client = Client.objects.create(
            first_name=validated_data["first_name"],
            last_name=validated_data["last_name"],
            email=validated_data['email'],
            phone=validated_data['phone'],
            mobile=validated_data['mobile'],
            company_name=validated_data['company_name'],
            sales_contact=sales_contact,
            is_prospect=validated_data['is_prospect'],
            date_revokated=validated_data['date_revokated'],
        )
        client.save()
        return client

class ContractSerializer(serializers.ModelSerializer):
    """
    ModelSerializer that serializes Contract instances.
    """
    client = ClientSerializer(read_only=True)

    class Meta:
        model = Contract
        fields = ['id',
                  'client',
                  'date_signed',
                  'status',
                  'amount',
                  'payement_due',
                  'date_revokated']

    def create(self, validated_data):
        client = Client.objects.get(pk=self.context.get("view").kwargs["client_pk"])
        if client.is_prospect:
            raise serializers.ValidationError(
                "You can't create a contract to a client who is still a prospect"
                )
        else:
            contract = Contract.objects.create(
                client=client,
                date_signed=validated_data["date_signed"],
                status=validated_data["status"],
                amount=validated_data["amount"],
                payement_due=validated_data["payement_due"],
                date_revokated=validated_data['date_revokated'],
                )
            contract.save()
            return contract

class EventSerializer(serializers.ModelSerializer):
    """
    ModelSerializer that serializes Event instances.
    """
    contract = ContractSerializer(read_only=True)
    support_contact = serializers.SlugRelatedField(
        queryset=Employee.objects.filter(department='support'),
        slug_field='username',
    )

    class Meta:
        model = Event
        fields = ['id',
                  'name',
                  'contract',
                  'support_contact',
                  'attendees',
                  'start_date',
                  'end_date',
                  'notes',
                  'created_at',
                  'updated_at',
                  'date_revokated']

    def create(self, validated_data):
        contract = Contract.objects.get(pk=self.context.get("view").kwargs["contract_pk"])
        if not contract.status:
            raise serializers.ValidationError(
                "You can't create an event while a contract is still not signed"
                )
        else:
            event = Event.objects.create(
                name=validated_data["name"],
                contract=contract,
                support_contact=validated_data["support_contact"],
                attendees=validated_data["attendees"],
                start_date=validated_data["start_date"],
                end_date=validated_data["end_date"],
                notes=validated_data['notes'],
                date_revokated=validated_data['date_revokated'],
            )
            event.save()
            return event


import numpy as np
from tsp import get_cost
import random
from collections import deque

def tnm_selection(pop, costs, max_tnm):  # tournament selection
    selection_ix = np.random.randint(len(pop))  # [0, len(pop)-1]
    for _ in np.random.randint(0, len(pop), max_tnm - 1):  # [0, len(pop)-1]
        # check if better (e.g. perform a tournament)
        if costs[_] < costs[selection_ix]:
            selection_ix = _
    return pop[selection_ix]

def rw_selection(pop, costs, _):  # roulette-wheel selection
    costs = [max(costs) - x for x in costs]  # avoid negative scores
    sum_score = sum(costs)
    return pop[np.random.choice(len(pop), p=[x / sum_score for x in costs])]
    # # naive implementation
    # sum_score = sum(scores)
    # pick = random.uniform(0, sum_score)  # [low, high)
    # current = 0
    # for i in range(len(pop)):
    #     current += scores[i]
    #     if current > pick:
    #         return pop[i]

def elt_selection(pop, costs, _):  # elitism
    return pop[np.argmin(costs)]

def crossover(p_1, p_2, r_cross):
    """
    order 1 crossover / OX / order crossover
    :param p_1: parent 1
    :param p_2: parent 2
    :param r_cross: rate of crossover
    """
    if random.random() < r_cross:
        c1, c2 = p_1.copy(), p_2.copy()
        pt_1 = random.randint(0, len(p_1)-1)
        pt_2 = random.randint(0, len(p_2)-1)
        while pt_1 == pt_2:
            pt_2 = random.randint(0, len(p_2)-1)  # let pt_1 != pt_2
        pt_1, pt_2 = (pt_1, pt_2) if pt_1 < pt_2 else(pt_2, pt_1)  # let pt_1 < pt_2
        for _ in range(pt_2 - pt_1):
            c1.remove(p_2[pt_1 + _])
            c2.remove(p_1[pt_1 + _])
        return [c1[:pt_1] + p_2[pt_1:pt_2] + c1[pt_1:], c2[:pt_1] + p_1[pt_1:pt_2] + c2[pt_1:]]
    else:
        return [p_1, p_2]

def mutation(p, r_mut):
    """
    center inverse mutation / CIM
    :param p: parent
    :param r_mut: rate of mutation
    """
    if random.random() < r_mut:
        pt = random.randint(0, len(p)-1)
        return p[:pt][::-1] + p[pt:][::-1]
    else:
        return p

def ga(n, adj_mat, n_pop, r_cross, r_mut, selection_md, max_tnm, term_count):
    pop = [random.sample(range(n), n) for _ in range(n_pop)]
    best_sol, best_cost = pop[0], get_cost(n, adj_mat, pop[0])  # randomly initialize best!
    data = {'cost': deque([]), 'best_cost': deque([])}  # cost means avg_cost
    count = 0
    # select parents
    if selection_md == 'tnm':
        selection_fnc = tnm_selection
    elif selection_md == 'rw':
        selection_fnc = rw_selection
    elif selection_md == 'elt':
        selection_fnc = elt_selection
    else:
        assert 0, 'unknown selection function'
    while True:
        costs = [get_cost(n, adj_mat, _) for _ in pop]
        last_best_cost = best_cost
        for i in range(n_pop):
            if costs[i] < best_cost:
                best_sol, best_cost = pop[i], costs[i]
        if last_best_cost == best_cost:  # best_cost not change
            count += 1
        else:  # best_cost change
            count = 0
        data['cost'].append(np.mean(costs))
        data['best_cost'].append(best_cost)
        if count > term_count:
            return best_sol, best_cost, data
        selected = [selection_fnc(pop, costs, max_tnm=max_tnm) for _ in range(n_pop)]
        # create the next generation
        children = list()
        for i in range(0, n_pop, 2):
            p1, p2 = selected[i], selected[i+1]  # get selected parents in pairs
            for c in crossover(p1, p2, r_cross):  # crossover and mutation
                children.append(mutation(c, r_mut))  # store for next generation
        pop = children  # replace population


import boto3

class SQS(object):
    def __init__(self, queue_name):
        self.client = boto3.client('sqs')
        self.queue_name = queue_name
        self.queue_url = self.get_queue_url()

    def get_queue_url(self):
        response = self.client.get_queue_url(
            QueueName=self.queue_name,
        )
        return response['QueueUrl']

    def send_message(self, body):
        response = self.client.send_message(
            QueueUrl=self.queue_url,
            MessageBody=body
        )
        return response

    def send_fifo_message(self, body, group_id='1'):
        response = self.client.send_message(
            QueueUrl=self.queue_url,
            MessageBody=body,
            MessageGroupId=group_id,
        )
        return response

    def receive_messages(self, number_of_messages=10):
        response = self.client.receive_message(
            QueueUrl=self.queue_url,
            MaxNumberOfMessages=number_of_messages
        )
        return response.get('Messages')

    def delete_message(self, receipt_handle):
        response = self.client.delete_message(
            QueueUrl=self.queue_url,
            ReceiptHandle=receipt_handle
        )
        return response


import _init_paths
import argparse
import glob
import os

import tensorflow as tf
from tensorflow.python.profiler.model_analyzer import Profiler
from tensorflow.python.profiler.model_analyzer import ALL_ADVICE
import numpy as np
from tqdm import tqdm

from config.ConfigParams import ConfigParams
from image.ImageUtils import ImageUtils
from model.TensorflowModel import TensorflowModel

def doParsing():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
                                     description="TF Profiling script")
    parser.add_argument("--datasetTestDir", required=True, type=str, help="Dataset test directory")
    parser.add_argument("--configFile", required=True, type=str, help="Model config file")
    parser.add_argument("--modelPath", required=False, type=str, default="./export/graph.pb",
                        help="Filepath with trained model")
    parser.add_argument("--numImages", required=False, type=int, default=100,
                        help="Number of images to use during profiling")
    args = parser.parse_args()
    return args

def main():
    """
    Script to export TF profiling results
    following 1.13.1 up-to-date documentation https://www.tensorflow.org/api_docs/python/tf/profiler/Profiler
    You can use https://github.com/tensorflow/profiler-ui to view results.
    """
    args = doParsing()
    print(args)

    # Load config (it includes preprocessing type)
    config = ConfigParams(args.configFile)

    # Load model
    model = TensorflowModel(args.modelPath)

    print("Loaded model from " + args.modelPath)

    inputPlaceholder = model.getGraph().get_tensor_by_name(config.inputName + ":0")
    outputTensor = model.getGraph().get_tensor_by_name(config.outputName + ":0")

    profiler = Profiler(model.getGraph())

    options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
    run_metadata = tf.RunMetadata()

    # One by one image prediction forcing CPU usage
    with model.getSession() as sess:

        with tf.device("/gpu:0"):

            for ix, file in tqdm(enumerate(sorted(glob.glob(args.datasetTestDir + "/*.jpg"))[:args.numImages])):

                image = ImageUtils.loadImage(file)
                # Resize image and preprocess (inception or vgg preprocessing based on config)
                processedImage = ImageUtils.preprocessing(image=image, width=config.inputSize, height=config.inputSize,
                                                          preprocessingType=config.preprocessType,
                                                          meanRGB=config.meanRGB)

                # Convert colorspace
                processedImage = ImageUtils.convertImageFormat(processedImage, format=config.inputFormat)

                # Add 1st dimension for image index in batch
                processedImage = np.expand_dims(processedImage, axis=0)

                # Get and print TOP1 class
                result = sess.run(outputTensor, feed_dict={inputPlaceholder: processedImage},
                                  options=options, run_metadata=run_metadata)
                print(os.path.basename(file) + " -> " + str(np.argmax(result[0])))

                profiler.add_step(ix, run_metadata)

                # Profile the parameters of your model.
                #profiler.profile_name_scope(options=(tf.profiler.ProfileOptionBuilder
                #                                     .trainable_variables_parameter()))

                # Or profile the timing of your model operations.
                opts = tf.profiler.ProfileOptionBuilder.time_and_memory()
                profiler.profile_operations(options=opts)

                # Or you can generate a timeline:
                # opts = (tf.profiler.ProfileOptionBuilder(
                #     tf.profiler.ProfileOptionBuilder.time_and_memory())
                #         .with_step(ix)
                #         .with_timeline_output("timeline_step.json").build())
                #profiler.profile_graph(options=opts)

    os.makedirs(os.path.join('profiling', 'tfprof', config.architecture), exist_ok=True)
    # Auto detect problems and generate advice.
    profiler.advise(ALL_ADVICE)

    with open(os.path.join('profiling', 'tfprof', config.architecture, 'profiler.context'), 'wb') as f:
        f.write(profiler.serialize_to_string())

    print("Test finished")

if __name__ == '__main__':
    main()