//! /system/drivers/usb-xhci-bus — the xHCI (USB 3) host-controller bus driver.
//! The device manager spawns **one instance per controller** it discovers (a
//! machine can carry several), passing the controller's device-tree id as
//! argv[1]; this instance claims that device and no other, so multiple
//! instances never fight over hardware.
//!
//! M18.2 (this increment): after the hello, real hardware — map the xHC's
//! register window (the first memory BAR; resource 0 is the ECAM config
//! space), read the capability registers, and walk the root-hub ports: one
//! `child_added` report to the manager per connected port, carrying the port
//! number and the PORTSC speed class as identity. No transfer rings yet —
//! descriptors and USB class matching are the USB track; the connect bit and
//! speed come straight from PORTSC, which reflects hardware state whether or
//! not the controller is running.

const std = @import("std");
const runtime = @import("runtime");
const protocol = runtime.device_manager_protocol;
const device = runtime.device;
const usb_ids = @import("usb-ids");
const usb_abi = @import("usb-abi");
const transfer = @import("usb-transfer-protocol");
const library = @import("usb-xhci-library.zig");

/// The controller engine (reset, rings, transfers), stood up in `initialise`.
var controller: ?library.Controller = null;

/// This driver's service endpoint (registered as `.usb_bus`), where class-driver
/// requests, signals, and the interrupt-poll timer all arrive.
var service_endpoint: runtime.ipc.Handle = 0;

/// How often the driver drains the event ring for interrupt reports (~125 Hz),
/// re-armed each tick. Frequent enough for responsive input.
const poll_interval_ms: u64 = 8;

/// The class driver endpoints that opened each device, so interrupt reports can
/// be pushed back to them. Keyed by the device token (the interface's device id).
const Open = struct {
    used: bool = false,
    device_token: u64 = 0,
    report_endpoint: usize = 0,
};
var opens = [_]Open{.{}} ** 16;

fn recordOpen(device_token: u64, report_endpoint: usize) void {
    for (&opens) |*open| {
        if (open.used and open.device_token == device_token) {
            open.report_endpoint = report_endpoint;
            return;
        }
    }
    for (&opens) |*open| {
        if (!open.used) {
            open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint };
            return;
        }
    }
}

fn reportEndpointFor(device_token: u64) ?usize {
    for (&opens) |*open| {
        if (open.used and open.device_token == device_token) return open.report_endpoint;
    }
    return null;
}

var controller_id: u64 = protocol.no_device;

/// Claim the assigned controller, find its register window, and hello the
/// manager. Any failure returns false: the process exits cleanly, which the
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
fn initialise(endpoint: runtime.ipc.Handle) bool {
    service_endpoint = endpoint;
    if (!device.claim(controller_id)) {
        std.log.info("unable to claim controller device {d}", .{controller_id});
        return false;
    }

    // Fetch our own descriptor back for the controller's resources.
    const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: out of memory\n");
        return false;
    };
    const total = device.enumerate(buffer);
    const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
        if (d.id == controller_id) break d;
    } else {
        std.log.info("device {d} not in the device tree", .{controller_id});
        return false;
    };

    // The xHC's registers live behind the first memory BAR. Resource 0 is the
    // function's ECAM configuration space (M15), so the walk starts at 1.
    var register_index: u64 = 0;
    const register_window = for (descriptor.resources[1..@intCast(descriptor.resource_count)], 1..) |resource, index| {
        if (resource.kind == @intFromEnum(device.ResourceKind.memory)) {
            register_index = index;
            break resource;
        }
    } else {
        std.log.info("controller device {d} has no register BAR", .{controller_id});
        return false;
    };
    std.log.info("claimed controller device {d} (registers at 0x{x}, {d} bytes)", .{
        controller_id,
        register_window.start,
        register_window.len,
    });
    register_base = device.mmioMap(controller_id, register_index) orelse {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: mmio_map failed\n");
        return false;
    };

    // Bring the controller up: reset it, stand up the command and event rings,
    // and start it running (the hardware half lives in usb-xhci-library.zig).
    controller = library.Controller.init(register_base) orelse {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: controller reset/bring-up failed\n");
        return false;
    };
    std.log.info("controller running ({d} slots, {d}-byte contexts)", .{
        controller.?.max_slots,
        controller.?.context_size,
    });
    // The proof of life: a No-Op command round-trips the command ring, the event
    // ring, the doorbell, and the cycle-bit bookkeeping. If this completes, the
    // engine is sound; transfers build on exactly this machinery.
    if (controller.?.noOpCommand()) {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: command ring running (no-op ok)\n");
    } else {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: no-op command did not complete\n");
        return false;
    }

    // The handshake: role, protocol version, assignment — inside the manager's
    // deadline (the lookup retries cover the manager still registering).
    var manager: ?runtime.ipc.Handle = null;
    var tries: u32 = 0;
    while (manager == null and tries < 100) : (tries += 1) {
        manager = runtime.ipc.lookup(.device_manager);
        if (manager == null) runtime.system.sleep(20);
    }
    const h = manager orelse {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: no device manager to hello\n");
        return false;
    };
    manager_handle = h; // the tick's hot-plug dispatch reports through this
    const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = controller_id };
    var reply: [protocol.message_maximum]u8 = undefined;
    const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: hello call failed\n");
        return false;
    };
    if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: hello refused\n");
        return false;
    }
    _ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n");

    scanPorts(h);

    // Arm the poll timer that drains interrupt reports from the event ring. It is
    // re-armed on each tick in onNotification; class drivers subscribe later.
    _ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
    return true;
}

