from __future__ import annotations
from typing import Callable, SupportsFloat
from gaphas.matrix import Matrix
from gaphas.solver import NORMAL, BaseConstraint, Variable
from gaphas.types import Pos, SupportsFloatPos, TypedProperty
[docs]
class Position:
    """A point constructed of two `Variable`'s.
    >>> vp = Position(3, 5)
    >>> vp.x, vp.y
    (Variable(3, 20), Variable(5, 20))
    >>> vp.pos
    (Variable(3, 20), Variable(5, 20))
    >>> vp[0], vp[1]
    (Variable(3, 20), Variable(5, 20))
    """
    def __init__(self, x, y, strength=NORMAL):
        self._x = Variable(x, strength)
        self._y = Variable(y, strength)
        self._handlers: set[Callable[[Position, Pos], None]] = set()
        self._setting_pos = 0
    def add_handler(self, handler: Callable[[Position, Pos], None]) -> None:
        if not self._handlers:
            self._x.add_handler(self._propagate_x)
            self._y.add_handler(self._propagate_y)
        self._handlers.add(handler)
    def remove_handler(self, handler: Callable[[Position, Pos], None]) -> None:
        self._handlers.discard(handler)
        if not self._handlers:
            self._x.remove_handler(self._propagate_x)
            self._y.remove_handler(self._propagate_y)
    def notify(self, oldpos: Pos) -> None:
        for handler in self._handlers:
            handler(self, oldpos)
    def _propagate_x(self, variable, oldval):
        if not self._setting_pos:
            self.notify((oldval, self._y.value))
    def _propagate_y(self, variable, oldval):
        if not self._setting_pos:
            self.notify((self._x.value, oldval))
    @property
    def strength(self) -> int:
        """Strength."""
        return self._x.strength
    def _set_x(self, v: SupportsFloat) -> None:
        self._x.value = v
    x: TypedProperty[Variable, SupportsFloat]
    x = property(lambda s: s._x, _set_x, doc="Position.x")
    def _set_y(self, v: SupportsFloat) -> None:
        self._y.value = v
    y: TypedProperty[Variable, SupportsFloat]
    y = property(lambda s: s._y, _set_y, doc="Position.y")
    def _set_pos(self, pos: Position | SupportsFloatPos) -> None:
        """Set handle position (Item coordinates)."""
        oldpos = (self._x.value, self._y.value)
        self._setting_pos += 1
        try:
            self._x.value, self._y.value = pos
        finally:
            self._setting_pos -= 1
        self.notify(oldpos)
    pos: TypedProperty[tuple[Variable, Variable], Position | SupportsFloatPos]
    pos = property(lambda s: (s._x, s._y), _set_pos, doc="The position.")
    def tuple(self) -> tuple[float, float]:
        return (self._x.value, self._y.value)
    def __str__(self):
        return f"<{self.__class__.__name__} object on ({self._x}, {self._y})>"
    __repr__ = __str__
    def __getitem__(self, index):
        """Shorthand for returning the x(0) or y(1) component of the point.
        >>> h = Position(3, 5)
        >>> h[0]
        Variable(3, 20)
        >>> h[1]
        Variable(5, 20)
        """
        return (self._x, self._y)[index]
    def __iter__(self):
        return iter((self._x, self._y))
    def __eq__(self, other):
        return isinstance(other, Position) and self.x == other.x and self.y == other.y 
[docs]
class MatrixProjection(BaseConstraint):
    def __init__(self, pos: Position, matrix: Matrix):
        proj_pos = Position(0, 0, pos.strength)
        super().__init__(proj_pos.x, proj_pos.y, pos.x, pos.y)
        self._orig_pos = pos
        self._proj_pos = proj_pos
        self.matrix = matrix
        self.solve_for(self._proj_pos.x)
    def add_handler(self, handler):
        """Add a callback handler."""
        if not self._handlers:
            self.matrix.add_handler(self._on_matrix_changed)
        super().add_handler(handler)
    def remove_handler(self, handler):
        """Remove a previously assigned handler."""
        super().remove_handler(handler)
        if not self._handlers:
            self.matrix.remove_handler(self._on_matrix_changed)
    @property
    def pos(self) -> Position:
        """The projected position."""
        return self._proj_pos
    def _set_x(self, x):
        self._proj_pos.x = x
    x: TypedProperty[Variable, SupportsFloat]
    x = property(
        lambda s: s._proj_pos.x, _set_x, doc="The projected position's ``x`` part."
    )
    def _set_y(self, y):
        self._proj_pos.y = y
    y: TypedProperty[Variable, SupportsFloat]
    y = property(
        lambda s: s._proj_pos.y, _set_y, doc="The projected position's ``y`` part."
    )
    def mark_dirty(self, var):
        if var is self._orig_pos.x or var is self._orig_pos.y:
            super().mark_dirty(self._orig_pos.x)
            super().mark_dirty(self._orig_pos.y)
        else:
            super().mark_dirty(self._proj_pos.x)
            super().mark_dirty(self._proj_pos.y)
    def solve_for(self, var):
        if var is self._orig_pos.x or var is self._orig_pos.y:
            self._orig_pos.x, self._orig_pos.y = self.matrix.inverse().transform_point(
                *self._proj_pos
            )
        else:
            self._proj_pos.x, self._proj_pos.y = self.matrix.transform_point(
                *self._orig_pos
            )
    def _on_matrix_changed(self, matrix, _orig):
        self.mark_dirty(self._orig_pos.x)
        self.notify()
    def __getitem__(self, index):
        return self._proj_pos[index]