"""Module used to deal with error messages."""

# ========================
# Python internal packages
# ========================
# E
import enum

# S
import sys

# T
import typing

# W
import warnings


class TermColor(enum.Enum):
    """ANSI color codes used for terminal output formatting."""

    BLUE = "\033[34m"
    YELLOW = "\033[33m"
    RED = "\033[31m"
    CLEAN = "\033[0m"

    def __str__(self) -> str:
        """Return the value of the enumeration member.

        Returns
        -------
        `str`
            The value of a called enumeration.
        """
        return self.value


class Formatter:
    """Utility class to generate formatted enum values for logging."""

    __NB_DIGIT: int = 2

    def __init__(
        self, color: TermColor, identifier: str, has_category: bool = False
    ) -> None:
        """Initialize a formatter instance.

        Parameters
        ----------
        color : `TermColor`
            Terminal color to apply to the formatted message.

        identifier : `str`
            Short identifier used in the formatted message.

        has_category : `bool`, optional
            Whether to include an exception category placeholder in the
            formatted message. By default, `False`.
        """
        self.__count: int = 1

        if has_category:
            # Double formatting: enum creation + runtime formatting.
            category: str = "[{{{{category}}}}]"
        else:
            category: str = ""

        self.__format: str = (
            f"{color}[{identifier}{{code:0{Formatter.__NB_DIGIT}d}}]"
            f"{category} {{message}}{TermColor.CLEAN}"
        )

    def generate_enum(
        self, enum_class: type[enum.Enum], message: str
    ) -> enum.Enum:
        """Generate a formatted enumeration value.

        Parameters
        ----------
        enum_class : `type[enum.Enum]`
            The enumeration class being instantiated.

        message : `str`
            The raw message associated with the enumeration member.

        Returns
        -------
        `enum.Enum`
            A newly created enumeration member with formatted value, message,
            and code attributes.
        """
        formated_message: str = self.__format.format(
            code=self.__count,
            message=message,
        )

        new_object: enum.Enum = str.__new__(enum_class, formated_message)

        new_object.message = message
        new_object.code = self.__count
        new_object._value_ = formated_message

        self.__count += 1

        return new_object


class Info(str, enum.Enum):
    """Enumeration of informational messages."""

    formatter = enum.nonmember(Formatter(TermColor.BLUE, "Inf"))

    def __new__(cls: enum.Enum, message: str):
        """Create a new formatted informational enumeration member.

        Parameters
        ----------
        message : `str`
            Raw message template associated with the enumeration member.

        Returns
        -------
        `Info`
            A formatted enumeration member.
        """
        return cls.formatter.generate_enum(cls, message)

    def __call__(self, **k_w_arg) -> None:
        """Print the formatted informational message.

        Parameters
        ----------
        **k_w_arg
            Keyword arguments used to format the message template.
        """
        print(self.value.format(**k_w_arg), file=sys.stderr)

    COMPUTE_PHARMACOPHORE = "Computing pharmacophore {status}"
    ADD_HYDROGEN = "Adding hydrogen for structure '{file}'."


class Warn(str, enum.Enum):
    """Enumeration of warning messages."""

    default_hook = warnings.formatwarning
    formatter = enum.nonmember(Formatter(TermColor.YELLOW, "Warn", True))

    def __new__(cls: enum.Enum, message):
        """Create a new formatted warning enumeration member.

        Parameters
        ----------
        message : `str`
            Raw warning message template.

        Returns
        -------
        `Warn`
            A formatted warning enumeration member.
        """
        return cls.formatter.generate_enum(cls, message)

    def __call__(self, **k_w_arg) -> None:
        """Emit the warning message.

        Parameters
        ----------
        **k_w_arg
            Keyword arguments used to format the warning message.
        """
        warnings.formatwarning = Warn.format_warning

        try:
            warnings.warn(self.value.format(**k_w_arg))
        finally:
            warnings.formatwarning = Warn.default_hook

    @staticmethod
    def format_warning(
        message: str, category: type, filename, lineno, line=None
    ):
        """Custom formatting function for Python warnings.

        Parameters
        ----------
        message : `str`
            Warning message instance.

        category : `type`
            Warning category.

        filename
            **[NOT USED]**

        lineno
            **[NOT USED]**

        line : optional
            **[NOT USED]**

        Returns
        -------
        `str`
            Formatted warning string.
        """
        del filename, lineno, line

        return str(message).format(category=category.__name__) + "\n"

    SELECTION_NOT_SUPPORTED = (
        "Selection not supported for file extension '{extension}'."
    )
    AROMATICITY = (
        "Checking and assigning aromaticity for input file '{file}', with "
        "selection '{selection}'."
    )
    SKIP_CYCLE = "Skipping cycle of size {length} atoms."
    MDA_GUESS = "MDAnalysis says `{warning}`."
    NO_IMPLICIT = (
        "`NoImplicit` set to `False` for file '{file}' with selection "
        "'{selection}'. Bond orders and charges inferring disable."
    )


