//! USB class-driver client: the helper a keyboard, mouse, or mass-storage driver
//! uses to reach its device through the xHCI bus driver, so it never hand-rolls
//! the transfer-protocol IPC. Layered over `ipc` and the shared
//! `usb-transfer-protocol` wire format, the way `input.zig` layers over the input
//! service and `device.zig` over the raw device calls.
//!
//! A class driver, spawned with its interface's assigned device id as argv[1]:
//!     if (!usb.helloManager(id)) return;              // meet the spawn deadline
//!     var device = usb.open(id) orelse return;        // open + get its endpoints
//!     _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
//!     _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
//!     while (true) { ... ipc.replyWait(device.endpoint, ...) ... }  // its own loop
//!
//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport`
//! messages (the class driver runs a bare `replyWait` loop to read them, because
//! the service harness drops buffered-message payloads — see service.zig).

const std = @import("std");
const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("usb-transfer-protocol");
const device_manager = @import("device-manager-protocol");

pub const Endpoint = protocol.Endpoint;
pub const InterruptReport = protocol.InterruptReport;
pub const max_report_data = protocol.max_report_data;

// Endpoint transfer types (EndpointDescriptor attributes), for `findEndpoint`.
pub const transfer_type_bulk: u8 = 2;
pub const transfer_type_interrupt: u8 = 3;

/// An opened USB device: the bus endpoint to send requests to, this driver's own
/// endpoint that reports arrive on, the device token, and the interface's
/// endpoints (so a driver need not re-read the configuration descriptor).
pub const Device = struct {
    bus: ipc.Handle,
    endpoint: ipc.Handle,
    token: u64,
    class: u8,
    subclass: u8,
    protocol_code: u8,
    interface_number: u8,
    endpoint_count: usize = 0,
    endpoints: [protocol.max_reported_endpoints]Endpoint = undefined,

    /// The interface's first endpoint of the given transfer type and direction
    /// (`transfer_type_bulk` / `transfer_type_interrupt`), or null.
    pub fn findEndpoint(self: *const Device, transfer_type: u8, direction_in: bool) ?Endpoint {
        for (self.endpoints[0..self.endpoint_count]) |endpoint| {
            if (endpoint.transfer_type == transfer_type and (endpoint.address & 0x80 != 0) == direction_in) return endpoint;
        }
        return null;
    }

    fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
        var request = protocol.ControlRequest{
            .device_token = self.token,
            .setup = setup,
            .direction_in = @intFromBool(direction_in),
            .data_length = @intCast(data.len),
        };
        if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data);
        var reply: [@sizeOf(protocol.ControlReply)]u8 = undefined;
        const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
        if (length < @sizeOf(protocol.ControlReply)) return null;
        const control_reply = std.mem.bytesToValue(protocol.ControlReply, reply[0..@sizeOf(protocol.ControlReply)]);
        if (control_reply.status != 0) return null;
        const actual = @min(control_reply.actual_length, data.len);
        if (direction_in and actual > 0) @memcpy(data[0..actual], control_reply.data[0..actual]);
        return actual;
    }

    /// A control transfer with no data stage (SET_PROTOCOL, SET_IDLE, ...). The
    /// `setup` is a bit-cast `usb_abi.Request`.
    pub fn controlOut(self: *Device, setup: [8]u8) bool {
        return self.controlTransfer(setup, false, &.{}) != null;
    }

    /// A device-to-host control transfer, returning the bytes read into `out`.
    pub fn controlIn(self: *Device, setup: [8]u8, out: []u8) ?usize {
        return self.controlTransfer(setup, true, out);
    }

    /// Begin periodic IN polling of an interrupt endpoint; reports flow back to
    /// `self.endpoint` as asynchronous `InterruptReport` messages.
    pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
        var request = protocol.InterruptSubscribeRequest{
            .device_token = self.token,
            .endpoint_address = endpoint_address,
            .max_length = max_length,
        };
        var reply: [@sizeOf(protocol.InterruptSubscribeReply)]u8 = undefined;
        const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return false;
        if (length < @sizeOf(protocol.InterruptSubscribeReply)) return false;
        return std.mem.bytesToValue(protocol.InterruptSubscribeReply, reply[0..@sizeOf(protocol.InterruptSubscribeReply)]).status == 0;
    }

    /// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
    /// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
    pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
        var request = protocol.BulkRequest{
            .device_token = self.token,
            .physical_address = physical,
            .length = length,
            .endpoint_address = endpoint_address,
        };
        var reply: [@sizeOf(protocol.BulkReply)]u8 = undefined;
        const replied = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
        if (replied < @sizeOf(protocol.BulkReply)) return null;
        const bulk_reply = std.mem.bytesToValue(protocol.BulkReply, reply[0..@sizeOf(protocol.BulkReply)]);
        if (bulk_reply.status != 0) return null;
        return bulk_reply.actual_length;
    }
};

/// Look up the USB bus and open the device with the assigned id, handing over a
/// freshly created endpoint for asynchronous interrupt reports. Retries while the
/// bus is still coming up (a class driver races the bus driver at boot).
pub fn open(device_id: u64) ?Device {
    var attempts: usize = 0;
    const bus = while (attempts < 100) : (attempts += 1) {
        if (ipc.lookup(.usb_bus)) |handle| break handle;
        system.sleep(20);
    } else return null;

    const endpoint = ipc.createIpcEndpoint() orelse return null;
    var request = protocol.OpenRequest{ .device_id = device_id };
    var reply: [@sizeOf(protocol.OpenReply)]u8 = undefined;
    const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null;
    if (result.len < @sizeOf(protocol.OpenReply)) return null;
    const open_reply = std.mem.bytesToValue(protocol.OpenReply, reply[0..@sizeOf(protocol.OpenReply)]);
    if (open_reply.status != 0) return null;

    var device = Device{
        .bus = bus,
        .endpoint = endpoint,
        .token = open_reply.device_token,
        .class = open_reply.interface_class,
        .subclass = open_reply.interface_subclass,
        .protocol_code = open_reply.interface_protocol,
        .interface_number = open_reply.interface_number,
        .endpoint_count = @min(open_reply.endpoint_count, protocol.max_reported_endpoints),
    };
    for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index];
    return device;
}

/// Hello the device manager as a class driver (Role.device) so a supervised
/// spawn meets its hello deadline. Retries while the manager comes up.
pub fn helloManager(device_id: u64) bool {
    var attempts: usize = 0;
    const manager = while (attempts < 100) : (attempts += 1) {
        if (ipc.lookup(.device_manager)) |handle| break handle;
        system.sleep(20);
    } else return false;

    const hello = device_manager.Hello{ .role = @intFromEnum(device_manager.Role.device), .device_id = device_id };
    var reply: [device_manager.message_maximum]u8 = undefined;
    const length = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch return false;
    if (length < device_manager.reply_size) return false;
    return std.mem.bytesToValue(device_manager.HelloReply, reply[0..device_manager.reply_size]).status == 0;
}
