from __future__ import annotations

import logging
import threading
from typing import Callable

from evdev import InputDevice, categorize, ecodes, list_devices

from .input_devices import is_keyboard as _is_keyboard

logger = logging.getLogger(__name__)

_ALT_KEYS = {"KEY_LEFTALT", "KEY_RIGHTALT"}
_CTRL_KEYS = {"KEY_LEFTCTRL", "KEY_RIGHTCTRL"}


class ModifierWatcher:
    """Reports Ctrl+Alt up/down, read directly from /dev/input via evdev -
    same rationale as HotkeyListener: works regardless of compositor, and
    doesn't depend on the overlay window receiving any input itself (it's
    normally click-through).

    Used to drive the overlay's Ctrl+Alt+drag-to-move: the overlay toggles
    its own click-through state on combo down/up (see OverlayWindow), then
    lets GTK's own drag gesture handle the actual move while it's held -
    querying pointer position via X11 to hit-test a click-through window
    was tried first but found unreliable under XWayland (query_pointer()
    returned a frozen position that never updated).

    Plain Alt was tried first but collides with GNOME/Mutter's own built-in
    "Alt+drag moves any window" binding
    (org.gnome.desktop.wm.preferences mouse-button-modifier, upstream
    default `<Alt>`) - Mutter installs an exclusive passive grab for that
    modifier+button combo at the X server level, so on any system still at
    the GNOME default (observed: stock Fedora/Bazzite GNOME) the click never
    reaches this app at all, regardless of click-through toggling. That
    setting is a single exact modifier, so requiring Ctrl+Alt together can't
    collide with it on any value of that setting, and isn't reserved by
    wlroots compositors either.
    """

    def __init__(self, on_change: Callable[[bool], None]):
        self._on_change = on_change
        self._threads: list[threading.Thread] = []
        self._alt_held = False
        self._ctrl_held = False
        self._combo_active = False

    def start(self) -> None:
        devices = [InputDevice(path) for path in list_devices()]
        keyboards = [d for d in devices if _is_keyboard(d)]
        if not keyboards:
            raise RuntimeError(
                "No keyboard-like input device found under /dev/input. "
                "Make sure your user is in the 'input' group (and re-login)."
            )
        for dev in keyboards:
            t = threading.Thread(target=self._read_keyboard, args=(dev,), daemon=True)
            t.start()
            self._threads.append(t)
        logger.info("Modifier watcher listening on %d keyboard(s)", len(keyboards))

    def _read_keyboard(self, dev: InputDevice) -> None:
        try:
            for event in dev.read_loop():
                if event.type != ecodes.EV_KEY:
                    continue
                key_event = categorize(event)
                keycode = key_event.keycode
                name = keycode[0] if isinstance(keycode, list) else keycode
                if name in _ALT_KEYS:
                    self._alt_held = key_event.keystate != key_event.key_up
                elif name in _CTRL_KEYS:
                    self._ctrl_held = key_event.keystate != key_event.key_up
                else:
                    continue
                combo = self._alt_held and self._ctrl_held
                if combo != self._combo_active:
                    self._combo_active = combo
                    self._on_change(combo)
        except OSError:
            logger.warning("Input device %s disconnected", dev.path)
