import asyncio
import traceback
from .. import filtering, helper, exception
from .. import (
    flavor, chat_flavors, inline_flavors, is_event,
    message_identifier, origin_identifier)

# Mirror traditional version
from ..helper import (
    Sender, Administrator, Editor, openable,
    StandardEventScheduler, StandardEventMixin)


async def _invoke(fn, *args, **kwargs):
    if asyncio.iscoroutinefunction(fn):
        return await fn(*args, **kwargs)
    else:
        return fn(*args, **kwargs)


def _create_invoker(obj, method_name):
    async def d(*a, **kw):
        method = getattr(obj, method_name)
        return await _invoke(method, *a, **kw)
    return d


class Microphone(object):
    def __init__(self):
        self._queues = set()

    def add(self, q):
        self._queues.add(q)

    def remove(self, q):
        self._queues.remove(q)

    def send(self, msg):
        for q in self._queues:
            try:
                q.put_nowait(msg)
            except asyncio.QueueFull:
                traceback.print_exc()
                pass


class Listener(helper.Listener):
    async def wait(self):
        """
        Block until a matched message appears.
        """
        if not self._patterns:
            raise RuntimeError('Listener has nothing to capture')

        while 1:
            msg = await self._queue.get()

            if any(map(lambda p: filtering.match_all(msg, p), self._patterns)):
                return msg


from concurrent.futures._base import CancelledError

class Answerer(object):
    """
    When processing inline queries, ensures **at most one active task** per user id.
    """

    def __init__(self, bot, loop=None):
        self._bot = bot
        self._loop = loop if loop is not None else asyncio.get_event_loop()
        self._working_tasks = {}

    def answer(self, inline_query, compute_fn, *compute_args, **compute_kwargs):
        """
        Create a task that calls ``compute fn`` (along with additional arguments
        ``*compute_args`` and ``**compute_kwargs``), then applies the returned value to
        :meth:`.Bot.answerInlineQuery` to answer the inline query.
        If a preceding task is already working for a user, that task is cancelled,
        thus ensuring at most one active task per user id.

        :param inline_query:
            The inline query to be processed. The originating user is inferred from ``msg['from']['id']``.

        :param compute_fn:
            A function whose returned value is given to :meth:`.Bot.answerInlineQuery` to send.
            May return:

            - a *list* of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_
            - a *tuple* whose first element is a list of `InlineQueryResult <https://core.telegram.org/bots/api#inlinequeryresult>`_,
              followed by positional arguments to be supplied to :meth:`.Bot.answerInlineQuery`
            - a *dictionary* representing keyword arguments to be supplied to :meth:`.Bot.answerInlineQuery`

        :param \*compute_args: positional arguments to ``compute_fn``
        :param \*\*compute_kwargs: keyword arguments to ``compute_fn``
        """

        from_id = inline_query['from']['id']

        async def compute_and_answer():
            try:
                query_id = inline_query['id']

                ans = await _invoke(compute_fn, *compute_args, **compute_kwargs)

                if isinstance(ans, list):
                    await self._bot.answerInlineQuery(query_id, ans)
                elif isinstance(ans, tuple):
                    await self._bot.answerInlineQuery(query_id, *ans)
                elif isinstance(ans, dict):
                    await self._bot.answerInlineQuery(query_id, **ans)
                else:
                    raise ValueError('Invalid answer format')
            except CancelledError:
                # Cancelled. Record has been occupied by new task. Don't touch.
                raise
            except:
                # Die accidentally. Remove myself from record.
                del self._working_tasks[from_id]
                raise
            else:
                # Die naturally. Remove myself from record.
                del self._working_tasks[from_id]

        if from_id in self._working_tasks:
            self._working_tasks[from_id].cancel()

        t = self._loop.create_task(compute_and_answer())
        self._working_tasks[from_id] = t


class AnswererMixin(helper.AnswererMixin):
    Answerer = Answerer  # use async Answerer class


