//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
//! mass-storage driver) says to the xHCI bus driver over its well-known
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware —
//! it reaches its device entirely through these messages, the way a PS/2 keyboard
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by
//! `Operation`, the vfs-protocol / device-manager-protocol pattern.
//!
//! The shape:
//!   - **open** (a capability-passing `ipc.callCap`): the class driver hands over
//!     its own endpoint (for asynchronous interrupt reports) and its assigned
//!     device id, and receives a `device_token` plus its interface's endpoints.
//!   - **control / bulk** (synchronous `ipc.call`): one transfer, answered when
//!     it completes. Control data travels inline (descriptors, HID/MSC class
//!     requests are all small); bulk data travels by **physical address** — the
//!     class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has
//!     to cross the 256-byte IPC boundary.
//!   - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
//!     interrupt endpoint; each report the device produces is then pushed to the
//!     class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`),
//!     exactly how the input service delivers events.
//!
//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI.
//! A multi-controller machine would need a per-controller endpoint (the device
//! manager handing each class driver the right one); noted, not built.

/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM).
pub const message_maximum: usize = 256;

/// The largest inline control-transfer payload. Sized so a whole message
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class
/// requests are all far smaller.
pub const max_inline_data: usize = 200;

/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport`
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are
/// 8 bytes, boot mouse reports 3–4.
pub const max_report_data: usize = 48;

/// Endpoints per interface reported back in an open reply (a boot HID interface
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
pub const max_reported_endpoints: usize = 4;

pub const Operation = enum(u32) {
    open = 0,
    control = 1,
    interrupt_subscribe = 2,
    bulk = 3,
};

/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
/// the bus driver already parsed during enumeration.
pub const Endpoint = extern struct {
    /// EndpointDescriptor address: direction in bit 7, number in bits 3:0.
    address: u8,
    /// 0 control, 1 isochronous, 2 bulk, 3 interrupt.
    transfer_type: u8,
    max_packet_size: u16,
    interval: u8,
    reserved: [3]u8 = .{ 0, 0, 0 },
};

/// open: the class driver's receive endpoint rides as the call's capability, and
/// `device_id` is the interface's assigned id (its argv[1]).
pub const OpenRequest = extern struct {
    operation: u32 = @intFromEnum(Operation.open),
    reserved: u32 = 0,
    device_id: u64,
};

/// The answer to open: a token scoping every later request to this device, the
/// interface's class triple (a sanity check), and its endpoints.
pub const OpenReply = extern struct {
    status: i32,
    endpoint_count: u32,
    device_token: u64,
    interface_class: u8,
    interface_subclass: u8,
    interface_protocol: u8,
    interface_number: u8,
    reserved2: u32 = 0,
    endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
};

/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`.
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the
/// reply carries up to `data_length` bytes back.
pub const ControlRequest = extern struct {
    operation: u32 = @intFromEnum(Operation.control),
    reserved: u32 = 0,
    device_token: u64,
    setup: [8]u8,
    direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT)
    reserved2: u8 = 0,
    data_length: u16,
    reserved3: u32 = 0,
    data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};

pub const ControlReply = extern struct {
    status: i32, // 0 success, negative on failure/stall
    actual_length: u32,
    data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};

/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each
/// report the device returns is pushed to the caller's endpoint (handed over at
/// open) as an asynchronous `InterruptReport`.
pub const InterruptSubscribeRequest = extern struct {
    operation: u32 = @intFromEnum(Operation.interrupt_subscribe),
    reserved: u32 = 0,
    device_token: u64,
    endpoint_address: u8,
    reserved2: u8 = 0,
    max_length: u16, // bytes to request per poll (the endpoint's max packet size)
};

pub const InterruptSubscribeReply = extern struct {
    status: i32,
    reserved: u32 = 0,
};

/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own
/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk
/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT.
pub const BulkRequest = extern struct {
    operation: u32 = @intFromEnum(Operation.bulk),
    reserved: u32 = 0,
    device_token: u64,
    physical_address: u64,
    length: u32,
    endpoint_address: u8,
    reserved2: u8 = 0,
    reserved3: u16 = 0,
};

pub const BulkReply = extern struct {
    status: i32,
    actual_length: u32,
};

/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's
/// endpoint. `Received.isMessage()` is set; there is no reply owed.
pub const InterruptReport = extern struct {
    device_token: u64,
    endpoint_address: u8,
    length: u8,
    reserved: u16 = 0,
    data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
};

comptime {
    const std = @import("std");
    // Every synchronous message must fit one IPC message; the async report must
    // fit an ipc_send payload slot.
    std.debug.assert(@sizeOf(ControlRequest) <= message_maximum);
    std.debug.assert(@sizeOf(ControlReply) <= message_maximum);
    std.debug.assert(@sizeOf(OpenReply) <= message_maximum);
    std.debug.assert(@sizeOf(InterruptReport) <= 64);
}
