#!/usr/bin/python3
"""mood-inputd — root helper that tells the session what kind of input hardware is in use.

Writes /run/mood/input.json (world-readable, rewritten only when something changes):
  touch   names of touchscreens        pen     names of pens / stylus digitisers
  switch  true if a tablet-mode switch exists (2-in-1 hinge / detachable keyboard)
  tablet  current state of that switch
  last    "touch" | "pen" | "key"  — the kind of input used most recently
Only event *types* are looked at (a finger went down, a key went down); key codes are never
stored or logged. The session's mood-touch daemon uses this for tablet mode and the
on-screen keyboard ("show it when I'm using the touchscreen, hide it when I type").
"""
import fcntl
import json
import os
import select
import sys
import time

try:
    import evdev
    from evdev import ecodes as E
except ImportError:
    print("mood-inputd: python3-evdev missing", file=sys.stderr)
    sys.exit(0)

OUT = "/run/mood/input.json"
RESCAN = 3.0


def classify(dev):
    caps = dev.capabilities(absinfo=False)
    keys = set(caps.get(E.EV_KEY, []))
    abs_ = set(caps.get(E.EV_ABS, []))
    sw = set(caps.get(E.EV_SW, []))
    props = set(dev.input_props())
    kinds = set()
    if (E.BTN_TOOL_PEN in keys or E.BTN_STYLUS in keys) and E.INPUT_PROP_DIRECT in props:
        kinds.add("pen")
    elif E.ABS_MT_POSITION_X in abs_ and (E.INPUT_PROP_DIRECT in props or E.BTN_TOUCH in keys and E.BTN_TOOL_FINGER not in keys):
        kinds.add("touch")
    if E.SW_TABLET_MODE in sw:
        kinds.add("switch")
    if E.KEY_A in keys and E.KEY_SPACE in keys and E.KEY_Z in keys:
        kinds.add("key")
    return kinds


def switch_on(dev, code):
    """EVIOCGSW: current switch bits (python-evdev has no wrapper for it)."""
    buf = bytearray(8)
    try:
        fcntl.ioctl(dev.fd, (2 << 30) | (len(buf) << 16) | (ord("E") << 8) | 0x1B, buf)
    except OSError:
        return False
    return bool(buf[code // 8] >> (code % 8) & 1)


class Inputd:
    def __init__(self):
        self.devs = {}      # path -> (InputDevice, kinds)
        self.state = {"touch": [], "pen": [], "switch": False, "tablet": False, "last": "key", "keyboards": []}
        self.written = None

    def rescan(self):
        paths = set(evdev.list_devices())
        for p in list(self.devs):
            if p not in paths:
                self.drop(p)
        for p in paths - set(self.devs):
            try:
                d = evdev.InputDevice(p)
                kinds = classify(d)
            except OSError:
                continue
            if not kinds:
                d.close()
                continue
            self.devs[p] = (d, kinds)
            if "switch" in kinds:
                self.state["tablet"] = switch_on(d, E.SW_TABLET_MODE)
        self.refresh()

    def drop(self, p):
        d, _ = self.devs.pop(p, (None, None))
        try:
            d and d.close()
        except OSError:
            pass

    def refresh(self):
        st = self.state
        st["touch"] = sorted({d.name for d, k in self.devs.values() if "touch" in k})
        st["pen"] = sorted({d.name for d, k in self.devs.values() if "pen" in k})
        st["keyboards"] = sorted({d.name for d, k in self.devs.values() if "key" in k and "touch" not in k and "pen" not in k})
        st["switch"] = any("switch" in k for _, k in self.devs.values())
        if not st["switch"]:
            st["tablet"] = False
        self.write()

    def write(self):
        blob = json.dumps(self.state, sort_keys=True)
        if blob == self.written:
            return
        os.makedirs(os.path.dirname(OUT), exist_ok=True)
        tmp = OUT + ".tmp"
        with open(tmp, "w") as f:
            f.write(blob)
        os.chmod(tmp, 0o644)
        os.replace(tmp, OUT)
        self.written = blob

    def handle(self, kinds, ev):
        st = self.state
        if ev.type == E.EV_SW and ev.code == E.SW_TABLET_MODE:
            st["tablet"] = bool(ev.value)
        elif ev.type == E.EV_KEY and ev.value == 1:
            if ev.code in (E.BTN_TOOL_PEN, E.BTN_TOOL_RUBBER) or ev.code == E.BTN_TOUCH and "pen" in kinds:
                st["last"] = "pen"
            elif ev.code == E.BTN_TOUCH and "touch" in kinds:
                st["last"] = "touch"
            elif "key" in kinds and E.KEY_ESC <= ev.code <= E.KEY_MICMUTE and ev.code not in MODIFIERS:
                st["last"] = "key"
        else:
            return
        self.write()

    def run(self):
        self.rescan()
        next_scan = time.monotonic() + RESCAN
        while True:
            fds = {d.fd: (p, d, k) for p, (d, k) in self.devs.items()}
            timeout = max(0.0, next_scan - time.monotonic())
            try:
                ready, _, _ = select.select(list(fds), [], [], timeout)
            except (OSError, ValueError):
                ready = []
                self.devs = {p: v for p, v in self.devs.items() if v[0].fd >= 0}
            for fd in ready:
                p, d, k = fds[fd]
                try:
                    for ev in d.read():
                        self.handle(k, ev)
                except (OSError, BlockingIOError) as e:
                    if getattr(e, "errno", None) not in (11,):
                        self.drop(p)
                        self.refresh()
            if time.monotonic() >= next_scan:
                self.rescan()
                next_scan = time.monotonic() + RESCAN


MODIFIERS = {E.KEY_LEFTCTRL, E.KEY_RIGHTCTRL, E.KEY_LEFTSHIFT, E.KEY_RIGHTSHIFT, E.KEY_LEFTALT, E.KEY_RIGHTALT,
             E.KEY_LEFTMETA, E.KEY_RIGHTMETA, E.KEY_CAPSLOCK, E.KEY_NUMLOCK, E.KEY_POWER, E.KEY_VOLUMEUP, E.KEY_VOLUMEDOWN,
             E.KEY_MUTE, E.KEY_BRIGHTNESSUP, E.KEY_BRIGHTNESSDOWN}

if __name__ == "__main__":
    if "--once" in sys.argv:
        d = Inputd()
        d.rescan()
        print(json.dumps(d.state, indent=1))
        for p, (dev, k) in sorted(d.devs.items()):
            print(p, dev.name, sorted(k))
    else:
        Inputd().run()