class CallbackQueryCoordinator(helper.CallbackQueryCoordinator):
    def augment_send(self, send_func):
        async def augmented(*aa, **kw):
            sent = await send_func(*aa, **kw)

            if self._enable_chat and self._contains_callback_data(kw):
                self.capture_origin(message_identifier(sent))

            return sent
        return augmented

    def augment_edit(self, edit_func):
        async def augmented(msg_identifier, *aa, **kw):
            edited = await edit_func(msg_identifier, *aa, **kw)

            if (edited is True and self._enable_inline) or (isinstance(edited, dict) and self._enable_chat):
                if self._contains_callback_data(kw):
                    self.capture_origin(msg_identifier)
                else:
                    self.uncapture_origin(msg_identifier)

            return edited
        return augmented

    def augment_delete(self, delete_func):
        async def augmented(msg_identifier, *aa, **kw):
            deleted = await delete_func(msg_identifier, *aa, **kw)

            if deleted is True:
                self.uncapture_origin(msg_identifier)

            return deleted
        return augmented

    def augment_on_message(self, handler):
        async def augmented(msg):
            if (self._enable_inline
                    and flavor(msg) == 'chosen_inline_result'
                    and 'inline_message_id' in msg):
                inline_message_id = msg['inline_message_id']
                self.capture_origin(inline_message_id)

            return await _invoke(handler, msg)
        return augmented


class InterceptCallbackQueryMixin(helper.InterceptCallbackQueryMixin):
    CallbackQueryCoordinator = CallbackQueryCoordinator


class IdleEventCoordinator(helper.IdleEventCoordinator):
    def augment_on_message(self, handler):
        async def augmented(msg):
            # Reset timer if this is an external message
            is_event(msg) or self.refresh()
            return await _invoke(handler, msg)
        return augmented

    def augment_on_close(self, handler):
        async def augmented(ex):
            try:
                if self._timeout_event:
                    self._scheduler.cancel(self._timeout_event)
                    self._timeout_event = None
            # This closing may have been caused by my own timeout, in which case
            # the timeout event can no longer be found in the scheduler.
            except exception.EventNotFound:
                self._timeout_event = None
            return await _invoke(handler, ex)
        return augmented


class IdleTerminateMixin(helper.IdleTerminateMixin):
    IdleEventCoordinator = IdleEventCoordinator


class Router(helper.Router):
    async def route(self, msg, *aa, **kw):
        """
        Apply key function to ``msg`` to obtain a key, look up routing table
        to obtain a handler function, then call the handler function with
        positional and keyword arguments, if any is returned by the key function.

        ``*aa`` and ``**kw`` are dummy placeholders for easy nesting.
        Regardless of any number of arguments returned by the key function,
        multi-level routing may be achieved like this::

            top_router.routing_table['key1'] = sub_router1.route
            top_router.routing_table['key2'] = sub_router2.route
        """
        k = self.key_function(msg)

        if isinstance(k, (tuple, list)):
            key, args, kwargs = {1: tuple(k) + ((),{}),
                                 2: tuple(k) + ({},),
                                 3: tuple(k),}[len(k)]
        else:
            key, args, kwargs = k, (), {}

        try:
            fn = self.routing_table[key]
        except KeyError as e:
            # Check for default handler, key=None
            if None in self.routing_table:
                fn = self.routing_table[None]
            else:
                raise RuntimeError('No handler for key: %s, and default handler not defined' % str(e.args))

        return await _invoke(fn, msg, *args, **kwargs)


