//! The device-manager protocol (docs/device-driver-development/device-manager.md):
//! what drivers and applications say to the device manager over
//! `/protocol/device-manager`. Defined through the envelope
//! (docs/os-development/protocol-namespace.md), so every packet — request, reply,
//! and pushed event alike — begins with the folded `Header`.
//!
//! **`Header.target` is the device id.** It was the `device_id` field of three
//! different messages; folding it into the header is what made the packed
//! leading operation byte disappear along with it. `no_device` addresses a
//! driver that serves no enumerated device.
//!
//! Two of the manager's four old operations were the reserved verbs under
//! another name and are gone from this protocol's own numbering: `enumerate`
//! (the tree, one `ChildEntry` per record in the reply tail) and `subscribe`
//! (the watcher's endpoint rides as the call's capability). What is left is the
//! driver-facing half — the handshake and the two tree reports.
//!
//! **`ChildAdded` travels in both directions, and says so twice.** A bus driver
//! *calls* `child_added` to report a device; the manager then *pushes* the same
//! struct to every subscriber as the `child_added` event. Operations and events
//! are numbered in separate spaces, so one struct under two numbers is exactly
//! how the envelope spells "one encoding, both directions" — and the direction
//! (call vs. send) already tells them apart.
//!
//! Deliberately contains nothing lifecycle-shaped: stopping, liveness (the
//! zero-length ping), and exit reasons are the universal vocabulary of
//! docs/process-lifecycle.md, not this protocol.

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

/// The protocol version a driver states in its hello, and the version this
/// contract answers `describe` with. A manager that cannot serve a driver's
/// version refuses the hello, and the mismatch is loud at startup instead of
/// quiet corruption later.
///
/// `describe` publishes the same number, but it cannot replace this: it tells a
/// *client* what the provider is, and here it is the **provider** that has to
/// learn what the client was built against in order to refuse it.
pub const version = 1;

/// `Header.target` for a driver that serves no enumerated device (a test
/// fixture, a synthetic source), and `ChildAdded`'s answer for a leaf that was
/// never `device_register`ed.
pub const no_device: u64 = ~@as(u64, 0);

/// Which bus a `child_added` came from — stated by the reporting bus driver so
/// the manager's /system/configuration/devices.csv matcher knows how to read the report's identity
/// (a PCI class triple vs a USB class triple are the same 24 bits but different
/// namespaces) and which `bus` column a rule must name to bind it. `unknown` is
/// the zero default, so an un-upgraded reporter fails to match rather than
/// binding to the wrong bus's rule.
pub const BusKind = enum(u8) {
    unknown = 0,
    pci = 1,
    usb = 2,
    acpi = 3,
};

/// What kind of driver is talking (docs/device-driver-development/driver-model.md's shapes).
pub const Role = enum(u8) {
    /// Owns a controller and reports the devices behind it (`child_added`).
    bus = 1,
    /// Serves one device, reached through a bus's transfer protocol.
    device = 2,
};

// --- the per-operation request parts ----------------------------------------
//
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
// device id: those are the packet header's, folded in once. No reply part
// carries a status either — that is the `Status` every reply already begins
// with, so the manager's old three `{status, reserved}` reply structs are gone.

/// The handshake, sent once by every driver the manager spawns — the manager's
/// one self-enforced deadline: spawned and silent past it means wrong binary,
/// wrong version, or wedged before main, and the stop sequence follows. The
/// device this driver was assigned (its argv[1]) is `Header.target`.
pub const Hello = extern struct {
    /// A `Role` value.
    role: u8,
    _padding: u8 = 0,
    /// The protocol version this driver was built against (`version`).
    version: u16 = version,
};

/// A bus driver reporting one device it discovered behind its controller
/// (docs/device-driver-development/device-manager.md "the tree"), and the payload
/// the manager pushes to its subscribers for the same event. Identity is the
/// bus's native language — for USB a port-speed class, for PCI the class triple.
/// The manager mirrors the child into its tree; when the reporting driver dies,
/// the manager prunes everything it reported (the children describe protocol
/// state that died with it) and the restarted instance rediscovers and
/// re-reports.
///
/// `Header.target` is the kernel device id this child was `device_register`ed
/// as — what the manager hands a matched driver as its argv assignment — or
/// `no_device` for an unregistered leaf (a USB port before the descriptor
/// track). That is the field that used to sit at the end of this struct.
///
/// **The field order is the size budget.** An event packet is the header plus
/// this, within 64 bytes, and three `u64`s round the whole struct up to a
/// multiple of eight whatever order they sit in — so the small fields are
/// packed tail-first into the space the rounding pays for anyway. `Define`
/// checks the result; this comment is why there is no slack in it.
pub const ChildAdded = extern struct {
    /// The reporting driver's own device (the controller) — the child's parent.
    parent: u64,
    /// Where on the bus (for USB: the root port number, 1-based).
    bus_address: u64,
    /// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI:
    /// the class triple; for ACPI devices, 0 — identity is the hid below).
    identity: u64,
    /// The PCI subsystem id, packed `(subsystem_vendor << 16) | subsystem_device`
    /// (so it reads vendor-first, matching the CSV's `ssvid:ssid`), or 0 when the
    /// device has no subsystem id (a bridge, or a non-PCI bus).
    subsystem: u32 = 0,
    /// The vendor id (PCI vendor / USB idVendor), or 0 when the bus has no such
    /// concept (ACPI). Carried so the manager's /system/configuration/devices.csv matcher can bind
    /// on vendor — a level the bus-native `identity` (a class triple) cannot express.
    vendor: u16 = 0,
    /// The device id (PCI device / USB idProduct), or 0. The most specific numeric
    /// level: this is what lets one virtio-gpu (1AF4:1050) be told from any other
    /// virtio display function without the driver re-confirming after it is spawned.
    device: u16 = 0,
    /// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices
    /// discovered by firmware string rather than a numeric bus identity. Empty
    /// (all zero) otherwise. Widens for FDT `compatible` strings later.
    hid: [8]u8 = .{0} ** 8,
    /// A `BusKind` value: which bus reported this child, so the manager reads the
    /// identity in the right namespace and matches against the right `bus` column.
    bus: u8 = @intFromEnum(BusKind.unknown),
    _padding: [7]u8 = .{0} ** 7,
};

