//! USB mass-storage class driver (Bulk-Only Transport + transparent SCSI).
//!
//! Spawned by the device manager when the xHCI bus driver reports a mass-storage
//! / SCSI / bulk-only interface (class 8, subclass 6, protocol 0x50); its device
//! id arrives as argv[1]. It owns no hardware: it opens its device through the
//! USB transfer protocol (`runtime.usb`), then drives it with the BOT command
//! cycle — CBW out, an optional data stage, CSW in — carrying SCSI commands
//! (READ CAPACITY, READ(10), WRITE(10)). Upward it is a block device: it serves
//! the block protocol under `.block`, the storage a FAT filesystem sits on.
//!
//! Block data never crosses IPC: read/write name a caller-owned DMA buffer by
//! physical address, which the data stage DMAs straight to/from.

const std = @import("std");
const runtime = @import("runtime");
const scsi = @import("scsi.zig");
const bot = @import("bulk-only-transport.zig");
const block_protocol = @import("block-protocol");
const dma = runtime.dma;

var device_id: u64 = 0;
var device: runtime.usb.Device = undefined;
var bulk_in: runtime.usb.Endpoint = undefined;
var bulk_out: runtime.usb.Endpoint = undefined;

// DMA buffers for the transport: the 31-byte CBW, the 13-byte CSW, and a page
// for the small command data (INQUIRY / READ CAPACITY / the self-check sector).
var command_wrapper: dma.Region = undefined;
var status_wrapper: dma.Region = undefined;
var command_data: dma.Region = undefined;

var next_tag: u32 = 1;
var block_size: u32 = 512;
var block_count: u64 = 0;

/// One Bulk-Only-Transport command: send the CBW, run the data stage (to/from
/// `data_physical`), read and validate the CSW. Returns true on a passed status.
fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length: u32) bool {
    const tag = next_tag;
    next_tag +%= 1;

    const wrapper: *bot.CommandBlockWrapper = @ptrFromInt(command_wrapper.virtual);
    wrapper.* = .{
        .tag = tag,
        .data_transfer_length = data_length,
        .flags = if (direction_in) bot.flag_data_in else 0,
        .lun = 0,
        .cdb_length = @intCast(cdb.len),
    };
    @memcpy(wrapper.cdb[0..cdb.len], cdb);

    if (device.bulk(bulk_out.address, command_wrapper.physical, @sizeOf(bot.CommandBlockWrapper)) == null) return false;
    if (data_length > 0) {
        const endpoint = if (direction_in) bulk_in.address else bulk_out.address;
        if (device.bulk(endpoint, data_physical, data_length) == null) return false;
    }
    if (device.bulk(bulk_in.address, status_wrapper.physical, @sizeOf(bot.CommandStatusWrapper)) == null) return false;

    const status: *const bot.CommandStatusWrapper = @ptrFromInt(status_wrapper.virtual);
    if (status.signature != bot.csw_signature or status.tag != tag) return false;
    return status.status == @intFromEnum(bot.CommandStatus.passed);
}

/// Set when bring-up failed with the device PRESENT (an opened device that then
/// failed a step): main exits nonzero, and the device manager restarts us with
/// backoff — a transient failure heals instead of leaving storage down forever.
/// Device-absent paths stay clean exits: nothing to serve, nothing to retry.
var bring_up_failed = false;

