from collections import OrderedDict
import logging
from typing import Sequence

import numpy as np
import scipy.signal
from scipy.stats import norm

from merlion.transform.base import TransformBase, InvertibleTransformBase
from merlion.utils import UnivariateTimeSeries, TimeSeries

logger = logging.getLogger(__name__)

class MovingAverage(InvertibleTransformBase):
    """
    Computes the n_steps-step moving average of the time series, with
    the given relative weights assigned to each time in the moving average
    (default is to take the non-weighted average). Zero-pads the input time
    series to the left before taking the moving average.
    """

    def __init__(self, n_steps: int = None, weights: Sequence[float] = None):
        super().__init__()
        assert (
            n_steps is not None and n_steps > 0
        ) or weights is not None, "Must specify at least one of n_steps or weights for MovingAverage"
        if weights is None:
            weights = np.ones(n_steps) / n_steps
        elif n_steps is None:
            n_steps = len(weights)
        else:
            assert len(weights) == n_steps
        assert len(weights) >= 2, "Must compute a moving average with a window size of at least 2"
        self.n_steps = n_steps
        self.weights = weights

    @property
    def requires_inversion_state(self):
        return False

    @property
    def _n_pad(self):
        return len(self.weights) - 1

    def train(self, time_series: TimeSeries):
        pass

    def __call__(self, time_series: TimeSeries) -> TimeSeries:
        new_vars = OrderedDict()
        for name, var in time_series.items():
            x = np.concatenate((np.full(self._n_pad, var.np_values[0]), var.np_values))
            y = scipy.signal.correlate(x, self.weights, mode="full")[self._n_pad : -self._n_pad]
            new_vars[name] = UnivariateTimeSeries(var.index, y)

        ret = TimeSeries(new_vars, check_aligned=False)
        ret._is_aligned = time_series.is_aligned
        return ret

    def _invert(self, time_series: TimeSeries) -> TimeSeries:
        new_vars = OrderedDict()
        for name, var in time_series.items():
            left_pad = np.cumsum(self.weights[-1:0:-1]) * var.np_values[0]
            right_pad = np.full(self._n_pad, var.np_values[-1])
            y = np.concatenate((left_pad, var.np_values, right_pad))
            x = scipy.signal.deconvolve(y, self.weights[-1::-1])[0][self._n_pad :]
            new_vars[name] = UnivariateTimeSeries(var.index, x)

        ret = TimeSeries(new_vars, check_aligned=False)
        ret._is_aligned = time_series.is_aligned
        return ret

class MovingPercentile(TransformBase):
    """
    Computes the n-step moving percentile of the time series.
    For datapoints at the start of the time series which are preceded by
    fewer than ``n_steps`` datapoints, the percentile is computed using only the
    available datapoints.
    """

    def __init__(self, n_steps: int, q: float):
        """
        :param q: The percentile to use. Between 0 and 100 inclusive.
        :param n_steps: The number of steps to use.
        """
        super().__init__()
        assert 0 <= q <= 100
        assert 1 <= n_steps
        self.n_steps = int(n_steps)
        self.q = q

    def train(self, time_series: TimeSeries):
        pass

    def __call__(self, time_series: TimeSeries) -> TimeSeries:
        new_vars = OrderedDict()
        for name, var in time_series.items():
            x = var.np_values
            new_x = []
            for i in range(len(x)):
                window = x[max(0, i - self.n_steps + 1) : i + 1]
                new_x.append(np.percentile(window, self.q))
            new_vars[name] = UnivariateTimeSeries(var.index, new_x)
        ret = TimeSeries(new_vars, check_aligned=False)
        ret._is_aligned = time_series.is_aligned
        return ret

class ExponentialMovingAverage(InvertibleTransformBase):
    r"""
    Computes the exponential moving average (normalized or un-normalized) of the
    time series, with smoothing factor alpha (lower alpha = more smoothing).
    alpha must be between 0 and 1.

    The unnormalized moving average ``y`` of ``x`` is computed as

    .. math::
        \begin{align*}
        y_0 & = x_0 \\
        y_i & = (1 - \alpha) \cdot y_{i-1} + \alpha \cdot x_i
        \end{align*}

    The normalized moving average ``y`` of ``x`` is computed as

    .. math::

        y_i = \frac{x_i + (1 - \alpha) x_{i-1} + \ldots + (1 - \alpha)^i x_0}
        {1 + (1 - \alpha) + \ldots + (1 - \alpha)^i}

    Upper and lower confidence bounds, ``l`` and ``u``, of the exponential moving
    average are computed using the exponential moving standard deviation, ``s``, and ``y`` as

    .. math::
        l_i = y_i + z_{\frac{1}{2} (1-p)} \times s_i \\
        u_i = u_o + z_{\frac{1}{2} (1+p)} \times s_i

    If condfidence bounds are included, the returned time series will contain
    the upper and lower bounds as additional univariates. For example if the
    transform is applied to a time series with two univariates "x" and "y",
    the resulting time series will contain univariates with the following names:
    "x", "x_lb", "x_ub", "y", "y_lb", "y_ub".
    """

    def __init__(self, alpha: float, normalize: bool = True, p: float = 0.95, ci: bool = False):
        """
        :param alpha: smoothing factor to use for exponential weighting.
        :param normalize: If True, divide by the decaying adjustment in
            beginning periods.
        :param p: confidence level to use if returning the upper and lower
            bounds of the confidence interval.
        :param ci: If True, return the the upper and lower confidence bounds
            of the the exponential moving average as well.
        """
        super().__init__()
        self.alpha = alpha
        self.normalize = normalize
        self.p = p
        self.ci = ci

    @property
    def requires_inversion_state(self):
        """
        ``False`` because the exponential moving average is stateless to invert.
        """
        return False

    def train(self, time_series: TimeSeries):
        pass

    def __call__(self, time_series: TimeSeries) -> TimeSeries:
        new_vars = OrderedDict()
        for name, var in time_series.items():
            emw = var.to_pd().ewm(alpha=self.alpha, adjust=self.normalize)
            ema = emw.mean()
            new_vars[name] = UnivariateTimeSeries.from_pd(ema)
            if self.ci:
                ems = emw.std()
                ems[0] = ems[1]
                new_vars[f"{name}_lb"] = UnivariateTimeSeries.from_pd(ema + norm.ppf(0.5 * (1 - self.p)) * ems)
                new_vars[f"{name}_ub"] = UnivariateTimeSeries.from_pd(ema + norm.ppf(0.5 * (1 + self.p)) * ems)

        ret = TimeSeries(new_vars, check_aligned=False)
        ret._is_aligned = time_series.is_aligned
        return ret

    def _invert(self, time_series: TimeSeries) -> TimeSeries:
        new_vars = OrderedDict()
        for name, var in time_series.items():
            # check whether varaiable is an upper or lower confidence bound
            if isinstance(name, str) and (name.endswith("_lb") or name.endswith("_ub")):
                continue
            t, y = var.index, var.np_values

            # Geometric series formula for (1 - alpha)^0 + ... + (1 - alpha)^i
            # to unnormalize the EWM before inverting it
            if self.normalize:
                weights = 1 - (1 - self.alpha) ** np.arange(1, len(y) + 1)
                y = y * weights / self.alpha
                x = y[1:] - (1 - self.alpha) * y[:-1]

            # Direct inversion of one-step update for unnormalized EWMA
            else:
                x = (y[1:] - (1 - self.alpha) * y[:-1]) / self.alpha

            x = np.concatenate((y[:1], x))
            new_vars[name] = UnivariateTimeSeries(t, x)

        ret = TimeSeries(new_vars, check_aligned=False)
        ret._is_aligned = time_series.is_aligned
        return ret

class DifferenceTransform(InvertibleTransformBase):
    """
    Applies a difference transform to the input time series. We include it
    as a moving average because we can consider the difference transform
    to be a 2-step moving "average" with weights w = [-1, 1].
    """

    def train(self, time_series: TimeSeries):
        pass

    def __call__(self, time_series: TimeSeries) -> TimeSeries:
        x0 = {}
        new_vars = OrderedDict()
        for name, var in time_series.items():
            x0[name] = var[0]
            if len(var) <= 1:
                logger.warning(f"Cannot apply a difference transform to a time series of length {len(var)} < 2")
                new_vars[name] = UnivariateTimeSeries([], [])
            else:
                new_vars[name] = UnivariateTimeSeries.from_pd(var.diff())[1:]

        self.inversion_state = x0
        ret = TimeSeries(new_vars, check_aligned=False)
        ret._is_aligned = time_series.is_aligned
        return ret

    def _invert(self, time_series: TimeSeries) -> TimeSeries:
        new_vars = OrderedDict()
        for name, var in time_series.items():
            t0, x0 = self.inversion_state[name]
            var = UnivariateTimeSeries([t0], [x0]).concat(var).cumsum()
            new_vars[name] = UnivariateTimeSeries.from_pd(var)

        ret = TimeSeries(new_vars, check_aligned=False)
        ret._is_aligned = time_series.is_aligned
        return ret

class LagTransform(InvertibleTransformBase):
    """
    Applies a lag transform to the input time series. Each x(i) gets mapped
    to x(i) - x(i-k). We include it as a moving average because we can consider
    the lag transform to be a k+1-step moving "average" with weights
    w = [-1, 0,..., 0, 1]. One may optionally left-pad the sequence with the
    first value in the time series.
    """

    def __init__(self, k: int, pad: bool = False):
        super().__init__()
        assert k >= 1
        self.k = k
        self.pad = pad

    def train(self, time_series: TimeSeries):
        pass

    def __call__(self, time_series: TimeSeries) -> TimeSeries:
        all_tk, all_xk = {}, {}
        new_vars = OrderedDict()
        for name, var in time_series.items():
            # Apply any x-padding or t-truncating necessary
            t, x = var.index, var.np_values
            all_xk[name] = x[: self.k]
            if self.pad:
                all_tk[name] = t[:0]
                x = np.concatenate((np.full(self.k, x[0]), x))
            else:
                all_tk[name] = t[: self.k]
                t = t[self.k :]

            if len(var) <= self.k and not self.pad:
                logger.warning(
                    f"Cannot apply a {self.k}-lag transform to a time series of length {len(var)} <= {self.k}"
                )
                new_vars[name] = UnivariateTimeSeries([], [])
            else:
                new_vars[name] = UnivariateTimeSeries(t, x[self.k :] - x[: -self.k])

        self.inversion_state = all_tk, all_xk
        return TimeSeries(new_vars)

    def _invert(self, time_series: TimeSeries) -> TimeSeries:
        all_tk, all_xk = self.inversion_state
        new_vars = OrderedDict()
        for name, var in time_series.items():
            tk, xk = all_tk[name], all_xk[name]
            t = tk.union(var.index)
            if len(t) == len(xk) + len(var):  # no padding
                y = np.concatenate((xk, var.np_values))
            elif len(t) == len(var):  # padding
                y = np.asarray(var.values)
                y[: len(xk)] = xk
            else:
                raise RuntimeError("Something went wrong: inversion state has unexpected size.")

            x = np.zeros(len(t))
            for i in range(self.k):
                x[i :: self.k] = np.cumsum(y[i :: self.k])
            new_vars[name] = UnivariateTimeSeries(t, x)
        return TimeSeries(new_vars)

    def compute_lag(self, var: UnivariateTimeSeries) -> UnivariateTimeSeries:
        t, x = var.index, var.np_values
        if self.pad:
            x = np.concatenate((np.full(self.k, x[0]), x))
        vals = x[self.k :] - x[: -self.k]
        times = t if self.pad else t[self.k :]
        return UnivariateTimeSeries(times, vals)


