"""Backend-agnostic property descriptors used by scenes and UI adapters.

The classes in this module describe editable state independently from any
specific frontend. A property knows how to read and write one semantic field
and reports an :class:`UpdateMask` telling the caller which parts of the
runtime must be refreshed after a change.
"""

from __future__ import annotations

from enum import IntFlag


#####################################################################################


class UpdateMask(IntFlag):
    """Describe which subsystems must react to a property mutation.

    Property setters return this mask through :meth:`Property.let`. Backends use
    it to decide whether they only need to redraw, rebuild property metadata, or
    switch the active camera slot.
    """

    NONE = 0
    REDRAW = 1
    REBUILD_PROPERTIES = 2
    CAMERA_SLOT_CHANGED = 4
    SYNC_SHARED_STATE = 8
    REBUILD_PROPERTY_SCHEMA = 16


class Property:
    """Describe one editable semantic field exposed by a scene or render.

    A property is backend-agnostic: it stores how to read and mutate a field,
    but does not depend on marimo, Qt, or any other presentation layer.

    Parameters
    ----------
    owner:
        Object that owns the concrete field or `set_<field>` mutator.
    field:
        Semantic field name on ``owner``.
    title:
        User-facing label within a property group.
    reset:
        Whether changing the property invalidates the current group layout and
        therefore requires property metadata to be rebuilt.
    param_key:
        Optional stable key used by higher-level sessions to share equivalent
        property values across compatible scenes.
    """

    def __init__(
        self,
        owner: object,
        field: str,
        title="noname",
        reset=False,
        param_key: str | None = None,
    ):
        self._owner = owner
        self._field = field
        self.title = title
        self.reset = reset
        self.param_key = None if param_key is None else str(param_key)

    @property
    def field(self):
        return self._field

    @property
    def kind(self):
        return "property"

    @property
    def invalidates(self):
        return bool(self.reset)

    @property
    def default_update_mask(self) -> UpdateMask:
        """Default refresh policy when the owner does not return an explicit mask."""
        return (
            UpdateMask.REDRAW
            | UpdateMask.REBUILD_PROPERTIES
            | UpdateMask.REBUILD_PROPERTY_SCHEMA
            if self.invalidates
            else UpdateMask.REDRAW
        )

    def get(self):
        """Read the current semantic value from the owner."""
        return getattr(self._owner, self._field)

    def let(self, value):
        """Write a new value and return the resulting :class:`UpdateMask`.

        If the owner implements ``set_<field>(value)``, that method is used and
        may return an explicit mask. Otherwise the attribute is assigned
        directly and :attr:`default_update_mask` is used.
        """
        setter = getattr(self._owner, f"set_{self._field}", None)
        if callable(setter):
            result = setter(value)
        else:
            setattr(self._owner, self._field, value)
            result = None
        if result is None:
            return self.default_update_mask
        return UpdateMask(result)

    def spec(self):
        """Return a typed snapshot suitable for backend adapters and serializers."""
        return {
            "kind": self.kind,
            "field": self.field,
            "title": self.title,
            "value": self.get(),
            "invalidates": self.invalidates,
            "param_key": self.param_key,
        }

    def __str__(self):
        resetable = "R " if self.reset else ""
        return f"{resetable}'{self.title}'"

    def __repr__(self):
        return str(self)


######################################################################################


class NumericProperty(Property):
    """Property representing a numeric scalar with finite UI metadata."""

    def __init__(self, *vargs, min=0.0, max=1.0, count=2, **kwargs):
        super().__init__(*vargs, **kwargs)
        if count < 1:
            raise ValueError("NumericProperty.count must be positive.")
        if min != max and count < 2:
            raise ValueError(
                "NumericProperty.count must be at least 2 when min and max differ."
            )
        self.min = min
        self.max = max
        self.count = count

    def __str__(self):
        prefix = super().__str__()
        return f"{prefix} = {self.get()}. Numeric {self.min} .. {self.max}"

    @property
    def kind(self):
        return "numeric"

    def spec(self):
        spec = super().spec()
        spec.update(
            {
                "min": self.min,
                "max": self.max,
                "count": self.count,
            }
        )
        return spec


######################################################################################


class BooleanProperty(Property):
    """Property representing a boolean toggle."""

    def __init__(self, *vargs, **kwargs):
        super().__init__(*vargs, **kwargs)

    def __str__(self):
        prefix = super().__str__()
        return f"{prefix} = {self.get()}"

    @property
    def kind(self):
        return "boolean"


######################################################################################


class ListProperty(Property):
    """Property constrained to one value from an explicit list."""

    def __init__(self, *vargs, values=[], **kwargs):
        assert len(values) > 0
        super().__init__(*vargs, **kwargs)
        self.values = values

    def get(self):
        value = getattr(self._owner, self._field)
        assert value in self.values
        return value

    def let(self, value):
        assert value in self.values
        return super().let(value)

    def get_index(self, value=None):
        if value is None:
            value = self.get()
        return next((n for n, v in enumerate(self.values) if v == value))

    def let_index(self, index):
        return self.let(self.values[index])

    def __str__(self):
        prefix = super().__str__()
        choices = ", ".join(f"{n}: `{v}`" for n, v in enumerate(self.values))
        return f"{prefix} = {self.get()}. One of {choices}"

    @property
    def kind(self):
        return "list"

    def spec(self):
        spec = super().spec()
        spec.update(
            {
                "values": list(self.values),
                "selected_index": self.get_index(),
            }
        )
        return spec


######################################################################################


class PropertiesCollection:
    """Indexable view over a property group.

    This helper exposes scene properties by title or positional index and keeps
    the mutation contract compact for headless scripts:

    ``scene["Render"]["Level"] = 0.5``
    """

    def __init__(self, props:list[Property]):
        self._props = props
        self._extract_names()

    def _extract_names(self):
        self._names = { p.title:n for n, p in enumerate(self._props) }

    def _resolve(self, key):
        if isinstance(key, str):
            if key not in self._names:
                raise KeyError(f"Property `{key}` does not exist.")
            return self._names[key]
        if isinstance(key, int):
            N = len(self._props)
            if not (0<=key<N):
                raise KeyError(f"Property index `{key}` does not belong to 0..{N}.")
            return key
        raise KeyError(f"Key type `{type(key)}` is not int or str.")

    def prop(self, key) -> Property:
        """Return the underlying :class:`Property` object."""
        return self._props[self._resolve(key)]

    def __getitem__(self, key):
        return self._props[self._resolve(key)].get()

    def __setitem__(self, key, value):
        self._props[self._resolve(key)].let(value)

    def __str__(self):
        return ', '.join(f"{n}:'{k}'" for k, n in self._names)

    def __repr__(self):
        return str(self)


######################################################################################