class DefaultRouterMixin(object):
    def __init__(self, *args, **kwargs):
        self._router = Router(flavor, {'chat': _create_invoker(self, 'on_chat_message'),
                                       'callback_query': _create_invoker(self, 'on_callback_query'),
                                       'inline_query': _create_invoker(self, 'on_inline_query'),
                                       'chosen_inline_result': _create_invoker(self, 'on_chosen_inline_result'),
                                       'shipping_query': _create_invoker(self, 'on_shipping_query'),
                                       'pre_checkout_query': _create_invoker(self, 'on_pre_checkout_query'),
                                       '_idle': _create_invoker(self, 'on__idle')})

        super(DefaultRouterMixin, self).__init__(*args, **kwargs)

    @property
    def router(self):
        """ See :class:`.helper.Router` """
        return self._router

    async def on_message(self, msg):
        """
        Called when a message is received.
        By default, call :meth:`Router.route` to handle the message.
        """
        await self._router.route(msg)


@openable
class Monitor(helper.ListenerContext, DefaultRouterMixin):
    def __init__(self, seed_tuple, capture, **kwargs):
        """
        A delegate that never times-out, probably doing some kind of background monitoring
        in the application. Most naturally paired with :func:`amanobot.aio.delegate.per_application`.

        :param capture: a list of patterns for ``listener`` to capture
        """
        bot, initial_msg, seed = seed_tuple
        super(Monitor, self).__init__(bot, seed, **kwargs)

        for pattern in capture:
            self.listener.capture(pattern)


@openable
class ChatHandler(helper.ChatContext,
                  DefaultRouterMixin,
                  StandardEventMixin,
                  IdleTerminateMixin):
    def __init__(self, seed_tuple,
                 include_callback_query=False, **kwargs):
        """
        A delegate to handle a chat.
        """
        bot, initial_msg, seed = seed_tuple
        super(ChatHandler, self).__init__(bot, seed, **kwargs)

        self.listener.capture([{'chat': {'id': self.chat_id}}])

        if include_callback_query:
            self.listener.capture([{'message': {'chat': {'id': self.chat_id}}}])


@openable
class UserHandler(helper.UserContext,
                  DefaultRouterMixin,
                  StandardEventMixin,
                  IdleTerminateMixin):
    def __init__(self, seed_tuple,
                 include_callback_query=False,
                 flavors=chat_flavors+inline_flavors, **kwargs):
        """
        A delegate to handle a user's actions.

        :param flavors:
            A list of flavors to capture. ``all`` covers all flavors.
        """
        bot, initial_msg, seed = seed_tuple
        super(UserHandler, self).__init__(bot, seed, **kwargs)

        if flavors == 'all':
            self.listener.capture([{'from': {'id': self.user_id}}])
        else:
            self.listener.capture([lambda msg: flavor(msg) in flavors, {'from': {'id': self.user_id}}])

        if include_callback_query:
            self.listener.capture([{'message': {'chat': {'id': self.user_id}}}])


class InlineUserHandler(UserHandler):
    def __init__(self, seed_tuple, **kwargs):
        """
        A delegate to handle a user's inline-related actions.
        """
        super(InlineUserHandler, self).__init__(seed_tuple, flavors=inline_flavors, **kwargs)


@openable
class CallbackQueryOriginHandler(helper.CallbackQueryOriginContext,
                                 DefaultRouterMixin,
                                 StandardEventMixin,
                                 IdleTerminateMixin):
    def __init__(self, seed_tuple, **kwargs):
        """
        A delegate to handle callback query from one origin.
        """
        bot, initial_msg, seed = seed_tuple
        super(CallbackQueryOriginHandler, self).__init__(bot, seed, **kwargs)

        self.listener.capture([
            lambda msg:
                flavor(msg) == 'callback_query' and origin_identifier(msg) == self.origin
        ])


@openable
class InvoiceHandler(helper.InvoiceContext,
                     DefaultRouterMixin,
                     StandardEventMixin,
                     IdleTerminateMixin):
    def __init__(self, seed_tuple, **kwargs):
        """
        A delegate to handle messages related to an invoice.
        """
        bot, initial_msg, seed = seed_tuple
        super(InvoiceHandler, self).__init__(bot, seed, **kwargs)

        self.listener.capture([{'invoice_payload': self.payload}])
        self.listener.capture([{'successful_payment': {'invoice_payload': self.payload}}])