var register_base: usize = 0;

/// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10)
/// decoded to human names — the boot-log breadcrumb for what actually enumerated on
/// a port, the USB analog of the pci-bus class-code line. A controller may redefine
/// these through its Supported Protocol capability, but the defaults cover every
/// speed QEMU and real hardware report at this (pre-descriptor) stage.
fn speedName(speed: u32) []const u8 {
    return switch (speed) {
        1 => "Full-speed (USB 2.0, 12 Mb/s)",
        2 => "Low-speed (USB 2.0, 1.5 Mb/s)",
        3 => "High-speed (USB 2.0, 480 Mb/s)",
        4 => "SuperSpeed (USB 3.0, 5 Gb/s)",
        5 => "SuperSpeedPlus (USB 3.1, 10 Gb/s)",
        else => "unknown speed",
    };
}

/// The root-hub scan and enumeration: for each connected port, bring the device
/// up (reset → enable slot → address), read its descriptors, and register +
/// report one child per interface — carrying the interface's (class, subclass,
/// protocol) triple as identity, which is what the device manager matches a
/// class driver against.
var manager_handle: ?runtime.ipc.Handle = null;

fn scanPorts(manager: runtime.ipc.Handle) void {
    const engine = if (controller) |*c| c else {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
        return;
    };
    std.log.info("{d} root-hub ports", .{engine.max_ports});

    var port: u32 = 1;
    var connected: u32 = 0;
    while (port <= engine.max_ports) : (port += 1) {
        if (!engine.portConnected(port)) continue;
        connected += 1;
        bringUpPort(manager, engine, port);
    }
    if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
}

/// Bring up whatever is on `port`: setup + enumerate + register/report one child
/// per interface. Shared by the boot scan and hot-plug (a port-change event with
/// the port now connected).
fn bringUpPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u32) void {
    const speed = (engine.portStatus(port) >> 10) & 0xF; // the PORTSC port-speed class
    std.log.info("port {d} connected — {s} (speed class {d})", .{ port, speedName(speed), speed });

    const usb_device = engine.setupDevice(port, speed) orelse {
        std.log.info("port {d} device setup failed", .{port});
        return;
    };
    if (!engine.enumerate(usb_device)) {
        std.log.info("port {d} enumeration failed", .{port});
        return;
    }
    std.log.info("port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{
        port,
        usb_device.device_descriptor.vendor_id,
        usb_device.device_descriptor.product_id,
        usb_device.interface_count,
    });

    for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
        // Record the id each interface was registered as, so a class driver
        // opening the interface (by that id) resolves to it.
        if (reportInterface(manager, port, interface.*)) |registered| {
            interface.registered_device_id = registered;
        }
    }

    // A hub (class 9) is bus infrastructure the bus drives itself: configure it
    // and power its downstream ports (docs/usb-hub.md). Its interfaces are still
    // reported above, but no external class driver binds it.
    if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
}