class Error(str, enum.Enum):
    """Enumeration of fatal error messages."""

    default_hook = sys.excepthook
    formatter = enum.nonmember(Formatter(TermColor.RED, "Err", True))

    def __new__(cls: enum.Enum, message) -> enum.Enum:
        """Create a new formatted error enumeration member.

        Parameters
        ----------
        message : `str`
            Raw error message template.

        Returns
        -------
        `Error`
            A formatted error enumeration member.
        """
        return cls.formatter.generate_enum(cls, message)

    def __call__(self, error: Exception, **k_w_arg) -> None:
        """Raise a formatted exception.

        Parameters
        ----------
        error : `Exception`
            Exception class to raise.

        **k_w_arg
            Keyword arguments used to format the error message.

        Raises
        ------
        `Exception`
            The formatted exception.
        """
        sys.excepthook = Error.except_hook
        raise error(self.value.format(**k_w_arg))

    @staticmethod
    def except_hook(exc_type, exc_value, traceback) -> None:
        """Custom exception hook for formatted error output.

        Parameters
        ----------
        exc_type
            Exception type.

        exc_value
            Exception instance.

        traceback
            Traceback object.
        """
        try:
            if "][{category}] " not in str(exc_value):
                sys.excepthook = Error.default_hook
                sys.excepthook(exc_type, exc_value, traceback)
                return None

            print(str(exc_value).format(category=exc_type.__name__))
            return None

        except:
            sys.excepthook = Error.default_hook
            sys.excepthook(exc_type, exc_value, traceback)

        return None

    GENERAL_ARGUMENT_ERROR = "{message}."
    FILE_NOT_FOUND = "File '{file}' does not exist."
    NOT_DIRECTORY = "'{file}' is a directory."
    WRONG_EXTENSION = (
        "File '{file}' extension is wrong. Accepted {type} format:\n"
        "- {format_list}."
    )
    PATH_NOT_FOUND = "Path '{path}' does not exist."
    ADD_HYDROGEN = (
        "Get '{value}' for '-a/--add_hydrogen', except one of these values:"
        "\n{except_value}"
    )
    EXCEPTED_LEN = (
        "Excepte {good_length} arguments for '{command}', got '{length}'."
    )
    SAME_PHARMA_OUTPUT = (
        "Got two times the same output for '--pharmacophore'. Got '{first}' "
        "and '{second}'."
    )
    BUILD_FAIL = (
        "File '{file}' parsed, but no molecule could be built. Please check "
        "the input file."
    )
    PARTIAL_BUILD = (
        "File '{file}' parsed, but the molecule is partially built. Please "
        "check the input file or try to set '-a/--add-hydrogen`."
    )
    MOL_2 = (
        "RDKit failed to parse the input molecule. The issue could be that "
        "the parser expects the atom-typing scheme used by Corina'. Try "
        "changing the atom-typing scheme, else check the input file '{file}'."
    )
    INDEX_OVER_LEN = (
        "Index '{index}' is out of range. Only {length} detected in the "
        "given file '{file}'."
    )
    MDA_FORMAT = "Given file extension '{extension}' is not a valid file type."
    WRONG_SELECTION = (
        "The selection '{selection}' contain unknown selection token."
    )
    EMPTY_SELECTION = (
        "The selection '{selection}' results in the selection of 0 atom."
    )
    FAMILY_NOT_REDEFINE = (
        "Found '{family}' in the feature list, but not found inside "
        "redefinition category."
    )
    WRONG_CATEGORY = (
        "Category '{category}' not found. Could not proceed with "
        "pharmacophore extraction."
    )
    HYDROGEN_ITERATOR = (
        "The hydrogen donor iterator must contain at least one pharmacophore."
    )
    NO_PHARMACOPHORE_DETECTED = (
        "For file '{file}'{message}, no pharmacophore were detected."
    )
    NO_INTERACTION = "No interaction detected."
    WRONG_KEY = "For '{category}', key '{key}' unmatched."
    OVER_CALL = "Object from class '{class_name}' overcalled."
    BAD_MAIN_KEY = (
        "Keys '[{key}]' should not be present inside the given configuration."
    )
    SAVE_PDB_FAIL = (
        "Failed to correct and add hydrogen to file '{file}'. Try protonating "
        "and checking the system."
    )
    WRONG_TYPING = (
        "Receive for field '{field}' type '{wrong_type}'. "
        "Excepted '{good_type}'."
    )
