//! USB HID boot keyboard driver.
//!
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
//! keyboard interface (class 3, subclass 1, protocol 1); its assigned device id
//! arrives as argv[1] and an optional layout name ("us", "gb", ...) as argv[2].
//! It owns no hardware: it opens its device through the USB transfer protocol
//! (`runtime.usb`), asks the device for the boot protocol, subscribes to its
//! interrupt-IN endpoint, and turns each 8-byte boot report into input-protocol
//! events, published to the input service — the USB analogue of ps2-bus/keyboard.
//!
//!   interrupt report -> hid-report diff -> key_down / key_up
//!                                       -> xkeyboard-config -> character -> key_press
//!
//! Because a USB keyboard's usages ARE the input protocol's keycodes (both are
//! HID keyboard page 0x07), the decode is nearly 1:1 — no scancode translation.

const std = @import("std");
const runtime = @import("runtime");
const usb_abi = @import("usb-abi");
const xkb = @import("xkeyboard-config");
const hid = @import("hid-report.zig");
const ipc = runtime.ipc;
const process = runtime.process;
const input_protocol = runtime.input_protocol;

// The modifier state a character lookup needs — derived from the report's
// modifier byte, plus the driver-tracked caps-lock toggle.
const ModifierSnapshot = struct {
    shift: bool,
    control: bool,
    right_alt: bool,
    caps_lock: bool,
};

/// The character a key produces under `modifiers`, or 0 for none — the layout
/// lookup for printable keys, with ASCII control characters for the keys every
/// consumer expects (Enter, Tab, Backspace, Escape), exactly as ps2-bus/keyboard.
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: ModifierSnapshot) u32 {
    const mapping = xkb.map(layout, usage, .{
        .shift = modifiers.shift,
        .caps_lock = modifiers.caps_lock,
        .level3 = modifiers.right_alt,
        .control = modifiers.control,
    });
    if (mapping.character) |character| return character;
    return switch (@as(input_protocol.Keycode, @enumFromInt(usage))) {
        .enter, .keypad_enter => '\n',
        .tab => '\t',
        .backspace => 0x08,
        .escape => 0x1B,
        else => 0,
    };
}

fn modifierWord(modifiers: u8) u32 {
    var word: u32 = 0;
    if (modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0) word |= input_protocol.modifier_shift;
    if (modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0) word |= input_protocol.modifier_control;
    if (modifiers & (hid.modifier_left_alt | hid.modifier_right_alt) != 0) word |= input_protocol.modifier_alt;
    return word;
}

pub fn main(init: runtime.process.Init) void {
    const argument = init.arguments.get(1) orelse {
        _ = runtime.system.write("/system/drivers/usb-hid/keyboard: missing device id (argv[1])\n");
        return;
    };
    const device_id = std.fmt.parseInt(u64, argument, 10) catch {
        std.log.info("malformed device id '{s}'", .{argument});
        return;
    };
    const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;

    // Hello the manager first (meet the spawn deadline), then open the device.
    if (!runtime.usb.helloManager(device_id)) {
        _ = runtime.system.write("/system/drivers/usb-hid/keyboard: hello to device manager failed\n");
        return;
    }
    var device = runtime.usb.open(device_id) orelse {
        std.log.info("could not open device {d}", .{device_id});
        return;
    };
    const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
        _ = runtime.system.write("/system/drivers/usb-hid/keyboard: no interrupt-IN endpoint\n");
        return;
    };

    // Ask for the boot protocol and an indefinite idle (report only on change).
    _ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
    _ = device.controlOut(@bitCast(usb_abi.setIdle(@enumFromInt(device.interface_number), 0, 0)));

    if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
        _ = runtime.system.write("/system/drivers/usb-hid/keyboard: interrupt subscribe failed\n");
        return;
    }

    var source = runtime.input.connectSource() orelse {
        _ = runtime.system.write("/system/drivers/usb-hid/keyboard: input service unavailable\n");
        return;
    };
    _ = process.bindSignals(device.endpoint);
    std.log.info("ok (device {d}, interface {d}, layout {s})", .{ device_id, device.interface_number, layout.name });

    var decoder = hid.KeyboardDecoder{};
    var caps_lock = false;
    var receive: [64]u8 = undefined;
    while (true) {
        const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
        if (!got.isNotification()) continue;
        if (process.signalsFrom(got.badge)) |signals| {
            if (signals.has(.terminate)) return;
            continue;
        }
        if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue;

        const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]);
        if (message.length < @sizeOf(hid.KeyboardReport)) continue;
        const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
        const transitions = decoder.feed(report);

        // Caps Lock toggles on its own key-down (a stateful lock, not a modifier).
        for (transitions.slice()) |transition| {
            if (transition.kind == .pressed and @as(input_protocol.Keycode, @enumFromInt(transition.usage)) == .caps_lock) caps_lock = !caps_lock;
        }

        const modifiers = ModifierSnapshot{
            .shift = report.modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0,
            .control = report.modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0,
            .right_alt = report.modifiers & hid.modifier_right_alt != 0,
            .caps_lock = caps_lock,
        };
        const modifier_word = modifierWord(report.modifiers);

        for (transitions.slice()) |transition| {
            switch (transition.kind) {
                .pressed => {
                    _ = source.publishKeyboardEvent(.{
                        .kind = @intFromEnum(input_protocol.EventKind.key_down),
                        .keycode = transition.usage,
                        .character = 0,
                        .modifiers = modifier_word,
                    });
                    const character = characterFor(layout, transition.usage, modifiers);
                    if (character != 0) {
                        _ = source.publishKeyboardEvent(.{
                            .kind = @intFromEnum(input_protocol.EventKind.key_press),
                            .keycode = transition.usage,
                            .character = character,
                            .modifiers = modifier_word,
                        });
                        // Echo the character to the log — a simple end-to-end
                        // keyboard check on real hardware: type a known phrase,
                        // then read it back from usb-hid-keyboard.log (or watch
                        // it appear live on screen in a -Ddiagnose boot, where
                        // the kernel console is a log sink). Printable ASCII and
                        // newline only; other keys are left to the input service.
                        if (character == '\n' or (character >= 0x20 and character < 0x7F)) {
                            _ = runtime.system.write(&[1]u8{@intCast(character)});
                        }
                    }
                },
                .released => {
                    _ = source.publishKeyboardEvent(.{
                        .kind = @intFromEnum(input_protocol.EventKind.key_up),
                        .keycode = transition.usage,
                        .character = 0,
                        .modifiers = modifier_word,
                    });
                },
            }
        }
    }
}
