Source code for pyflp._descriptors

# PyFLP - An FL Studio project file (.flp) parser
# Copyright (C) 2022 demberto
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details. You should have received a copy of the
# GNU General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.

"""Contains the descriptor and adaptor classes used by models and events."""

from __future__ import annotations

import abc
import enum
from typing import Any, Protocol, overload, runtime_checkable

from typing_extensions import Self, final

from pyflp._events import AnyEvent, EventEnum, StructEventBase
from pyflp._models import VE, EMT_co, EventModel, ItemModel, ModelBase
from pyflp.exceptions import PropertyCannotBeSet
from pyflp.types import T, T_co


@runtime_checkable
class ROProperty(Protocol[T_co]):
    """Protocol for a read-only descriptor."""

    def __get__(self, ins: Any, owner: Any = None) -> T_co | Self | None:
        ...


@runtime_checkable
class RWProperty(ROProperty[T], Protocol):
    """Protocol for a read-write descriptor."""

    def __set__(self, ins: Any, value: T) -> None:
        ...


class NamedPropMixin:
    def __init__(self, prop: str | None = None) -> None:
        self._prop = prop or ""

    def __set_name__(self, _: Any, name: str) -> None:
        if not self._prop:
            self._prop = name


class PropBase(abc.ABC, RWProperty[T]):
    def __init__(self, *ids: EventEnum, default: T | None = None, readonly: bool = False):
        self._ids = ids
        self._default = default
        self._readonly = readonly

    @overload
    def _get_event(self, ins: ItemModel[VE]) -> ItemModel[VE]:
        ...

    @overload
    def _get_event(self, ins: EventModel) -> AnyEvent | None:
        ...

    def _get_event(self, ins: ItemModel[VE] | EventModel):
        if isinstance(ins, ItemModel):
            return ins

        if not self._ids:
            if len(ins.events) > 1:  # Prevent ambiguous situations
                raise LookupError("Event ID not specified")

            return tuple(ins.events)[0]

        for id in self._ids:
            if id in ins.events:
                return ins.events.first(id)

    @property
    def default(self) -> T | None:  # Configure version based defaults here
        return self._default

    @abc.abstractmethod
    def _get(self, ev_or_ins: Any) -> T | None:
        ...

    @abc.abstractmethod
    def _set(self, ev_or_ins: Any, value: T) -> None:
        ...

    @final
    def __get__(self, ins: Any, owner: Any = None) -> T | Self | None:
        if ins is None:
            return self

        if owner is None:
            return NotImplemented

        event: Any = self._get_event(ins)
        if event is not None:
            return self._get(event)

        return self.default

    @final
    def __set__(self, ins: Any, value: T) -> None:
        if self._readonly:
            raise PropertyCannotBeSet(*self._ids)

        event: Any = self._get_event(ins)
        if event is not None:
            self._set(event, value)
        else:
            raise PropertyCannotBeSet(*self._ids)


class FlagProp(PropBase[bool]):
    """Properties derived from enum flags."""

    def __init__(
        self,
        flag: enum.IntFlag,
        *ids: EventEnum,
        prop: str = "flags",
        inverted: bool = False,
        default: bool | None = None,
    ) -> None:
        """
        Args:
            flag: The flag which is to be checked for.
            id: Event ID (required for MultiEventModel).
            prop: The dict key which contains the flags in a `Struct`.
            inverted: If this is true, property getter and setters
                      invert the value to be set / returned.
        """
        self._flag = flag
        self._flag_type = type(flag)
        self._prop = prop
        self._inverted = inverted
        super().__init__(*ids, default=default)

    def _get(self, ev_or_ins: Any) -> bool | None:
        if isinstance(ev_or_ins, (ItemModel, StructEventBase)):
            flags = ev_or_ins[self._prop]
        else:
            flags = ev_or_ins.value  # type: ignore

        if flags is not None:
            retbool = self._flag in self._flag_type(flags)
            return not retbool if self._inverted else retbool

    def _set(self, ev_or_ins: Any, value: bool) -> None:
        if self._inverted:
            value = not value

        if isinstance(ev_or_ins, (ItemModel, StructEventBase)):
            if value:
                ev_or_ins[self._prop] |= self._flag
            else:
                ev_or_ins[self._prop] &= ~self._flag
        else:
            if value:
                ev_or_ins.value |= self._flag  # type: ignore
            else:
                ev_or_ins.value &= ~self._flag  # type: ignore


class KWProp(NamedPropMixin, RWProperty[T]):
    """Properties derived from non-local event values.

    These values are passed to the class constructor as keyword arguments.
    """

    def __get__(self, ins: ModelBase | None, owner: Any = None) -> T | Self:
        if ins is None:
            return self

        if owner is None:
            return NotImplemented
        return ins._kw[self._prop]

    def __set__(self, ins: ModelBase, value: T) -> None:
        if self._prop not in ins._kw:
            raise KeyError(self._prop)
        ins._kw[self._prop] = value


class EventProp(PropBase[T]):
    """Properties bound directly to one of fixed size or string events."""

    def _get(self, ev_or_ins: AnyEvent) -> T | None:
        return ev_or_ins.value

    def _set(self, ev_or_ins: AnyEvent, value: T) -> None:
        ev_or_ins.value = value


class NestedProp(ROProperty[EMT_co]):
    def __init__(self, type: type[EMT_co], *ids: EventEnum) -> None:
        self._ids = ids
        self._type = type

    def __get__(self, ins: EventModel, owner: Any = None) -> EMT_co:
        if owner is None:
            return NotImplemented

        return self._type(ins.events.subtree(lambda e: e.id in self._ids))


class StructProp(PropBase[T], NamedPropMixin):
    """Properties obtained from a :class:`construct.Struct`."""

    def __init__(self, *ids: EventEnum, prop: str | None = None, **kwds: Any) -> None:
        super().__init__(*ids, **kwds)
        NamedPropMixin.__init__(self, prop)

    def _get(self, ev_or_ins: ItemModel[Any]) -> T | None:
        return ev_or_ins[self._prop]

    def _set(self, ev_or_ins: ItemModel[Any], value: T) -> None:
        ev_or_ins[self._prop] = value