import discord
from discord import Game
from discord.ext.commands import Bot
import asyncio
import os

BOT_PREFIX = "e!"
BOT_TOKEN = os.environ.get('EMBA_BOT_TOKEN')

current_announcement = {} # current announcement sent | Key: server id, Value: embedded object
announcement_channel = {} # channel to send announcements to | Key: server id, Value: channel

client = Bot(command_prefix=BOT_PREFIX)
client.remove_command('help')

@client.event
async def on_ready():
    """This function runs when the bot is started"""
    await client.change_presence(game = Game(name = 'the trumpet'))
    print('Logged in as')
    print(client.user.name)
    print(client.user.id)
    print('------')

@client.command(name='help', pass_context=True)
async def help(ctx):
    """help command"""
    # Only allowed to use commands if they have administrative permissions
    if ctx.message.author.server_permissions.administrator:
        # obtain the argument (remove the command and the leading and trailing whitespaces)
        argument = ctx.message.content[7:].strip()

        #check if user specified create module
        if argument == 'create':
            await helpcreate(ctx)
            return
        embed = discord.Embed(color = discord.Color.orange())
        embed.set_author(name='Help', icon_url='https://cdn.discordapp.com/attachments/561393414571294752/563071668533067901/announcement_icon.jpg')
        embed.add_field(
            name='Commands',
            value='*e!create [text]* - Creates an an embedded post, to learn more about create try e!help create\n*e!preview* - Sends a preview of the announcement to the requesting channel\n*e!channel [#channel]* - Sets the announcement channel\n*e!post* - post the announcement to the set channel',
            inline=True
        )
        await client.say(embed=embed)

async def helpcreate(ctx):
    embed = discord.Embed(color=discord.Color.orange())
    embed.set_author(name='Help | Create', icon_url='https://cdn.discordapp.com/attachments/561393414571294752/563071668533067901/announcement_icon.jpg')
    embed.add_field(
        name='Title',
        value='The first part of your input is the title of the announcement, | should be put at the end of the title and any field.\nIn the input: `e!create Contest | Details<This is a test` The title is `Contest`',
        inline=True
    )
    embed.add_field(
        name='Sections',
        value='Separate sections with |',
        inline=True
    )
    embed.add_field(
        name='Section Headers',
        value='^This is a section header, section headers are the first few words after a | before a < in inputed\nIn the input: `e!create Contest | Other Details<The event will take place...` Other Details would be the header',
        inline=True
    )
    await client.send_message(ctx.message.channel, embed=embed)

@client.command(name='create', pass_context=True)
async def create(ctx):
    """The bot creates an announcement and saves it for the server.  The user can then preview or post the announcement

    Args:
        - ctx: context of the command
    """
    # Only allowed to use commands if they have administrative permissions
    if ctx.message.author.server_permissions.administrator:
        # get the input (strip the command syntax)
        message = ctx.message.content[9:]

        # split message based on '|'
        input = message.split('|')

        if len(input) is 0:
            await client.send_message(ctx.message.channel, embed=discord.Embed(description='No contents specified!', color=discord.Color.red()))
            return

        # create embed with teal color
        announcement = discord.Embed(color=discord.Color.teal())

        # set author based on the first part of the users message
        announcement.set_author(name=input[0], icon_url='https://cdn.discordapp.com/attachments/561393414571294752/563071668533067901/announcement_icon.jpg')
        input = input[1:]
        for segment in input:
            # use the first word as the field name
            field_name = segment.split('<', 1)[0]

            # use the rest as the value
            field_text = segment.split('<', 1)[1]

            #add the field
            announcement.add_field(
                name=field_name,
                value=field_text,
                inline=True
            )
        # set announcement
        current_announcement[ctx.message.server.id] = announcement
        await client.send_message(ctx.message.channel, embed=discord.Embed(description='Announcement set! Preview with with e!preview', color=discord.Color.green()))

@client.command(name='channel', pass_context=True)
async def set_announcement_channel(ctx):
    """The bot sets the announcement channel specified by the user for the current server

    Args:
        - ctx: context of the command
    """
    # Only allowed to use commands if they have administrative permissions
    if ctx.message.author.server_permissions.administrator:
        # gets the channel id from form: <#0000000000> to 00000000000
        channel_id = ctx.message.content[10:].replace(" ", "").replace("<", "").replace("#", "").replace(">", "")

        # get the selected channel
        channel = client.get_channel(channel_id)
        if channel is None:
            await client.send_message(ctx.message.channel, embed=discord.Embed(description="Invalid channel", color=discord.Color.red()))
        else:
            #set announcement channel for current server
            announcement_channel[ctx.message.server.id] = channel
            await client.send_message(ctx.message.channel, embed=discord.Embed(description="Set announcements channel as " + channel.mention, color=discord.Color.green()))

@client.command(name='preview', pass_context=True)
async def preview(ctx):
    """The bot outputs a preview of the embedded announcement

    Args:
        - ctx: context of the command (used for getting the server id)
    """
    # Only allowed to use commands if they have administrative permissions
    if ctx.message.author.server_permissions.administrator:
        # get the servers announcement
        announcement = current_announcement.get(ctx.message.server.id)
        if announcement is None:
            await client.send_message(ctx.message.channel, embed=discord.Embed(description="No announcement message set! Set an announcement with e!create", color=discord.Color.red()))
        else:
            await client.send_message(ctx.message.channel, embed=announcement)

@client.command(name='post', pass_context=True)
async def post(ctx):
    """The bot posts the servers announcement to the specified announcement channel

    Args:
        - ctx: context of the command (used for getting the server id)
    """
    # Only allowed to use commands if they have administrative permissions
    if ctx.message.author.server_permissions.administrator:
        # get server announcement
        announcement = current_announcement.get(ctx.message.server.id)
        if announcement is None: # if no announcement set
            await client.send_message(ctx.message.channel, embed=discord.Embed(description="No announcement message set! Set an announcement with e!create", color=discord.Color.red()))
            return
        # get announcement channel
        channel = announcement_channel.get(ctx.message.server.id)
        if channel is None: # if no announcement channel set
            await client.send_message(ctx.message.channel, embed=discord.Embed(description="No announcement channel set! Set an announcement channel with e!channel", color=discord.Color.red()))
        else:
            everyone = ctx.message.server.roles[0]
            await client.send_message(channel, everyone)
            await client.send_message(channel, embed=announcement)

client.run(BOT_TOKEN)


import redis
import json
from datetime import datetime
from threading import Thread
import socket

redis_app = redis.Redis(host="red01", port=6379)

def make_decision(conn):
    sub = redis_app.pubsub()
    sub.subscribe('messages')

    last_time = datetime.now()
    decision = 0
    for message in sub.listen():
        if message is None:
            continue
        data = message["data"]
        if not isinstance(data, int):
            d = json.loads(data.decode().replace("'", '"'))
            decision += d["payload"]
            diff = datetime.now() - last_time
            if diff.seconds * 1000000 + diff.microseconds > 5000000:
                last_time = datetime.now()
                last_call_reply = last_time.strftime("%Y-%m-%dT%H:%M:%S")
                reply = {"datetime": last_call_reply, "status": "up" if decision >= 0 else "down"}
                conn.sendall(str(reply).encode())
                redis_app.set(str(last_time.timestamp()), "up" if decision >= 0 else "down")
                decision = 0
                print(reply)

def socket_conn():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("0.0.0.0", 7001))
    s.listen()
    while True:
        try:
            print("Connecting")
            conn, address = s.accept()
            print(address)
            thread1 = Thread(target=make_decision,
                           args=[conn])
            thread1.start()
        except KeyboardInterrupt:
            try:
                conn.close()
            except:
                pass
            finally:
                s.close()

if __name__ == "__main__":
    socket_conn()


"""atlas URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
import logging

import djangosaml2
from django.conf import settings as django_settings
from django.http import HttpResponse
from django.urls import include, path, re_path

logger = logging.getLogger(__name__)

urlpatterns = [
    path("", include("index.urls")),
    path("saml2/", include("djangosaml2.urls")),
    path("test/", djangosaml2.views.EchoAttributesView.as_view()),
    path("mail/", include("mail.urls")),
    path("analytics/", include("analytics.urls")),
    path("reports/", include("report.urls")),
    path("initiatives/", include("initiative.urls")),
    path("terms/", include("term.urls")),
    path("users/", include("user.urls")),
    path("search/", include("search.urls")),
    path("collections/", include("collection.urls")),
    path("etl/", include("etl.urls")),
]

# if in dev or test use local auth
if (
    hasattr(django_settings, "DEBUG")
    and getattr(django_settings, "DEBUG")
    or hasattr(django_settings, "TESTING")
    and getattr(django_settings, "TESTING")
    or hasattr(django_settings, "DEMO")
    and getattr(django_settings, "DEMO")
):
    urlpatterns.append(path("accounts/", include("django.contrib.auth.urls")))

if hasattr(django_settings, "DEBUG") and getattr(django_settings, "DEBUG"):
    import debug_toolbar

    urlpatterns += (path("__debug__/", include(debug_toolbar.urls)),)

import sys
import traceback

def full_stack():
    """Return full stack trace of an exception.

    :returns: full stack trace of an exception.
    """
    exc = sys.exc_info()[0]
    if exc is not None:
        frame = sys.exc_info()[-1].tb_frame.f_back
        stack = traceback.extract_stack(frame)
    else:
        stack = traceback.extract_stack()[:-1]  # last one would be full_stack()
    trc = "Traceback (most recent call last):\n"
    stackstr = trc + "".join(traceback.format_list(stack))
    if exc is not None:
        # pylint: disable=bad-str-strip-call
        stackstr += "  " + traceback.format_exc().lstrip(trc)
    return stackstr

def custom_error_view(request, exception=None):
    """Log 500 errors."""
    logger.error(full_stack())
    logger.warning(exception)
    return HttpResponse(
        "Ops, there was an error. Please try again in a few minutes.", status=500
    )

def custom_warning_view(request, exception=None):
    """Log 400 errors."""
    logger.warning(full_stack())
    logger.warning(exception)
    return HttpResponse("Ops, that page doesn't exist!", status=404)

handler500 = custom_error_view
handler404 = custom_warning_view


from . import anchor as _anchor

from ..commons import logger, InstanceTable, siibra_tqdm, __version__
from ..core import concept
from ..core import space, region, parcellation

from typing import Union, TYPE_CHECKING, List, Dict, Type, Tuple, BinaryIO
from hashlib import md5
from collections import defaultdict
from zipfile import ZipFile

if TYPE_CHECKING:
    from ..retrieval.datasets import EbrainsDataset
    TypeDataset = EbrainsDataset

class ParseLiveQueryIdException(Exception):
    pass

class EncodeLiveQueryIdException(Exception):
    pass

class NotFoundException(Exception):
    pass

_README_TMPL = """
Downloaded from siibra toolsuite.
siibra-python version: {version}

