import json
import logging
import re
import string

import discord

from .abc import AssetLibrary

__all__ = ("EmojiFormatter", "Emojis")

# The default fallback value that is returned when requesting
# a missing emoji. In Discord on desktop, this is the red question mark.
SENTINEL = "\N{BLACK QUESTION MARK ORNAMENT}"


class EmojiFormatter(string.Formatter):
    """
    A custom :class:`string.Formatter` that replaces ``{e:key}`` template
    strings with an emoji retrieved through :meth:`EmojiLibrary.get`.

    Positional and keyword parameters passed into :meth:`EmojiFormatter.format`
    are formatted as usual, and are expected to resolve any other template
    strings found within the provided format string.

    .. _formatter: https://docs.python.org/3/library/string.html#string.Formatter

    .. note::

        See `string.Formatter <formatter>`_ for information on string formatters.

    Parameters
    ----------
    emoji_library: EmojiLibrary
        The emoji library to draw emojis from.
    """

    def __init__(self, emoji_library):
        self.emojis = emoji_library

    def format(self, format_string, *args, **kwargs):
        formatted = self.vformat(format_string, args, kwargs)
        formatted = formatted.format(*args, **kwargs)
        return formatted

    def get_value(self, key, args, kwargs):
        return key

    def convert_field(self, value, conversion):
        if conversion is None:
            return value
        else:
            return f"{value}!{conversion}"

    def format_field(self, value, format_spec):
        if value == "e":
            if format_spec != "":
                return str(self.emojis.get(format_spec))
            else:
                return f"{{{value}}}"
        elif format_spec != "":
            return f"{{{value}:{format_spec}}}"
        else:
            return f"{{{value}}}"

    def get_field(self, field_name, args, kwargs):
        return field_name, None


class Emojis(AssetLibrary):
    """
    :class:`AssetLibrary` for :class:`discord.PartialEmoji` objects.

    Creates :class:`discord.PartialEmoji` objects from JSON-files.
    
    Parameters
    ----------
    sentinel: Optional[Union[str, discord.PartialEmoji, discord.Emoji]]
        The fallback value that is returned when a requested emoji is not
        found. Should be either a string of a unicode emoji, or any of the
        emoji types found in ``discord.py``.

        Defaults to \N{BLACK QUESTION MARK ORNAMENT}.
    """

    def __init__(self, sentinel=None):
        super().__init__(sentinel=sentinel or SENTINEL)
        self.logger = logging.getLogger("senko.emojis")
        self.formatter = EmojiFormatter(self)

    def load_file(self, file):
        """
        Load emojis from a file.

        Expects the file to be in JSON format, containing a single map that
        maps strings to strings, which can either be unicode or discord emojis.   

        Parameters
        ----------
        file: Union[str, os.PathLike]
            The file to load.
        """
        with open(file, "r", encoding="utf-8") as fileobj:
            data = json.load(fileobj)

        index = dict()
        pattern = re.compile(r"<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$")

        for key, value in data.items():
            if not isinstance(key, str):
                t = type(key).__name__
                raise TypeError(f"Bad type {t!r} for emoji key (must be str)!")
            
            if not isinstance(value, str):
                t = type(key).__name__
                raise TypeError(f"Bad type {t!r} for value of emoji {key!r} (must be str)!")

            match = pattern.match(value)

            if match:
                anim = bool(match.group(1))
                name = match.group(2)
                id = int(match.group(3))
                partial = discord.PartialEmoji(animated=anim, name=name, id=id)
            else:
                partial = discord.PartialEmoji(animated=False, name=value, id=None)

            index[key] = partial

        self.objects.update(index)

    def load_dir(self, path, walk=True, ignore_errors=False):
        """
        Walk through a directory and attempt to load all json files in it.

        Parameters
        ----------
        path: Union[str, os.PathLike]
            The directory path.
        walk: Optional[bool]
            Whether to walk the directory tree. Defaults to ``True``.
        ignore_errors: Optional[bool]
            Whether to not stop upon encountering errors.
        """
        before = len(self.objects)
        super().load_dir(path, ["json"], walk=walk, ignore_errors=ignore_errors)
        after = len(self.objects)

        if after > before:
            diff = after - before
            self.logger.info(f"Loaded {diff} emoji(s).")

    def format(self, string, *args, **kwargs):
        """
        Format a string with emojis and the provided parameters.

        Replaces template substrings like ``{e:key}`` with the corresponding
        emoji. Missing emojis are logged and are replaced with the sentinel
        value instead.

        Example
        -------
        .. code-block:: python3

            string = "{e:senko} Senko likes {e:aburaage} Tofu."
            string = bot.emojis.format(string)

        Parameters
        ----------
        string: str
            The string to format.
        
        Returns
        -------
        str
            The formatted string.
        """
        return self.formatter.format(string, *args, **kwargs)

