__all__ = ('GUI_STATE_CANCELLED', 'GUI_STATE_CANCELLING', 'GUI_STATE_READY', 'GUI_STATE_SWITCHING_CTX',
    'GUI_STATE_SWITCHING_PAGE', 'PaginationBase')

from ...backend.futures import Task, CancelledError
from ...discord.client_core import KOKORO
from ...discord.exceptions import DiscordException, ERROR_CODES

GUI_STATE_READY = 0
GUI_STATE_SWITCHING_PAGE = 1
GUI_STATE_CANCELLING = 2
GUI_STATE_CANCELLED = 3
GUI_STATE_SWITCHING_CTX = 4

GUI_STATE_VALUE_TO_NAME = {
    GUI_STATE_READY : 'ready',
    GUI_STATE_SWITCHING_PAGE : 'switching_page',
    GUI_STATE_CANCELLING : 'cancelling',
    GUI_STATE_CANCELLED : 'cancelled',
    GUI_STATE_SWITCHING_CTX : 'switching_context',
}

class PaginationBase:
    """
    Base class for pagination like objects.
    
    Attributes
    ----------
    _canceller : `None` or `function`
        The function called when the ``Pagination`` is cancelled or when it expires. This is a onetime use and after
        it was used, is set as `None`.
    
    _task_flag : `int`
        A flag to store the state of the ``Pagination``.
        
        Possible values:
        +---------------------------+-------+-----------------------------------------------------------------------+
        | Respective name           | Value | Description                                                           |
        +===========================+=======+=======================================================================+
        | GUI_STATE_READY           | 0     | The Pagination does nothing, is ready to be used.                     |
        +---------------------------+-------+-----------------------------------------------------------------------+
        | GUI_STATE_SWITCHING_PAGE  | 1     | The Pagination is currently changing it's page.                       |
        +---------------------------+-------+-----------------------------------------------------------------------+
        | GUI_STATE_CANCELLING      | 2     | The pagination is currently changing it's page, but it was cancelled  |
        |                           |       | meanwhile.                                                            |
        +---------------------------+-------+-----------------------------------------------------------------------+
        | GUI_STATE_CANCELLED       | 3     | The pagination is, or is being cancelled right now.                   |
        +---------------------------+-------+-----------------------------------------------------------------------+
        | GUI_STATE_SWITCHING_CTX   | 4     | The Pagination is switching context. Not used by the default class,   |
        |                           |       | but expected.                                                         |
        +---------------------------+-------+-----------------------------------------------------------------------+
    
    _timeouter : `None` or ``Timeouter``
        Executes the timing out feature on the ``Pagination``.
    
    channel : ``ChannelTextBase`` instance
        The channel where the ``Pagination`` is executed.
    
    client : ``Client`` of ``Embed`` (or any compatible)
        The client who executes the ``Pagination``.
    
    message : `None` or ``Message``
        The message on what the ``Pagination`` is executed.
    """
    __slots__ = ('_canceller', '_task_flag', '_timeouter', 'channel', 'client', 'message')
    
    async def __new__(cls, client, channel):
        """
        Pagination instances should have asynchronous constructor.
        
        Parameters
        ----------
        
        Raises
        ------
        NotImplementedError
        """
        raise NotImplementedError
    
    
    async def __call__(self, client, event):
        """
        Called when a reaction is added or removed from the respective message.
        
        This method is a coroutine.
        
        Parameters
        ----------
        client : ``Client``
            The client who executes the ``Pagination``.
        event : ``ReactionAddEvent``, ``ReactionDeleteEvent``
            The received event.
        """
        pass
    
    
    async def _canceller_function(self, exception):
        """
        Used when the ``Pagination`` is cancelled.
        
        First of all removes the pagination from waitfors, so it will not wait for reaction events, then sets the
        ``._task_flag`` of the it to `GUI_STATE_CANCELLED`.
        
        If `exception` is given as `TimeoutError`, then removes the ``Pagination``'s reactions from the respective
        message.
        
        This method is a coroutine.
        
        Parameters
        ----------
        exception : `None` or ``BaseException`` instance
            Exception to cancel the ``Pagination`` with.
        """
        client = self.client
        message = self.message
        
        client.events.reaction_add.remove(message, self)
        client.events.reaction_delete.remove(message, self)
        
        if self._task_flag == GUI_STATE_SWITCHING_CTX:
            # the message is not our, we should not do anything with it.
            return
        
        self._task_flag = GUI_STATE_CANCELLED
        
        await self._handle_close_exception(exception)
    
    
    async def _handle_close_exception(self, exception):
        """
        Handles close exception if any.
        
        This method is a coroutine.
        
        Parameters
        ----------
        exception : `None` or `BaseException`
            The close exception to handle.
        
        Returns
        -------
        exception_handled : `bool`
            Whether the exception was handled.
        """
        if exception is None:
            return
        
        client = self.client
        message = self.message
        
        if isinstance(exception, CancelledError):
            try:
                await client.message_delete(message)
            except BaseException as err:
                if isinstance(err, ConnectionError):
                    # no internet
                    return
                
                if isinstance(err, DiscordException):
                    if err.code in (
                            ERROR_CODES.unknown_channel, # channel deleted
                            ERROR_CODES.unknown_message, # message deleted
                            ERROR_CODES.invalid_access, # client removed
                                ):
                        return
                
                await client.events.error(client, f'{self!r}.close', err)
            
            return True
        
        if isinstance(exception, TimeoutError):
            if self.channel.cached_permissions_for(client).can_manage_messages:
                try:
                    await client.reaction_clear(message)
                except BaseException as err:
                    
                    if isinstance(err, ConnectionError):
                        # no internet
                        return
                    
                    if isinstance(err, DiscordException):
                        if err.code in (
                                ERROR_CODES.unknown_message, # message deleted
                                ERROR_CODES.unknown_channel, # channel deleted
                                ERROR_CODES.invalid_access, # client removed
                                ERROR_CODES.invalid_permissions, # permissions changed meanwhile
                                    ):
                            return
                    
                    await client.events.error(client, f'{self!r}._canceller_function', err)
            
            return True
        
        if isinstance(exception, PermissionError):
            return True
        
        return False
        
    
    def cancel(self, exception=None):
        """
        Cancels the pagination, if it is not cancelled yet.
        
        Parameters
        ----------
        exception : `None` or ``BaseException`` instance, Optional
            Exception to cancel the pagination with. Defaults to `None`
        """
        if self._task_flag in (GUI_STATE_READY, GUI_STATE_SWITCHING_PAGE, GUI_STATE_CANCELLING):
            self._task_flag = GUI_STATE_CANCELLED
        
        canceller = self._canceller
        if canceller is None:
            return
        
        self._canceller = None
        
        timeouter = self._timeouter
        if (timeouter is not None):
            timeouter.cancel()
        
        return Task(canceller(self, exception), KOKORO)
    
    
    def __repr__(self):
        """Returns the pagination instance's representation."""
        repr_parts = [
            '<', self.__class__.__name__,
            ' client=', repr(self.client),
            ', channel=', repr(self.channel),
            ', state='
        ]
        
        task_flag = self._task_flag
        repr_parts.append(repr(task_flag))
        repr_parts.append(' (')
        
        task_flag_name = GUI_STATE_VALUE_TO_NAME[task_flag]
        
        repr_parts.append(task_flag_name)
        repr_parts.append(')')
        
        # Third party things go here
        
        repr_parts.append('>')
        return ''.join(repr_parts)
    
    
    def is_active(self):
        """
        Returns whether the menu is still active.
        
        Returns
        -------
        is_active : `bool`
        """
        return (self._task_flag in (GUI_STATE_READY, GUI_STATE_SWITCHING_PAGE))