All releated resources (e.g. doi, web resources) are categorized under publications.

Name
----
{name}

Description
-----------
{description}

Modality
--------
{modality}

{publications}
"""
_README_PUBLICATIONS = """
Publications
------------
{doi}

{ebrains_page}

{authors}

{publication_desc}

"""

class Feature:
    """
    Base class for anatomically anchored data features.
    """

    SUBCLASSES: Dict[Type['Feature'], List[Type['Feature']]] = defaultdict(list)

    CATEGORIZED: Dict[str, Type['InstanceTable']] = defaultdict(InstanceTable)

    category: str = None

    def __init__(
        self,
        modality: str,
        description: str,
        anchor: _anchor.AnatomicalAnchor,
        datasets: List['TypeDataset'] = []
    ):
        """
        Parameters
        ----------
        modality: str
            A textual description of the type of measured information
        description: str
            A textual description of the feature.
        anchor: AnatomicalAnchor
        datasets : list
            list of datasets corresponding to this feature
        """
        self._modality_cached = modality
        self._description = description
        self._anchor_cached = anchor
        self.datasets = datasets

    @property
    def modality(self):
        # allows subclasses to implement lazy loading of the modality
        return self._modality_cached

    @property
    def anchor(self):
        # allows subclasses to implement lazy loading of an anchor
        return self._anchor_cached

    def __init_subclass__(cls, configuration_folder=None, category=None, do_not_index=False):

        # Feature.SUBCLASSES serves as an index where feature class inheritance is cached. When users
        # queries a branch on the hierarchy, all children will also be queried. There are usecases where
        # such behavior is not desired (e.g. ProxyFeature, which wraps livequery features id to capture the
        # query context).
        # do_not_index flag allow the default index behavior to be toggled off.

        if do_not_index is False:

            # extend the subclass lists
            # Iterate over all mro, not just immediate base classes
            for BaseCls in cls.__mro__:
                # some base classes may not be sub class of feature, ignore these
                if not issubclass(BaseCls, Feature):
                    continue
                cls.SUBCLASSES[BaseCls].append(cls)

        cls._live_queries = []
        cls._preconfigured_instances = None
        cls._configuration_folder = configuration_folder
        cls.category = category
        if category is not None:
            cls.CATEGORIZED[category].add(cls.__name__, cls)
        return super().__init_subclass__()

    @classmethod
    def _get_subclasses(cls):
        return {Cls.__name__: Cls for Cls in cls.SUBCLASSES}

    @property
    def description(self):
        """Allows subclasses to overwrite the description with a function call."""
        if self._description:
            return self._description
        for ds in self.datasets:
            if ds.description:
                return ds.description
        return ''

    @property
    def LICENSE(self) -> str:
        return '\n'.join([ds.LICENSE for ds in self.datasets])

    @property
    def doi_or_url(self) -> str:
        return '\n'.join([
            url.get("url")
            for ds in self.datasets
            for url in ds.urls
        ])

    @property
    def authors(self):
        return [
            contributer['name']
            for ds in self.datasets
            for contributer in ds.contributors
        ]

    @property
    def name(self):
        """Returns a short human-readable name of this feature."""
        return f"{self.__class__.__name__} ({self.modality}) anchored at {self.anchor}"

    @classmethod
    def get_instances(cls, **kwargs) -> List['Feature']:
        """
        Retrieve objects of a particular feature subclass.
        Objects can be preconfigured in the configuration,
        or delivered by Live queries.
        """
        if not hasattr(cls, "_preconfigured_instances"):
            return []

        if cls._preconfigured_instances is not None:
            return cls._preconfigured_instances

        if cls._configuration_folder is None:
            cls._preconfigured_instances = []
            return cls._preconfigured_instances

        from ..configuration.configuration import Configuration
        conf = Configuration()
        Configuration.register_cleanup(cls.clean_instances)
        assert cls._configuration_folder in conf.folders
        cls._preconfigured_instances = [
            o for o in conf.build_objects(cls._configuration_folder)
            if isinstance(o, cls)
        ]
        logger.debug(
            f"Built {len(cls._preconfigured_instances)} preconfigured {cls.__name__} "
            f"objects from {cls._configuration_folder}."
        )
        return cls._preconfigured_instances

    def plot(self, *args, **kwargs):
        """Feature subclasses override this with their customized plot methods."""
        raise NotImplementedError("Generic feature class does not have a standardized plot.")

    @classmethod
    def clean_instances(cls):
        """ Removes all instantiated object instances"""
        cls._preconfigured_instances = None

    def matches(self, concept: concept.AtlasConcept) -> bool:
        if self.anchor and self.anchor.matches(concept):
            self.anchor._last_matched_concept = concept
            return True
        self.anchor._last_matched_concept = None
        return False

    @property
    def last_match_result(self):
        return None if self.anchor is None \
            else self.anchor.last_match_result

    @property
    def last_match_description(self):
        return "" if self.anchor is None \
            else self.anchor.last_match_description

    @property
    def id(self):
        prefix = ''
        for ds in self.datasets:
            if hasattr(ds, "id"):
                prefix = ds.id + '--'
                break
        return prefix + md5(self.name.encode("utf-8")).hexdigest()

    def _export(self, fh: ZipFile):
        """
        Internal implementation. Subclasses can override but call super()._export(fh).
        This allows all classes in the __mro__ to have the opportunity to append files
        of interest.
        """
        ebrains_page = "\n".join(
            {ds.ebrains_page for ds in self.datasets if getattr(ds, "ebrains_page", None)}
        )
        doi = "\n".join({
            u.get("url")
            for ds in self.datasets if ds.urls
            for u in ds.urls
        })
        authors = ", ".join({
            cont.get('name')
            for ds in self.datasets if ds.contributors
            for cont in ds.contributors
        })
        publication_desc = "\n".join({ds.description for ds in self.datasets})
        if (ebrains_page or doi) and authors:
            publications = _README_PUBLICATIONS.format(
                ebrains_page="EBRAINS page\n" + ebrains_page if ebrains_page else "",
                doi="DOI\n" + doi if doi else "",
                authors="Authors\n" + authors if authors else "",
                publication_desc="Publication description\n" + publication_desc if publication_desc else ""
            )
        else:
            publications = "Note: could not obtain any publication information. The data may not have been published yet."
        fh.writestr(
            "README.md",
            _README_TMPL.format(
                version=__version__,
                name=self.name,
                description=self.description,
                modality=self.modality,
                publications=publications
            )
        )

    def export(self, filelike: Union[str, BinaryIO]):
        """
        Export as a zip archive.

        Args:
            filelike (string or filelike): name or filehandle to write the zip file. User is responsible to ensure the correct extension (.zip) is set.
        """
        fh = ZipFile(filelike, "w")
        self._export(fh)
        fh.close()

    @staticmethod
    def serialize_query_context(feat: 'Feature', concept: concept.AtlasConcept) -> str:
        """
        Serialize feature from livequery and query context.

        It is currently impossible to retrieve a livequery with a generic UUID.
        As such, the query context (e.g. region, space or parcellation) needs to
        be encoded in the id.

        Whilst it is possible to (de)serialize *any* queries, the method is setup to only serialize
        livequery features.

        The serialized livequery id follows the following pattern:

        <livequeryid_version>::<feature_cls_name>::<query_context>::<unserialized_id>

        Where:

        - livequeryid_version: version of the serialization. (e.g. lq0)
        - feature_cls_name: class name to query. (e.g. BigBrainIntensityProfile)
        - query_context: string to retrieve atlas concept in the query context. Can be one of the following:
            - s:<space_id>
            - p:<parcellation_id>
            - p:<parcellation_id>::r:<region_id>
        - unserialized_id: id prior to serialization

        See test/features/test_feature.py for tests and usages.
        """
        if not hasattr(feat.__class__, '_live_queries'):
            raise EncodeLiveQueryIdException(f"generate_livequery_featureid can only be used on live queries, but {feat.__class__.__name__} is not.")

        encoded_c = []
        if isinstance(concept, space.Space):
            encoded_c.append(f"s:{concept.id}")
        elif isinstance(concept, parcellation.Parcellation):
            encoded_c.append(f"p:{concept.id}")
        elif isinstance(concept, region.Region):
            encoded_c.append(f"p:{concept.parcellation.id}")
            encoded_c.append(f"r:{concept.name}")

        if len(encoded_c) == 0:
            raise EncodeLiveQueryIdException("no concept is encoded")

        return f"lq0::{feat.__class__.__name__}::{'::'.join(encoded_c)}::{feat.id}"

    @classmethod
    def deserialize_query_context(Cls, feature_id: str) -> Tuple[Type['Feature'], concept.AtlasConcept, str]:
        """
        Deserialize id into query context.

        See docstring of serialize_query_context for context.
        """
        lq_version, *rest = feature_id.split("::")
        if lq_version != "lq0":
            raise ParseLiveQueryIdException("livequery id must start with lq0::")

        clsname, *concepts, fid = rest

        Features = Cls.parse_featuretype(clsname)

        if len(Features) == 0:
            raise ParseLiveQueryIdException(f"classname {clsname!r} could not be parsed correctly. {feature_id!r}")
        F = Features[0]

        concept = None
        for c in concepts:
            if c.startswith("s:"):
                if concept is not None:
                    raise ParseLiveQueryIdException("Conflicting spec.")
                concept = space.Space.registry()[c.replace("s:", "")]
            if c.startswith("p:"):
                if concept is not None:
                    raise ParseLiveQueryIdException("Conflicting spec.")
                concept = parcellation.Parcellation.registry()[c.replace("p:", "")]
            if c.startswith("r:"):
                if concept is None:
                    raise ParseLiveQueryIdException("region has been encoded, but parcellation has not been populated in the encoding, {feature_id!r}")
                if not isinstance(concept, parcellation.Parcellation):
                    raise ParseLiveQueryIdException("region has been encoded, but previous encoded concept is not parcellation")
                concept = concept.get_region(c.replace("r:", ""))
        if concept is None:
            raise ParseLiveQueryIdException(f"concept was not populated: {feature_id!r}")

        return (F, concept, fid)

    @classmethod
    def parse_featuretype(cls, feature_type: str) -> List[Type['Feature']]:
        ftypes = {
            feattype
            for FeatCls, feattypes in cls.SUBCLASSES.items()
            if all(w.lower() in FeatCls.__name__.lower() for w in feature_type.split())
            for feattype in feattypes
        }
        if len(ftypes) > 1:
            return [ft for ft in ftypes if getattr(ft, 'category')]
        else:
            return list(ftypes)

    @classmethod
    def livequery(cls, concept: Union[region.Region, parcellation.Parcellation, space.Space], **kwargs) -> List['Feature']:
        if not hasattr(cls, "_live_queries"):
            return []

        live_instances = []
        for QueryType in cls._live_queries:
            argstr = f" ({', '.join('='.join(map(str,_)) for _ in kwargs.items())})" \
                if len(kwargs) > 0 else ""
            logger.info(
                f"Running live query for {QueryType.feature_type.__name__} "
                f"objects linked to {str(concept)}{argstr}"
            )
            q = QueryType(**kwargs)
            features = [
                Feature.wrap_livequery_feature(feat, Feature.serialize_query_context(feat, concept))
                for feat in q.query(concept)
            ]
            live_instances.extend(features)
        return live_instances

    @classmethod
    def match(cls, concept: Union[region.Region, parcellation.Parcellation, space.Space], feature_type: Union[str, Type['Feature'], list], **kwargs) -> List['Feature']:
        """
        Retrieve data features of the desired modality.

        Parameters
        ----------
        concept: AtlasConcept
            An anatomical concept, typically a brain region or parcellation.
        modality: subclass of Feature
            specififies the type of features ("modality")
        """
        if isinstance(feature_type, list):
            # a list of feature types is given, collect match results on those
            assert all(
                (isinstance(t, str) or issubclass(t, cls))
                for t in feature_type
            )
            return list(dict.fromkeys(
                sum((
                    cls.match(concept, t, **kwargs) for t in feature_type
                ), [])
            ))

        if isinstance(feature_type, str):
            # feature type given as a string. Decode the corresponding class.
            # Some string inputs, such as connectivity, may hit multiple matches.
            ftype_candidates = cls.parse_featuretype(feature_type)
            if len(ftype_candidates) == 0:
                raise ValueError(
                    f"feature_type {str(feature_type)} did not match with any "
                    f"features. Available features are: {', '.join(cls.SUBCLASSES.keys())}"
                )
            logger.info(
                f"'{feature_type}' decoded as feature type/s: "
                f"{[c.__name__ for c in ftype_candidates]}."
            )
            return cls.match(concept, ftype_candidates, **kwargs)

        assert issubclass(feature_type, Feature)

        if not isinstance(concept, (region.Region, parcellation.Parcellation, space.Space)):
            raise ValueError(
                "Feature.match / siibra.features.get only accepts Region, "
                "Space and Parcellation objects as concept."
            )

        msg = f"Matching {feature_type.__name__} to {concept}"
        instances = [
            instance
            for f_type in cls.SUBCLASSES[feature_type]
            for instance in f_type.get_instances()
        ]

        preconfigured_instances = [
            f for f in siibra_tqdm(
                instances, desc=msg, total=len(instances), disable=(not instances)
            ) if f.matches(concept)
        ]

        live_instances = feature_type.livequery(concept, **kwargs)

        return list(dict.fromkeys(preconfigured_instances + live_instances))

    @classmethod
    def get_instance_by_id(cls, feature_id: str, **kwargs):
        try:
            F, concept, fid = cls.deserialize_query_context(feature_id)
            return [
                f
                for f in F.livequery(concept, **kwargs)
                if f.id == fid or f.id == feature_id
            ][0]
        except ParseLiveQueryIdException:
            return [
                inst
                for Cls in Feature.SUBCLASSES[Feature]
                for inst in Cls.get_instances()
                if inst.id == feature_id
            ][0]
        except IndexError:
            raise NotFoundException

    @classmethod
    def get_ascii_tree(cls):
        # build an Ascii representation of class hierarchy
        # under this feature class
        from anytree.importer import DictImporter
        from anytree import RenderTree

        def create_treenode(feature_type):
            return {
                'name': feature_type.__name__,
                'children': [
                    create_treenode(c)
                    for c in feature_type.__subclasses__()
                ]
            }
        D = create_treenode(cls)
        importer = DictImporter()
        tree = importer.import_(D)
        return "\n".join(
            "%s%s" % (pre, node.name)
            for pre, _, node in RenderTree(tree)
        )

    @staticmethod
    def wrap_livequery_feature(feature: 'Feature', fid: str):
        """
        Wrap live query features, override only the id attribute.

        Some features do not have setters for the id property. The ProxyFeature class
        allow the id property to be overridden without touching the underlying class.

        See docstring of serialize_query_context for further context.
        """
        class ProxyFeature(feature.__class__, do_not_index=True):

            # override __class__ property
            # some instances of features accesses inst.__class__
            @property
            def __class__(self):
                return self.inst.__class__

            def __init__(self, inst: Feature, fid: str):
                self.inst = inst
                self.fid = fid

            def __str__(self) -> str:
                return self.inst.__str__()

            def __repr__(self) -> str:
                return self.inst.__repr__()

            @property
            def id(self):
                return self.fid

            def __getattr__(self, __name: str):
                return getattr(self.inst, __name)

        return ProxyFeature(feature, fid)


from datetime import date

# UmlClass translates plaintext UML file into .hpp and .cpp files
class UmlClass:

    # Initialize with these values to return to default .hpp/.cpp in loop
    def __init__(self, className):
        self.name = className

        # List of private and public members
        self.hppPrivate = ["private:"]
        self.hppPublic = ["public:"]

        # Starting .cpp template
        self.cpp = [
            "// Copyright (C) 2016. All rights reserved.",
            "// Created: " + str(date.today()),
            "",
            "#include \"" + self.name + ".hpp\""
        ]

        # Starting .hpp template
        self.hpp = [
            "// Copyright (C) 2016. All rights reserved.",
            "// Created: " + str(date.today()),
            "",
            "#ifndef " + self.name + "_hpp",
            "#define " + self.name + "_hpp",
            "",
            "class " + self.name,
            "{",
            "};",
            "#endif"
        ]

    # Soft tab size 4
    indent = "    "

    # Adds a given string to public member list
    def addToPublic(self, pubMember):
        self.hppPublic.append(pubMember)

    # Adds a given string to private member list
    def addToPrivate(self, privMember):
        self.hppPrivate.append(privMember)

    # Checks if given string is a member function
    def isFunction(self, line):
        if "(" in line and ")" in line and "(C)" not in line:
            return True

    # Searches and includes libraries for data types that need them
    # Requires namespace to be assigned before this function is called
    def includeLibs(self):

        # Data types being searched
        stringFound = False
        vectorFound = False

        for line in self.hpp:
            if "std::string" in line:
                stringFound = True
            if "std::vector" in line:
                vectorFound = True

        # Include libraries, along with extra space if found
        if stringFound:
            self.hpp[self.hpp.index("class " + self.name):1] = ["#include <string>"]
        if vectorFound:
            self.hpp[self.hpp.index("class " + self.name):1] = ["#include <vector>"]
        if stringFound or vectorFound:
            self.hpp[self.hpp.index("class " + self.name):1] = [""]

    # Builds hpp using public and private member lists
    def buildHpp(self):
        # Insert public members after {, where class begins
        self.hpp[self.hpp.index("{")+1:1] = self.hppPublic
        # Insert private members after last public member
        self.hpp[self.hpp.index(self.hppPublic[-1])+1:1] = self.hppPrivate

    # Formats hpp list into proper .hpp syntax
    def formatHpp(self):
        self.moveReturnType()
        self.addNamespace()
        self.includeLibs()
        self.indentAndSemiColon()

    # Build and format hpp and cpp lists
    def build(self):
        self.buildHpp()
        self.formatHpp()
        self.buildCpp()
        self.formatCpp()

    # Adds std:: prefix to data types that require it
    def addNamespace(self):
        for line in self.hpp:
            i = self.hpp.index(line)
            if "string" in line:
                line = line.replace("string", "std::string", 1)
                self.hpp[i] = line
            if "vector" in line:
                line = line.replace("vector", "std::vector", 1)
                self.hpp[i] = line

    # Swaps location of returned data type
    def moveReturnType(self):
        for line in self.hpp:
            i = self.hpp.index(line)
            if self.isMember(line):
                if " : " in line:
                    line = line[line.index(":")+1:].strip() + " " + line[:line.index(":")].strip()
                else:
                    line = "void " + line
            self.hpp[i] = line

    # Checks if given string is a member function of data member
    # This determines if a line needs to be formatted or not
    def isMember(self, line):
        notMember = ["/", "#", "class ", "{", "}", "public:", "private:", "protected:", "return"]
        if any(notM in line for notM in notMember) or line == "":
            return False
        else:
            return True

    # Indents a member and adds semicolon at the end
    def indentAndSemiColon(self):
        for line in self.hpp:
            i = self.hpp.index(line)
            if self.isMember(line):
                line = self.indent + line + ";"
                self.hpp[i] = line

    # Builds cpp list using members in hpp list
    # Copies member without indent and semicolon
    def buildCpp(self):
        for line in self.hpp:
            if (self.isFunction(line)):
                self.cpp.append(line[len(self.indent):-1]) # remove indent and ;

    # Adds class namespace and function body along with return statement (if needed)
    def formatCpp(self):
        for line in self.cpp:
            i = self.cpp.index(line)
            if self.isMember(line):
                line = self.insertClassName(line)
                # Add function body
                line = "\n" + line + "\n{\n" + self.indent + self.insertReturnType(line) + "\n}"
                self.cpp[i] = line

    # Returns the return statement (if needed)
    def insertReturnType(self, line):
        line = line[:line.index("(")]

        # If return type exists, construct a return statement
        if len(line.split()) > 1:
            if line[:line.index(self.name)].strip() == "void":
                return ""
            else:
                return "\n" + self.indent + "return " + self.findReturnDefault(line) + ";"
        else:
            return ""

    # If there is a return type, inserts a default return value
    # This will prevent compilation errors from the outset
    def findReturnDefault(self, line):
        line = line[:line.index(self.name)].strip()

        # Check for data types and return some default zero value
        if line == "int" or line == "char" or line == "double" or line == "float" or line == "short":
            return "0"
        elif line == "std::string":
            return "\"\""
        elif line == "bool":
            return "false"
        elif line == "void":
            return ""
        else:
            return ""

    # Returns the class namespace used in the .cpp file
    def insertClassName(self, line):
        # Search and insert className::
        splitLine = line.split()
        for bit in splitLine:
            j = splitLine.index(bit)
            if "(" in bit:
                bit = self.name + "::" + bit
                splitLine[j] = bit
        if len(splitLine) > 1:
            line = " ".join(splitLine)
        else:
            line = "".join(splitLine)
        return line

    # Builds hpp and cpp lists and creates all relevant files
    def createFiles(self):
        self.build()

        # Create .hpp
        self.hppFile = open(self.name + ".hpp", "w")
        for line in self.hpp:
            self.hppFile.write(line + "\n")
        self.hppFile.close()

        # Create .cpp
        self.cppFile = open(self.name + ".cpp", "w")
        for line in self.cpp:
            self.cppFile.write(line + "\n")
        self.cppFile.close()

# Main function
print("[ UML to CPP ] Create .hpp and .cpp files from UML.txt")
print("> Attempting to create files...")
umlFile = open("UML.txt", 'r')

uml = umlFile.read().splitlines() # pull UML into memory, 1 line per element
umlFile.close()
filesCreated = 0

for line in uml:
    if line[:5] == "class" and line[-1:] == "{":
        obj = UmlClass(line[6:-2])
    elif line[:1] == "+":
        obj.addToPublic(line[1:])
    elif line[:1] == "-":
        obj.addToPrivate(line[1:])
    elif line == "}":
        obj.createFiles()
        filesCreated += 2
    elif line == "":
        continue
    else:
        print("> Syntax error in UML file")

print("> " + str(filesCreated) + " files created")


"""empty message