// A compact topology-unique port key for a hub downstream port: 1000 + slot*100
// + port. Stays a few digits (the id tag "P<key>I<iface>" has an 8-byte cap)
// while never colliding with a root port (1..N) or another (hub, port).
fn hubPortKey(hub_slot: u8, port: u16) u32 {
    return 1000 + @as(u32, hub_slot) * 100 + port;
}

/// Service a change on hub downstream `port`: dispatch a connect (enumerate the
/// new device) or a disconnect (tear the old one down). Recurses for a hub
/// behind a hub — a nested hub is set up on connect and its downstream devices
/// torn down first on disconnect.
fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void {
    const status = engine.hubPortStatusAck(hub, port) orelse return;
    const connected = library.Controller.hubPortConnected(status);
    std.log.info("hub slot {d} port {d} status 0x{x:0>8} ({s})", .{ hub.slot_id, port, status, if (connected) "connected" else "empty" });
    const existing = engine.deviceOnHubPort(hub, port);

    if (!connected) {
        if (existing) |dev| tearDownHubDevice(manager, engine, dev);
        return;
    }
    if (existing != null) return; // already up

    const usb_device = engine.serviceHubPort(hub, port) orelse return;
    if (!engine.enumerate(usb_device)) {
        std.log.info("hub slot {d} port {d}: enumeration failed", .{ hub.slot_id, port });
        return;
    }
    std.log.info("hub slot {d} port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{
        hub.slot_id, port,
        usb_device.device_descriptor.vendor_id,
        usb_device.device_descriptor.product_id,
        usb_device.interface_count,
    });
    for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
        if (reportInterface(manager, hubPortKey(hub.slot_id, port), interface.*)) |registered| {
            interface.registered_device_id = registered;
        }
    }
    if (deviceIsHub(usb_device)) _ = engine.setupHub(usb_device);
}

/// Tear down a device that disconnected from a hub: recursively tear down its
/// own downstream devices first if it is a hub, report each interface removed,
/// then Disable Slot. Mirrors tearDownPort for a hub-attached device.
fn tearDownHubDevice(manager: runtime.ipc.Handle, engine: *library.Controller, dev: *library.Device) void {
    // A hub that left takes its whole subtree with it — tear children down first.
    if (dev.is_hub) {
        while (engine.nextChildOf(dev.slot_id, 0)) |child| tearDownHubDevice(manager, engine, child);
    }
    std.log.info("hub device slot {d} disconnected", .{dev.slot_id});
    const key = hubPortKey(dev.parent_slot, dev.parent_port);
    for (dev.interfaces[0..dev.interface_count]) |*interface| {
        if (interface.registered_device_id == 0) continue;
        const event = protocol.ChildRemoved{
            .parent = controller_id,
            .bus_address = (@as(u64, key) << 8) | interface.number,
        };
        var reply: [protocol.message_maximum]u8 = undefined;
        _ = runtime.ipc.call(manager, std.mem.asBytes(&event), &reply) catch {};
        interface.registered_device_id = 0;
    }
    engine.tearDownDevice(dev);
}

/// Whether an enumerated device is a hub — class 9 at the device or the
/// interface level (a hub's single interface is class 9/0/0).
fn deviceIsHub(usb_device: *const library.Device) bool {
    if (usb_device.device_descriptor.device_class == @intFromEnum(usb_ids.Class.hub)) return true;
    for (usb_device.interfaces[0..usb_device.interface_count]) |interface| {
        if (interface.class == @intFromEnum(usb_ids.Class.hub)) return true;
    }
    return false;
}

/// Tear down whatever was on `port` after an unplug: report each registered
/// interface as removed (the manager prunes the node, notifies watchers, and
/// stops the class driver's world honestly), then release the controller-side
/// device state (Disable Slot).
fn tearDownPort(manager: runtime.ipc.Handle, engine: *library.Controller, port: u32) void {
    const usb_device = engine.deviceOnPort(port) orelse return;
    std.log.info("port {d} disconnected", .{port});
    for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
        if (interface.registered_device_id == 0) continue;
        const event = protocol.ChildRemoved{
            .parent = controller_id,
            .bus_address = (@as(u64, port) << 8) | interface.number,
        };
        var reply: [protocol.message_maximum]u8 = undefined;
        _ = runtime.ipc.call(manager, std.mem.asBytes(&event), &reply) catch {
            std.log.info("child-removed report for port {d} interface {d} failed", .{ port, interface.number });
        };
        interface.registered_device_id = 0;
    }
    engine.tearDownDevice(usb_device);
}

