/*
 * Copyright (C) 2026 Casey Tunturi
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

// One live hand between the face and usb-hid-inject.
//
// The page never opens /dev/hidg* and never spawns a process per pointer
// report. This singleton owns one streaming helper while USB Hands is joined,
// batches motion to the display clock, and exposes the same verbs over
// QuickShell IPC so the active Souveraine agent can use the hand Casey opened.
// Configfs and role changes remain sessiond -> usb-signaller business.
pragma Singleton
pragma ComponentBehavior: Bound

import QtQuick
import Quickshell
import Quickshell.Io
import qs

Singleton {
    id: root

    property bool active: false
    property bool ready: false
    property string lastError: ""

    readonly property bool modeReady: UsbState.mode === "hid" || UsbState.mode === "kvm"
    readonly property bool permitted: root.active && !GlobalStates.screenLockSecure
    readonly property bool canInject: root.permitted && root.modeReady && root.ready

    // Loaded only while this surface is joined. It tells the agent what the
    // newly-present hand can do without teaching every ordinary conversation
    // about a USB gadget it is not using.
    readonly property string skill: "USB Hands is joined to this conversation. "
        + "Casey is using the phone as a trackpad and voice-first controller for an attached host. "
        + "When he asks you to type or press a key there, use the existing bridge: "
        + "`qs -c souveraine ipc call usbHands sendText TEXT`, "
        + "`qs -c souveraine ipc call usbHands tap KEY`, "
        + "`qs -c souveraine ipc call usbHands key KEY MODIFIERS`, or "
        + "`qs -c souveraine ipc call usbHands click left|right|middle`. "
        + "Ask `qs -c souveraine ipc call usbHands status` rather than assuming the cable is armed. "
        + "Do not inject anything Casey did not ask to send to the attached host."

    signal controllerChanged()
    signal refused(string reason)

    function open(): bool {
        if (GlobalStates.screenLockSecure) {
            root.lastError = "Unlock before giving the glass a hand on another machine";
            root.refused(root.lastError);
            return false;
        }
        root.lastError = "";
        root.active = true;
        UsbState.refresh();
        root._reconcile();
        root.controllerChanged();
        return true;
    }

    function close() {
        if (root.ready) {
            root._flushPointer();
            injector.write("release\n");
        }
        root.active = false;
        root.ready = false;
        root._pendingX = 0;
        root._pendingY = 0;
        root._pendingWheel = 0;
        flushTimer.stop();
        root._reconcile();
        root.controllerChanged();
    }

    function arm() {
        if (!root.active) {
            root.lastError = "USB Hands is not joined";
            root.refused(root.lastError);
            root.controllerChanged();
            return false;
        }
        if (!UsbState.hasMode("hid_mode")) {
            root.lastError = "The installed gadget does not advertise HID mode";
            root.refused(root.lastError);
            root.controllerChanged();
            return false;
        }
        UsbState.setMode("hid");
        return true;
    }

    function retry() {
        root.lastError = "";
        if (injector.running)
            injector.running = false;
        root._reconcile();
    }

    function _reconcile() {
        const wanted = root.permitted && root.modeReady;
        if (!wanted) {
            root.ready = false;
            if (injector.running)
                injector.running = false;
            return;
        }
        if (!injector.running) {
            root.ready = false;
            injector.running = true;
        }
    }

    function _notReadyReason(): string {
        if (!root.active) return "USB Hands is not joined";
        if (GlobalStates.screenLockSecure) return "The glass is locked";
        if (!root.modeReady) return "Arm HID or KVM mode first";
        if (root.lastError.length > 0) return root.lastError;
        return "The HID bridge is still waking";
    }

    function _write(command): bool {
        if (!root.canInject) {
            root.lastError = root._notReadyReason();
            root.refused(root.lastError);
            root.controllerChanged();
            return false;
        }
        injector.write(command + "\n");
        return true;
    }

    function sendText(value): bool {
        const lines = String(value ?? "").replace(/\r\n/g, "\n").split("\n");
        if (lines.length === 1 && lines[0].length === 0)
            return false;
        for (let i = 0; i < lines.length; i++) {
            if (lines[i].length > 0 && !root._write("type " + lines[i]))
                return false;
            if (i + 1 < lines.length && !root._write("key enter"))
                return false;
        }
        return true;
    }

    function key(name, modifiers = ""): bool {
        const keyName = String(name ?? "").trim();
        const mods = String(modifiers ?? "").trim().replace(/[,]+/g, " ");
        if (!/^[A-Za-z0-9]+$/.test(keyName)
                || (mods.length > 0 && !/^[A-Za-z ]+$/.test(mods))) {
            root.lastError = "Key names and modifiers must be plain words";
            root.refused(root.lastError);
            root.controllerChanged();
            return false;
        }
        return root._write("key " + keyName + (mods.length > 0 ? " " + mods : ""));
    }

    function click(button = "left"): bool {
        const name = String(button ?? "left").toLowerCase();
        if (!["left", "right", "middle"].includes(name)) {
            root.lastError = "Unknown pointer button: " + name;
            root.refused(root.lastError);
            root.controllerChanged();
            return false;
        }
        root._flushPointer();
        return root._write("click " + name);
    }

    property int _pendingX: 0
    property int _pendingY: 0
    property int _pendingWheel: 0

    function movePointer(x, y, wheel = 0): bool {
        if (!root.canInject) {
            root.lastError = root._notReadyReason();
            root.refused(root.lastError);
            root.controllerChanged();
            return false;
        }
        root._pendingX += Math.round(Number(x));
        root._pendingY += Math.round(Number(y));
        root._pendingWheel += Math.round(Number(wheel));
        if (!flushTimer.running)
            flushTimer.start();
        return true;
    }

    function _flushPointer() {
        if (!root.ready)
            return;
        const x = root._pendingX;
        const y = root._pendingY;
        const wheel = root._pendingWheel;
        root._pendingX = 0;
        root._pendingY = 0;
        root._pendingWheel = 0;
        if (x !== 0 || y !== 0 || wheel !== 0)
            root._write("pointer " + x + " " + y + " " + wheel + " 0");
    }

    Timer {
        id: flushTimer
        interval: 16
        repeat: false
        onTriggered: root._flushPointer()
    }

    Process {
        id: injector
        command: ["usb-hid-inject", "stream"]
        stdinEnabled: true

        stdout: SplitParser {
            splitMarker: "\n"
            onRead: line => {
                if (line.trim() !== "ready")
                    return;
                root.ready = true;
                root.lastError = "";
                root.controllerChanged();
            }
        }

        stderr: SplitParser {
            splitMarker: "\n"
            onRead: line => {
                const message = line.trim().replace(/^error:\s*/, "");
                if (message.length === 0)
                    return;
                root.lastError = message;
                root.controllerChanged();
            }
        }

        onExited: (exitCode, exitStatus) => {
            root.ready = false;
            if (root.permitted && root.modeReady && root.lastError.length === 0)
                root.lastError = "HID bridge exited (" + exitCode + ")";
            root.controllerChanged();
        }
    }

    Connections {
        target: UsbState

        function onRefreshed() {
            root._reconcile();
            root.controllerChanged();
        }

        function onChangeFailed(reason) {
            root.lastError = reason;
            root.controllerChanged();
        }
    }

    Connections {
        target: GlobalStates

        function onScreenLockSecureChanged() {
            if (GlobalStates.screenLockSecure && root.active)
                root.close();
        }
    }

    onActiveChanged: root._reconcile()
    onModeReadyChanged: {
        root._reconcile();
        root.controllerChanged();
    }

    IpcHandler {
        target: "usbHands"

        function status(): string {
            return JSON.stringify({
                active: root.active,
                ready: root.ready,
                mode: UsbState.mode,
                error: root.lastError
            });
        }

        function sendText(text: string): string {
            return root.sendText(text) ? "sent" : root._notReadyReason();
        }

        function key(keyName: string, modifiers: string): string {
            return root.key(keyName, modifiers) ? "sent" : root._notReadyReason();
        }

        function tap(keyName: string): string {
            return root.key(keyName) ? "sent" : root._notReadyReason();
        }

        function click(button: string): string {
            return root.click(button) ? "sent" : root._notReadyReason();
        }

        function pointer(x: int, y: int, wheel: int): string {
            return root.movePointer(x, y, wheel) ? "sent" : root._notReadyReason();
        }
    }
}
