import collections
import concurrent.futures
import logging
import time
from datetime import timedelta

from concurrentevents._exceptions import Cancel, StartError
from concurrentevents.event import Event, Start, Exit
from concurrentevents.eventhandler import EventHandler
from concurrentevents.tools import threadmonitor

logger = logging.getLogger('concurrentevents')


class EventManager:
    """
    A bare bones event framework using concurrent futures for threading

    Kwargs:
        :param threads: The number of threads, default to none for automatic calculation
        :type threads: int, optional
        :param class:'verbosity': The level of logs expected from the event system
        :type class:'verbosity': class:`concurrentevents.Verbosity`, int, optional
        :param process_tracking: Boolean for output of event system progress to logger
        :type process_tracking: bool, optional
        :param debug: Boolean for larger debug in the main thread
        :type debug: bool, optional
    """

    def __init__(self, threads=0, process_tracking=False, debug=False):
        """Constructor Method"""
        if threads < -1 if isinstance(threads, int) else True:
            raise ValueError(f"EventManager logger argument must be a int greater than 0 not {threads}")

        if not isinstance(process_tracking, bool):
            raise TypeError(f"EventManager process_tracking argument must be a bool not {process_tracking}")

        if not isinstance(debug, bool):
            raise TypeError(f"EventManager debug argument must be a bool not {debug}")

        # Main dictionary for all event functionality
        # Stores handlers mapped to events to allow for quick access to specific ordered functions
        # Outline:
        #   key: Event
        #   value: List of handler functions
        self.handlers = collections.defaultdict(list)

        # Futures Dictionary
        # This is a private dictionary used internally so that the main thread can manage and clean
        #   the running futures generated by the event system
        # Outline:
        #   key: Future Object
        #   value: (fn, args, kwargs)
        self.__futures = {}

        self.__thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=threads or None)

        # This boolean is set so that after firing the start event on the main thread there
        #   are no other instances of it that could be created and thus no duplicate cleaners
        self.started = False

        # This boolean is set so that any dedicated while loops not made by the Thread Pool Executor
        #   will be able to check if the program is meant to end
        self.exit = None

        # Option to enable logging of progress tracking for all events needed to run
        self.progress_tracking = process_tracking
        self.fired = 0
        self.finished = 0
        self.errors = 0
        self.canceled = 0

        # Debug boolean for main loop debug statements
        self.debug = debug

    def add_handlers(self, *event_handlers):
        """
        Establishes relationships between `EventManager` and `EventHandler` or decorated function

        Args:
            :param handlers: A list of `EventHandlers` or functions that will be added to the handler dictionary
        """
        # Parse and pull all handlers passed in
        # This enforces a strict relationship between the handler and manager
        for event_handler in event_handlers:

            # If the handler is an EventHandler
            if isinstance(event_handler, EventHandler):

                # Assign self to cls event_manager
                event_handler.__class__.event_manager = self

                # Loop through class and get catch functions
                for var in dir(event_handler):
                    func = getattr(event_handler, var)
                    if hasattr(func, 'event'):
                        self.handlers[func.event].append(func)

            # Check if function is callable and has an attribute called event
            elif callable(event_handler) and hasattr(event_handler, 'event'):
                self.handlers[event_handler.event].append(event_handler)

        return self

    def __handle(self, event):
        """
        Operates function specified to handle a specific event on that event

        This is the basis for the entire event system in terms of handling events in order
        and making sure the exit event ends the program after it runs

        Args:
            :param event: An event object to be handled
            :type event: class:`concurrentevents.Event`
        """
        event_name = event.__class__.__name__

        if isinstance(event, Exit):
            self.exit = event.timeout

        if event_name in self.handlers.keys():
            handlers = sorted(self.handlers[event_name], key=lambda h: h.priority)
            logger.info(f" {[h.__name__ for h in handlers]} handling {event.__str__()}")

            for h in handlers:
                try:
                    h(*event.args, **event.kwargs)
                except Cancel:
                    if self.progress_tracking:
                        self.canceled += 1
                    break

    def _submit(self, f, *args, **kwargs):
        """
        Interacts with the threadpool and futures dictionary to organize and track events

        This is implemented as the only methodology to put a function into the threadpool because,
        it forces all futures generated by putting something in the threadpool to be added to the
        futures list

        Parameters:
            :param f: A function object
            :type f: class:`function`
            :param `*args`: Arguments for f
            :param `**kwargs`: Keyword arguments for f
        """
        future = self.__thread_pool.submit(f, *args, **kwargs)
        self.__futures[future] = (f, args, kwargs)
        return future

    def fire(self, event):
        """
        A filter method for events that checks to see if it needs to be sent to the Thread Pool

        The purpose of this function is to validate not modify anything that
        wanted to be fired as an event.
        This intermediary step helps to eliminate anything that gets fired but
        isn't an event and any events that don't have handlers

        Args:
            :param event: An event that was fired
            :type event: Event


        :raises ValueError: Raised for when a non event is passed into the event param
        :raises KeyError: Raised for when there no handlers for the event
        """
        if not isinstance(event, Event):
            raise ValueError

        # Check to see if there are handlers ready to catch the event
        if event.__class__.__name__ not in self.handlers.keys() and not isinstance(event, Exit):
            raise KeyError(f"No Handlers For {event.__class__.__name__}")

        # DEBUG
        if not self.exit:
            logger.debug(f"Firing {event}")

            self._submit(self.__handle, event)
            if self.progress_tracking:
                self.fired += 1

    def start(self):
        """
        Starts the event sequence

        Fires the start event to trigger the beginning of all functionality
        After firing the start event the main while loop is started for debug
        and cleaning of completed futures
        """
        if self.started:
            raise StartError("EventManager is already running")
        self.started = True

        s = time.time()

        self.fire(Start())

        while not (self.exit or len(self.__futures.keys()) == 0):
            if self.debug:
                futures = len(list(self.__futures))
                running = len([ft for ft in list(self.__futures.keys()) if ft.running()])
                monitored = threadmonitor._monitoring

                process_info = f"EventManager Loop (futures={futures}, " \
                               f"running={running}, " \
                               f"monitored={monitored})"
                logger.info(process_info)

            for f, fn in self.__futures.copy().items():
                if f.done():
                    self.__futures.pop(f)
                    try:
                        if self.progress_tracking:
                            self.finished += 1

                            if fn[0].__name__ != '__handle':
                                future_info = f"{fn[0].__name__}(args={fn[1]}, kwargs={fn[2]})"
                            else:
                                future_info = fn[1][0]
                            logger.debug(f"{future_info} ({self.finished}/{self.fired})")

                        # Raises errors
                        f.result()
                    except Exception:
                        logger.exception(fn)
                        if self.progress_tracking:
                            self.errors += 1

            time.sleep(0.25)

        if not self.exit:
            self.fire(Exit())

        waited = concurrent.futures.wait(self.__futures.keys(), timeout=self.exit)
        if self.progress_tracking:
            self.finished += len(waited.done)

        if len(waited.not_done) > 0:
            logger.error("Final Cleaning Timed Out")

        logger.debug(f"Runtime {timedelta(seconds=time.time() - s)}")
        if self.progress_tracking:
            logger.debug(f"Events Fired ({self.fired})")

            logger.debug(f"Events Finished ({self.finished})")
            logger.debug(f"Events Errors ({self.errors})")

            logger.debug(f"Events Canceled ({self.canceled})")

        self.__thread_pool.shutdown()
