//! Pure decoders for USB HID **boot-protocol** reports — the simplified,
//! fixed-format reports a boot keyboard and boot mouse send, the USB analog of
//! the PS/2 scancode and mouse-packet decoders. No I/O: these turn report bytes
//! into make/break transitions and motion, which the usb-hid drivers publish to
//! the input service. Host-testable in isolation (like mouse-packet.zig).
//!
//! "Boot protocol" is a USB HID term (USB HID 1.11 §B) — the device reports in
//! this fixed layout after SET_PROTOCOL(boot); it has nothing to do with system
//! boot.

const std = @import("std");

// --- keyboard ---------------------------------------------------------------

/// The 8-byte boot keyboard report: a modifier bitmap, a reserved byte, and up
/// to six concurrently-pressed key usages.
pub const KeyboardReport = extern struct {
    modifiers: u8 = 0,
    reserved: u8 = 0,
    keys: [6]u8 = .{ 0, 0, 0, 0, 0, 0 },
};

// The modifier byte's bits (HID keyboard boot report).
pub const modifier_left_control: u8 = 1 << 0;
pub const modifier_left_shift: u8 = 1 << 1;
pub const modifier_left_alt: u8 = 1 << 2;
pub const modifier_left_gui: u8 = 1 << 3;
pub const modifier_right_control: u8 = 1 << 4;
pub const modifier_right_shift: u8 = 1 << 5;
pub const modifier_right_alt: u8 = 1 << 6;
pub const modifier_right_gui: u8 = 1 << 7;

pub const TransitionKind = enum { pressed, released };

/// One key going down or up. `usage` is a HID keyboard-page usage — modifier keys
/// map to usages 224..231 — which is exactly the input protocol's `Keycode`.
pub const Transition = struct { kind: TransitionKind, usage: u8 };

// A report can change at most all 8 modifiers and all 6 keys at once.
pub const max_transitions = 8 + 6;

pub const Transitions = struct {
    items: [max_transitions]Transition = undefined,
    count: usize = 0,

    fn add(self: *Transitions, transition: Transition) void {
        if (self.count < self.items.len) {
            self.items[self.count] = transition;
            self.count += 1;
        }
    }

    pub fn slice(self: *const Transitions) []const Transition {
        return self.items[0..self.count];
    }
};

/// Turns a stream of boot keyboard reports into make/break transitions by diffing
/// each report against the last.
pub const KeyboardDecoder = struct {
    previous: KeyboardReport = .{},

    pub fn feed(self: *KeyboardDecoder, current: KeyboardReport) Transitions {
        var out = Transitions{};

        // Rollover: 0x01 (ErrorRollOver) means more keys are held than the report
        // can carry, so the key array is invalid. Emit nothing and keep the prior
        // state (so the eventual releases still resolve against real keys).
        for (current.keys) |key| {
            if (key == 0x01) return out;
        }

        // Modifiers: one make/break per changed bit; modifier usages are 224..231.
        const changed = current.modifiers ^ self.previous.modifiers;
        var bit: u3 = 0;
        while (true) : (bit += 1) {
            const mask = @as(u8, 1) << bit;
            if (changed & mask != 0) {
                out.add(.{
                    .kind = if (current.modifiers & mask != 0) .pressed else .released,
                    .usage = 224 + @as(u8, bit),
                });
            }
            if (bit == 7) break;
        }

        // Keys made: present now, absent before.
        for (current.keys) |key| {
            if (key != 0 and !contains(&self.previous.keys, key)) out.add(.{ .kind = .pressed, .usage = key });
        }
        // Keys broken: present before, absent now.
        for (self.previous.keys) |key| {
            if (key != 0 and !contains(&current.keys, key)) out.add(.{ .kind = .released, .usage = key });
        }

        self.previous = current;
        return out;
    }
};

fn contains(keys: *const [6]u8, value: u8) bool {
    for (keys) |key| {
        if (key == value) return true;
    }
    return false;
}

// --- mouse ------------------------------------------------------------------

/// A decoded boot mouse report: the button bitmap and relative motion. The wheel
/// byte is present only on 4-byte reports (QEMU's usb-mouse sends one).
pub const MouseReport = struct {
    buttons: u8 = 0,
    dx: i8 = 0,
    dy: i8 = 0,
    wheel: i8 = 0,
    has_wheel: bool = false,
};

pub const mouse_button_left: u8 = 1 << 0;
pub const mouse_button_right: u8 = 1 << 1;
pub const mouse_button_middle: u8 = 1 << 2;

/// Parse a 3- or 4-byte boot mouse report. Note HID reports Y in screen
/// convention (positive = down), so — unlike PS/2 — `dy` is NOT negated.
pub fn parseMouse(bytes: []const u8) ?MouseReport {
    if (bytes.len < 3) return null;
    return .{
        .buttons = bytes[0],
        .dx = @bitCast(bytes[1]),
        .dy = @bitCast(bytes[2]),
        .wheel = if (bytes.len >= 4) @bitCast(bytes[3]) else 0,
        .has_wheel = bytes.len >= 4,
    };
}

// --- tests ------------------------------------------------------------------

test "keyboard diff produces make and break transitions" {
    var decoder = KeyboardDecoder{};

    // Press 'a' (usage 4).
    var t = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } });
    try std.testing.expectEqual(@as(usize, 1), t.count);
    try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
    try std.testing.expectEqual(@as(u8, 4), t.items[0].usage);

    // Hold 'a', press 'b' (usage 5): only 'b' is new.
    t = decoder.feed(.{ .keys = .{ 4, 5, 0, 0, 0, 0 } });
    try std.testing.expectEqual(@as(usize, 1), t.count);
    try std.testing.expectEqual(@as(u8, 5), t.items[0].usage);

    // Release everything: 'a' and 'b' both break.
    t = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
    try std.testing.expectEqual(@as(usize, 2), t.count);
    try std.testing.expectEqual(TransitionKind.released, t.items[0].kind);

    // Press Left Shift (modifier bit 1 -> usage 225).
    t = decoder.feed(.{ .modifiers = modifier_left_shift });
    try std.testing.expectEqual(@as(usize, 1), t.count);
    try std.testing.expectEqual(@as(u8, 225), t.items[0].usage);
    try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
}

test "rollover report is ignored but state is preserved" {
    var decoder = KeyboardDecoder{};
    _ = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); // press 'a'

    const rollover = decoder.feed(.{ .keys = .{ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } });
    try std.testing.expectEqual(@as(usize, 0), rollover.count);

    // 'a' is still considered down, so releasing all keys now breaks it.
    const release = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
    try std.testing.expectEqual(@as(usize, 1), release.count);
    try std.testing.expectEqual(@as(u8, 4), release.items[0].usage);
    try std.testing.expectEqual(TransitionKind.released, release.items[0].kind);
}

test "mouse report parses motion without inverting Y" {
    const three = parseMouse(&.{ mouse_button_left, 5, 0xFB }).?; // dy = -5
    try std.testing.expectEqual(mouse_button_left, three.buttons);
    try std.testing.expectEqual(@as(i8, 5), three.dx);
    try std.testing.expectEqual(@as(i8, -5), three.dy);
    try std.testing.expect(!three.has_wheel);

    const four = parseMouse(&.{ 0, 0, 0, 0xFF }).?; // wheel = -1
    try std.testing.expect(four.has_wheel);
    try std.testing.expectEqual(@as(i8, -1), four.wheel);

    try std.testing.expect(parseMouse(&.{ 0, 0 }) == null); // too short
}
