"""
    Descriptor class for cached properties.
"""

# Part of typed-descriptors
# Copyright (C) 2023 Hashberg Ltd

from __future__ import annotations
import sys
from typing import (
    Any,
    Literal,
    Optional,
    Protocol,
    Type,
    TypeVar,
    cast,
    final,
    overload,
)
from typing_validation import validate
from .base import DescriptorBase, T

if sys.version_info[1] >= 12:
    from typing import Self
else:
    from typing_extensions import Self

T_co = TypeVar("T_co", covariant=True)
""" Covariant type variable for generic values. """


class ValueFunction(Protocol[T_co]):
    """
    Structural type for the value function of a :class:`Prop`.
    """

    def __call__(self, instance: Any, /) -> T_co:
        """
        Computes and returns the value for a :class:`Prop`,
        in the context of the given instance.
        """


class Prop(DescriptorBase[T]):
    """
    A descriptor class for cached properties, supporting:

    - static type checking for the property value
    - lazy caching (value only cached at first read)

    See :class:`DescriptorBase` for details on how the property value is
    cached in each instance.
    """

    __value_fun: ValueFunction[T]

    __slots__ = ("__value_fun",)

    @overload
    def __init__(
        self,
        ty: Type[T],
        /,
        value: ValueFunction[T],
        *,
        attr_name: Optional[str] = None,
        lax: Literal[False] = False,
    ) -> None:
        ...

    @overload
    def __init__(
        self,
        ty: Any,
        /,
        value: ValueFunction[T],
        *,
        attr_name: Optional[str] = None,
        lax: Literal[True],
    ) -> None:
        ...

    def __init__(
        self,
        ty: Type[T],
        /,
        value: ValueFunction[T],
        *,
        attr_name: Optional[str] = None,
        lax: bool = False,
    ) -> None:
        """
        Creates a new property with the given type and value function.

        :param ty: the type of the property
        :param value: function computing the property value
        :param attr_name: the name of the backing attribute for the property
                          cache, or :obj:`None` to use a default name
        :param lax: if set to :obj:`True`, suppresses static typechecking
                    of the ``ty`` argument, allowing more general types to
                    be specified for runtime typechecks.

        :raises TypeError: if the type is not a valid type
        :raises TypeError: if the value function is not callable

        .. note ::

            If ``lax=True`` is set, the static typechecker can no longer
            infer ``T`` from the ``ty`` argument. It will infer ``T`` from
            the ``validator`` argument, if it is given and it is statically
            typed; otherwise, a static type for the descriptor will have to
            be set by hand.

        :meta public:
        """
        super().__init__(ty, attr_name=attr_name)
        if value is not None and not callable(value):
            raise TypeError(f"Expected callable 'value', got {value!r}.")
        self.__value_fun = value

    @final
    @property
    def value_fun(self) -> ValueFunction[T]:
        """
        The function called to produce a value for this property on a given
        instance.
        It is called by the getter when the property doesn't have a cached
        value, and the value returned is automatically cached.
        """
        return self.__value_fun

    @final
    def is_cached_on(self, instance: Any) -> bool:
        """
        Whether the property is cached on the given instance.
        """
        validate(instance, self.owner)
        return hasattr(instance, self.attr_name)

    @final
    def cache_on(self, instance: Any) -> None:
        """
        Caches the property value on the given instance.
        Can be used to force property computation at a desired time,
        overriding the default lazy behaviour.

        :raises AttributeError: if the property is already cached.
        """
        validate(instance, self.owner)
        if hasattr(instance, self.attr_name):
            raise AttributeError(f"Property {self} is already cached.")
        value = self.value_fun(instance)
        validate(value, self.type)
        setattr(instance, self.attr_name, value)

    @overload
    def __get__(self, instance: None, _: Type[Any]) -> Self:
        ...

    @overload
    def __get__(self, instance: Any, _: Type[Any]) -> T:
        ...

    @final
    def __get__(self, instance: Any, _: Type[Any]) -> T | Self:
        """
        If the descriptor is accessed on an instance, returns the value of
        the property on the given instance.

        If the descriptor is accessed on the owner class, i.e. if
        ``instance`` is :obj:`None`, returns the :class:`Prop` object.

        :meta public:
        """
        if instance is None:
            return self
        try:
            return cast(T, getattr(instance, self.attr_name))
        except AttributeError:
            value = self.value_fun(instance)
            validate(value, self.type)
            setattr(instance, self.attr_name, value)
            return value

    __set__ = None
    """
        Property values cannot be set.

        :meta public:
    """

    @final
    def __delete__(self, instance: Any) -> None:
        """
        Deletes the property cache on the given instance.

        :raises AttributeError: if the property is not cached,
                                see :meth:`is_cached_on`.

        :meta public:
        """
        if not hasattr(instance, self.attr_name):
            raise AttributeError(f"Property {self} is not cached.")
        delattr(instance, self.attr_name)

    def __str__(self) -> str:
        """
        Representation of this property, inclusive of the following info:

        - the :attr:`owner` name
        - the property :attr:`name`

        An example:

        .. code-block ::

            Color.hue
        """
        type_name = type(self).__name__
        owner_name = self.owner.__name__
        attr_name = self.attr_name
        return f"{type_name} {owner_name}.{attr_name}"

    def __repr__(self) -> str:
        """
        Representation of this property, inclusive of the following info:

        - the :class:`Prop` subclass
        - the :attr:`owner` name
        - the property :attr:`name`
        - the property :attr:`type`

        An example:

        .. code-block ::

            <Prop Color.rgb: tuple[int, int, int]>

        """
        descr_cls = type(self).__name__
        owner = self.owner.__name__
        name = self.attr_name
        ty = (
            self.type.__name__
            if isinstance(self.type, type)
            else str(self.type)
        )
        return f"<{descr_cls} {owner}.{name}: {ty}>"
