'''TODO: Documentation
'''

from bge import logic
import time
# from uplogic.physics import on_collision  # noqa


def on_pre_draw(callback):
    EventManager.update_on.append(callback)


def on_post_draw(callback):
    EventManager.update_on.append(callback)


class _DeferredCallback:


    def callback(self):
        ...

    def __init__(self, callback) -> None:
        self.callback = callback
        logic.getCurrentScene().post_draw.append(self._call)

    def _call(self):
        self.callback()
        if self._call in logic.getCurrentScene().post_draw:
            logic.getCurrentScene().post_draw.remove(self._call)


def later(callback):
    class _DeferredCallback():

        def __init__(self, callback) -> None:
            self.callback = callback
            logic.getCurrentScene().post_draw.append(self._call)

        def _call(self):
            self.callback()
            logic.getCurrentScene().post_draw.remove(self._call)
    _DeferredCallback(callback)


def get_event_manager():
    update = EventManager.update_on
    if update is None:
        update = logic.getCurrentScene().post_draw
    if EventManager.update not in update:
        update.append(EventManager.update)
    return EventManager


def set_update_loop(loop):
    EventManager.set_update_on(loop)


class EventManager:
    '''Manager for `Event` objects, not inteded for manual use.
    '''
    events = {}
    bindings = []
    scheduled = []
    done = []

    update_on = None

    @classmethod
    def set_update_on(cls, li):
        old = cls.update_on
        if li is None:
            li = logic.getCurrentScene().post_draw
        if old is None:
            old = logic.getCurrentScene().post_draw
        if cls.update in old:
            old.remove(cls.update)
        if cls.update not in li:
            li.append(cls.update)
        cls.update_on = li

    @classmethod
    def update(cls):
        cbs = cls.bindings.copy()
        cbs.extend(cls.scheduled)
        for cb in cbs:
            cb()

    @classmethod
    def log(cls):
        if cls.events:
            print('Events:')
            for evt in cls.events:
                print(f'\t{evt}:\t{cls.events[evt].content}')

    @classmethod
    def schedule(cls, cb):
        get_event_manager()
        cls.scheduled.append(cb)

    @classmethod
    def cancel(cls, cb):
        get_event_manager()
        if cb in cls.scheduled:
            cls.scheduled.remove(cb)

    @classmethod
    def bind(cls, cb):
        get_event_manager()
        cls.bindings.append(cb)

    @classmethod
    def unbind(cls, cb):
        get_event_manager()
        if cb in cls.bindings:
            cls.bindings.remove(cb)

    @classmethod
    def release(cls, cb):
        get_event_manager()
        cls.cancel(cb)

    @classmethod
    def register(cls, event):
        get_event_manager()
        cls.events[event.id] = event
        cls.schedule(event.remove)

    @classmethod
    def send(cls, id, content, messenger, target) -> None:
        get_event_manager()
        Event(id, content, messenger, target)

    @classmethod
    def receive(cls, id, target=None):
        get_event_manager()
        evt = cls.events.get(id, None)
        if evt:
            if evt.target is None:
                return evt
            elif evt.target is not None and evt.target is target:
                return evt
        return None
        # return evt if evt and target is evt.target else None

    @classmethod
    def consume(cls, id):
        get_event_manager()
        return cls.events.pop(id, None)

    @classmethod
    def clear_schedule(cls):
        cls.scheduled.clear()

    @classmethod
    def clear_bindings(cls):
        cls.bindings.clear()

class Event():
    '''Event generated by `uplogic.events.send()`.

    **Not intended for manual use.**

    :param id: Identifier of the event; can be any object.
    :param content: This can be used to store data in an event.
    :param messenger: Can be used to store an object.
    '''

    def __init__(self, id: int, content=None, messenger=None, target=None):
        self.id = id
        self.content = content
        self.messenger = messenger
        self.target = target
        EventManager.schedule(self.register)

    def register(self):
        EventManager.register(self)
        EventManager.cancel(self.register)

    def remove(self):
        EventManager.events.pop(self.id, None)
        EventManager.cancel(self.remove)


def clear_schedule():
    EventManager.clear_schedule()


def send(id: int, content=None, messenger=None, target=None) -> None:
    '''Send an event that can be reacted to.

    :param id: Identifier of the event; can be anything, not just `int`.
    :param content: This can be used to store data in an event.
    :param messenger: Can be used to store an object.
    :param target: Target object. If defined, only target object can receive.
    '''
    EventManager.send(id, content, messenger, target)


def receive(id, target=None) -> Event:
    '''Check if an event has occured.

    :param id: Identifier of the event; can be any object.
    :param target: Target object. If defined, only target object can receive.

    :returns: `Event` with `id`, `content` and `messenger` as attributes.
    '''
    return EventManager.receive(id, target)