/// Register one interface as a resource-less child of the controller and report
/// it to the device manager. The identity is the packed USB class triple, so the
/// manager can match a class driver (HID keyboard, mouse, mass storage); the
/// registered device id becomes that driver's argv[1] assignment. Returns the
/// registered device id, or null if registration or the report failed.
fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.InterfaceInfo) ?u64 {
    const identity = usb_ids.packTriple(interface.class, interface.subclass, interface.protocol);

    // A USB device is reached through its controller, not by MMIO, so the child
    // carries no resources; register() allows that. Its bus-local identity — the
    // (port, interface) address, written as a short "P<port>I<interface>" tag in
    // the hid field — makes each interface a distinct kernel node (the register
    // dedup keys on class/pci_class/hid/resources, all otherwise identical here)
    // and keeps re-registration idempotent across a bus restart: the same port
    // and interface always map back to the same device id.
    var descriptor = std.mem.zeroes(device.DeviceDescriptor);
    descriptor.class = @intFromEnum(device.DeviceClass.usb_device);
    descriptor.pci_class = device.no_pci_class;
    descriptor.resource_count = 0;
    var hid_buffer: [8]u8 = undefined;
    const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch "";
    descriptor.hid_len = hid_text.len;
    @memcpy(descriptor.hid[0..hid_text.len], hid_text);
    const registered = device.register(controller_id, &descriptor) orelse {
        std.log.info("register refused for port {d} interface {d}", .{ port, interface.number });
        return null;
    };

    const report = protocol.ChildAdded{
        .parent = controller_id,
        .bus_address = (@as(u64, port) << 8) | interface.number,
        .identity = identity,
        .device_id = registered,
    };
    var reply: [protocol.message_maximum]u8 = undefined;
    _ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
        std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
        return null;
    };
    std.log.info("port {d} interface {d} class {d}/{d}/{d} registered as device {d}", .{
        port,
        interface.number,
        interface.class,
        interface.subclass,
        interface.protocol,
        registered,
    });
    return registered;
}

/// Serve the USB transfer protocol: a class driver opens its device, then issues
/// control / interrupt-subscribe / bulk requests against it.
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
    _ = sender;
    if (message.len < 4) return 0;
    const operation = std.mem.readInt(u32, message[0..4], .little);
    return switch (operation) {
        @intFromEnum(transfer.Operation.open) => handleOpen(message, reply, capability),
        @intFromEnum(transfer.Operation.control) => handleControl(message, reply),
        @intFromEnum(transfer.Operation.interrupt_subscribe) => handleSubscribe(message, reply),
        @intFromEnum(transfer.Operation.bulk) => handleBulk(message, reply),
        else => 0,
    };
}

fn writeReply(reply: []u8, value: anytype) usize {
    const bytes = std.mem.asBytes(&value);
    @memcpy(reply[0..bytes.len], bytes);
    return bytes.len;
}

/// open: resolve the assigned device id to an interface, remember the caller's
/// endpoint (for interrupt reports), and answer with a device token + the
/// interface's endpoints so the class driver need not re-read the config.
fn handleOpen(message: []const u8, reply: []u8, capability: ?runtime.ipc.Handle) usize {
    if (message.len < @sizeOf(transfer.OpenRequest)) return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
    const request = std.mem.bytesToValue(transfer.OpenRequest, message[0..@sizeOf(transfer.OpenRequest)]);
    const engine = if (controller) |*c| c else return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
    const found = engine.findInterface(request.device_id) orelse return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });

    if (capability) |endpoint| recordOpen(request.device_id, endpoint);

    var open_reply = transfer.OpenReply{
        .status = 0,
        .endpoint_count = found.interface.endpoint_count,
        .device_token = request.device_id,
        .interface_class = found.interface.class,
        .interface_subclass = found.interface.subclass,
        .interface_protocol = found.interface.protocol,
        .interface_number = found.interface.number,
    };
    const count = @min(found.interface.endpoint_count, transfer.max_reported_endpoints);
    for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
        open_reply.endpoints[index] = .{
            .address = endpoint.address,
            .transfer_type = endpoint.transfer_type,
            .max_packet_size = endpoint.max_packet_size,
            .interval = endpoint.interval,
        };
    }
    return writeReply(reply, open_reply);
}

