//! USB HID boot mouse driver.
//!
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
//! mouse interface (class 3, subclass 1, protocol 2); its assigned device id
//! arrives as argv[1]. Like the keyboard driver it owns no hardware: it opens its
//! device through the USB transfer protocol (`runtime.usb`), asks for the boot
//! protocol, subscribes to its interrupt-IN endpoint, and turns each 3- or 4-byte
//! boot report into input-protocol mouse events published to the input service.
//!
//! Unlike PS/2, HID reports Y in screen convention (positive = down), so motion
//! is passed straight through (the decode in hid-report.zig does not negate it).

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

// The current pressed-button bitmask in input-protocol terms.
fn buttonMask(buttons: u8) u32 {
    var mask: u32 = 0;
    if (buttons & hid.mouse_button_left != 0) mask |= input_protocol.mouse_button_left;
    if (buttons & hid.mouse_button_right != 0) mask |= input_protocol.mouse_button_right;
    if (buttons & hid.mouse_button_middle != 0) mask |= input_protocol.mouse_button_middle;
    return mask;
}

pub fn main(init: runtime.process.Init) void {
    const argument = init.arguments.get(1) orelse {
        _ = runtime.system.write("/system/drivers/usb-hid/mouse: 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;
    };

    if (!runtime.usb.helloManager(device_id)) {
        _ = runtime.system.write("/system/drivers/usb-hid/mouse: 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/mouse: no interrupt-IN endpoint\n");
        return;
    };

    _ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));

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

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

    var previous_buttons: u8 = 0;
    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)]);
        const length = @min(message.length, message.data.len);
        const report = hid.parseMouse(message.data[0..length]) orelse continue;
        const mask = buttonMask(report.buttons);

        // Button transitions: one event per changed button bit.
        const changed = report.buttons ^ previous_buttons;
        inline for (.{
            .{ hid.mouse_button_left, input_protocol.mouse_button_left },
            .{ hid.mouse_button_right, input_protocol.mouse_button_right },
            .{ hid.mouse_button_middle, input_protocol.mouse_button_middle },
        }) |pair| {
            if (changed & pair[0] != 0) {
                _ = source.publishMouseEvent(.{
                    .kind = @intFromEnum(if (report.buttons & pair[0] != 0) input_protocol.MouseEventKind.button_down else input_protocol.MouseEventKind.button_up),
                    .button = pair[1],
                    .dx = 0,
                    .dy = 0,
                    .scroll_x = 0,
                    .scroll_y = 0,
                    .buttons = mask,
                });
            }
        }
        previous_buttons = report.buttons;

        // Relative motion (dy straight through — HID Y is already screen convention).
        if (report.dx != 0 or report.dy != 0) {
            _ = source.publishMouseEvent(.{
                .kind = @intFromEnum(input_protocol.MouseEventKind.motion),
                .button = 0,
                .dx = report.dx,
                .dy = report.dy,
                .scroll_x = 0,
                .scroll_y = 0,
                .buttons = mask,
            });
        }

        // Wheel (4-byte reports only): positive = scroll up.
        if (report.has_wheel and report.wheel != 0) {
            _ = source.publishMouseEvent(.{
                .kind = @intFromEnum(input_protocol.MouseEventKind.scroll),
                .button = 0,
                .dx = 0,
                .dy = 0,
                .scroll_x = 0,
                .scroll_y = report.wheel,
                .buttons = mask,
            });
        }
    }
}
