//! Block-device client: the helper a filesystem uses to read and write a block
//! device (a USB stick, via usb-storage) without hand-rolling the block-protocol
//! IPC. Layered over `ipc` and the shared `block-protocol` wire format, like
//! `runtime.usb` over the transfer protocol.
//!
//! Transfers name a caller-owned DMA buffer by physical address (from
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
//! limit — the same handoff usb-storage uses toward the controller.

const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc");
const time = @import("time");
const block_protocol = @import("block-protocol");

const Protocol = block_protocol.Protocol;

pub const Geometry = struct { block_size: u32, block_count: u64 };

pub const Device = struct {
    endpoint: ipc.Handle,

    /// The device's block size and total block count.
    pub fn geometry(self: Device) ?Geometry {
        var reply: [block_protocol.message_maximum]u8 = undefined;
        const answered = self.call(.geometry, {}, null, &reply) orelse return null;
        const result = Protocol.decodeReply(.geometry, answered) orelse return null;
        return .{ .block_size = result.block_size, .block_count = result.block_count };
    }

    /// Hand the block server a DMA-region capability (`handle` — from a `shareable`
    /// dma_alloc) so it forwards it to the controller and the buffer's physical
    /// addresses become reachable by the device. Call once per buffer before naming it
    /// in `read`/`write`. Harmless success when no IOMMU is enforcing.
    pub fn attach(self: Device, handle: ipc.Handle) bool {
        var reply: [block_protocol.message_maximum]u8 = undefined;
        return self.call(.attach, {}, handle, &reply) != null;
    }

    /// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
    pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
        var reply: [block_protocol.message_maximum]u8 = undefined;
        return self.call(.read, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
    }

    /// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
    pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
        var reply: [block_protocol.message_maximum]u8 = undefined;
        return self.call(.write, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
    }

    /// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so
    /// prior writes survive a power-off. A filesystem calls this before the machine
    /// goes down; no data transfer, so the buffer arguments are unused.
    pub fn flush(self: Device) bool {
        var reply: [block_protocol.message_maximum]u8 = undefined;
        return self.call(.flush, {}, null, &reply) != null;
    }

    /// One request at the driver. `target` is always 0: one endpoint per device, so
    /// there is no object within the peer to address.
    fn call(
        self: Device,
        comptime operation: Protocol.Operation,
        request: Protocol.RequestOf(operation),
        capability: ?ipc.Handle,
        reply: []u8,
    ) ?[]u8 {
        var packet: [block_protocol.message_maximum]u8 = undefined;
        const framed = Protocol.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null;
        const answer = ipc.callCap(self.endpoint, framed, reply, capability) catch return null;
        const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
        if (status.status != 0) return null;
        return reply[0..answer.len];
    }
};

/// One open attempt, no waiting — for a server that retries on its own
/// timer (the fat service) instead of blocking its harness in here.
pub fn tryOpen() ?Device {
    if (channel.openEndpoint("block")) |handle| return .{ .endpoint = handle };
    return null;
}

/// Open `/protocol/block`, retrying generously while the USB storage chain
/// (controller reset, enumeration, mass-storage bring-up) comes up.
pub fn open() ?Device {
    // Patient: the whole USB storage chain (firmware discovery, xHCI reset and
    // enumeration, mass-storage bring-up) must complete first, which can take
    // tens of seconds under emulation.
    var attempts: usize = 0;
    // 30 s covers the slowest observed healthy chain (a flaky QEMU enumeration
    // completed at ~24 s); a machine whose stick genuinely failed setup should
    // not sit a further minute pretending otherwise.
    while (attempts < 600) : (attempts += 1) {
        if (channel.openEndpoint("block")) |handle| return .{ .endpoint = handle };
        time.sleepMillis(50);
    }
    return null;
}
