from typing import Any, Generic, Optional, TypeVar

from statham.dsl.constants import NotPassed
from statham.dsl.elements.base import Element
from statham.dsl.helpers import custom_repr_args


PropType = TypeVar("PropType")


class _Property(Generic[PropType]):
    """Descriptor for a property on an object.

    Used to bind information about the enclosing object to a DSL Element,
    e.g. "required". Should be instantiated via `Property` to enable
    type inference on instances.
    """

    required: bool
    parent: Any
    element: Element
    source: Optional[str]
    name: Optional[str]

    def __init__(
        self,
        element: Element[PropType],
        *,
        required: bool = False,
        source: str = None,
    ):
        self.element = element
        self.required = required
        self.name: Optional[str] = None
        self.source: Optional[str] = source
        self.parent = None

    def __eq__(self, other):
        if not isinstance(other, _Property):
            return False
        return (
            self.element == other.element
            and self.required == other.required
            and self.source == other.source
        )

    def evolve(self, name: str) -> "_Property":
        """Generate renamed property object to pass into nested elements."""
        property_: _Property[PropType] = _Property(
            element=self.element, required=self.required, source=self.source
        )
        property_.bind_name(name)
        property_.bind_class(self.parent)
        return property_

    def bind_name(self, name: str):
        if not self.source:
            self.source = name
        self.name = name

    def bind_class(self, parent: Any):
        self.parent = parent

    def __call__(self, value):
        if not isinstance(self.element.default, NotPassed) and isinstance(
            value, NotPassed
        ):
            value = self.element.default
        if isinstance(value, NotPassed):
            return value
        return self.element(value, self)

    def __repr__(self):
        repr_args = custom_repr_args(self)
        if self.source == self.name:
            _ = repr_args.kwargs.pop("source")
        return f"{self.__class__.__name__}{repr(repr_args)}"

    @property
    def annotation(self):
        if self.required or not isinstance(
            getattr(self.element, "default", NotPassed()), NotPassed
        ):
            return self.element.annotation
        return f"Maybe[{self.element.annotation}]"

    def python(self) -> str:
        prop_def = repr(self).replace(type(self).__name__, Property.__name__)
        return (
            (f"{self.name}: {self.annotation} = {prop_def}")
            if self.name
            else prop_def
        )


UNBOUND_PROPERTY: _Property = _Property(Element(), required=False)
UNBOUND_PROPERTY.bind_name("<unbound>")
UNBOUND_PROPERTY.bind_class(Element())


# Behaves as a wrapper for the `_Property` class.
# pylint: disable=invalid-name
def Property(element: Element, *, required: bool = False, source: str = None):
    """Descriptor for adding a property when declaring an object schema model.

    Return value is typed to inform instance-level interface. See type stubs of
    this module for more detail.

    :param element: The JSONSchema Element object accepted by this property.
    :param required: Whether this property is required. If false, then this
        field may be omitted when data is passed to the outer object's
        constructor.
    :param source: The source name of this property. Only necessary if it must
        differ from that of the attribute, for example when the property name
        conflicts with a reserved keyword. For example, to express a property
        called `default`, one could do the following:
        ```python
        class MyObject(Object):

            # Default for the whole object
            default = {"default": "string"}

            # Property called default
            _default: str = Property(String(), source="default")
        ```
    """
    return _Property(element, required=required, source=source)
