__version__ = "0.3"
__author__ = "Thomas Baldauf"
__email__ = "thomas.baldauf@dlr.de"
__license__ = "MIT"
__birthdate__ = '15.11.2021'
__status__ = 'prod'  # options are: dev, test, prod

import time

# sfctools Modules
from ..datastructs.collection import Collection
from .clock import Clock
from .singleton import Singleton
from ..core.custom_warnings import warning_on_one_line

# Modules for Visuals
# from ..world_visuals.qt_viz_window import QtVizWindow
from ..world_visuals.worldvisuals import WorldVisuals

# PyQt5 Modules
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QFrame, QPushButton, QGridLayout, QWidget
from PyQt5.QtGui import QPainter, QColor, QPixmap, QPen
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtCore import QLoggingCategory, Qt, QEvent
from PyQt5.QtGui import QPixmap, QPainter, QPdfWriter
from PyQt5.QtCore import QRectF, QPointF, QMarginsF, QSizeF, QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QFileDialog

# general modules
import os
import warnings
import pandas as pd
import numpy as np
import sys
import gc
import threading


class World(Singleton):
    """
    This is the world where agents live in. There can only be one world at a time.
    """

    use_sfc_array = False  # boolean switch to indicate whether SfcArrays are in use

    def __init__(self, geometry=None):
        """
        The world constructor.

        Args:
            geometry (tuple): Window geometry (if visualization is enabled)
        """

        if hasattr(self, "initialized"):  # only initialize once
            return
        self.initialized = True

        # register the agents in a data structure
        self.agent_registry = Collection(kind="list")
        # register the helper structures here
        self.helper_registry = Collection(kind="list")

        self.globals = {}  # globally accessible data here
        self.agent_types = []

        self._visuals = WorldVisuals(geometry=geometry)

        warnings.formatwarning = warning_on_one_line

    def __getitem__(self, k):
        """
        short version of World().get_agents_of_type('...')
        """
        return self.get_agents_of_type(k)

    def get_agent_types(self):
        """
        Constructs a list of all agent types registered so far (i.e. keys of agent registry dict).

        :return list: list of agents
        """
        return self.agent_types  # return list directly

    def link(self, verbose=False):
        """
        Calls the 'link' fun in all the agents to inter-link them in the world
        """
        pool_a = list(self.helper_registry.values())
        pool_b = list(self.agent_registry.values())

        # if verbose:
        #    print("[WORLD] LINK ... %i agents" % (len(pool_a)+len(pool_b)))

        for agent_list in pool_a + pool_b:
            for agent in list(set(agent_list)):
                if hasattr(agent, "link"):
                    if callable(getattr(agent, "link")):
                        agent.link()
                        if verbose:
                            print("[WORLD] LINK", agent)
                    else:
                        if verbose:
                            warnings.warn(
                                "[WORLD] LINK: Link attr of %s is not callable. Skipping..." % str(agent))
                else:
                    if verbose:
                        warnings.warn(
                            "[WORLD] LINK: %s has no link attr." % agent)

                    # NOTE it is ok if it has no link attr

    def reset(self, verbose=False):
        """
        resets the registry of all agents (deletes agents)

        :param verbose: boolean (default False), triggers a reset warning if True
        """

        if verbose:
            warnings.warn("WORLD RESET - deleting all agents.")

        for agent_list in self.agent_registry.values():
            for agent in agent_list:
                try:
                    self.remove_agent(agent)
                    del agent
                except Exception as e:
                    warnings.warn(str(e))

        for elements in self.helper_registry.values():
            for element in elements:
                try:
                    self.remove_helper(element)
                    del element
                except Exception as e:
                    warnings.warn(str(e))

        # register the agents in a data structure
        self.agent_registry = Collection(kind="list")
        # register the helpers in a data structure
        self.helper_registry = Collection(kind="list")

        clock = Clock()
        if clock is not None:
            clock.reset()

        gc.collect()

    def register_agent(self, agent, verbose=False):
        """
        inserts a new agent under the tag of its class

        :param agent: an agent object to be registered
        """
        if verbose:
            print("[WORLD] Reigster Agent %s of class %s " %
                  (agent, agent.__class__))

        if agent.__class__ not in self.agent_types:
            # append new key if doesnt exist yet
            self.agent_types.append(agent.__class__)

        # write new agent to data structure
        self.agent_registry.insert(agent.__class__.__name__, agent)
        return len(self.agent_registry[agent.__class__.__name__])-1

    def register_helper(self, object):
        """
        inserts a new helper object under the tag of its class

        :param object: instance to store
        """
        self.helper_registry.insert(
            object.__class__.__name__, object)  # write new object

    def find_agent(self, name):
        """
        search for agent by name and alias

        :return: agent instance
        """

        for agent_list in self.agent_registry.values():
            for agent in agent_list:
                if agent.name == name or agent.alias == name or str(agent) == name:
                    return agent

        return None

    def remove_agent(self, agent, verbose=False):
        """
        removes an agent from the tag of its class (i.e. from the world)

        :param agent: instance to be removed
        """
        if verbose:
            print("[WORLD] Remove Agent %s of class %s " %
                  (agent, agent.__class__))

        if len(self.agent_registry[agent.__class__.__name__]) <= 1 and agent.__class__ in self.agent_types:
            # last agent? -> remove whole class from registry
            self.agent_types.remove(agent.__class__)

        # remove the agent from the tag
        self.agent_registry.remove(agent.__class__.__name__, agent)
        del agent  # delete the instance to assure proper removal of the agent

    def remove_helper(self, agent):
        """
        removes an agent from the tag of its class (i.e. from the world)

        :param agent: instance to be removed
        """
        # self.agent_types.remove(agent.__class__)
        # remove the agent from the tag
        self.helper_registry.remove(agent.__class__.__name__, agent)
        del agent  # delete the instance to assure proper removal of the agent

    def write_global(self, key, data):
        """
        write globally accessible data here

        :param key: key for the data (generic)
        :param data: value of the data (generic)
        """
        self.globals[key] = data

    def read_global(self, key):
        """
        reads global variable by key
        """
        return self.globals[key]

    def get_agents_of_type(self, agent_type):
        """
        gets all agents belonging to a certain tpe

        :param agent_type: key to search for in self.agent_registry
        :return: list of instances or empty list

        Example

        .. code-block:: python

            from sfctools import Agent,World

            class MyAgent(Agent):
                def __init__(self):
                    super().init()
                    # ...

            [MyAgent() for i in range(10)] # create 10 agents

            mylist = World().get_agents_of_type("MyAgent")
            print(mylist)

        """

        return self.agent_registry[agent_type]

    @property
    def visuals(self) -> WorldVisuals:
        return self._visuals
