"""Defines the Serializable protocol, which is what we expect things to have, so we can serialize and deserialize them

"""

from pathlib import PurePath
from typing import Any, ClassVar, Protocol, Sequence, Type, TypeVar, runtime_checkable

try:  # To allow for older Python versions where Self was not supported
    from typing import Self
except ImportError:
    Self = TypeVar("Self")

import numpy as np
from cytoolz import valmap

T = TypeVar("T", bound="Serializable")
D = TypeVar("D", bound="_Deserializable")


class _Deserializable(Protocol):
    @classmethod
    def deserialize(cls: Type[D], d: dict[str, Any], *, supported: dict[str, Type["Serializable"]]) -> D:
        ...


@runtime_checkable
class Serializable(Protocol):
    """Protocol for classes that we can serialize to JSON-able formats and deserialize from.

    Serializable objects must be capable of being equal to themselves by going through serialization and deserialization

    """

    ser_identifier: ClassVar[str]

    def serialize(self) -> tuple[str, dict[str, Any]]:
        """Serializes the object to return its class' identifier and the data that allows to recreate the instance.

        Returns
        -------
        str, dict[str, data]
            The ser_identifier and the data that allows to recreate an instance.

        """
        return self.ser_identifier, _ensure_list(_save_attributes(self))

    @classmethod
    def deserialize(cls: Type[Self], d: dict[str, Any], *, supported: dict[str, Type["Serializable"]]) -> Self:
        """Create a new instance given the data generated by an instance's serialize method.

        Parameters
        ----------
        d: dict[str, Any]
            The data dictionary returned by the original object's serialize method.
        supported: dict[str, Type[Serializable]]
            A dictionary of which types are supported for deserialization.

        """
        return cls(**_default_tuple(d))

    def __eq__(self: T, other: T) -> bool:
        ...


def deserialize_default(data: tuple[str, dict[str, Any]],
                        supported: dict[str, Type[Serializable]],
                        default: Type[_Deserializable] | None = None) -> _Deserializable:
    """Deserialize the data given by a Serializable object's `serialize` method given class deserialization

    Parameters
    ----------
    data: str, dict[str, Any]
        The result from the object's Serialize method. See :func:Serializable.serialize.
    supported: dict[str, Type[Serializable]]
        A mapping of serialization identifiers to the classes they represent.
    default: Type[Serializable] or None
        The default type to use if the serialization identifier is not recognized. If None, raises an error if the
        identifier isn't there.

    Returns
    -------
    Serializable
        The deserialized object

    """
    name, d = data
    if default is not None:
        typ = supported.get(name, default)
    else:
        if name not in supported:
            raise TypeError(f"Deserialization of {name} from json isn't supported by the deserializer")
        typ = supported[name]
    return typ.deserialize(d, supported=supported)


def _save_attributes(self) -> dict:
    sd = {k: self.__getattribute__(k) for k in self.__slots__} if self.__slots__ else {}
    return self.__dict__ | sd


def _default_tuple(d: dict) -> dict:
    return valmap(lambda x: tuple(x) if isinstance(x, list) else x, d)


def _prefer_list(x):
    if isinstance(x, Sequence) and not isinstance(x, str):
        return list(x)
    if isinstance(x, np.ndarray):
        return x.tolist()
    if isinstance(x, PurePath):
        return str(x)
    return x


def _ensure_list(d: dict) -> dict:
    return valmap(_prefer_list, d)
