//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
//! mass-storage driver) says to the xHCI bus driver over `/protocol/usb-transfer`
//! to drive its device. The class driver owns no hardware — it reaches its device
//! entirely through these packets, the way a PS/2 keyboard driver reaches the
//! 8042 through the ps2-bus.
//!
//! Defined through the envelope (docs/os-development/protocol-namespace.md), so
//! every packet begins with the folded `Header`. **`Header.target` is the device
//! token** — the per-open handle the bus driver hands back, which every request
//! but `open` addressed through a `device_token` field of its own before the
//! rebase. `open` itself addresses the *assigned device id*, because that is what
//! the caller has before there is a token.
//!
//! The shape:
//!   - **open** (a capability-passing call): the class driver hands over its own
//!     endpoint (for asynchronous interrupt reports); the target is its assigned
//!     device id, and the reply carries a `device_token` plus its interface's
//!     endpoints.
//!   - **control / bulk** (synchronous calls): one transfer, answered when it
//!     completes. Control data travels **in the packet's tail** in both
//!     directions (descriptors, HID/MSC class requests are all small), so the
//!     fixed parts stay tiny and `Status.len` is the transferred length — the
//!     envelope's own field for "how many bytes follow", which is precisely what
//!     the old `actual_length` said. 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 packet floor.
//!   - **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 `interrupt_report` event.
//!     It stays one of **this protocol's own verbs**, not the reserved
//!     `subscribe`: the reserved verb means "push me this provider's events" and
//!     carries the subscriber's endpoint, while this names one endpoint address
//!     on one device and a poll length, and the endpoint it pushes to was handed
//!     over at `open`. Same word, different contract.
//!   - **dma_attach**: a class driver hands the controller a DMA-region
//!     capability (riding the call's cap slot) so the controller binds that
//!     buffer into its IOMMU domain and may then DMA to the physical addresses
//!     inside it. Needed once per buffer the class driver will name in a `bulk`
//!     transfer (its own, or one forwarded to it).
//!
//! Single controller assumption: one provider serves QEMU's one xHCI. A
//! multi-controller machine would need the controller in the target (or the
//! spawner wiring each class driver its own channel); noted, not built.

const std = @import("std");
const envelope = @import("envelope");

/// The largest control-transfer data stage. It rides the packet's tail, so the
/// bound is the call floor less the header and the fixed request part — derived
/// rather than declared, which is what keeps it honest when a field moves.
pub const max_inline_data: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(Control);

/// The largest interrupt report pushed asynchronously. An event packet is the
/// header plus the payload within 64 bytes, so this is what is left after the
/// report's own four bytes of framing: boot keyboard reports are 8 bytes, boot
/// mouse reports 3–4, and the whole HID boot vocabulary fits many times over.
/// A device that produces more has its report truncated, never split.
pub const max_report_data: usize = 40;

/// 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;

/// 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 },
};

// --- the per-operation request and reply parts ------------------------------
//
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
// device token: those are the packet header's, folded in once. No reply carries
// a status either — that is the `Status` every reply begins with.

/// The answer to `open`: the token every later packet puts in `Header.target`,
/// the interface's class triple (a sanity check), and its endpoints.
pub const Opened = extern struct {
    device_token: u64,
    endpoint_count: u32,
    interface_class: u8,
    interface_subclass: u8,
    interface_protocol: u8,
    interface_number: u8,
    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 on `Header.target`. `setup` is a bit-cast
/// `usb_abi.Request`. For an OUT transfer the data stage is the request's tail;
/// for an IN transfer it comes back as the reply's tail, and `Status.len` is how
/// much of it arrived.
pub const Control = extern struct {
    setup: [8]u8,
    /// 1 = device-to-host (IN), 0 = host-to-device (OUT).
    direction_in: u8,
    _padding: u8 = 0,
    /// Bytes of data stage: what an IN transfer asks for, and what an OUT
    /// transfer's tail carries.
    data_length: u16,
    _padding2: u32 = 0,
};

/// `interrupt_subscribe`: begin periodic IN polling of an interrupt endpoint of
/// `Header.target`. Each report the device returns is pushed to the endpoint the
/// caller handed over at `open`, as an `interrupt_report` event.
pub const InterruptSubscribe = extern struct {
    endpoint_address: u8,
    _padding: u8 = 0,
    /// Bytes to request per poll (the endpoint's max packet size).
    max_length: u16,
};

/// `bulk`: one bulk IN or OUT transfer on `Header.target`. `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 Bulk = extern struct {
    physical_address: u64,
    length: u32,
    endpoint_address: u8,
    _padding: u8 = 0,
    _padding2: u16 = 0,
};