fn initialise(endpoint: runtime.ipc.Handle) bool {
    _ = endpoint;
    if (!runtime.usb.helloManager(device_id)) {
        _ = runtime.system.write("/system/drivers/usb-storage: hello to device manager failed\n");
        return false;
    }
    device = runtime.usb.open(device_id) orelse {
        std.log.info("could not open device {d}", .{device_id});
        return false;
    };
    bulk_in = device.findEndpoint(runtime.usb.transfer_type_bulk, true) orelse {
        _ = runtime.system.write("/system/drivers/usb-storage: no bulk-IN endpoint\n");
        bring_up_failed = true;
        return false;
    };
    bulk_out = device.findEndpoint(runtime.usb.transfer_type_bulk, false) orelse {
        _ = runtime.system.write("/system/drivers/usb-storage: no bulk-OUT endpoint\n");
        bring_up_failed = true;
        return false;
    };
    command_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
    status_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
    command_data = dma.alloc(4096, dma.coherent) orelse return false;

    // Bring the LUN up: wait for it to be ready (clearing the initial unit-attention
    // with REQUEST SENSE), identify it, and read its capacity.
    var tries: u32 = 0;
    while (tries < 10) : (tries += 1) {
        const ready = scsi.testUnitReady();
        if (transact(&ready, false, 0, 0)) break;
        const sense = scsi.requestSense(18);
        _ = transact(&sense, true, command_data.physical, 18);
        runtime.system.sleep(50);
    }
    const inquiry = scsi.inquiry(36);
    _ = transact(&inquiry, true, command_data.physical, 36);

    const capacity_command = scsi.readCapacity10();
    if (!transact(&capacity_command, true, command_data.physical, 8)) {
        _ = runtime.system.write("/system/drivers/usb-storage: READ CAPACITY failed\n");
        bring_up_failed = true;
        return false;
    }
    var capacity_bytes: [8]u8 = undefined;
    const capacity_source: [*]const u8 = @ptrFromInt(command_data.virtual);
    @memcpy(&capacity_bytes, capacity_source[0..8]);
    const capacity = scsi.parseCapacity(capacity_bytes);
    block_size = capacity.block_size;
    block_count = @as(u64, capacity.last_lba) + 1;
    std.log.info("ready ({d} blocks x {d} bytes)", .{ block_count, block_size });

    // Self-check: read block 0 and log its trailing signature (0x55AA for a boot
    // sector) — proof READ(10) works end to end over the bulk path.
    const read0 = scsi.read10(0, 1);
    if (block_size <= 4096 and transact(&read0, true, command_data.physical, block_size)) {
        const sector: [*]const u8 = @ptrFromInt(command_data.virtual);
        std.log.info("block 0 signature 0x{x:0>2}{x:0>2}", .{ sector[510], sector[511] });
    }
    return true;
}

/// Serve the block protocol: geometry, and whole-block read/write to/from the
/// caller's DMA buffer (named by physical address).
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
    _ = sender;
    _ = capability;
    if (message.len < block_protocol.request_size) return 0;
    const request = std.mem.bytesToValue(block_protocol.Request, message[0..block_protocol.request_size]);
    switch (request.operation) {
        @intFromEnum(block_protocol.Operation.geometry) => {
            return writeReply(reply, .{ .status = 0, .block_size = block_size, .block_count = block_count });
        },
        @intFromEnum(block_protocol.Operation.read) => {
            const count: u16 = @intCast(request.count);
            const cdb = scsi.read10(@intCast(request.lba), count);
            const ok = transact(&cdb, true, request.physical, request.count * block_size);
            return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
        },
        @intFromEnum(block_protocol.Operation.write) => {
            const count: u16 = @intCast(request.count);
            const cdb = scsi.write10(@intCast(request.lba), count);
            const ok = transact(&cdb, false, request.physical, request.count * block_size);
            return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
        },
        @intFromEnum(block_protocol.Operation.flush) => {
            // SYNCHRONIZE CACHE: commit the device's write cache to flash. No data
            // stage. Makes prior writes durable before a caller (init at shutdown)
            // cuts power. A device without a volatile cache reports success anyway.
            const cdb = scsi.synchronizeCache10();
            const ok = transact(&cdb, false, 0, 0);
            return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = 0 });
        },
        else => return 0,
    }
}

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

pub fn main(init: runtime.process.Init) void {
    const argument = init.arguments.get(1) orelse {
        _ = runtime.system.write("/system/drivers/usb-storage: missing device id (argv[1])\n");
        return;
    };
    device_id = std.fmt.parseInt(u64, argument, 10) catch {
        std.log.info("malformed device id '{s}'", .{argument});
        return;
    };
    runtime.service.run(block_protocol.message_maximum, .{
        .service = .block,
        .init = initialise,
        .on_message = onMessage,
    });
    // A failure exit (nonzero -> .aborted) tells the device manager to restart
    // us with backoff; a clean return means there was nothing to serve.
    if (bring_up_failed) runtime.system.exit(1);
}
