"""Classes that are adding some functionality for default components.

.. hint::

        Components in your code are automatically replaced with the ones from this module.
        You can use `discord.ui.View` instead of `ezcord.View`.
"""

from __future__ import annotations

import asyncio
import inspect
from typing import Callable

from .internal.dc import discord
from .internal.deprecation import warn_deprecated
from .logs import log

_view_checks: list[Callable] = []
_view_check_failures: list[Callable] = []

__all__ = ("event", "Modal", "View", "EzView", "EzModal", "DropdownPaginator")


def _check_coro(func):
    if not asyncio.iscoroutinefunction(func):
        raise TypeError(f"Event registered must be a coroutine function, not {type(func)}")


def _check_params(func, amount):
    parameters = inspect.signature(func).parameters
    if len(parameters) != amount:
        raise ValueError(
            f"Event method must have '{amount}' parameters, has '{len(parameters)}' instead"
        )


def event(coro):
    """A decorator to register custom checks and error handlers for Discord components.

    Example
    -------
    .. code-block:: python

        @ezcord.event
        async def view_check(interaction):
            return interaction.user.id == 12345  # Returns True or False

        @ezcord.event
        async def on_view_check_failure(interaction):
            await interaction.response.send_message("You can't use this!")
    """
    _check_coro(coro)

    name = coro.__name__.lstrip("_")

    if name == "view_check":
        _check_params(coro, 1)
        _view_checks.append(coro)
    elif name == "on_view_check_failure":
        _check_params(coro, 1)
        _view_check_failures.append(coro)
    elif name == "on_view_error":
        raise ValueError(
            f"This event was removed in version 0.7.5, use 'Bot.on_view_error' instead: "
            f"https://docs.pycord.dev/en/master/api/clients.html#discord.Bot.on_modal_error"
        )
    elif name == "on_modal_error":
        raise ValueError(
            f"This event was removed in version 0.7.5, use 'Bot.on_modal_error' instead: "
            f"https://docs.pycord.dev/en/master/api/clients.html#discord.Bot.on_view_error"
        )
    else:
        raise ValueError(f"Invalid event name: '{coro.__name__}'")

    log.debug(f"Registered event **{coro.__name__}**")


class View(discord.ui.View):
    ignore_timeout_errors: bool = False

    """Base class for all :class:`discord.ui.View` classes that adds some functionality.

    Parameters
    ----------
    ignore_timeout_error:
        If ``True``, views will not raise an exception if an error occurs during the timeout event.
    """

    def __init__(self, *args, ignore_timeout_error: bool = False, **kwargs):
        self.ignore_timeout_error = ignore_timeout_error
        super().__init__(*args, **kwargs)

    async def interaction_check(self, interaction: discord.Interaction) -> bool:
        """Returns ``True`` if all custom checks return ``True`` or if no custom checks are registered."""
        for coro in _view_checks:
            if not await coro(interaction):
                return False
        return True

    async def on_check_failure(self, interaction: discord.Interaction) -> None:
        """This method is called if :meth:`interaction_check` returns ``False``."""
        for coro in _view_check_failures:
            await coro(interaction)

    async def on_timeout(self) -> None:
        """If ``disable_on_timeout`` is set to ``True``, this will disable all components,
        unless the viw has been explicitly stopped.
        """
        try:
            return await super().on_timeout()
        except discord.NotFound:
            return
        except discord.HTTPException as e:
            if self.ignore_timeout_error or View.ignore_timeout_errors:
                return
            log.exception(
                f"Error in View **{type(self).__name__}** ({type(self).__module__}) ```{e}```",
                exc_info=e,
            )


class Modal(discord.ui.Modal):
    """Base class for all :class:`discord.ui.Modal` classes."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


class EzView(View):
    """Alias for :class:`View`."""

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        warn_deprecated("ezcord.EzView", "discord.ui.View", "0.7", "0.8")


class EzModal(Modal):
    """Alias for :class:`Modal`."""

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        warn_deprecated("ezcord.EzModal", "discord.ui.Modal", "0.7", "0.8")


# replace all default components with Ezcord components
discord.ui.View = View
discord.ui.Modal = Modal


class DropdownPaginator(discord.ui.Select):
    """A dropdown paginator that can be used to paginate through a list of options.

    This is useful when we have more options than Discord allows in a single dropdown menu.

    Parameters
    ----------
    options:
        The options that will be displayed in the dropdown.
    next_page_label:
        The label of the next page button.
    previous_page_label:
        The label of the previous page button.
    next_page_emoji:
        The emoji of the next page button.
    previous_page_emoji:
        The emoji of the previous page button.
    page:
        The current page of the dropdown.
    **kwargs:
        Additional keyword arguments that are passed to :class:`discord.ui.Select`.
    """

    def __init__(
        self,
        options: list[discord.SelectOption],
        next_page_label: str = "Next page",
        previous_page_label: str = "Previous page",
        next_page_emoji: str = "⬇️",
        previous_page_emoji: str = "⬆️",
        page: int = 0,
        **kwargs,
    ):
        self.page = page

        self.next_page_label = next_page_label
        self.previous_page_label = previous_page_label
        self.next_page_emoji = next_page_emoji
        self.previous_page_emoji = previous_page_emoji

        self.total_options = options
        self.current_options = self.load_options(options, self.page)
        self.kwargs = kwargs

        super().__init__(options=self.current_options, **kwargs)

    @property
    def item_selected(self):
        """Returns ``True`` if the user selected an item from the dropdown.
        If the user clicked on the next or previous page button, this will return ``False``.
        """

        return "ez_next" not in self.values and "ez_previous" not in self.values

    async def callback(self, interaction: discord.Interaction):
        """Edit the dropdown menu if the user selects a page option."""

        def set_current_options(x):
            if isinstance(x, DropdownPaginator):
                self.options = self.current_options
                return self
            else:
                return x

        if self.check_next_page():
            new_children = map(set_current_options, self.view.children)

            self.view.children = list(new_children)
            await interaction.response.edit_message(view=self.view)
            return

        elif self.check_previous_page():
            new_children = map(set_current_options, self.view.children)

            self.view.children = list(new_children)
            await interaction.response.edit_message(view=self.view)
            return

    def check_next_page(self) -> bool:
        """Returns True if the user clicked on the next page button.
        In this case, the dropdown menu will be edited.
        """
        if "ez_next" in self.values:
            self.page += 1
            self.current_options = self.load_options(self.total_options, self.page)
            return True
        return False

    def check_previous_page(self) -> bool:
        """Returns True if the user clicked on the previous page button.
        In this case, the dropdown menu will be edited.
        """
        if "ez_previous" in self.values:
            self.page -= 1
            self.current_options = self.load_options(self.total_options, self.page)
            return True
        return False

    def load_options(
        self, options: list[discord.SelectOption], chunk: int = 0
    ) -> list[discord.SelectOption]:
        """Split the options into chunks and append options for next/previous pages."""

        chunk_size = 23
        if len(options) > chunk_size:
            x = [
                options[option : option + chunk_size]
                for option in range(0, len(options), chunk_size)
            ]

            new_options = x[chunk]
            if len(new_options) == chunk_size:
                new_options.append(
                    discord.SelectOption(
                        label=self.next_page_label, value="ez_next", emoji=self.next_page_emoji
                    )
                )

            if chunk > 0:
                new_options.insert(
                    0,
                    discord.SelectOption(
                        label=self.previous_page_label,
                        value="ez_previous",
                        emoji=self.previous_page_emoji,
                    ),
                )
        else:
            new_options = options

        return new_options