import json
import logging
import re
import string

import discord

from .abc import AssetLibrary

__all__ = ("EmojiFormatter", "Emojis")

# The default fallback value that is returned when requesting
# a missing emoji. In Discord on desktop, this is the red question mark.
SENTINEL = "\N{BLACK QUESTION MARK ORNAMENT}"


class EmojiFormatter(string.Formatter):
    """
    A custom :class:`string.Formatter` that replaces ``{e:key}`` template
    strings with an emoji retrieved through :meth:`EmojiLibrary.get`.

    Positional and keyword parameters passed into :meth:`EmojiFormatter.format`
    are formatted as usual, and are expected to resolve any other template
    strings found within the provided format string.

    .. _formatter: https://docs.python.org/3/library/string.html#string.Formatter

    .. note::

        See `string.Formatter <formatter>`_ for information on string formatters.

    Parameters
    ----------
    emoji_library: EmojiLibrary
        The emoji library to draw emojis from.
    """

    def __init__(self, emoji_library):
        self.emojis = emoji_library

    def format(self, format_string, *args, **kwargs):
        formatted = self.vformat(format_string, args, kwargs)
        formatted = formatted.format(*args, **kwargs)
        return formatted

    def get_value(self, key, args, kwargs):
        return key

    def convert_field(self, value, conversion):
        if conversion is None:
            return value
        else:
            return f"{value}!{conversion}"

    def format_field(self, value, format_spec):
        if value == "e":
            if format_spec != "":
                return str(self.emojis.get(format_spec))
            else:
                return f"{{{value}}}"
        elif format_spec != "":
            return f"{{{value}:{format_spec}}}"
        else:
            return f"{{{value}}}"

    def get_field(self, field_name, args, kwargs):
        return field_name, None


