"""Generic things."""

# ========================
# Python internal packages
# ========================
# D
import dataclasses

# E
import enum

# T
import typing

# ==============
# Module package
# ==============
# M
from .messenger import (
    Error,
)


class DataClass(typing.Protocol):
    """To make dataclass typing."""
    pass


class CsvWriter:
    """Utility class to serialize a list of pharmacophore frames into a `.csv`
    format.

    The input `data` is expected to be a list, representing frames. Each items
    are actually a dictionary of list, representing a collection like
    pharmacophore or properties. Then, each items of this last list are a
    dataclass.

    ```python
    [
        # Frame 1.
        {
            # First properties.
            "pharmacophore_A": [
                # First element of properties (for instance, H-bond donnor #1).
                dataclass(),
                dataclass(),
                ...
            ]
            "pharmacophore_B": [dataclass(), dataclass(), ...]
        }
        # Frame 2.
        {
            "pharmacophore_A": [dataclass(), dataclass(), ...]
            "pharmacophore_B": [dataclass(), dataclass(), ...]
        }
    ]
    ```

    So in summary, we have frames of collection of dataclass. Which are list,
    of dict of list, of dataclass.
    """

    def __init__(
        self,
        data: list[dict[str, list[DataClass]]],
        collection_name: str,
        miscellaneous: DataClass,
    ) -> None:
        """Initializes the `.csv` writer with data and header construction.

        Parameters
        ----------
        data : `list[dict[str, list[DataClass]]]`
            Frames of collection of dataclass. Which are list, of dict of list,
            of dataclass.

        collection_name : `str`
            The name to be used as the leading column in the CSV header for the
            collection.

        miscelleneous : `DataClass`
            Configuration dataclass containing CSV formatting options.
        """
        # Data must be a list (frames) of dict of list (collection) of
        # dataclass.
        self.__data: list[dict[str, list[DataClass]]] = data
        self.__miscellaneous: DataClass = miscellaneous

        self.head: str = self.__build_header(collection_name)
        self.body: str = self.__simulation_to_line(data)

    def __build_header(self, collection_name: str) -> str:
        """Builds the `.csv` header from the first dataclass in the data.

        Parameters
        ----------
        collection_name : `str`
            The label for the first column in the header.

        Returns
        -------
        `str`
            A formatted CSV header string in uppercase.
        """
        header: str = f"{collection_name}{self.__miscellaneous.csv_separator}"

        # Here, we get first item `[0]`, which is a dataclass, from the first
        # dict item `next(iter())`, from the first frame `self.__data[0]`.
        data_class: DataClass = next(iter(self.__data[0].values()))[0]

        header += self.__fetch_field_name(data_class)
        header = header + "FRAME\n"

        return header.upper()

    def __fetch_field_name(
        self, data_class: DataClass, key: typing.Optional[str] = ""
    ) -> str:
        """Recursively extract field names from a dataclass.

        Parameters
        ----------
        data_class : `DataClass`
            The dataclass instance from which to extract field names.

        key : `str`, optional
            Prefix used for nested field names.

        Returns
        -------
        `str`
            A CSV-formatted string of field names.
        """
        header: str = ""

        for field_i in dataclasses.fields(data_class):
            if not field_i.repr:
                continue

            field_i_value: typing.Any = getattr(data_class, field_i.name)
            current_key: str = f"{key}{field_i.name}"

            if dataclasses.is_dataclass(field_i_value):
                header += self.__fetch_field_name(
                    field_i_value, field_i.name + "_"
                )
                continue

            header += f"{current_key}{self.__miscellaneous.csv_separator}"

        return header

    def __data_class_to_line(self, data_class: DataClass) -> str:
        """Converts a dataclass object to a CSV line.

        Parameters
        ----------
        data_class : `DataClass`
            The dataclass instance to convert.

        Returns
        -------
        `str`
            String representation of the dataclass, as `.csv` format.
        """
        to_string: str = ""

        for field_i in dataclasses.fields(data_class):
            # Skip unwanted fields.
            if not field_i.repr:
                continue

            value: str = getattr(data_class, field_i.name)

            if dataclasses.is_dataclass(value):
                to_string += self.__data_class_to_line(value)
                continue

            if str(value) == "None":
                value = ""

            to_string += f"{value}{self.__miscellaneous.csv_separator}"

        return to_string

    def __collection_to_line(
        self, collection: dict[str, DataClass]
    ) -> list[str]:
        """Converts a collection to a list of `.csv` lines.

        Parameters
        ----------
        collection : `dict[str, DataClass]`
            A collection of dataclass. Which are dict of list, of dataclass.

        Returns
        -------
        `list[str]`
            The list of `.csv` formatted string lines for the given collection.
        """
        to_list: list[str] = []

        for key, item in collection.items():
            for data_class in item:
                to_list += [
                    f"{key}{self.__miscellaneous.csv_separator}"
                    f"{self.__data_class_to_line(data_class)}"
                ]

        return to_list

    def __simulation_to_line(self, simulation: list[dict]) -> str:
        """Converts a full simulation (frames of collections) to `.csv` string.

        Parameters
        ----------
        simulation : `list[dict]`
            A list representing frames. Each items is a collection of
            dataclass.

        Returns
        -------
        `str`
            Full `.csv` formatted string for all frames.
        """
        to_string: str = ""

        for i, frame in enumerate(simulation):
            to_string += f"{i}\n".join(self.__collection_to_line(frame))
            to_string += f"{i}\n"

        return to_string

    def __str__(self) -> str:
        """Returns the complete `.csv` string representation.

        Returns
        -------
        `str`
            Header and body of the `.csv` file to generate.
        """
        return self.head + self.body


