"""
Base event loop interface.

The naming convention is kept similar to asyncio as much as possible.

A special thanks to asyncio (tulip), Twisted, Tornado and Trollius for setting
a good example on how to implement event loops. Possible, in the future, we'll
run entirely on top of asyncio, but right now, we're still supporting Python 2.
"""
from __future__ import unicode_literals

import sys
from abc import ABCMeta, abstractmethod

from six import with_metaclass

from prompt_toolkit.log import logger

__all__ = [
    'EventLoop',
    'get_traceback_from_context',
]


class EventLoop(with_metaclass(ABCMeta, object)):
    """
    Eventloop interface.
    """
    def __init__(self):
        self._exception_handler = None

    @abstractmethod
    def run_until_complete(self, future, inputhook=None):
        """
        Keep running until this future has been set.
        Return the Future's result, or raise its exception.
        """

    def run_forever(self, inputhook=None):
        """
        Run loop forever.
        """
        f = self.create_future()
        self.run_until_complete(f, inputhook=inputhook)

    @abstractmethod
    def close(self):
        """
        Clean up of resources. Eventloop cannot be reused a second time after
        this call.
        """

    @abstractmethod
    def add_reader(self, fd, callback):
        """
        Start watching the file descriptor for read availability and then call
        the callback.
        """

    @abstractmethod
    def remove_reader(self, fd):
        """
        Stop watching the file descriptor for read availability.
        """

    def add_win32_handle(self, handle, callback):
        """
        Add a Windows Handle to the event loop.
        (Only applied to win32 loops.)
        """
        raise NotImplementedError

    def remove_win32_handle(self, handle):
        """
        Remove a Windows Handle from the event loop.
        (Only applied to win32 loops.)
        """
        raise NotImplementedError

    @abstractmethod
    def run_in_executor(self, callback, _daemon=False):
        """
        Run a long running function in a background thread. (This is
        recommended for code that could block the event loop.)
        Similar to Twisted's ``deferToThread``.
        """

    @abstractmethod
    def call_from_executor(self, callback, _max_postpone_until=None):
        """
        Call this function in the main event loop. Similar to Twisted's
        ``callFromThread``.

        :param _max_postpone_until: `None` or `time.time` value. For internal
            use. If the eventloop is saturated, consider this task to be low
            priority and postpone maximum until this timestamp. (For instance,
            repaint is done using low priority.)

            Note: In the past, this used to be a datetime.datetime instance,
                  but apparently, executing `time.time` is more efficient: it
                  does fewer system calls. (It doesn't read /etc/localtime.)
        """

    def create_future(self):
        """
        Create a `Future` object that is attached to this loop.
        This is the preferred way of creating futures.
        """
        from .future import Future
        return Future(loop=self)

    def set_exception_handler(self, handler):
        """
        Set the exception handler.
        """
        assert handler is None or callable(handler)
        self._exception_handler = handler

    def get_exception_handler(self):
        """
        Return the exception handler.
        """
        return self._exception_handler

    def call_exception_handler(self, context):
        """
        Call the current event loop exception handler.
        (Similar to ``asyncio.BaseEventLoop.call_exception_handler``.)
        """
        if self._exception_handler:
            try:
                self._exception_handler(context)
            except Exception:
                logger.error('Exception in default exception handler',
                             exc_info=True)
        else:
            try:
                self.default_exception_handler(context)
            except Exception:
                logger.error('Exception in default exception handler '
                             'while handling an unexpected error '
                             'in custom exception handler',
                             exc_info=True)

    def default_exception_handler(self, context):
        """
        Default exception handling.

        Thanks to asyncio for this function!
        """
        message = context.get('message')
        if not message:
            message = 'Unhandled exception in event loop'

        exception = context.get('exception')
        if exception is not None:
            tb = get_traceback_from_context(context)
            exc_info = (type(exception), exception, tb)
        else:
            exc_info = False

        log_lines = [message]
        for key in sorted(context):
            if key in ('message', 'exception'):
                continue
            value = context[key]
            value = repr(value)
            log_lines.append('{}: {}'.format(key, value))

        logger.error('\n'.join(log_lines), exc_info=exc_info)


def get_traceback_from_context(context):
    """
    Get the traceback object from the context.
    """
    exception = context.get('exception')
    if exception:
        if hasattr(exception, '__traceback__'):
            return exception.__traceback__
        else:
            # call_exception_handler() is usually called indirectly
            # from an except block. If it's not the case, the traceback
            # is undefined...
            return sys.exc_info()[2]

    return None
