from __future__ import annotations

from abc import ABC
from contextlib import contextmanager
from typing import Any, Generator, Iterator, List

from starknet_py.serialization._calldata_reader import (
    CairoData,
    CalldataReader,
    OutOfBoundsError,
)
from starknet_py.serialization.errors import InvalidTypeException, InvalidValueException


class Context(ABC):
    """
    Holds information about context when (de)serializing data. This is needed to inform what and where went
    wrong during processing. Every separate (de)serialization should have its own context.
    """

    _namespace_stack: List[str]

    def __init__(self):
        self._namespace_stack = []

    @property
    def current_entity(self):
        """
        Name of currently processed entity.

        :return: transformed path.
        """
        return ".".join(self._namespace_stack)

    @contextmanager
    def push_entity(self, name: str) -> Generator:
        """
        Manager used for maintaining information about names of (de)serialized types. Wraps some errors with
        custom errors, adding information about the context.

        :param name: name of (de)serialized entity.
        """
        # This ensures the name will be popped if everything is ok. In case an exception is raised we want the stack to
        # be filled to wrap the error at the end.
        self._namespace_stack.append(name)
        yield
        self._namespace_stack.pop()

    def ensure_valid_value(self, valid: bool, text: str):
        if not valid:
            raise InvalidValueException(f"{self._error_prefix}: {text}.")

    def ensure_valid_type(self, value: Any, valid: bool, expected_type: str):
        if not valid:
            raise InvalidTypeException(
                f"{self._error_prefix}: expected {expected_type}, "
                f"received '{value}' of type '{type(value)}'."
            )

    @contextmanager
    def _wrap_errors(self):
        try:
            yield
        except OutOfBoundsError as err:
            action_name = (
                f"deserialize '{self.current_entity}'"
                if self._namespace_stack
                else "deserialize"
            )
            # This way we can precisely inform user what's wrong when reading calldata.
            raise InvalidValueException(
                f"Not enough data to {action_name}. "
                f"Can't read {err.requested_size} values at position {err.position}, {err.remaining_len} available."
            ) from err

        # Those two are based on ValueError and TypeError, we have to catch them early
        except (InvalidValueException, InvalidTypeException) as err:
            raise err

        except ValueError as err:
            raise InvalidValueException(f"{self._error_prefix}: {err}") from err
        except TypeError as err:
            raise InvalidTypeException(f"{self._error_prefix}: {err}") from err

    @property
    def _error_prefix(self):
        if not self._namespace_stack:
            return "Error"
        return f"Error at path '{self.current_entity}'"


class SerializationContext(Context):
    """
    Context used during serialization.
    """

    # Type is iterator, because ContextManager doesn't work with pyright :|
    # https://github.com/microsoft/pyright/issues/476
    @classmethod
    @contextmanager
    def create(cls) -> Iterator[SerializationContext]:
        context = cls()
        with context._wrap_errors():
            yield context


class DeserializationContext(Context):
    """
    Context used during deserialization.
    """

    reader: CalldataReader

    def __init__(self, calldata: CairoData):
        """
        Don't use default constructor. Use DeserializationContext.create context manager.
        """
        super().__init__()
        self._namespace_stack = []
        self.reader = CalldataReader(calldata)

    @classmethod
    @contextmanager
    def create(cls, data: CairoData) -> Iterator[DeserializationContext]:
        context = cls(data)
        with context._wrap_errors():
            yield context
            context._ensure_all_values_read(len(data))

    def _ensure_all_values_read(self, total_len: int):
        values_not_used = self.reader.remaining_len
        if values_not_used != 0:
            # We want to output up to 3 values. It there is more they will be truncated like "0x1,0x1,0x1..."
            max_values_to_show = 3
            values_to_show = min(values_not_used, max_values_to_show)
            example = ",".join(hex(v) for v in self.reader.read(values_to_show))
            suffix = "..." if values_not_used > max_values_to_show else ""

            raise InvalidValueException(
                f"Last {values_not_used} values '{example}{suffix}' out of total {total_len} "
                "values were not used during deserialization."
            )