/// control: one EP0 control transfer, small data inline both ways.
fn handleControl(message: []const u8, reply: []u8) usize {
    if (message.len < @sizeOf(transfer.ControlRequest)) return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
    const request = std.mem.bytesToValue(transfer.ControlRequest, message[0..@sizeOf(transfer.ControlRequest)]);
    const engine = if (controller) |*c| c else return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
    const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });

    const setup = std.mem.bytesToValue(usb_abi.Request, &request.setup);
    const direction_in = request.direction_in != 0;
    const data_length = @min(request.data_length, transfer.max_inline_data);
    var data: [transfer.max_inline_data]u8 = undefined;
    if (!direction_in) @memcpy(data[0..data_length], request.data[0..data_length]);

    const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in);
    var control_reply = transfer.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 };
    if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]);
    return writeReply(reply, control_reply);
}

/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously.
fn handleSubscribe(message: []const u8, reply: []u8) usize {
    if (message.len < @sizeOf(transfer.InterruptSubscribeRequest)) return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
    const request = std.mem.bytesToValue(transfer.InterruptSubscribeRequest, message[0..@sizeOf(transfer.InterruptSubscribeRequest)]);
    const engine = if (controller) |*c| c else return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
    const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
    const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
    const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
    const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint);
    return writeReply(reply, transfer.InterruptSubscribeReply{ .status = if (ok) 0 else -1 });
}

/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
/// address), so sector-sized data never crosses IPC.
fn handleBulk(message: []const u8, reply: []u8) usize {
    if (message.len < @sizeOf(transfer.BulkRequest)) return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
    const request = std.mem.bytesToValue(transfer.BulkRequest, message[0..@sizeOf(transfer.BulkRequest)]);
    const engine = if (controller) |*c| c else return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
    const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
    const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
    const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length);
    return writeReply(reply, transfer.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
}

/// The poll timer landed: drain any interrupt reports off the event ring and push
/// each to the class driver that subscribed, then re-arm the timer.
fn onNotification(badge: u64) void {
    if (badge & runtime.ipc.notify_timer_bit == 0) return;
    if (controller) |*engine| {
        engine.pump();
        while (engine.takePortChange()) |port| {
            const manager = manager_handle orelse break;
            if (engine.portConnected(port)) {
                if (engine.deviceOnPort(port) == null) bringUpPort(manager, engine, port);
            } else {
                tearDownPort(manager, engine, port);
            }
        }
        // Downstream hub-port changes (docs/usb-hub.md): a device connected on a
        // hub's downstream port is enumerated and registered here, so a keyboard
        // behind a hub reaches its class driver like one on a root port.
        // Cap per tick: even if a hub's change bits refuse to clear, the driver
        // must not spin here — it services a bounded batch and yields to the
        // next tick (and to storage, input, everything else).
        var serviced: u32 = 0;
        while (engine.takeHubChange()) |change| {
            const manager = manager_handle orelse break;
            bringUpBehindHub(manager, engine, change.hub, change.port);
            serviced += 1;
            if (serviced >= 32) break;
        }
        while (engine.takeReport()) |report| {
            var message = transfer.InterruptReport{
                .device_token = report.device_token,
                .endpoint_address = report.endpoint_address,
                .length = @intCast(@min(report.length, transfer.max_report_data)),
            };
            const n = @min(report.length, transfer.max_report_data);
            @memcpy(message.data[0..n], report.data[0..n]);
            _ = runtime.ipc.send(report.report_endpoint, std.mem.asBytes(&message));
        }
    }
    _ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
}

pub fn main(init: runtime.process.Init) void {
    const argument = init.arguments.get(1) orelse {
        _ = runtime.system.write("/system/drivers/usb-xhci-bus: missing controller device id (argv[1])\n");
        return;
    };
    controller_id = std.fmt.parseInt(u64, argument, 10) catch {
        std.log.info("malformed controller device id '{s}'", .{argument});
        return;
    };
    runtime.service.run(transfer.message_maximum, .{
        .service = .usb_bus,
        .init = initialise,
        .on_message = onMessage,
        .on_notification = onNotification,
    });
}