/// A bus driver reporting a device gone (hot-unplug), and the payload pushed to
/// subscribers for it.
///
/// **This is the one message whose target stays 0.** A removal is addressed by
/// the composite (parent, bus address) — the reporter knows where the device
/// *was*, not necessarily what id it had been registered under — and a single
/// `u64` cannot carry a pair. So the address stays in the payload, where it
/// always was, and the header addresses the provider itself.
pub const ChildRemoved = extern struct {
    parent: u64,
    bus_address: u64,
};

/// One record of the reserved `enumerate` reply: the manager's mirror, one
/// entry per known child, packed into the reply tail. The count is
/// `Status.len / @sizeOf(ChildEntry)` — the envelope's reply length says how
/// many arrived, so no count header is spent on saying it twice.
pub const ChildEntry = extern struct {
    parent: u64,
    bus_address: u64,
    identity: u64,
};

/// How many `ChildEntry` records one `enumerate` reply can carry. Paging joins
/// the protocol if a tree ever outgrows one packet.
pub const entries_per_reply: usize = (envelope.packet_maximum - envelope.prefix_size) / @sizeOf(ChildEntry);

pub const Protocol = envelope.Define(.{
    .name = "device-manager",
    .version = version,
    .operations = &.{
        // The driver-facing half. `enumerate` and `subscribe` are not here: they
        // are the reserved verbs, which mean the same thing at every provider.
        .{ .name = "hello", .request = Hello },
        .{ .name = "child_added", .request = ChildAdded },
        .{ .name = "child_removed", .request = ChildRemoved },
    },
    .events = &.{
        // The watcher-facing half — the same two structs, pushed rather than
        // called, in the events' own numbering space.
        .{ .name = "child_added", .payload = ChildAdded },
        .{ .name = "child_removed", .payload = ChildRemoved },
    },
});

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

/// What the manager sizes its buffers to — the call floor, as every protocol does.
pub const message_maximum: usize = Protocol.message_maximum;

test "a tree report fits the push floor with the header folded in" {
    // The dual-use struct is the tight one: `child_added` is both a call and an
    // event, and the event floor is 64 bytes *including* the header. Forty-one
    // bytes of content, rounded to 48 by the three u64s' alignment, plus the
    // 16-byte header — exactly on the floor, which is what folding the operation
    // byte and the device id out of the payload bought.
    try std.testing.expectEqual(@as(usize, 48), @sizeOf(ChildAdded));
    try std.testing.expectEqual(envelope.post_maximum, Protocol.event_maximum);
    try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
    // Ten records per enumerate reply — what the old count-header layout carried.
    try std.testing.expectEqual(@as(usize, 10), entries_per_reply);
}

test "the verb and event numbering, and the device id in the header" {
    try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.hello));
    try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.child_added));
    try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.child_removed));
    // Events number in their own space, so the same two reports start at 16 too.
    try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Event.child_added));
    try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Event.child_removed));
    // The manager's own enumerate/subscribe became the RESERVED verbs, below the
    // protocol range entirely.
    try std.testing.expectEqual(@as(u32, 1), envelope.operation_enumerate);
    try std.testing.expectEqual(@as(u32, 2), envelope.operation_subscribe);

    var buffer: [message_maximum]u8 = undefined;
    const hello = Protocol.encodeRequest(.hello, 7, .{ .role = @intFromEnum(Role.bus) }, &.{}, &buffer).?;
    try std.testing.expectEqual(@as(u64, 7), envelope.headerOf(hello).?.target);
    try std.testing.expectEqual(@as(u16, 1), Protocol.decodeRequest(.hello, hello).?.version);
}

test "one struct, two numbers: the report a bus calls and the event a watcher is pushed" {
    const report = ChildAdded{
        .parent = 3,
        .bus_address = 1,
        .identity = 0x030000,
        .bus = @intFromEnum(BusKind.pci),
        .vendor = 0x1AF4,
    };

    var call: [message_maximum]u8 = undefined;
    const called = Protocol.encodeRequest(.child_added, 42, report, &.{}, &call).?;
    try std.testing.expectEqual(Operation.child_added, Protocol.operationOf(called).?);
    try std.testing.expectEqual(@as(u64, 42), envelope.headerOf(called).?.target);

    var push: [envelope.post_maximum]u8 = undefined;
    const pushed = Protocol.encodeEvent(.child_added, 42, report, &push).?;
    try std.testing.expectEqual(envelope.post_maximum, pushed.len);
    try std.testing.expectEqual(Event.child_added, Protocol.eventOf(pushed).?);
    try std.testing.expectEqual(@as(u16, 0x1AF4), Protocol.decodeEvent(.child_added, pushed).?.vendor);
    // Same bytes after the prefix, different verb in it — the direction is what
    // tells a call from a push, and the numbering spaces never collide.
    try std.testing.expectEqualSlices(u8, called[envelope.prefix_size..], pushed[envelope.prefix_size..]);
}