/// How many bytes a bulk transfer actually moved. It cannot ride `Status.len`
/// the way a control transfer's does: nothing follows a bulk reply, because the
/// data went to the caller's DMA buffer rather than into the packet.
pub const Transferred = extern struct { actual_length: u32 };

/// One asynchronous interrupt report, pushed to the endpoint the class driver
/// handed over at `open`. The device it came from is `Header.target`.
pub const InterruptReport = extern struct {
    endpoint_address: u8,
    length: u8,
    _padding: u16 = 0,
    data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
};

pub const Protocol = envelope.Define(.{
    .name = "usb-transfer",
    .version = 1,
    .operations = &.{
        // open: the target is the interface's assigned device id (its argv[1]),
        // and the class driver's receive endpoint rides as the capability.
        .{ .name = "open", .reply = Opened },
        .{ .name = "control", .request = Control },
        .{ .name = "interrupt_subscribe", .request = InterruptSubscribe },
        .{ .name = "bulk", .request = Bulk, .reply = Transferred },
        // dma_attach: the region capability rides the call's cap slot; the
        // target says which caller's device is attaching, so there is nothing
        // left for a body to carry.
        .{ .name = "dma_attach" },
    },
    .events = &.{
        .{ .name = "interrupt_report", .payload = InterruptReport },
    },
});

pub const Operation = Protocol.Operation;
pub const Event = Protocol.Event;

/// What both sides size their buffers to — the call floor, as every protocol does.
pub const message_maximum: usize = Protocol.message_maximum;

test "the budgets, re-verified by Define rather than by hand" {
    // What the hand-rolled comptime asserts used to say, now said by `Define`
    // — and counting the header, which the old checks did not.
    try std.testing.expectEqual(@as(usize, 224), max_inline_data);
    try std.testing.expect(Protocol.request_maximum <= envelope.packet_maximum);
    try std.testing.expect(Protocol.reply_maximum <= envelope.packet_maximum);
    // The report was 48 bytes of data in a 64-byte struct that had no room left
    // for a header. Folding the device token into the target and trimming the
    // data to 40 leaves the whole packet at 60 of the 64-byte push floor.
    try std.testing.expectEqual(@as(usize, 44), @sizeOf(InterruptReport));
    try std.testing.expectEqual(@as(usize, 60), Protocol.event_maximum);
    try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
}

test "the verb numbering, and the device token in the header" {
    try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open));
    try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.control));
    try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.interrupt_subscribe));
    try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.bulk));
    try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.dma_attach));
    try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.interrupt_report));

    var buffer: [message_maximum]u8 = undefined;
    const packet = Protocol.encodeRequest(.control, 9, .{
        .setup = .{ 0, 6, 0, 1, 0, 0, 18, 0 },
        .direction_in = 1,
        .data_length = 18,
    }, &.{}, &buffer).?;
    try std.testing.expectEqual(@as(u64, 9), envelope.headerOf(packet).?.target);
    try std.testing.expectEqual(@as(u16, 18), Protocol.decodeRequest(.control, packet).?.data_length);
}

test "a control OUT carries its data stage as the packet's tail" {
    var buffer: [message_maximum]u8 = undefined;
    const payload = [_]u8{ 1, 2, 3, 4 };
    const packet = Protocol.encodeRequest(.control, 5, .{
        .setup = .{ 0x21, 11, 0, 0, 0, 0, 4, 0 },
        .direction_in = 0,
        .data_length = payload.len,
    }, &payload, &buffer).?;
    try std.testing.expectEqualSlices(u8, &payload, Protocol.requestTail(.control, packet));

    // And the answer to an IN: the bytes follow the (empty) fixed reply part,
    // with `Status.len` counting exactly them.
    const answered = Protocol.encodeReply(.control, 0, {}, &payload, &buffer).?;
    try std.testing.expectEqual(@as(u32, payload.len), envelope.statusOf(answered).?.len);
    try std.testing.expectEqualSlices(u8, &payload, Protocol.replyTail(.control, answered));
}
