## 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.

"""Registry of per-key layouts, keyed by feature + a device-class match.

Layouts register themselves with a matcher callable. `layout_for(feature, hint)`
returns the first matching layout, or None when no model-specific layout is
known — in which case the editor renders a flat strip of all reported zones.
"""

from __future__ import annotations

from collections.abc import Callable

from ..layout import Layout
from . import headset_g522
from . import keyboard_ansi
from . import keyboard_iso_azerty
from . import keyboard_iso_qwerty
from . import keyboard_iso_qwertz
from . import keyboard_jis
from . import mouse_g502x

# (feature_id, matcher, layout). Matcher receives a `hint` dict the editor
# assembles from the device (kind, wpid, codename, name, zones list, etc.).
_REGISTRY: list[tuple[int, Callable[[dict], bool], Layout]] = []


def register_layout(feature: int, matcher: Callable[[dict], bool], layout: Layout) -> None:
    _REGISTRY.append((feature, matcher, layout))


def layout_for(feature: int, hint: dict) -> Layout | None:
    for f, match, layout in _REGISTRY:
        if f == feature and match(hint):
            return layout
    return None


def _name_contains(*needles: str) -> Callable[[dict], bool]:
    """Build a matcher that returns True if any needle is a substring of the
    device's name or codename (case-insensitive). Useful for device-family
    layouts where multiple wpids share an LED arrangement.
    """
    folded = tuple(n.upper() for n in needles)

    def match(hint: dict) -> bool:
        for field in ("codename", "name"):
            value = hint.get(field)
            if not value:
                continue
            up = str(value).upper()
            if any(n in up for n in folded):
                return True
        return False

    return match


# --- Keyboard region routing ---
# Country code → layout family. Codes from HID++ feature 0x4540 KeyboardLayout.
_KEYBOARD_FAMILY_BY_COUNTRY: dict[int, str] = {
    1: "ansi",
    # ISO QWERTY (UK + ES/IT/PT/BE/Nordic — same shape, different keycap legends)
    2: "iso_qwerty",
    5: "iso_qwerty",
    8: "iso_qwerty",
    0x0B: "iso_qwerty",
    0x0D: "iso_qwerty",
    0x0E: "iso_qwerty",
    0x0F: "iso_qwerty",
    0x16: "iso_qwerty",
    0x1D: "iso_qwerty",
    0x21: "iso_qwerty",
    0x24: "iso_qwerty",
    # ISO QWERTZ (DE/Swiss)
    3: "iso_qwertz",
    7: "iso_qwertz",
    # ISO AZERTY (FR)
    4: "iso_azerty",
    # JIS
    9: "jis",
    0x3E: "jis",
}

_FAMILY_LAYOUTS = {
    "ansi": (keyboard_ansi.LAYOUT_FULL, keyboard_ansi.LAYOUT_TKL),
    "iso_qwerty": (keyboard_iso_qwerty.LAYOUT_FULL, keyboard_iso_qwerty.LAYOUT_TKL),
    "iso_qwertz": (keyboard_iso_qwertz.LAYOUT_FULL, keyboard_iso_qwertz.LAYOUT_TKL),
    "iso_azerty": (keyboard_iso_azerty.LAYOUT_FULL, keyboard_iso_azerty.LAYOUT_TKL),
    "jis": (keyboard_jis.LAYOUT_FULL, keyboard_jis.LAYOUT_TKL),
}


def _has_numpad(hint: dict) -> bool:
    """Numpad presence is read from the device's reported zone bitmap rather
    than counting zones — G515 reports phantom zones (47, 97, 99-103, 254)
    that diverge from the keycap count.
    """
    zones = set(hint.get("zones", ()))
    return 80 in zones or 95 in zones


def _keyboard_family(hint: dict) -> str:
    """Pick a layout family from the device's HID++ keyboard layout country
    code. Defaults to "ansi" when the code is missing or unknown.
    """
    code = hint.get("keyboard_layout")
    if code is None:
        return "ansi"
    return _KEYBOARD_FAMILY_BY_COUNTRY.get(int(code), "ansi")


def _keyboard_matcher(family: str, full_size: bool) -> Callable[[dict], bool]:
    def match(hint: dict) -> bool:
        if hint.get("kind") != "keyboard":
            return False
        if _has_numpad(hint) != full_size:
            return False
        return _keyboard_family(hint) == family

    return match


# PER_KEY_LIGHTING_V2 = 0x8081
for _family, (_full, _tkl) in _FAMILY_LAYOUTS.items():
    register_layout(0x8081, _keyboard_matcher(_family, full_size=True), _full)
    register_layout(0x8081, _keyboard_matcher(_family, full_size=False), _tkl)

register_layout(0x8081, _name_contains("G502 X"), mouse_g502x.LAYOUT)
# HEADSET_RGB_HOSTMODE = 0x0620
register_layout(0x0620, _name_contains("G522"), headset_g522.LAYOUT)
