from collections.abc import Sequence

from .choices import Choices
from .immutablelist import ImmutableList


class validated_property:
    def __init__(
        self,
        *constants,
        string=False,
        integer=False,
        number=False,
        color=False,
        initial=None,
    ):
        """Define a simple validated property attribute.

        :param constants: Explicitly allowable values.
        :param string: Are strings allowed as values?
        :param integer: Are integers allowed as values?
        :param number: Are numbers allowed as values?
        :param color: Are colors allowed as values?
        :param initial: The initial value for the property. If the property has not been
            explicitly set, this is what is returned when it's accessed.
        """
        self.choices = Choices(
            *constants, string=string, integer=integer, number=number, color=color
        )
        self.initial = None if initial is None else self.validate(initial)

    def __set_name__(self, owner, name):
        self.name = name
        owner._BASE_PROPERTIES[owner].add(name)
        owner._BASE_ALL_PROPERTIES[owner].add(name)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self

        return getattr(obj, f"_{self.name}", self.initial)

    def __set__(self, obj, value):
        if value is self:
            # This happens during autogenerated dataclass __init__ when no value is
            # supplied.
            return

        if value is None:
            raise ValueError(
                "Python `None` cannot be used as a style value; "
                f"to reset a property, use del `style.{self.name}`."
            )

        value = self.validate(value)

        if (current := getattr(obj, f"_{self.name}", None)) is None:
            # If the value has not been explicitly set already, then we always want to
            # assign to the attribute -- even if the value being assigned is identical
            # to the initial value.
            setattr(obj, f"_{self.name}", value)
            if value != self.initial:
                obj.apply(self.name)

        elif value != current:
            setattr(obj, f"_{self.name}", value)
            obj.apply(self.name)

    def __delete__(self, obj):
        try:
            delattr(obj, f"_{self.name}")
        except AttributeError:
            pass
        else:
            obj.apply(self.name)

    @property
    def _name_if_set(self):
        return f" {self.name}" if hasattr(self, "name") else ""

    def validate(self, value):
        try:
            return self.choices.validate(value)
        except ValueError:
            raise ValueError(
                f"Invalid value {value!r} for property{self._name_if_set}; "
                f"Valid values are: {self.choices}"
            )

    def is_set_on(self, obj):
        return hasattr(obj, f"_{self.name}")


class list_property(validated_property):
    def validate(self, value):
        if isinstance(value, str):
            value = [value]
        elif not isinstance(value, Sequence):
            raise TypeError(
                f"Value for list property{self._name_if_set} must be a sequence."
            )

        if not value:
            name = getattr(self, "name", "prop_name")
            raise ValueError(
                "List properties cannot be set to an empty sequence; "
                f"to reset a property, use del `style.{name}`."
            )

        # This could be a comprehension, but then the error couldn't specify which value
        # is at fault.
        result = []
        for item in value:
            try:
                item = self.choices.validate(item)
            except ValueError:
                raise ValueError(
                    f"Invalid item value {item!r} for list "
                    f"property{self._name_if_set}; Valid values are: {self.choices}"
                )
            result.append(item)

        return ImmutableList(result)