Revision ID: 76fdf3992bab
Revises: 1e480ccc916b
Create Date: 2022-01-27 12:05:33.967430

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '76fdf3992bab'
down_revision = '1e480ccc916b'
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    system_settings = op.create_table('system_settings',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('mail', sa.JSON(), nullable=False),
    sa.Column('calendar', sa.JSON(), nullable=False),
    sa.PrimaryKeyConstraint('id', name=op.f('pk_system_settings'))
    )
    # ### end Alembic commands ###

    op.execute(
        system_settings.insert().values(
            mail={'MAIL_SERVER': None, 'MAIL_PORT': 587, 'MAIL_USE_TLS': True, 'MAIL_USE_SSL': True, 'MAIL_SENDER': None},
            calendar={'CALENDAR_LINK': None, 'CALENDAR_CATEGORIES': [], 'CALENDAR_SYNC_INTERVALL': 5}
        )
    )

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('system_settings')
    # ### end Alembic commands ###


"""
Created at 2019-12-22 12:52

@author: jinyanliu
"""
import collections
from enum import Enum

class Method(Enum):
    DEAL_INTO_NEW_STACK = "deal into new stack"
    DEAL_WITH_INCREMENT = "deal with increment"
    CUT = "cut"

def get_instructions_list():
    instructions_list = []
    with open("day_22_input") as lines:
        for line in lines:
            instructions_list.append(line)
    # print(instructions_list)
    return instructions_list