class Emojis(AssetLibrary):
    """
    :class:`AssetLibrary` for :class:`discord.PartialEmoji` objects.

    Creates :class:`discord.PartialEmoji` objects from JSON-files.
    
    Parameters
    ----------
    sentinel: Optional[Union[str, discord.PartialEmoji, discord.Emoji]]
        The fallback value that is returned when a requested emoji is not
        found. Should be either a string of a unicode emoji, or any of the
        emoji types found in ``discord.py``.

        Defaults to \N{BLACK QUESTION MARK ORNAMENT}.
    """

    def __init__(self, sentinel=None):
        super().__init__(sentinel=sentinel or SENTINEL)
        self.logger = logging.getLogger("senko.emojis")
        self.formatter = EmojiFormatter(self)

    def load_file(self, file):
        """
        Load emojis from a file.

        Expects the file to be in JSON format, containing a single map that
        maps strings to strings, which can either be unicode or discord emojis.   

        Parameters
        ----------
        file: Union[str, os.PathLike]
            The file to load.
        """
        with open(file, "r", encoding="utf-8") as fileobj:
            data = json.load(fileobj)

        index = dict()
        pattern = re.compile(r"<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$")

        for key, value in data.items():
            if not isinstance(key, str):
                t = type(key).__name__
                raise TypeError(f"Bad type {t!r} for emoji key (must be str)!")
            
            if not isinstance(value, str):
                t = type(key).__name__
                raise TypeError(f"Bad type {t!r} for value of emoji {key!r} (must be str)!")

            match = pattern.match(value)

            if match:
                anim = bool(match.group(1))
                name = match.group(2)
                id = int(match.group(3))
                partial = discord.PartialEmoji(animated=anim, name=name, id=id)
            else:
                partial = discord.PartialEmoji(animated=False, name=value, id=None)

            index[key] = partial

        self.objects.update(index)

    def load_dir(self, path, walk=True, ignore_errors=False):
        """
        Walk through a directory and attempt to load all json files in it.

        Parameters
        ----------
        path: Union[str, os.PathLike]
            The directory path.
        walk: Optional[bool]
            Whether to walk the directory tree. Defaults to ``True``.
        ignore_errors: Optional[bool]
            Whether to not stop upon encountering errors.
        """
        before = len(self.objects)
        super().load_dir(path, ["json"], walk=walk, ignore_errors=ignore_errors)
        after = len(self.objects)

        if after > before:
            diff = after - before
            self.logger.info(f"Loaded {diff} emoji(s).")

    def format(self, string, *args, **kwargs):
        """
        Format a string with emojis and the provided parameters.

        Replaces template substrings like ``{e:key}`` with the corresponding
        emoji. Missing emojis are logged and are replaced with the sentinel
        value instead.

        Example
        -------
        .. code-block:: python3

            string = "{e:senko} Senko likes {e:aburaage} Tofu."
            string = bot.emojis.format(string)

        Parameters
        ----------
        string: str
            The string to format.
        
        Returns
        -------
        str
            The formatted string.
        """
        return self.formatter.format(string, *args, **kwargs)

# The default fallback value that is returned when requesting # a missing emoji. In Discord on desktop, this is the red question mark. A custom :class:`string.Formatter` that replaces ``{e:key}`` template     strings with an emoji retrieved through :meth:`EmojiLibrary.get`.      Positional and keyword parameters passed into :meth:`EmojiFormatter.format`     are formatted as usual, and are expected to resolve any other template     strings found within the provided format string.      .. _formatter: https://docs.python.org/3/library/string.html#string.Formatter      .. note::          See `string.Formatter <formatter>`_ for information on string formatters.      Parameters     ----------     emoji_library: EmojiLibrary         The emoji library to draw emojis from. :class:`AssetLibrary` for :class:`discord.PartialEmoji` objects.      Creates :class:`discord.PartialEmoji` objects from JSON-files.          Parameters     ----------     sentinel: Optional[Union[str, discord.PartialEmoji, discord.Emoji]]         The fallback value that is returned when a requested emoji is not         found. Should be either a string of a unicode emoji, or any of the         emoji types found in ``discord.py``.          Defaults to \N{BLACK QUESTION MARK ORNAMENT}. Load emojis from a file.          Expects the file to be in JSON format, containing a single map that         maps strings to strings, which can either be unicode or discord emojis.             Parameters         ----------         file: Union[str, os.PathLike]             The file to load. Walk through a directory and attempt to load all json files in it.          Parameters         ----------         path: Union[str, os.PathLike]             The directory path.         walk: Optional[bool]             Whether to walk the directory tree. Defaults to ``True``.         ignore_errors: Optional[bool]             Whether to not stop upon encountering errors. Format a string with emojis and the provided parameters.          Replaces template substrings like ``{e:key}`` with the corresponding         emoji. Missing emojis are logged and are replaced with the sentinel         value instead.          Example         -------         .. code-block:: python3              string = "{e:senko} Senko likes {e:aburaage} Tofu."             string = bot.emojis.format(string)          Parameters         ----------         string: str             The string to format.                  Returns         -------         str             The formatted string.