from typing import (
    Any,
    Dict,
    Union,
    cast,
)

from eth_utils import (
    keccak,
)
from eth_utils.toolz import (
    pipe,
)
from hexbytes import (
    HexBytes,
)
import rlp
from typing_extensions import Self  # move to typing when >= py311


class HashableRLP(rlp.Serializable):  # type: ignore
    r"""
    An extension of :class:`rlp.Serializable`. In addition to the below
    functions, the class is iterable.

    Use like:

    ::

        class MyRLP(HashableRLP):
            fields = (
                ('name1', rlp.sedes.big_endian_int),
                ('name2', rlp.sedes.binary),
                etc...
            )

        my_obj = MyRLP(name2=b'\xff', name1=1)
        list(my_obj) == [1, b'\xff']
        # note that the iteration order is always in RLP-defined order
    """

    @classmethod
    def from_dict(cls, field_dict: Dict[str, Any]) -> Self:
        r"""
        In addition to the standard initialization of.

        ::

            my_obj = MyRLP(name1=1, name2=b'\xff')

        This method enables initialization with.

        ::

            my_obj = MyRLP.from_dict({'name1': 1, 'name2': b'\xff'})

        In general, the standard initialization is preferred, but
        some approaches might favor this API, like when using
        :meth:`toolz.functoolz.pipe`.

        ::

            return eth_utils.toolz.pipe(
                my_dict,
                normalize,
                validate,
                MyRLP.from_dict,
            )

        :param dict field_dict: the dictionary of values to initialize with
        :returns: the new rlp object
        :rtype: HashableRLP
        """
        return cls(**field_dict)

    @classmethod
    def from_bytes(cls, serialized_bytes: Union[bytes, bytearray]) -> Self:
        """
        Shorthand invocation for :meth:`rlp.decode` using this class.

        :param bytes serialized_bytes: the byte string to decode
        :return: the decoded object
        :rtype: HashableRLP
        """
        decoded = rlp.decode(serialized_bytes, cls)
        return cast(Self, decoded)

    def hash(self) -> HexBytes:
        """
        :returns: the hash of the encoded bytestring
        :rtype: ~hexbytes.main.HexBytes
        """
        return pipe(
            self,
            rlp.encode,
            keccak,
            HexBytes,
        )

    def __iter__(self) -> Any:
        if hasattr(self, "fields"):
            return iter(getattr(self, field) for field, _ in self.fields)
        else:
            return super().__iter__()

    def as_dict(self) -> Dict[str, Any]:
        """
        Convert rlp object to a dict

        :returns: mapping of RLP field names to field values
        :rtype: dict
        """
        try:
            _as_dict = super().as_dict()
            return cast(Dict[str, Any], _as_dict)
        except AttributeError:
            _as_dict = vars(self)
            return cast(Dict[str, Any], _as_dict)
