## Copyright (C) 2026  Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Visual layout primitives for the per-key color editor.

This module is pure data. It does not import GTK and does not import from
`lib.logitech_receiver`. It is therefore relocatable into a shared package
when the frontend/backend split happens.
"""

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import field


@dataclass(frozen=True)
class Cell:
    """One paintable cell in a layout.

    `zone_id` is the firmware identifier the device uses for this LED. It is
    matched against the device's reported zone list at bind time; cells with
    no matching device zone are drawn disabled.
    """

    zone_id: int
    row: int
    col: int
    width: float = 1.0
    height: float = 1.0
    group: str = "main"
    label: str = ""
    x: float | None = None
    y: float | None = None


@dataclass(frozen=True)
class Layout:
    """A device-class visual layout.

    Cells in `strip_groups` are rendered as a flat row beneath the matrix
    region, regardless of their row/col fields. Cells outside `strip_groups`
    are placed by row/col on the main matrix.

    `extra_zones` is a curated allowlist of zone ids that may appear in the
    bottom strip when the device reports them but they are not covered by a
    layout cell. Zones outside the allowlist are dropped — Logitech firmware
    bitmaps enumerate phantom/reserved slots (e.g. G515 reports 47, 97, 99-103,
    254) that aren't physical keys. Set to `None` to disable filtering.
    """

    cells: tuple[Cell, ...]
    rows: int
    cols: int
    strip_groups: tuple[str, ...] = ("strip",)
    supported_tools: tuple[str, ...] = ("single", "rect", "bucket", "gradient")
    extra_zones: frozenset[int] | None = None
    description: str = ""

    def matrix_cells(self) -> tuple[Cell, ...]:
        return tuple(c for c in self.cells if c.group not in self.strip_groups)

    def strip_cells(self) -> tuple[Cell, ...]:
        return tuple(c for c in self.cells if c.group in self.strip_groups)

    def by_zone(self) -> dict[int, Cell]:
        return {c.zone_id: c for c in self.cells}


@dataclass(frozen=True)
class BoundCell:
    """A Cell augmented with bind state, returned by `binding.bind`."""

    cell: Cell
    bound: bool


@dataclass(frozen=True)
class BoundLayout:
    """Result of binding a Layout against a sink's reported zones.

    `matrix` and `strip` are tuples of BoundCell in render order. `unmapped`
    holds zones the device reported that no Layout cell claimed; these get
    appended to the strip with synthesized cells.
    """

    matrix: tuple[BoundCell, ...] = field(default_factory=tuple)
    strip: tuple[BoundCell, ...] = field(default_factory=tuple)
    unmapped: tuple[int, ...] = field(default_factory=tuple)