class ShuffleComputer:
    def __init__(self, start_deck):
        self._current_deck = start_deck

    @property
    def current_deck(self):
        return self._current_deck

    @current_deck.setter
    def current_deck(self, value):
        self._current_deck = value

    def deal_with_increment(self, step):
        deck_len = len(self.current_deck)
        new_deck = {}
        new_deck[0] = self.current_deck[0]
        current_old_deck_position = 0
        current_new_deck_position = 0

        while len(new_deck) < len(self.current_deck):
            if current_new_deck_position + step < deck_len:
                new_deck[current_new_deck_position + step] = self.current_deck[current_old_deck_position + 1]
                current_old_deck_position = current_old_deck_position + 1
                current_new_deck_position = current_new_deck_position + step
            else:
                new_deck[current_new_deck_position + step - (deck_len - 1) - 1] = self.current_deck[
                    current_old_deck_position + 1]
                current_old_deck_position = current_old_deck_position + 1
                current_new_deck_position = current_new_deck_position + step - (deck_len - 1) - 1

        # print(self.current_deck)
        # print(new_deck)
        new_deck = collections.OrderedDict(sorted(new_deck.items()))
        self.current_deck = new_deck

    def deal_into_new_stack(self):
        target_list = []
        new_deck = {}
        for value in self.current_deck.values():
            target_list.append(value)
        target_list.reverse()
        i = 0
        for value in target_list:
            new_deck[i] = value
            i += 1

        # print(new_deck)
        self.current_deck = new_deck

    def cut(self, size):
        target_list = []
        new_deck = {}
        for value in self.current_deck.values():
            target_list.append(value)

        if size >= 0:
            new_list = target_list[size:]
            new_list = new_list + target_list[:size]
        else:
            size = abs(size)
            new_list = target_list[:(len(target_list) - size)]
            new_list = target_list[(len(target_list) - size):] + new_list
        i = 0
        for value in new_list:
            new_deck[i] = value
            i += 1

        # print(new_deck)
        self.current_deck = new_deck

    def get_position_of_card(self, card_number):
        for key, value in self.current_deck.items():
            if card_number == value:
                return key

    def get_instructions_and_actions(self):
        return {
            Method.DEAL_WITH_INCREMENT.value: self.deal_with_increment,
            Method.DEAL_INTO_NEW_STACK.value: self.deal_into_new_stack,
            Method.CUT.value: self.cut
        }

def get_solution_1():
    map_dict = {}
    for i in range(0, 10007):
        map_dict[i] = i

    computer = ShuffleComputer(map_dict)

    for instruction in get_instructions_list():
        if Method.DEAL_WITH_INCREMENT.value in instruction:
            input = [int(s) for s in str.split(instruction) if s.isdigit()][0]
            computer.get_instructions_and_actions()[Method.DEAL_WITH_INCREMENT.value](input)
        elif Method.DEAL_INTO_NEW_STACK.value in instruction:
            computer.get_instructions_and_actions()[Method.DEAL_INTO_NEW_STACK.value]()
        elif Method.CUT.value in instruction:
            input = int(instruction.split(" ")[1])
            computer.get_instructions_and_actions()[Method.CUT.value](input)

    print(computer.current_deck)

    return computer.get_position_of_card(2019)

if __name__ == "__main__":
    print(get_solution_1())


import os

import yaml
import numpy as np
import pandas as pd
from tqdm import tqdm
from numba import jit

import indicators

def get_config(config_path):
	with open(config_path) as f:
		config = yaml.load(f, Loader = yaml.FullLoader)
	return config

def get_fx_file_paths(fx_pair, agg_level, agg_magnitude, tech_indicators, data_dir, is_test = False,
    train_end = '2019-02-01'):
    input_path = os.path.join(data_dir, 'csv')
    output_path = os.path.join(data_dir, 'npy')
    train_end = np.datetime64(train_end)
    phases = ['train', 'test']
    file_paths = {phase :
        os.path.join(output_path,
            f"""{fx_pair.upper()}_{str(agg_magnitude)}_{agg_level}_{phase}_{'_'.join([str(y)
                for x in list(tech_indicators.items())
                for y in x]).replace(', ', '_')}.npy""")
        for phase in phases}
    # Return if data exists
    if os.path.exists(file_paths['train']):
        return file_paths['train']
    # Otherwise create it
    print(f'Creating data {file_paths}')
    file_list = [x for x in os.listdir(input_path) if fx_pair in x]
    fx_npy = {x : None for x in phases}
    for file_name in tqdm(file_list, total = len(file_list)):
        # Read file and setup
        df = pd.read_csv(os.path.join(input_path, file_name), header = None,
            names = ['fx_pair', 'timestamp', 'bid', 'ask'])
        df = df.drop('fx_pair', axis = 1)
        df['timestamp'] = pd.to_datetime(df['timestamp'],
            format = '%Y%m%d %H:%M:%S.%f')
        df = df.dropna()
        df = df[['timestamp', 'bid', 'ask']]
        df['price'] = df[['bid', 'ask']].mean(axis = 1)
        df = df.drop(['bid', 'ask'], axis = 1)
        # Set phase
        if df['timestamp'].max() < train_end:
            phase = 'train'
        else:
            phase = 'test'
        # Aggregate data at level and magnitude
        df = df.set_index('timestamp')
        df = df.resample(f'{agg_magnitude}{agg_level}',
            label = 'right').ohlc()['price'].reset_index()
        df['timestamp'] = df['timestamp'].astype(int)
        # Calc technical indicators
        for indicator,params in tech_indicators.items():
            if indicator == 'macd':
                df = indicators.macd(df, *params)
            elif indicator == 'rsi':
                df = indicators.rsi(df, *params)
            elif indicator == 'adx':
                df = indicators.adx(df, *params)
            else:
                raise Exception(f'Unrecognized technical indicator {indicator}')
        # Add to phase numpy array
        df_np = df.dropna().to_numpy()
        assert np.sum(np.isnan(df_np)) == 0
        if fx_npy[phase] is None:
            fx_npy[phase] = df_np
        else:
            fx_npy[phase] = np.vstack([fx_npy[phase], df_np])

    for phase in fx_npy.keys():
        np.save(file_paths[phase], fx_npy[phase])
    file_path = file_paths['test'] if is_test else file_paths['train']
    return file_path

@jit(nopython = True)
def calc_exit_price(tick_data, start_index, position, stop_loss, pip_size, enter_price):
    stop_loss_amount = stop_loss * pip_size
    if position == 'long':
        max_price = enter_price
        for i in range(start_index,tick_data.shape[0]):
            _, bid, ask = tick_data[i,:]
            stop_loss_price = max_price - stop_loss_amount
            if max_price < bid:
                max_price = bid
                continue
            elif bid <= stop_loss_price:
                return stop_loss_price
            elif i == (tick_data.shape[0] - 1):
                return bid
    elif position == 'short':
        min_price = enter_price
        for i in range(start_index,tick_data.shape[0]):
            _, bid, ask = tick_data[i,:]
            stop_loss_price = min_price + stop_loss_amount
            if min_price > ask:
                min_price = ask
                continue
            elif ask >= stop_loss_price:
                return stop_loss_price
            elif i == (tick_data.shape[0] - 1):
                return ask
    else:
        return np.nan


import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt
from Run_MAXBAND import RunMaxband
from Process_Results import ProcessResults
from config import Config
from Utilities import create_tooltip, tooltips
import matplotlib as matplotlib
matplotlib.use('TkAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg,
    NavigationToolbar2Tk
)

Cfg = Config()

status_dict = {
    2: 'Solution Found',  # Indicating that the problem is solvable, but potentially a local optimum
    1: 'Optimal Solution Found',  # Global optimum, problem solved
    0: 'No Solution Found',  # Still working on the problem
    -1: 'No Solution Exists',  # The problem is infeasible
    -2: 'Solution is Unbounded'  # The problem is unbounded
}

