//! PS/2 Keyboard Driver
//!
//! Spawned by the ps2-bus driver once the controller is initialized and the port
//! has passed its interface test and device reset. The bus driver hands us our
//! device HID as argv[1] and, optionally, a layout name (`"us"`, `"gb"`, ...) as
//! argv[2].
//!
//! The 8042's ports (0x60/0x64) and IRQ1 live on the PNP0303 node, which the
//! ps2-bus driver exclusively owns — so this driver never touches the hardware.
//! Instead it **attaches** to the bus (handing over its endpoint as a capability)
//! and receives every scancode byte as a forwarded asynchronous message. Each byte
//! feeds the set-2 decoder; a decoded key becomes input-protocol events:
//!
//!   scancode byte -> HID usage keycode -> key_down / key_up
//!                                      -> xkeyboard-config -> character -> key_press

const std = @import("std");
const runtime = @import("runtime");
const xkb = @import("xkeyboard-config");
const ps2 = @import("ps2-library.zig");
const scancode = @import("scancode.zig");
const device = runtime.device;
const ipc = runtime.ipc;
const protocol = runtime.input_protocol;

/// Look up the ps2-bus service, retrying while the bus (which spawned us before
/// registering) is still coming up.
fn lookupBus() ?ipc.Handle {
    var attempts: usize = 0;
    while (attempts < 100) : (attempts += 1) {
        if (ipc.lookup(.ps2_bus)) |handle| return handle;
        runtime.system.sleep(50);
    }
    return null;
}

/// The character a pressed key produces under `modifiers`, or 0 for none. The
/// layout lookup answers for printable keys; the keys whose keysym has no Unicode
/// mapping but that every consumer still expects as a character (Enter, Tab,
/// Backspace, Escape) are given their ASCII control characters here.
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: scancode.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(protocol.Keycode, @enumFromInt(usage))) {
        .enter, .keypad_enter => '\n',
        .tab => '\t',
        .backspace => 0x08,
        .escape => 0x1B,
        else => 0,
    };
}

/// The input protocol's modifier word for a snapshot.
fn modifierWord(modifiers: scancode.ModifierSnapshot) u32 {
    var word: u32 = 0;
    if (modifiers.shift) word |= protocol.modifier_shift;
    if (modifiers.control) word |= protocol.modifier_control;
    if (modifiers.alt) word |= protocol.modifier_alt;
    return word;
}

pub fn main(init: runtime.process.Init) void {
    const hid = init.arguments.get(1).?;
    if (hid.len == 0) {
        _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no HID argument\n");
        return;
    }
    std.log.info("starting for hid {s}", .{hid});

    const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
        _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: out of memory\n");
        return;
    };
    if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
        std.log.info("no device for hid {s}", .{hid});
        return;
    }

    // The layout is a spawn argument so a later settings source can choose it;
    // absent (as today) it defaults to us.
    const layout_name = init.arguments.get(2) orelse "us";
    const layout = xkb.byName(layout_name) orelse xkb.us;
    std.log.info("layout {s}", .{layout.name});

    // Attach to the bus: hand it our endpoint, and it forwards every byte the
    // keyboard sends (it owns the controller; we own the decoding).
    const bus = lookupBus() orelse {
        _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ps2-bus service unavailable\n");
        return;
    };
    const endpoint = ipc.createIpcEndpoint() orelse {
        _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no endpoint\n");
        return;
    };
    var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.keyboard) };
    var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
    const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
        _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach call failed\n");
        return;
    };
    if (attached.len < @sizeOf(ps2.AttachReply) or
        std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
    {
        _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach refused\n");
        return;
    }

    // Broadcast keyboard events through the input service so programs can listen
    // for them (docs/input.md).
    var source = runtime.input.connectSource() orelse {
        _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: input service unavailable\n");
        return;
    };
    _ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ok\n");

    var decoder = scancode.Decoder{};
    var state = scancode.KeyboardState{};
    var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
    while (true) {
        const got = ipc.replyWait(endpoint, &.{}, &receive, null);
        if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
        const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);

        const key = decoder.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
        const transition = state.apply(key);
        const modifiers = modifierWord(transition.modifiers);

        switch (transition.action) {
            .pressed => {
                _ = source.publishKeyboardEvent(.{
                    .kind = @intFromEnum(protocol.EventKind.key_down),
                    .keycode = key.usage,
                    .character = 0,
                    .modifiers = modifiers,
                });
                const character = characterFor(layout, key.usage, transition.modifiers);
                if (character != 0) {
                    _ = source.publishKeyboardEvent(.{
                        .kind = @intFromEnum(protocol.EventKind.key_press),
                        .keycode = key.usage,
                        .character = character,
                        .modifiers = modifiers,
                    });
                }
            },
            // Typematic repeat: the key did not physically go down again, so no
            // key_down — but it keeps producing its character.
            .repeated => {
                const character = characterFor(layout, key.usage, transition.modifiers);
                if (character != 0) {
                    _ = source.publishKeyboardEvent(.{
                        .kind = @intFromEnum(protocol.EventKind.key_press),
                        .keycode = key.usage,
                        .character = character,
                        .modifiers = modifiers,
                    });
                }
            },
            .released => {
                _ = source.publishKeyboardEvent(.{
                    .kind = @intFromEnum(protocol.EventKind.key_up),
                    .keycode = key.usage,
                    .character = 0,
                    .modifiers = modifiers,
                });
            },
        }
    }
}