def consume(id: int):
    '''Check if an event has occured. This will remove the event.

    :param id: Identifier of the event; can be any object.

    :returns: `Event` with `id`, `content` and `messenger` as attributes.
    '''
    return EventManager.consume(id)


def bind(id: int, callback, *args):
    '''Bind a callback to an event. This callback will be executed whenever
    the specified event is received.

    **Required signature**:
    ```
    def callback(evt):
        ...
    ```

    :param id: Identifier of the event; can be any object.
    :param callback: This callback will be called every time the event is
    triggered.
    '''
    class _BoundCallback():
        def __init__(self, id, cb, *args) -> None:
            self.id = id
            self.callback = cb
            self.args = args
            EventManager.bind(self._check_evt)

        def _check_evt(self):
            evt = receive(self.id)
            if evt:
                self.callback(evt, *self.args)

        def unbind(self):
            EventManager.unbind(self._check_evt)

        def release(self):
            EventManager.unbind(self._check_evt)

    return _BoundCallback(id, callback, *args)


def bind_once(id: int, callback, *args):
    '''Bind a callback to an event and automatically unbind when received.

    Required signature: `def cb(evt)`

    :param id: Identifier of the event; can be any object.
    :param callback: This callback will be called every time the event is
    triggered.
    '''
    class _BoundCallback():
        def __init__(self, id, cb, *args) -> None:
            self.id = id
            self.callback = cb
            self.args = args
            EventManager.bind(self._check_evt)

        def _check_evt(self):
            evt = receive(self.id)
            if evt:
                self.callback(evt, *self.args)
                self.unbind()

        def unbind(self):
            EventManager.unbind(self._check_evt)

        def release(self):
            EventManager.unbind(self._check_evt)

    return _BoundCallback(id, callback, *args)


class ULEvent(Event):
    # XXX: Remove legacy support
    pass


class ScheduledEvent():
    '''Event generated by `uplogic.events.schedule()`.

    :param delay: Delay with which to send the event in seconds.
    :param id: Identifier of the event; can be any object
    :param content: This can be used to store data in an event.
    :param messenger: Can be used to store an object.
    '''

    def __init__(self, delay, id, content, messenger):
        self.time = time.time()
        self.delay = self.time + delay
        self.id = id
        self.content = content
        self.messenger = messenger
        self._consumed = False
        EventManager.schedule(self._send_scheduled)

    @property
    def time_left(self):
        return self.delay - time.time()

    def _send_scheduled(self):
        if time.time() >= self.delay:
            self.consume()

    def cancel(self):
        EventManager.cancel(self._send_scheduled)

    def consume(self):
        if self._consumed:
            return
        self.delay = time.time()
        self._consumed = True
        EventManager.cancel(self._send_scheduled)
        Event(self.id, self.content, self.messenger)


class _ScheduledEvent(ScheduledEvent):
    # XXX: Remove legacy support
    pass


def schedule(id: str, delay=0.0, content=None, messenger=None) -> ScheduledEvent:
    '''Send an event that can be reacted to with a delay.

    :param id: Identifier of the event; can be any object. If `id` is callable, `content` can be used as argument.
    :param delay: Time delay in seconds.
    :param content: This can be used to store data in an event.
    :param messenger: Can be used to store an object.
    '''
    if callable(id):
        return ScheduledCallback(id, delay, content)
    return ScheduledEvent(delay, id, content, messenger)


class ScheduledCallback():
    '''Event generated by `uplogic.events.schedule_callback()`.

    **Not intended for manual use.**

    :param cb: Callback to be evaluated.
    :param delay: Delay with which to call the function in seconds.
    :param arg: If this is defined, callback will be called with this
    argument.
    '''

    def __init__(self, cb, delay=0.0, arg=None):
        self.time = time.time()
        self.delay = self.time + delay
        self.callback = cb
        self.arg = arg
        self._consumed = False
        EventManager.schedule(self._call_scheduled)

    @property
    def time_left(self):
        return self.delay - time.time()

    def _call_scheduled(self):
        if time.time() >= self.delay:
            self.consume()

    def consume(self):
        if self._consumed:
            return
        self.delay = time.time()
        self._consumed = True
        EventManager.cancel(self._call_scheduled)
        if self.arg is not None:
            self.callback(self.arg)
        else:
            self.callback()

    def cancel(self):
        EventManager.cancel(self._call_scheduled)


class _ScheduledCallback(ScheduledCallback):
    # XXX: Remove legacy support
    pass


def schedule_callback(cb, delay=0.0, arg=None) -> ScheduledCallback:
    '''Call a function with a delay. The function can have an argument when
    defined as a keyword.

    Callback cannot return anything.

    :param cb: Callback to be evaluated.
    :param delay: Delay with which to call the function in seconds.
    :param arg: If this is defined, callback will be called with this
    argument.
    '''
    return ScheduledCallback(cb, delay, arg)