def check_float(new_val) -> bool:
    valid = False
    try:
        if isinstance(float(new_val), float):
            valid = True
    except ValueError:
        print(f"Must enter a float, which {new_val} is not")
        valid = False
    return valid

class MaxbandGUI(tk.Tk):

    def __init__(self):
        super().__init__()
        self.title('MAXBAND MILP')
        self.protocol("WM_DELETE_WINDOW", self.quit_program)
        self.geometry("1500x900")
        self.min_signals = 2
        self.max_signals = 10
        self.current_nsignals = 7

        self.single_inputs = ['nsignals', 'c_min', 'c_max', 'v_min', 'v_max', 'inv_dv_min', 'inv_dv_max', 'k']
        self.single_entry_variables = dict.fromkeys(self.single_inputs)
        self.single_entries: dict = dict.fromkeys(self.single_inputs)
        self.single_entry_labels: dict = dict.fromkeys(self.single_inputs)

        self.segment_inputs = ['outbound_d', 'inbound_d']
        self.segment_entry_variables = dict.fromkeys(self.segment_inputs)
        self.segment_entries: dict = dict.fromkeys(self.segment_inputs)
        self.segment_side_entry_labels: dict = dict.fromkeys(self.segment_inputs)
        self.segment_top_entry_labels: list = []

        self.signal_inputs = ['outbound_r', 'inbound_r', 'outbound_l', 'inbound_l', 'outbound_tau', 'inbound_tau']
        self.signal_entry_variables = dict.fromkeys(self.signal_inputs)
        self.signal_entries: dict = dict.fromkeys(self.signal_inputs)
        self.signal_side_entry_labels: dict = dict.fromkeys(self.signal_inputs)
        self.signal_top_entry_labels: list = []

        self.constraint_flags: list[str] = ['leftturnleadlag', 'lt_leadlag_flag', 'lt_leadlead_flag', 'lt_laglag_flag',
                                            'mi_mj_max_1_flag', 'm_max_1_flag', 'tau_cstr_flag', 'tau_sum_flag',
                                            'w_0_flag', 'w_mono_flag']
        self.checkbutton_variables: dict = dict.fromkeys(self.constraint_flags)
        self.constraint_checkbuttons: dict = dict.fromkeys(self.constraint_flags)
        self.constraint_checkbutton_labels: dict = dict.fromkeys(self.constraint_flags)

        self.inputs: dict = {}
        self.outputs: dict = {}
        self.status = None
        self.run_counter = 0

        self.print_labels: dict = {}
        self.figure_canvas = None
        self.figure_toolbar = None

        self.check_signals = (self.register(self.check_signals), '%P')
        self.check_float_wrapper = (self.register(check_float), '%P')
        self.__create_widgets()

    def __create_widgets(self) -> None:
        self.create_segment_inputs()
        self.create_signal_inputs()
        self.create_single_inputs()
        self.create_constraint_selections()
        self.create_run_maxband_button()

        self.draw_widgets()
        self.fill_from_config()
        self.create_widget_tooltips()

    def quit_program(self) -> None:
        print('Shutting down')
        self.quit()
        self.destroy()

    def create_segment_inputs(self) -> None:
        outbound_d_list: list = []
        inbound_d_list: list = []
        segment_side_labels = ['Outbound length [m]', 'Inbound length [m]']

        for i in range(self.current_nsignals-1):
            outbound_d_list.append(tk.StringVar())
            inbound_d_list.append(tk.StringVar())

        self.segment_entry_variables: dict = {
            'outbound_d': outbound_d_list,
            'inbound_d': inbound_d_list
        }

        for key in self.segment_entries:
            self.segment_entries[key] = [None] * (self.current_nsignals - 1)

        for i, segment_input in enumerate(self.segment_inputs):
            self.segment_side_entry_labels[segment_input] = ttk.Label(self, text=segment_side_labels[i])
            for segment_index in range(self.current_nsignals-1):
                self.segment_entries[segment_input][segment_index] = \
                    ttk.Entry(self,
                              textvariable=self.segment_entry_variables[segment_input][segment_index],
                              validatecommand=self.check_float_wrapper,
                              validate='key',
                              width=6)

        for i in range(self.current_nsignals-1):
            self.segment_top_entry_labels.append(ttk.Label(self, text=f'Segment {i + 1}'))

    def create_signal_inputs(self) -> None:
        outbound_r_list: list = []
        inbound_r_list: list = []
        outbound_l_list: list = []
        inbound_l_list: list = []
        outbound_tau_list: list = []
        inbound_tau_list: list = []

        for i in range(self.current_nsignals):
            outbound_r_list.append(tk.StringVar())
            inbound_r_list.append(tk.StringVar())
            outbound_l_list.append(tk.StringVar())
            inbound_l_list.append(tk.StringVar())
            outbound_tau_list.append(tk.StringVar())
            inbound_tau_list.append(tk.StringVar())

        self.signal_entry_variables: dict = {
            'outbound_r': outbound_r_list,
            'inbound_r': inbound_r_list,
            'outbound_l': outbound_l_list,
            'inbound_l': inbound_l_list,
            'outbound_tau': outbound_tau_list,
            'inbound_tau': inbound_tau_list
        }

        for key in self.signal_entries:
            self.signal_entries[key] = [None] * self.current_nsignals

        side_labels = ['Outbound red time [cycles]', 'Inbound red time [cycles]',
                       'Outbound left turn time [cycles]', 'Inbound left turn time [cycles]',
                       'Outbound queue clearance time [cycles]', 'Inbound queue clearance time [cycles]']

        for i, signal_input in enumerate(self.signal_inputs):
            self.signal_side_entry_labels[signal_input] = ttk.Label(self, text=side_labels[i])
            for signal_index in range(self.current_nsignals):
                self.signal_entries[signal_input][signal_index] = \
                    ttk.Entry(self,
                              textvariable=self.signal_entry_variables[signal_input][signal_index],
                              validatecommand=self.check_float_wrapper,
                              validate='key',
                              width=6)

        for i in range(self.current_nsignals):
            self.signal_top_entry_labels.append(ttk.Label(self, text=f'Signal {i + 1}'))

    def create_single_inputs(self) -> None:
        single_input_labels = ['nsignals [-]', 'c_min [s]', 'c_max [s]', 'v_min [km/h]', 'v_max [km/h]',
                               'inv_dv_min [h/km]', 'inv_dv_max [h/km]', 'k [-]']

        self.single_entry_variables: dict = {
            'nsignals': tk.StringVar(),
            'c_min': tk.StringVar(),
            'c_max': tk.StringVar(),
            'v_min': tk.StringVar(),
            'v_max': tk.StringVar(),
            'inv_dv_min': tk.StringVar(),
            'inv_dv_max': tk.StringVar(),
            'k': tk.StringVar()
        }

        self.single_entries['nsignals'] = ttk.Spinbox(self,
                                                      textvariable=self.single_entry_variables['nsignals'],
                                                      from_=self.min_signals,
                                                      to=self.max_signals,
                                                      increment=1,
                                                      validatecommand=self.check_signals,
                                                      validate='key',
                                                      width=3)
        self.single_entries['nsignals'].bind('<<Increment>>', self.increase_nsignals)
        self.single_entries['nsignals'].bind('<<Decrement>>', self.decrease_nsignals)
        self.single_entry_variables['nsignals'].set(self.current_nsignals)

        for single_input in self.single_inputs[1:]:
            self.single_entries[single_input] = \
                ttk.Entry(self,
                          textvariable=self.single_entry_variables[single_input],
                          validatecommand=self.check_float_wrapper,
                          validate='key',
                          width=4)

        for i in range(len(self.single_inputs)):
            self.single_entry_labels[self.single_inputs[i]] = ttk.Label(self, text=single_input_labels[i])

    def create_constraint_selections(self) -> None:
        self.checkbutton_variables: dict = {
            'leftturnleadlag': tk.BooleanVar(),
            'lt_leadlag_flag': tk.BooleanVar(),
            'lt_leadlead_flag': tk.BooleanVar(),
            'lt_laglag_flag': tk.BooleanVar(),
            'mi_mj_max_1_flag': tk.BooleanVar(),
            'm_max_1_flag': tk.BooleanVar(),
            'tau_cstr_flag': tk.BooleanVar(),
            'tau_sum_flag': tk.BooleanVar(),
            'w_0_flag': tk.BooleanVar(),
            'w_mono_flag': tk.BooleanVar()
        }
        for i, flag in enumerate(self.constraint_flags):
            checkbutton = ttk.Checkbutton(master=self, name=flag, variable=self.checkbutton_variables[flag],
                                          onvalue=True, offvalue=False)
            self.constraint_checkbuttons[flag] = checkbutton
            self.constraint_checkbutton_labels[flag] = ttk.Label(master=self, text=flag)

    def create_run_maxband_button(self) -> None:
        run_maxband_button = ttk.Button(self, text='Run Maxband', command=self.maxband_process)
        run_maxband_button.grid(column=2, row=13)

    def draw_widgets(self) -> None:
        single_inputs_header = ttk.Label(self, text='Single Inputs', font='Helvetica 12 bold')
        single_inputs_header.grid(column=0, row=0, columnspan=2)
        for i, key in enumerate(self.single_entries):
            self.single_entry_labels[key].grid(column=0, row=i+1)
            self.single_entries[key].grid(column=1, row=i+1)

        segment_inputs_header = ttk.Label(self, text='Segment Inputs', font='Helvetica 12 bold')
        segment_inputs_header.grid(column=2, row=0)
        for i, segment_input in enumerate(self.segment_inputs):
            self.segment_side_entry_labels[segment_input].grid(column=2, row=i+1)
            for segment_index in range(self.current_nsignals-1):
                self.segment_entries[segment_input][segment_index].grid(column=segment_index+3, row=i+1)
        for i in range(self.current_nsignals-1):
            self.segment_top_entry_labels[i].grid(column=i+3, row=0)

        signal_inputs_header = ttk.Label(self, text='Signal Inputs', font='Helvetica 12 bold')
        signal_inputs_header.grid(column=2, row=4)
        for i, signal_input in enumerate(self.signal_inputs):
            self.signal_side_entry_labels[signal_input].grid(column=2, row=i+5)
            for signal_index in range(self.current_nsignals):
                self.signal_entries[signal_input][signal_index].grid(column=signal_index+3, row=i+5)
        for i in range(self.current_nsignals):
            self.signal_top_entry_labels[i].grid(column=i+3, row=4)

        for i, check_button in enumerate(self.constraint_flags):
            self.constraint_checkbuttons[check_button].grid(column=1, row=i+10)
            self.constraint_checkbutton_labels[check_button].grid(column=0, row=i+10)

    def fill_from_config(self) -> None:
        if not Cfg.check_for_config_file():  # There is no config to fill from
            return
        single_input_dict = Cfg.get_single_input_config()
        for key in self.single_entry_variables:
            self.single_entry_variables[key].set(single_input_dict[key])

        segment_input_dict = Cfg.get_segment_input_config()
        for key in self.segment_entry_variables:
            segment_input_list = list(segment_input_dict[key].strip("['']").split("', '"))
            for segment in range(self.current_nsignals-1):
                self.segment_entry_variables[key][segment].set(segment_input_list[segment])

        signal_input_dict = Cfg.get_signal_input_config()
        for key in self.signal_entry_variables:
            signal_input_list = list(signal_input_dict[key].strip("['']").split("', '"))
            for signal in range(self.current_nsignals):
                self.signal_entry_variables[key][signal].set(signal_input_list[signal])

        selection_input_dict = Cfg.get_selection_input_config()
        for key in self.checkbutton_variables:
            self.checkbutton_variables[key].set(selection_input_dict[key])

    def get_single_inputs(self) -> dict:
        single_inputs = dict.fromkeys(self.single_inputs)
        for inputs in self.single_inputs:
            single_inputs[inputs] = self.single_entry_variables[inputs].get()
        return single_inputs

    def get_segment_inputs(self) -> dict:
        segment_inputs = dict.fromkeys(self.segment_inputs)
        for inputs in self.segment_inputs:
            segment_inputs[inputs] = list([inp.get() for inp in self.segment_entries[inputs]])
        return segment_inputs

    def get_signal_inputs(self) -> dict:
        signal_inputs = dict.fromkeys(self.signal_inputs)
        for inputs in self.signal_inputs:
            signal_inputs[inputs] = list([inp.get() for inp in self.signal_entries[inputs]])
        return signal_inputs

    def get_selections(self) -> dict:
        selections = dict.fromkeys(self.constraint_flags)
        for flag in self.constraint_flags:
            selections[flag] = self.checkbutton_variables[flag].get()
        return selections

    def get_all_inputs(self) -> dict:
        all_inputs = dict()
        all_inputs['SingleInputs'] = self.get_single_inputs()
        all_inputs['SegmentInputs'] = self.get_segment_inputs()
        all_inputs['SignalInputs'] = self.get_signal_inputs()
        all_inputs['Selections'] = self.get_selections()
        return all_inputs

    def increase_nsignals(self, event) -> None:
        self.current_nsignals = int(self.single_entry_variables['nsignals'].get())
        print(self.current_nsignals, len(self.signal_entry_variables[self.signal_inputs[0]]))
        if self.current_nsignals >= 10 or len(self.signal_entry_variables[self.signal_inputs[0]]) == self.max_signals:
            print('Cannot go higher')
            return
        for i, key in enumerate(self.segment_entries):
            self.segment_entry_variables[key].append(tk.StringVar())
            self.segment_entries[key].append(ttk.Entry(self,
                                                       textvariable=self.segment_entry_variables[key][-1],
                                                       validatecommand=self.check_float_wrapper,
                                                       validate='key',
                                                       width=6))
            self.segment_entries[key][-1].grid(column=self.current_nsignals+2, row=i+1)

        self.segment_top_entry_labels.append(ttk.Label(self, text=f'Segment {self.current_nsignals}'))
        self.segment_top_entry_labels[-1].grid(column=self.current_nsignals+2, row=0)

        for i, key in enumerate(self.signal_inputs):
            self.signal_entry_variables[key].append(tk.StringVar())
            self.signal_entries[key].append(ttk.Entry(self,
                                                      textvariable=self.signal_entry_variables[key][-1],
                                                      validatecommand=self.check_float_wrapper,
                                                      validate='key',
                                                      width=6))
            self.signal_entries[key][-1].grid(column=self.current_nsignals+3, row=i+5)

        self.signal_top_entry_labels.append(ttk.Label(self, text=f'Signal {self.current_nsignals+1}'))
        self.signal_top_entry_labels[-1].grid(column=self.current_nsignals+3, row=4)

    def decrease_nsignals(self, event) -> None:
        #TODO: investigate usage of widget.forget() instead of destroy().
        # Make this method more robust... sometimes doesn't work
        # Pack forget means the widgets still exsist, so they can cause memory leak if repeatedly created
        self.current_nsignals = int(self.single_entry_variables['nsignals'].get())
        print(self.current_nsignals, len(self.signal_entry_variables[self.signal_inputs[0]]))
        if self.current_nsignals <= 2 or len(self.signal_entry_variables[self.signal_inputs[0]]) == self.min_signals:
            print('Cannot go lower')
            return
        for key in self.segment_entries:
            self.segment_entries[key][self.current_nsignals-2].destroy()
            self.segment_entry_variables[key].pop()
        self.segment_top_entry_labels[self.current_nsignals-2].destroy()

        for key in self.signal_inputs:
            self.signal_entries[key][self.current_nsignals-1].destroy()
            self.signal_entry_variables[key].pop()
        self.signal_top_entry_labels[self.current_nsignals-1].destroy()

    def check_signals(self, new_val) -> bool:
        valid = False
        try:
            if isinstance(int(new_val), int):
                valid = True
            if int(new_val) < self.min_signals:
                print(f"Minimum value is {self.min_signals}")
                valid = False
            if int(new_val) > self.max_signals:
                print(f"Maximum value is {self.max_signals}")
                valid = False
        except ValueError:
            print(f"Must enter an integer, which {new_val} is not")
            valid = False
        return valid

    def create_widget_tooltips(self) -> None:
        for label in self.single_entry_labels:
            create_tooltip(self.single_entry_labels[label], tooltips[label])
        for label in self.segment_side_entry_labels:
            create_tooltip(self.segment_side_entry_labels[label], tooltips[label])
        for label in self.signal_side_entry_labels:
            create_tooltip(self.signal_side_entry_labels[label], tooltips[label])
        for label in self.constraint_checkbutton_labels:
            create_tooltip(self.constraint_checkbutton_labels[label], tooltips[label])

    def maxband_process(self) -> None:
        """"Handles the run_maxband button press, destroying previous results, gathering all input, saving input to
        config, running maxband and finally printing and plotting the new results"""
        self.destroy_previous_print_results()
        self.destroy_previous_plot_frame()
        self.inputs = self.get_all_inputs()
        Cfg.store_dict_into_config_file(self.inputs)
        rm = RunMaxband(self.inputs)
        self.outputs, self.status = rm.run_maxband()
        self.run_counter += 1
        PR = ProcessResults(self.inputs, self.outputs)
        processed_results = PR.get_processed_results_dict()
        self.create_print_labels(processed_results)
        self.draw_print_labels()
        if int(self.status) != 1:
            return
        self.create_plot_frame(PR)
        self.draw_plot_frame()

    def create_print_labels(self, processed_results) -> None:
        self.print_labels['status_label'] = ttk.Label(self, text=f'Problem status: {status_dict[self.status]}')
        if int(self.status) != 1:
            return
        self.print_labels['ct_label'] = ttk.Label(self, text=f'Cycle Time: {1/self.outputs["inv_CT"]:.1f} [s]')

        if float(self.inputs['SingleInputs']['k']) == 1:  # outbound bandwidth equals inbound bandwidth
            self.print_labels['band_label'] = ttk.Label(self, text=f'Bandwidth: {self.outputs["OB_band"]/self.outputs["inv_CT"]:.1f} [s]')
            self.print_labels['frac_band_label'] = ttk.Label(self, text=f'Fractional bandwidth: {self.outputs["OB_band"]:.2f} [cycles]')

        elif float(self.inputs['SingleInputs']['k']) != 1:
            self.print_labels['ob_band_label'] = ttk.Label(self, text=f'Outbound bandwidth: {self.outputs["OB_band"]/self.outputs["inv_CT"]:.1f} [s]')
            self.print_labels['ob_frac_band_label'] = ttk.Label(self, text=f'Fractional outbound bandwidth: {self.outputs["OB_band"]:.2f} [cycles]')
            self.print_labels['ib_band_label'] = ttk.Label(self, text=f'Inbound bandwidth: {self.outputs["IB_band"]/self.outputs["inv_CT"]:.1f} [s]')
            self.print_labels['ib_frac_band_label'] = ttk.Label(self, text=f'Fractional inbound bandwidth: {self.outputs["IB_band"]:.2f} [cycles]')

        self.print_labels['ob_speeds_label'] = ttk.Label(self, text=f'Outbound speeds: {processed_results["OB_speeds"]} [km/h]')
        self.print_labels['ib_speeds_label'] = ttk.Label(self, text=f'Inbound speeds: {processed_results["IB_speeds"]} [km/h]')

    def draw_print_labels(self) -> None:
        for i, key in enumerate(self.print_labels):
            self.print_labels[key].grid(column=0, row=19+i, columnspan=3, sticky='W')

    def destroy_previous_print_results(self) -> None:
        if self.run_counter == 0:  # First run, there is no output, return
            return
        for key in self.print_labels:
            self.print_labels[key].destroy()
        self.print_labels: dict = {}

    def create_plot_frame(self, PR) -> None:
        # TODO: implement a way to enter intersection names dynamically
        # intersection_names = ['2016', '2017', '2018', '2019', '2020', '2021', '2024']
        fig = Figure(figsize=(8, 5))
        # fig.patch.set_facecolor('skyblue')  # this can set the color around the plot, inside the frame
        # TODO: Implement some plot customization options
        PR.plot_MAXBAND(fig)
        fig.tight_layout()
        self.figure_canvas = FigureCanvasTkAgg(fig, self)
        self.figure_toolbar = NavigationToolbar2Tk(self.figure_canvas, self, pack_toolbar=False)

    def draw_plot_frame(self) -> None:
        self.figure_canvas.get_tk_widget().grid(column=3, row=12, columnspan=50, rowspan=28, sticky='NSEW')
        self.figure_toolbar.grid(columns=3, row=12+28, columnspan=50)

    def destroy_previous_plot_frame(self) -> None:
        if self.run_counter == 0:
            return
        self.figure_canvas.get_tk_widget().destroy()
        self.figure_canvas = None
        self.figure_toolbar.destroy()
        self.figure_toolbar = None


