from __future__ import annotations

import selectors
import threading
from typing import Callable

from evdev import InputDevice, categorize, ecodes, list_devices

from .input_devices import is_keyboard

_CANCEL_KEY = "KEY_ESC"


def capture_next_combo(on_captured: Callable[[tuple[str, ...] | None], None]) -> None:
    """Wait in a background thread for the user to press (and release) a
    key combo and report it as a sorted tuple of evdev key names - Escape
    pressed alone reports None (cancelled) instead.

    Tracks every key held down until they're *all* released again, using
    the peak simultaneously-held set as the captured combo - so e.g.
    holding Ctrl then F1 then releasing both captures ("KEY_F1",
    "KEY_LEFTCTRL"), not just whichever key happened to complete the chord.

    Used by the settings window's hotkey "click to record" buttons: since
    hotkeys are matched by this same evdev keycode naming (see hotkeys.py/
    state.py), capturing it directly avoids the user needing to know
    evdev's naming (KEY_F1, KEY_KP1, ...) to type it in by hand.

    `on_captured` runs on this background thread, not the GTK main thread -
    callers touching GTK widgets from it must marshal via GLib.idle_add.
    """
    def _run() -> None:
        keyboards = [d for d in (InputDevice(p) for p in list_devices()) if is_keyboard(d)]
        if not keyboards:
            on_captured(None)
            return
        sel = selectors.DefaultSelector()
        for dev in keyboards:
            sel.register(dev, selectors.EVENT_READ)
        held: set[str] = set()
        peak: set[str] = set()
        try:
            while True:
                for key, _mask in sel.select():
                    dev = key.fileobj
                    for raw_event in dev.read():
                        if raw_event.type != ecodes.EV_KEY:
                            continue
                        key_event = categorize(raw_event)
                        keycode = key_event.keycode
                        name = keycode[0] if isinstance(keycode, list) else keycode
                        if key_event.keystate == key_event.key_down:
                            held.add(name)
                            peak |= held
                        elif key_event.keystate == key_event.key_up:
                            held.discard(name)
                            if not held and peak:
                                if peak == {_CANCEL_KEY}:
                                    on_captured(None)
                                else:
                                    on_captured(tuple(sorted(peak)))
                                return
        finally:
            sel.close()
            for dev in keyboards:
                dev.close()

    threading.Thread(target=_run, daemon=True).start()