class KeyIdentifier(enum.Enum):
    """Enumeration for identified to which type of data is inside the `.csv`
    file. Basically, it works as follow:

    - `PHARMACOPHORE`: The line contains pharmacophore related information,
      like geometrical centre.
    - `ATOM`: The line contains atoms related, like their position, residue ID,
      etc.
    - `NORM`: Specific to aromatic pharmacophores, the norm from the cycle
      plane.
    """

    PHARMACOPHORE = "pharmacophore"
    ATOM = "atom"
    NORM = "norm"

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

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


class PharmacophoreFamily(enum.Enum):
    """Define all pharmacophore families."""

    DONOR = "donor"
    ACCEPTOR = "acceptor"
    NEGATIVE = "negative"
    POSITIVE = "positive"
    AROMATIC = "aromatic"
    HYDROPHOBE = "hydrophobe"
    LUMPED_HYDROPHOBE = "lumped_hydrophobe"
    HALOGEN_DONOR = "halogen_donor"
    HALOGEN_ACCEPTOR = "halogen_acceptor"

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

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

    def __lt__(self, other: typing.Any | str) -> bool:
        """Compare pharmacophore types based on their string values.
        Implemented to be compatible with `sorted()`.

        Parameters
        ----------
        other : `typing.Any | str`
            A string or `typing.Any` that can be casted as a `str` to compare against.

        Returns
        -------
        `bool`
            `True` if the value of this member is less than the other. `False`
            otherwise.
        """
        return str(self) < str(other)


class InteractionType(enum.Enum):
    """Define all type of possible interaction."""

    HYDROPHOBIC = "hydrophobic"
    HYDROGEN_BOND = "hydrogen_bond"
    PI_STACKING = "pi_stacking"
    PI_CATION = "pi_cation"
    PI_ANION = "pi_anion"
    SALT_BRIDGE = "salt_bridge"
    HALOGEN_BOND = "halogen_bond"

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

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


def check_typing(data_class: DataClass) -> None:
    """Validate runtime types of dataclass fields.

    Parameters
    ----------
    data_class : `DataClass`
        The dataclass instance to validate.

    Raises
    ------
    `TypeError`
        If a field value does not match its declared type.
    """
    for field_i in dataclasses.fields(data_class):
        value: typing.Any = getattr(data_class, field_i.name)

        if isinstance(value, field_i.type):
            continue

        Error.WRONG_TYPING(
            TypeError,
            field=field_i.name,
            wrong_type=type(value).__name__,
            good_type=field_i.type.__name__,
        )