"contains class for persons"
#--------------------------------------------------------------------------------------------------#
import logging
import math
import time
import person

class Group:
    "class for handling a crowd of persons"
    def __init__(self):
        self.person_instances = []
        self.logger = logging.getLogger('training_logger')
        self.person_counter = 0

    def next_frame_aging(self, frame_count):
        "ages each person by one and de-lists 'done' persons"
        persons_temp =[]
        for person_instance in self.person_instances:
            person_instance.age_one(frame_count)  # age every person each frame
            if person_instance.done is False:
                persons_temp.append(person_instance)
                self.logger.debug('next_frame_aging(): person_id %s was aged by one, but is not '\
                    'too old, i.e. is still part of person_instances', person_instance.person_id)
        self.person_instances = persons_temp

    def get_closest_person(self, contour_centroid_xt_new, contour_centroid_yt_new):
        "selects the closest person. This is algorithm can still be improved on: Think of a "\
        "situation where two centroids are to be matched to two persons. both are within the "\
        "new_person_x_thresholds, but the one that is closest to person 1 should actually be "\
        "fitted to person 2, and the valid 'leftover' centroid, should be assigend to person 1"
        distances = []
        closest_person_id = None
        # sort persons by age, look which contours are within their new_person_thresholds and
        # assign the cluster to this person
        for person_instance in self.person_instances:
            person_instance_distance = math.sqrt(\
                (person_instance.contour_centroid_xt - contour_centroid_xt_new)**2
                + (person_instance.contour_centroid_yt - contour_centroid_yt_new)**2
            )
            # introduce additional pull if person is of age
            person_instance_distance =max(0, person_instance_distance \
                - min(person_instance.age * 2, 75))
            distances.append((person_instance.person_id, person_instance_distance))
        try:
            closest_person_id = min(distances, key = lambda t: t[1])[0]
        except ValueError:
            pass

        self.logger.debug("closest person to contour centroids (%s,%s) is personID %s "\
            ,  contour_centroid_xt_new, contour_centroid_yt_new, closest_person_id)
        return closest_person_id

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

    def calculate_cluster_person_distance_list(self, cluster_instances, parameter_space_instance):
        """calculates distances from clusters to persons. If no person is in reach, a new person is
        created and assigned"""
        self.logger.debug("calculate_cluster_person_distance_list(): creating"\
            "cluster_person_distance_list...")
        cluster_person_distance_list = []
        for cluster_instance in cluster_instances:
            one_person_within_threshold = False

            for person_instance in self.person_instances:
                distance = int(math.sqrt(\
                    (person_instance.contour_centroid_xt - cluster_instance.centroid_x)**2
                    + (person_instance.contour_centroid_yt - cluster_instance.centroid_y)**2))
                self.logger.debug("calculate_cluster_person_distance_list(): person_instance"\
                    ".person_id %s and cluster_instance.cluster_id %s have distance %s"\
                    , person_instance.person_id, cluster_instance.cluster_id, distance)
                if distance < parameter_space_instance.new_person_horizontal_threshold:
                    cluster_person_distance_list.append([ cluster_instance.cluster_id\
                        , person_instance.person_id, distance])
                    self.logger.debug("calculate_cluster_person_distance_list(): appended to "\
                        "cluster_person_distance_list: %s, %s, %s "\
                        , person_instance.person_id, cluster_instance.cluster_id, distance)
                    one_person_within_threshold = True

            if one_person_within_threshold is False:
                person_instance = person.MyPerson(self.person_counter\
                    , cluster_instance.centroid_x, cluster_instance.centroid_y\
                    , parameter_space_instance.person_age_threshold)
                self.logger.debug("calculate_cluster_person_distance_list(): no existing person in"\
                    "reach, created a new person: person_counter %s, cluster_id %s,  y %s, x %s"\
                    , str(self.person_counter), str(cluster_instance.cluster_id)\
                    ,  str(cluster_instance.centroid_x), str(cluster_instance.centroid_y))
                self.person_instances.append(person_instance)
                self.person_counter += 1
                distance = 0
                cluster_person_distance_list.append([cluster_instance.cluster_id\
                    , person_instance.person_id, distance])

        return cluster_person_distance_list

    def person_to_cluster_assignment(self, cluster_person_distance_list):
        "resolves matching: every person to one contour within thresholds.For those with more than"\
        "one contour at choice, let persons with one choice choose first. then go through the"\
        "persons with more than one choice and assign the closest cluster form the clusters that"\
        " are still available "
        free_clusters_set = set([ x[0] for x in cluster_person_distance_list ])
        free_persons_set = set([ x[1] for x in cluster_person_distance_list ])
        person_to_cluster_assignmen_list = [] # reset person_to_cluster_assignment
        # first iteration:
        self.logger.debug("person_to_cluster_assignment(): free_clusters_set = %s "\
            ", free_persons_set = %s starting first iteration to assign clusters to persons..."\
            , free_clusters_set, free_persons_set)
        self.person_instances.sort(key=lambda x: x.age, reverse=False)
        for person_instance in self.person_instances:
            prospects = [ x for x in cluster_person_distance_list if x[1] \
                == person_instance.person_id]
            self.logger.debug("person_id %s has the following prospective clusters: %s" \
                ,person_instance.person_id, prospects)
            if len(prospects) == 1 and prospects[0][0] in free_clusters_set:
                person_to_cluster_assignmen_list.append([person_instance.person_id\
                    , prospects[0][0]])
                free_clusters_set.remove(prospects[0][0])
                free_persons_set.remove(prospects[0][1])
                self.logger.debug("cluster_id %s is assigned to person_id %s,because person_id has"\
                    "exactly one cluster in reach",  prospects[0][0], person_instance.person_id )

        self.logger.debug("lenth cluster_person_distance_list is %s before removing assinged "\
            "clusters", len(cluster_person_distance_list))
        cluster_person_distance_list_temp = []
        for item in cluster_person_distance_list:
            if item[0] in free_clusters_set and item[1] in free_persons_set:
                cluster_person_distance_list_temp.append(item)
        cluster_person_distance_list = cluster_person_distance_list_temp
        self.logger.debug("lenth cluster_person_distance_list is %s after removing assinged "\
            "clusters", len(cluster_person_distance_list))

        if len(cluster_person_distance_list) == 0:
            return person_to_cluster_assignmen_list

        # second iteration:
        self.logger.debug("starting second iteration to assign clusters to persons...")
        self.person_instances.sort(key=lambda x: x.age, reverse=False)
        for person_instance in self.person_instances:
            if person_instance.person_id in free_persons_set:
                prospects = [ x for x in cluster_person_distance_list if x[1] \
                    == person_instance.person_id]
                if len(prospects) == 0:
                    self.logger.debug("person_id %s has no prospects in second iteration. "\
                        "continuing to next person", person_instance.person_id)
                    continue
                self.logger.debug("person_id %s has the following prospects : %s (from "\
                    "cluster_person_distance_list)", person_instance.person_id, prospects)
                prospect_min = min(prospects, key = lambda t: t[2])
                self.logger.debug("person_id %s has the following min-prospect : %s (from "\
                    "cluster_person_distance_list)", person_instance.person_id, prospect_min)

                person_to_cluster_assignmen_list.append([person_instance.person_id\
                    , prospect_min[0]])
                free_clusters_set.remove(prospect_min[0])
                free_persons_set.remove(prospect_min[1])
                self.logger.debug("person_to_cluster_assignment(): free_clusters_set = %s, "\
                    "free_persons_set = %s in second iteration after removal"\
                    , free_clusters_set, free_persons_set)
                self.logger.debug("lenth cluster_person_distance_list is %s before removing "\
                    "assinged clusters", len(cluster_person_distance_list))
                cluster_person_distance_list_temp = []
                for item in cluster_person_distance_list:
                    if item[0] in free_clusters_set and item[1] in free_persons_set:
                        cluster_person_distance_list_temp.append(item)
                cluster_person_distance_list = cluster_person_distance_list_temp
                self.logger.debug("lenth cluster_person_distance_list is %s after removing "\
                    "assinged clusters", len(cluster_person_distance_list))

        # there can be person_instances without cluster, and also cluster without person instance.
        if len(free_clusters_set) > 0:
            self.logger.debug("clusters %s are unassigned after completion of assignment" \
                , free_clusters_set)
        return person_to_cluster_assignmen_list

    def runner2(self, cluster_instances, parameter_space_instance, ai_db,cnt_up, cnt_down\
        , frame_count):
        "makeover of the legendary runner "
        cluster_person_distance_list = self.calculate_cluster_person_distance_list(\
            cluster_instances, parameter_space_instance)
        person_to_cluster_assignmen_list = self.person_to_cluster_assignment(\
            cluster_person_distance_list)

        for person_cluster in person_to_cluster_assignmen_list:
            person_instance = [x for x in self.person_instances if x.person_id \
                == person_cluster[0]][0]
            cluster_instance = [x for x in cluster_instances if x.cluster_id \
                == person_cluster[1]][0]

            person_instance.update_coords(cluster_instance.centroid_x, cluster_instance.centroid_y)
            person_instance.update_age(parameter_space_instance.age_reset_down_thres\
                , parameter_space_instance.age_reset_up_thres)
            person_instance.update_thrust()

            self.logger.info("matched person to cluster: Person ID: %s | cluster_id %s | "\
                "frame_count %s | P(x/y): (%s / %s) | thrust_absolute: %s | thrust_binary %s | "\
                "age %s  ", str(person_instance.person_id), str(cluster_instance.cluster_id)\
                , str(frame_count), str(cluster_instance.centroid_x)\
                , str(cluster_instance.centroid_y)\
                , str(person_instance.thrust_absolute), str(person_instance.thrust_binary)\
                , str(person_instance.age))

            if person_instance.crossing_up(parameter_space_instance.thrust_abs_crossing_threshold\
                , parameter_space_instance.thrust_bin_crossing_threshold) is True:
                cnt_up += 1
                change_up_or_down = 1
                person_instance.has_crossed_up = True
                # reset thrust
                person_instance.thrust_absolute = 0
                person_instance.thrust_binary = 0

                #  write message into db
                capacity, latest_person_count, direction \
                    = ai_db.get_current_data()
                latest_person_count = max(latest_person_count \
                    + change_up_or_down * direction, 0)
                ai_db.set_current_data(capacity, latest_person_count, direction)

                # logging
                self.logger.info("ID: %s crossed going up at %s " \
                    , str(person_instance.get_id()), str(time.strftime("%c")))
                self.logger.debug("current person count: %s" \
                    ,  str(latest_person_count))
                #self.logger_reporting.info("%s;%s;",  str(change_up_or_down * direction) \
                #    ,str(latest_person_count) )

            if person_instance.crossing_down(parameter_space_instance.thrust_abs_crossing_threshold\
                , parameter_space_instance.thrust_bin_crossing_threshold) is True:
                cnt_down += 1
                change_up_or_down = -1
                person_instance.has_crossed_down = True
                # reset thrust
                person_instance.thrust_absolute = 0
                person_instance.thrust_binary = 0

                # write message into db
                capacity, latest_person_count, direction \
                    = ai_db.get_current_data()
                latest_person_count = max(latest_person_count \
                    + change_up_or_down * direction, 0)
                ai_db.set_current_data(capacity, latest_person_count, direction)

                #logging
                self.logger.info("ID: %s crossed going down at %s " \
                    , str(person_instance.get_id()), str(time.strftime("%c")))
                self.logger.debug("current person count: %s "\
                    , str(latest_person_count))
                #self.logger_reporting.info("%s;%s;", str(change_up_or_down * direction) \
                #    , str(latest_person_count))
        return cnt_up, cnt_down

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