import typing
import asyncio

from discord import User, Reaction, Message, Embed
from discord import HTTPException, InvalidArgument
from discord.ext import commands


class PaginatorSession:
    """
    Class that interactively paginates something.

    Parameters
    ----------
    ctx : Context
        The context of the command.
    timeout : float
        How long to wait for before the session closes.
    pages : List[Any]
        A list of entries to paginate.

    Attributes
    ----------
    ctx : Context
        The context of the command.
    timeout : float
        How long to wait for before the session closes.
    pages : List[Any]
        A list of entries to paginate.
    running : bool
        Whether the paginate session is running.
    base : Message
        The `Message` of the `Embed`.
    current : int
        The current page number.
    reaction_map : Dict[str, method]
        A mapping for reaction to method.
    """

    def __init__(self, ctx: commands.Context, *pages, **options):
        self.ctx = ctx
        self.timeout: int = options.get("timeout", 210)
        self.running = False
        self.base: Message = None
        self.current = 0
        self.pages = list(pages)
        self.destination = options.get("destination", ctx)
        self.reaction_map = {
            "⏮": self.first_page,
            "◀": self.previous_page,
            "▶": self.next_page,
            "⏭": self.last_page,
            "🛑": self.close,
        }

    def add_page(self, item) -> None:
        """
        Add a page.
        """
        raise NotImplementedError

    async def create_base(self, item) -> None:
        """
        Create a base `Message`.
        """
        await self._create_base(item)

        if len(self.pages) == 1:
            self.running = False
            return

        self.running = True
        for reaction in self.reaction_map:
            if len(self.pages) == 2 and reaction in "⏮⏭":
                continue
            await self.ctx.bot.add_reaction(self.base, reaction)

    async def _create_base(self, item) -> None:
        raise NotImplementedError

    async def show_page(self, index: int) -> None:
        """
        Show a page by page number.

        Parameters
        ----------
        index : int
            The index of the page.
        """
        if not 0 <= index < len(self.pages):
            return

        self.current = index
        page = self.pages[index]

        if self.running:
            await self._show_page(page)
        else:
            await self.create_base(page)

    async def _show_page(self, page):
        raise NotImplementedError

    def react_check(self, reaction: Reaction, user: User) -> bool:
        """

        Parameters
        ----------
        reaction : Reaction
            The `Reaction` object of the reaction.
        user : User
            The `User` or `Member` object of who sent the reaction.

        Returns
        -------
        bool
        """
        return (
            reaction.message.id == self.base.id
            and user.id == self.ctx.author.id
            and reaction.emoji in self.reaction_map.keys()
        )

    async def run(self) -> typing.Optional[Message]:
        """
        Starts the pagination session.

        Returns
        -------
        Optional[Message]
            If it's closed before running ends.
        """
        if not self.running:
            await self.show_page(self.current)
        while self.running:
            try:
                reaction, user = await self.ctx.bot.wait_for(
                    "reaction_add", check=self.react_check, timeout=self.timeout
                )
            except asyncio.TimeoutError:
                return await self.close(delete=False)
            else:
                action = self.reaction_map.get(reaction.emoji)
                await action()
            try:
                await self.base.remove_reaction(reaction, user)
            except (HTTPException, InvalidArgument):
                pass

    async def previous_page(self) -> None:
        """
        Go to the previous page.
        """
        await self.show_page(self.current - 1)

    async def next_page(self) -> None:
        """
        Go to the next page.
        """
        await self.show_page(self.current + 1)

    async def close(self, delete: bool = True) -> typing.Optional[Message]:
        """
        Closes the pagination session.

        Parameters
        ----------
        delete : bool, optional
            Whether or delete the message upon closure.
            Defaults to `True`.

        Returns
        -------
        Optional[Message]
            If `delete` is `True`.
        """
        self.running = False

        sent_emoji, _ = await self.ctx.bot.retrieve_emoji()
        await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji)

        if delete:
            return await self.base.delete()

        try:
            await self.base.clear_reactions()
        except HTTPException:
            pass

    async def first_page(self) -> None:
        """
        Go to the first page.
        """
        await self.show_page(0)

    async def last_page(self) -> None:
        """
        Go to the last page.
        """
        await self.show_page(len(self.pages) - 1)


class EmbedPaginatorSession(PaginatorSession):
    def __init__(self, ctx: commands.Context, *embeds, **options):
        super().__init__(ctx, *embeds, **options)

        if len(self.pages) > 1:
            for i, embed in enumerate(self.pages):
                footer_text = f"Page {i + 1} of {len(self.pages)}"
                if embed.footer.text:
                    footer_text = footer_text + " • " + embed.footer.text
                embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url)

    def add_page(self, item: Embed) -> None:
        if isinstance(item, Embed):
            self.pages.append(item)
        else:
            raise TypeError("Page must be an Embed object.")

    async def _create_base(self, item: Embed) -> None:
        self.base = await self.destination.send(embed=item)

    async def _show_page(self, page):
        await self.base.edit(embed=page)


class MessagePaginatorSession(PaginatorSession):
    def __init__(self, ctx: commands.Context, *messages, embed: Embed = None, **options):
        self.embed = embed
        self.footer_text = self.embed.footer.text if embed is not None else None
        super().__init__(ctx, *messages, **options)

    def add_page(self, item: str) -> None:
        if isinstance(item, str):
            self.pages.append(item)
        else:
            raise TypeError("Page must be a str object.")

    def _set_footer(self):
        if self.embed is not None:
            footer_text = f"Page {self.current+1} of {len(self.pages)}"
            if self.footer_text:
                footer_text = footer_text + " • " + self.footer_text
            self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url)

    async def _create_base(self, item: str) -> None:
        self._set_footer()
        self.base = await self.ctx.send(content=item, embed=self.embed)

    async def _show_page(self, page) -> None:
        self._set_footer()
        await self.base.edit(content=page, embed=self.embed)