"""
Logic to build state machines. Borrowed from Scapy's Automata concept.
"""
import collections
import logging
import threading

logger = logging.getLogger("packetracer")

STATE_TYPE_BEGIN = 0
STATE_TYPE_INTERM = 1  # default
STATE_TYPE_END = 2


class TimedCallback(threading.Thread):
    def __init__(self):
        self._obj = None
        self._is_running = True
        self._cb = None
        # assume this will never trigger
        self._timeout = 9999999
        self._event = threading.Event()
        super().__init__()
        self.start()

    def run(self):
        # logger.debug("starting cb iterator")

        while self._is_running:
            # logger.debug("cb: next round")
            self._event.clear()
            self._event.wait(timeout=self._timeout)

            # wait was interrupted
            if self._event.is_set():
                continue

            if self._cb is not None:
                # logger.debug("executing timeout cb")
                self._cb(self._obj)

    # logger.debug("cb iterator finished")

    def retrigger(self, obj, timeout, cb):
        self._obj = obj
        self._timeout = timeout
        self._cb = cb
        self._event.set()

    def set_inactive(self):
        self._cb = None
        self._timeout = 9999999
        self._event.set()

    def stop(self):
        self._is_running = False
        self._event.set()


_cb_threads = collections.defaultdict(TimedCallback)


def sm_state(state_type=STATE_TYPE_INTERM, timeout=None, timeout_cb=None):
    def gen(old_f):
        if timeout is not None and timeout_cb is None:
            logger.warning(
                "timeout set to %d but no timeout action for %r",
                timeout,
                old_f.__name__)

        # replace with new method to store state infos
        def new_f(self, *args, **kwds):
            # end of function (state) -> clear old one
            # logger.debug("getting cb class via %r", self.__class__)
            cb_thread = _cb_threads[self.__class__]
            cb_thread.set_inactive()

            ret = old_f(self, *args, **kwds)

            # start timeout after method reaches end
            if timeout is not None:
                # logger.debug("restarting timeout: %ds", timeout)
                cb_thread.retrigger(self, timeout, timeout_cb)

            return ret

        if state_type == STATE_TYPE_BEGIN:
            new_f._state_method_begin = True
        return new_f

    return gen


class AutomateMeta(type):
    def __new__(cls, clsname, clsbases, clsdict):
        t = type.__new__(cls, clsname, clsbases, clsdict)
        for key, val in clsdict.items():
            state_method = getattr(val, "_state_method_begin", None)

            if state_method is not None:
                t._state = key
                break
        return t


class StateMachine(object, metaclass=AutomateMeta):
    """
    This state machine allows to react on network stimulus (incoming packets)
    and imitate/build protocols.

    State_1 -> event: decide next state -> State_2 ...
    """

    def __init__(self, receive_cb):
        self._states = set()
        self._actions = set()
        self._receive_cb = receive_cb
        self._is_running = True
        self._old_f = None
        self._state = getattr(self, self._state, None)

        if self._state is None:
            logger.exception("no initial state defined!")

        self._receive_thread = threading.Thread(
            target=StateMachine.receive_cycler,
            args=[self]
        )
        self._receive_thread.start()

    @staticmethod
    def receive_cycler(obj):
        while obj._is_running:
            pkt = obj._receive_cb()

            try:
                obj._state(pkt)
            except:
                logger.warning(
                    "could not execute callback: %r",
                    obj._state
                )
        # ex.printstacktrace()

    # logger.debug("receive cycler finished")

    def stop(self):
        self._is_running = False
        # _receive_cb() (eg sockets) needs to be stopped first or this will likely hang
        self._receive_thread.join()

        try:
            _cb_thread = _cb_threads.get(self.__class__, None)
            _cb_thread.stop()
        except AttributeError:
            pass
        # logger.debug("no cb thread found")
        except Exception as ex:
            ex.printstacktrace()
