//! The envelope — the fixed prefix every danos packet begins with, and the
//! comptime `Define` that builds a protocol out of it. Layer L2 of
//! [communication.md](../../../docs/os-development/communication.md); the
//! authoritative description is
//! [protocol-namespace.md](../../../docs/os-development/protocol-namespace.md).
//!
//! This is the one module in the protocol domain that is not itself a protocol:
//! it is the shape every protocol is expressed in. A protocol module hands
//! `Define` its verbs and events and gets back numbered operations (never
//! colliding with the reserved range), typed encode/decode helpers, a
//! provider-side dispatch table that answers `describe` on its own — and, the
//! point of the exercise, compile-time proof that none of its packets can
//! exceed the transport floor. Errors that used to surface as runtime
//! truncation are compile errors, and "packets never fragment" is enforced at
//! the source rather than by review.
//!
//! **The prefix is folded, never stacked.** A `request`, `reply`, or `payload`
//! type names the bytes that follow the prefix — never the whole packet. The
//! verb and the object being addressed live in the prefix, so a protocol type
//! carries neither an `operation` field of its own nor a nested `Header`;
//! `rejectStacking` refuses one at compile time, and every size check adds
//! `prefix_size` exactly once.

const std = @import("std");

// --- the prefix -------------------------------------------------------------

/// Every packet a danos protocol transmits begins with this header — requests
/// on the synchronous call path, event packets on the asynchronous push path.
/// A reply spends the same 16 bytes on `Status` instead.
pub const Header = extern struct {
    /// The verb. Values below `first_protocol_operation` are the reserved
    /// universal verbs, which mean the same thing in every protocol.
    operation: u32,
    _padding: u32 = 0,
    /// **Object** addressing within the peer, never party addressing: which of
    /// the peer's objects this packet operates on — a volume, a layer, a node,
    /// a device. `0` addresses the provider itself, and a protocol with no
    /// objects never uses the field. *Which* party is at the other end was
    /// decided once, when the channel was opened, and *who sent this* is the
    /// kernel-stamped badge; neither is ever written here, which is what keeps
    /// the source unforgeable.
    target: u64 = 0,
};

/// Every reply begins with this. `len` counts the bytes that follow: the
/// reply's fixed part plus whatever variable tail the operation defines.
pub const Status = extern struct {
    status: i32, // 0, or a negative errno
    _padding: u32 = 0,
    len: u32 = 0,
    _padding2: u32 = 0,
};

/// The fixed prefix every packet spends — `Header` on a request or an event,
/// `Status` on a reply. One constant, because the two are deliberately the same
/// width: the packet budget does not depend on the direction.
pub const prefix_size: usize = @sizeOf(Header);

comptime {
    if (@sizeOf(Header) != 16 or @sizeOf(Status) != 16)
        @compileError("the envelope prefix is 16 bytes in both directions");
}

// --- the reserved verbs -----------------------------------------------------

/// Reserved verbs, answered by every provider. `Define` numbers a protocol's
/// own verbs from `first_protocol_operation`, so no protocol can reach in here.
pub const operation_describe: u32 = 0; // -> protocol name, version, target kinds
pub const operation_enumerate: u32 = 1; // -> the current targets, one per reply page
pub const operation_subscribe: u32 = 2; // capability = the subscriber's endpoint
pub const operation_unsubscribe: u32 = 3;
pub const first_protocol_operation: u32 = 16;

/// The optional body of a reserved `subscribe`: **which** of a provider's events
/// the subscriber wants, as a bit mask whose meaning the protocol defines (the
/// input service's device classes are the model). A reserved verb carries no
/// typed request, so this rides the packet's tail — and zero, which is also what
/// a subscribe that sent no body at all reads as, means *every* event.
///
/// The mask lives here rather than in each protocol because the subscriber
/// machinery is the service harness's (library/kernel/service.zig): the harness
/// records the number, the protocol decides what its bits mean, and neither has
/// to know the other.
pub const Subscription = extern struct { interest: u32 = 0 };

/// Frame a `subscribe` request. The subscriber's own endpoint travels as the
/// call's *capability*, never in the packet — that is what makes the reverse
/// path unforgeable.
pub fn encodeSubscribe(interest: u32, buffer: []u8) ?[]u8 {
    const header = Header{ .operation = operation_subscribe };
    const body = Subscription{ .interest = interest };
    return frame(std.mem.asBytes(&header), std.mem.asBytes(&body), &.{}, buffer);
}

/// Frame a bare `unsubscribe`: it names no event and no endpoint, because it
/// means "every subscription this task holds here" (one task, one voice).
pub fn encodeUnsubscribe(buffer: []u8) ?[]u8 {
    const header = Header{ .operation = operation_unsubscribe };
    return frame(std.mem.asBytes(&header), &.{}, &.{}, buffer);
}

/// The interest mask out of a `subscribe` packet's tail, on the provider's side.
/// A caller that sent no mask reads as the every-event mask.
pub fn decodeSubscribe(tail: []const u8) Subscription {
    if (tail.len < @sizeOf(Subscription)) return .{};
    return std.mem.bytesToValue(Subscription, tail[0..@sizeOf(Subscription)]);
}

/// The `describe` reply's fixed part, followed inline by `name_len` bytes of the
/// protocol's name. This is the version handshake: the version is asked for
/// once, at connect time, rather than re-carried by every packet out of a
/// 256-byte budget.
pub const Description = extern struct {
    version: u32,
    operation_count: u32,
    event_count: u32,
    name_len: u32,
};

/// Longest protocol name a `describe` reply can carry.
pub const name_maximum: usize = packet_maximum - prefix_size - @sizeOf(Description);

// --- the transport floor ----------------------------------------------------

/// The packet budget every protocol may assume on *any* transport. These are
/// the kernel-ipc transport's limits — `MESSAGE_MAXIMUM` and `POST_MAXIMUM` in
/// system/kernel/ipc-synchronous.zig — restated here because the kernel keeps
/// them private and a protocol has to compile against something. A fatter
/// transport raises its own ceiling; the floor does not move, so a protocol
/// that fits here fits everywhere (communication.md: ceilings are transport
/// properties, the floor is the protocol's contract).
pub const packet_maximum: usize = 256; // one request or one reply (ipc_call)
pub const post_maximum: usize = 64; // one event packet (ipc_send)

/// Whether a request or reply whose fixed part is `T` fits the call floor once
/// the prefix is counted. The folded rule in one line: `prefix_size` is added
/// exactly once, because `T` describes only what follows it. Exported so the
/// rule itself is testable — `Define` enforces it as a compile error.
pub fn fitsPacket(comptime T: type) bool {
    return prefix_size + @sizeOf(T) <= packet_maximum;
}

/// The same, against the much smaller push floor an event packet lives within.
pub fn fitsPost(comptime T: type) bool {
    return prefix_size + @sizeOf(T) <= post_maximum;
}

// --- reply statuses the envelope itself produces -----------------------------

/// Continued from the kernel's danos-native errno numbering
/// (system/kernel/ipc-synchronous.zig, which ends at `EPERM` = 9), so a client
/// reads one vocabulary whether the number came from the kernel or a provider.
/// Positive here, sent negated in `Status.status`, as the kernel spells it.
pub const ENOSYS: i32 = 10; // this protocol has no such operation
pub const EPROTO: i32 = 11; // malformed packet: shorter than the verb it names
pub const EBUSY: i32 = 12; // the thing asked for is held by someone still alive

/// Restated from the kernel's half of the numbering, because a provider refuses
/// too and userspace has no other place to read these from: `ENOENT` is "no such
/// name", `EPERM` "not permitted". The protocol registry answers an ungranted
/// bind with the second and a name a live provider already holds with `EBUSY`.
pub const ENOENT: i32 = 4;
pub const ENOSPC: i32 = 5;
pub const EPERM: i32 = 9;

// --- framing ----------------------------------------------------------------

/// The header of a received packet, or null when it is too short to have one.
pub fn headerOf(packet: []const u8) ?Header {
    if (packet.len < prefix_size) return null;
    return std.mem.bytesToValue(Header, packet[0..prefix_size]);
}

/// The status of a received reply, or null when it is too short to have one.
pub fn statusOf(packet: []const u8) ?Status {
    if (packet.len < prefix_size) return null;
    return std.mem.bytesToValue(Status, packet[0..prefix_size]);
}

/// Frame a bare `describe` request. Protocol-independent: the reserved verbs
/// are asked the same way of every provider.
pub fn encodeDescribe(buffer: []u8) ?[]u8 {
    const header = Header{ .operation = operation_describe };
    return frame(std.mem.asBytes(&header), &.{}, &.{}, buffer);
}

/// A decoded `describe` reply: the fixed part, plus the name that follows it.
pub const Described = struct {
    description: Description,
    name: []const u8,
};

/// Decode a `describe` reply packet. Null if it failed, was truncated, or is
/// not a description at all.
pub fn decodeDescribe(packet: []const u8) ?Described {
    const status = statusOf(packet) orelse return null;
    if (status.status != 0) return null;
    const body = packet[prefix_size..];
    if (body.len < @sizeOf(Description)) return null;
    const description = std.mem.bytesToValue(Description, body[0..@sizeOf(Description)]);
    const name = body[@sizeOf(Description)..];
    if (name.len < description.name_len) return null;
    return .{ .description = description, .name = name[0..description.name_len] };
}

/// The single framing point: prefix, then the fixed part, then the variable
/// tail, contiguous in one buffer. Null when the packet would not fit — a
/// packet is never split, so not fitting is a failure, not a continuation.
fn frame(prefix: []const u8, fixed: []const u8, tail: []const u8, buffer: []u8) ?[]u8 {
    const total = prefix.len + fixed.len + tail.len;
    if (total > buffer.len) return null;
    @memcpy(buffer[0..prefix.len], prefix);
    @memcpy(buffer[prefix.len..][0..fixed.len], fixed);
    @memcpy(buffer[prefix.len + fixed.len ..][0..tail.len], tail);
    return buffer[0..total];
}

/// The bytes of a fixed part — empty for `void`, which is how an operation says
/// "nothing but the verb".
fn bytesOf(comptime T: type, value: *const T) []const u8 {
    if (@sizeOf(T) == 0) return &.{};
    return @as([*]const u8, @ptrCast(value))[0..@sizeOf(T)];
}

/// Read a fixed part out of a packet body. A zero-sized part always succeeds
/// (there is nothing to be short of); anything else needs its full width.
fn valueOf(comptime T: type, body: []const u8) ?T {
    if (@sizeOf(T) == 0) return @as(T, undefined);
    if (body.len < @sizeOf(T)) return null;
    return std.mem.bytesToValue(T, body[0..@sizeOf(T)]);
}

// --- the specification ------------------------------------------------------

/// One verb of a protocol. `request` and `reply` describe the bytes *after* the
/// prefix; either may be `void`, meaning the verb (and its target) says it all.
pub const OperationSpecification = struct {
    name: []const u8,
    request: type = void,
    reply: type = void,
};

/// One event a provider pushes to its subscribers. `payload` is the bytes after
/// the `Header`, and the whole packet must fit the push floor.
pub const EventSpecification = struct {
    name: []const u8,
    payload: type = void,
};

/// What `Define` is given: the contract, whole.
pub const Specification = struct {
    /// The contract's name — the same word as its `/protocol/<name>` leaf and
    /// its `library/protocol/` module.
    name: []const u8,
    version: u32,
    operations: []const OperationSpecification = &.{},
    events: []const EventSpecification = &.{},
};

const reserved_names = [_][]const u8{ "describe", "enumerate", "subscribe", "unsubscribe" };

fn isReservedName(comptime name: []const u8) bool {
    for (reserved_names) |reserved| {
        if (std.mem.eql(u8, reserved, name)) return true;
    }
    return false;
}

/// Refuse a protocol type that carries the prefix inside itself. The header is
/// folded into every packet, so a type that also holds one would send it twice
/// and re-invent per-protocol addressing — the mistake the envelope exists to
/// prevent.
fn rejectStacking(comptime protocol: []const u8, comptime verb: []const u8, comptime T: type) void {
    switch (@typeInfo(T)) {
        .@"struct" => |info| for (info.fields) |field| {
            if (field.type == Header or field.type == Status) @compileError(std.fmt.comptimePrint(
                "protocol '{s}', verb '{s}': the envelope prefix is folded, not stacked — " ++
                    "drop the {s} field '{s}' and use the packet's own Header.operation / Header.target",
                .{ protocol, verb, @typeName(field.type), field.name },
            ));
        },
        else => {},
    }
}

fn nullDefault(comptime T: type) *const anyopaque {
    const empty: ?T = null;
    return @ptrCast(&empty);
}

// --- Define -----------------------------------------------------------------

/// Build a protocol from its specification. Everything below happens at compile
/// time; the generated type is what both sides of the conversation import.
///
/// ```zig
/// pub const Protocol = envelope.Define(.{
///     .name = "display",
///     .version = 1,
///     .operations = &.{
///         .{ .name = "configure_layer", .request = ConfigureLayer, .reply = void },
///         .{ .name = "blit", .request = Blit, .reply = void },
///     },
///     .events = &.{
///         .{ .name = "layer_lost", .payload = LayerLost },
///     },
/// });
/// ```
///
/// Refused at compile time, each with the protocol, the verb, and the numbers
/// named in the message:
///
/// - a request or reply that does not fit `packet_maximum` once `prefix_size`
///   is added (`.request = extern struct { bytes: [241]u8 }` — 241 + 16 = 257);
/// - an event payload that does not fit `post_maximum` the same way
///   (`.payload = extern struct { bytes: [49]u8 }` — 49 + 16 = 65);
/// - a type that stacks the prefix instead of folding it (a `Header` field);
/// - a verb named after a reserved one, or named twice.
///
/// A variable tail is bounded at *run* time instead, by `encodeRequest` and its
/// siblings, because only the caller knows how long it is.
pub fn Define(comptime specification: Specification) type {
    comptime {
        if (specification.name.len == 0) @compileError("a protocol needs a name");
        if (specification.name.len > name_maximum) @compileError(std.fmt.comptimePrint(
            "protocol '{s}': the name is {d} bytes, and a describe reply carries at most {d}",
            .{ specification.name, specification.name.len, name_maximum },
        ));

        for (specification.operations, 0..) |operation, index| {
            if (isReservedName(operation.name)) @compileError(std.fmt.comptimePrint(
                "protocol '{s}': '{s}' is a reserved universal verb — the envelope already answers it",
                .{ specification.name, operation.name },
            ));
            for (specification.operations[0..index]) |earlier| {
                if (std.mem.eql(u8, earlier.name, operation.name)) @compileError(std.fmt.comptimePrint(
                    "protocol '{s}': operation '{s}' is declared twice",
                    .{ specification.name, operation.name },
                ));
            }
            rejectStacking(specification.name, operation.name, operation.request);
            rejectStacking(specification.name, operation.name, operation.reply);
            if (!fitsPacket(operation.request)) @compileError(std.fmt.comptimePrint(
                "protocol '{s}', operation '{s}': the request is {d} bytes and the header {d}, " ++
                    "over the {d}-byte call floor — packets never fragment, so this has to shrink " ++
                    "or move its bulk to shared memory",
                .{ specification.name, operation.name, @sizeOf(operation.request), prefix_size, packet_maximum },
            ));
            if (!fitsPacket(operation.reply)) @compileError(std.fmt.comptimePrint(
                "protocol '{s}', operation '{s}': the reply is {d} bytes and the status header {d}, " ++
                    "over the {d}-byte call floor",
                .{ specification.name, operation.name, @sizeOf(operation.reply), prefix_size, packet_maximum },
            ));
        }

        for (specification.events, 0..) |event, index| {
            for (specification.events[0..index]) |earlier| {
                if (std.mem.eql(u8, earlier.name, event.name)) @compileError(std.fmt.comptimePrint(
                    "protocol '{s}': event '{s}' is declared twice",
                    .{ specification.name, event.name },
                ));
            }
            rejectStacking(specification.name, event.name, event.payload);
            if (!fitsPost(event.payload)) @compileError(std.fmt.comptimePrint(
                "protocol '{s}', event '{s}': the payload is {d} bytes and the header {d}, " ++
                    "over the {d}-byte push floor — an event carries the header too, so it is the " ++
                    "payload that has to give",
                .{ specification.name, event.name, @sizeOf(event.payload), prefix_size, post_maximum },
            ));
        }
    }

    return struct {
        pub const protocol_name: []const u8 = specification.name;
        pub const version: u32 = specification.version;

        /// What a provider sizes its receive and reply buffers to. A packet's
        /// fixed part may be far smaller, but any caller may send up to the
        /// floor and a short buffer truncates rather than refuses.
        pub const message_maximum: usize = packet_maximum;

        /// The widest packet this protocol's fixed parts can actually produce,
        /// prefix included — a diagnostic, and what a test pins.
        pub const request_maximum: usize = widest(specification.operations, .request);
        pub const reply_maximum: usize = widest(specification.operations, .reply);
        pub const event_maximum: usize = blk: {
            var widest_event: usize = prefix_size;
            for (specification.events) |event| widest_event = @max(widest_event, prefix_size + @sizeOf(event.payload));
            break :blk widest_event;
        };

        /// This protocol's verbs, numbered from `first_protocol_operation` in
        /// declaration order.
        pub const Operation = numbered(specification.operations, "name");

        /// This protocol's events, numbered from `first_protocol_operation` in
        /// their **own** space. Events travel only provider → subscriber over
        /// `ipc_send` and operations only client → provider over `ipc_call`, so
        /// the direction already tells the two apart; separate spaces mean
        /// appending an operation can never renumber a shipped event.
        pub const Event = numbered(specification.events, "name");

        /// The bytes after the `Header` on a request for `operation`.
        pub fn RequestOf(comptime operation: Operation) type {
            return specification.operations[indexOf(@intFromEnum(operation))].request;
        }

        /// The bytes after the `Status` on the reply to `operation`.
        pub fn ReplyOf(comptime operation: Operation) type {
            return specification.operations[indexOf(@intFromEnum(operation))].reply;
        }

        /// The bytes after the `Header` on an `event` packet.
        pub fn PayloadOf(comptime event: Event) type {
            return specification.events[indexOf(@intFromEnum(event))].payload;
        }

        // --- client side ----------------------------------------------------

        /// Frame `[Header][request][tail]`. `tail` is the variable part (a path,
        /// write bytes); pass `&.{}` when the verb has none. Null if the packet
        /// would exceed the buffer or the call floor.
        pub fn encodeRequest(
            comptime operation: Operation,
            target: u64,
            request: RequestOf(operation),
            tail: []const u8,
            buffer: []u8,
        ) ?[]u8 {
            const header = Header{ .operation = @intFromEnum(operation), .target = target };
            const packet = frame(std.mem.asBytes(&header), bytesOf(RequestOf(operation), &request), tail, buffer) orelse return null;
            return if (packet.len > packet_maximum) null else packet;
        }

        /// Frame `[Status][reply][tail]` — the provider's answer, for a provider
        /// that composes its own reply rather than using `Provider.dispatch`.
        pub fn encodeReply(
            comptime operation: Operation,
            status: i32,
            reply: ReplyOf(operation),
            tail: []const u8,
            buffer: []u8,
        ) ?[]u8 {
            const fixed = bytesOf(ReplyOf(operation), &reply);
            const head = Status{ .status = status, .len = @intCast(fixed.len + tail.len) };
            const packet = frame(std.mem.asBytes(&head), fixed, tail, buffer) orelse return null;
            return if (packet.len > packet_maximum) null else packet;
        }

        /// Frame `[Header][payload]` for an asynchronous push. Null if it would
        /// exceed the buffer or the push floor — an event that does not fit is
        /// dropped at the source, never split.
        pub fn encodeEvent(
            comptime event: Event,
            target: u64,
            payload: PayloadOf(event),
            buffer: []u8,
        ) ?[]u8 {
            const header = Header{ .operation = @intFromEnum(event), .target = target };
            const packet = frame(std.mem.asBytes(&header), bytesOf(PayloadOf(event), &payload), &.{}, buffer) orelse return null;
            return if (packet.len > post_maximum) null else packet;
        }

        /// Which of this protocol's verbs a packet names — null for a reserved
        /// verb, or for a number this protocol does not define.
        pub fn operationOf(packet: []const u8) ?Operation {
            const header = headerOf(packet) orelse return null;
            const index = header.operation -% first_protocol_operation;
            if (header.operation < first_protocol_operation or index >= specification.operations.len) return null;
            return @enumFromInt(header.operation);
        }

        /// Which of this protocol's events a pushed packet carries.
        pub fn eventOf(packet: []const u8) ?Event {
            const header = headerOf(packet) orelse return null;
            const index = header.operation -% first_protocol_operation;
            if (header.operation < first_protocol_operation or index >= specification.events.len) return null;
            return @enumFromInt(header.operation);
        }

        /// The fixed request part of a packet already known to name `operation`.
        pub fn decodeRequest(comptime operation: Operation, packet: []const u8) ?RequestOf(operation) {
            if (packet.len < prefix_size) return null;
            return valueOf(RequestOf(operation), packet[prefix_size..]);
        }

        /// The bytes after the fixed request part — empty when there are none.
        pub fn requestTail(comptime operation: Operation, packet: []const u8) []const u8 {
            const start = prefix_size + @sizeOf(RequestOf(operation));
            return if (packet.len <= start) &.{} else packet[start..];
        }

        /// The fixed reply part of a reply packet. Null on a short packet; the
        /// caller checks `statusOf(packet).status` for the provider's verdict.
        pub fn decodeReply(comptime operation: Operation, packet: []const u8) ?ReplyOf(operation) {
            if (packet.len < prefix_size) return null;
            return valueOf(ReplyOf(operation), packet[prefix_size..]);
        }

        /// The bytes after the fixed reply part, clipped to what `Status.len`
        /// says actually arrived.
        pub fn replyTail(comptime operation: Operation, packet: []const u8) []const u8 {
            const status = statusOf(packet) orelse return &.{};
            const start = prefix_size + @sizeOf(ReplyOf(operation));
            const end = @min(packet.len, prefix_size + @as(usize, status.len));
            return if (end <= start) &.{} else packet[start..end];
        }

        /// The payload of a pushed packet already known to carry `event`.
        pub fn decodeEvent(comptime event: Event, packet: []const u8) ?PayloadOf(event) {
            if (packet.len < prefix_size) return null;
            return valueOf(PayloadOf(event), packet[prefix_size..]);
        }

        // --- provider side --------------------------------------------------

        /// This protocol's dispatch table, bound to the provider's own state
        /// type. `describe` is answered here, from the specification; every verb
        /// this provider left null answers `-ENOSYS`, which is what makes the
        /// reserved verbs mean the same thing at every provider in the system.
        ///
        /// ```zig
        /// const Serve = Protocol.Provider(*Server);
        /// const handlers = Serve.Handlers{ .blit = onBlit, .configure_layer = onConfigureLayer };
        /// const reply_len = Serve.dispatch(server, handlers, message, sender, capability, reply);
        /// ```
        ///
        /// A handler returns the number of `answer.tail()` bytes it wrote, or a
        /// negative errno.
        pub fn Provider(comptime Context: type) type {
            return struct {
                /// A reserved verb a provider chooses to implement itself.
                /// `enumerate` writes its targets into the tail; `subscribe`
                /// takes the subscriber's endpoint from `invocation.capability`.
                pub const ReservedHandler = *const fn (Context, Invocation(void), Answer(void)) isize;

                /// One optional handler per verb, named exactly as the verb,
                /// plus the reserved verbs the envelope cannot answer alone.
                pub const Handlers = handlerTable(Context);

                /// Answer one received packet: writes `[Status][reply][tail]`
                /// into `reply` and returns its length. Zero means the reply
                /// buffer could not even hold a status, so nothing was written.
                pub fn dispatch(
                    context: Context,
                    handlers: Handlers,
                    packet: []const u8,
                    sender: u32,
                    capability: ?usize,
                    reply: []u8,
                ) usize {
                    if (reply.len < prefix_size) return 0;
                    const header = headerOf(packet) orelse return refuse(reply, -EPROTO);
                    const body = packet[prefix_size..];

                    if (header.operation == operation_describe) return describeInto(reply);

                    inline for (specification.operations, 0..) |operation, index| {
                        if (header.operation == first_protocol_operation + index) {
                            return invoke(
                                Context,
                                operation.request,
                                operation.reply,
                                @field(handlers, operation.name),
                                context,
                                header.target,
                                body,
                                sender,
                                capability,
                                reply,
                            );
                        }
                    }

                    const reserved: ?ReservedHandler = switch (header.operation) {
                        operation_enumerate => handlers.enumerate,
                        operation_subscribe => handlers.subscribe,
                        operation_unsubscribe => handlers.unsubscribe,
                        else => null,
                    };
                    return invoke(Context, void, void, reserved, context, header.target, body, sender, capability, reply);
                }
            };
        }

        // Shared by the protocol verbs and the reserved ones: decode, hand the
        // handler a typed invocation, stamp the status. One place, so a reserved
        // verb and a protocol verb behave identically.
        fn invoke(
            comptime Context: type,
            comptime RequestType: type,
            comptime ReplyType: type,
            handler: ?*const fn (Context, Invocation(RequestType), Answer(ReplyType)) isize,
            context: Context,
            target: u64,
            body: []const u8,
            sender: u32,
            capability: ?usize,
            reply: []u8,
        ) usize {
            const call = handler orelse return refuse(reply, -ENOSYS);
            const request = valueOf(RequestType, body) orelse return refuse(reply, -EPROTO);
            if (reply.len < prefix_size + @sizeOf(ReplyType)) return refuse(reply, -EPROTO);
            const produced = call(context, .{
                .target = target,
                .request = request,
                .tail = body[@min(@sizeOf(RequestType), body.len)..],
                .sender = sender,
                .capability = capability,
            }, .{ .buffer = reply[prefix_size..] });
            if (produced < 0) return refuse(reply, @intCast(produced));
            return succeed(reply, @sizeOf(ReplyType) + @as(usize, @intCast(produced)));
        }

        fn describeInto(reply: []u8) usize {
            const description = Description{
                .version = specification.version,
                .operation_count = specification.operations.len,
                .event_count = specification.events.len,
                .name_len = specification.name.len,
            };
            const total = @sizeOf(Description) + specification.name.len;
            if (reply.len < prefix_size + total) return refuse(reply, -EPROTO);
            @memcpy(reply[prefix_size..][0..@sizeOf(Description)], std.mem.asBytes(&description));
            @memcpy(reply[prefix_size + @sizeOf(Description) ..][0..specification.name.len], specification.name);
            return succeed(reply, total);
        }

        // "describe" is answered by the envelope, so it is the one reserved verb
        // with no slot in the table.
        const implementable_reserved = [_][]const u8{ "enumerate", "subscribe", "unsubscribe" };

        fn handlerTable(comptime Context: type) type {
            const count = specification.operations.len + implementable_reserved.len;
            var names: [count][]const u8 = undefined;
            var types: [count]type = undefined;
            var attributes: [count]std.builtin.Type.StructField.Attributes = undefined;
            for (specification.operations, 0..) |operation, index| {
                const Handler = *const fn (Context, Invocation(operation.request), Answer(operation.reply)) isize;
                names[index] = operation.name;
                types[index] = ?Handler;
                attributes[index] = .{ .default_value_ptr = nullDefault(Handler) };
            }
            const Reserved = *const fn (Context, Invocation(void), Answer(void)) isize;
            for (implementable_reserved, 0..) |name, offset| {
                const slot = specification.operations.len + offset;
                names[slot] = name;
                types[slot] = ?Reserved;
                attributes[slot] = .{ .default_value_ptr = nullDefault(Reserved) };
            }
            const frozen_names = names;
            const frozen_types = types;
            const frozen_attributes = attributes;
            return @Struct(.auto, null, &frozen_names, &frozen_types, &frozen_attributes);
        }
    };
}

// --- the provider's view of one packet --------------------------------------

/// What a provider's handler is given.
pub fn Invocation(comptime RequestType: type) type {
    return struct {
        /// Object addressing within this provider — the packet's `Header.target`.
        target: u64,
        /// The fixed request part, already decoded.
        request: RequestType,
        /// The bytes after it: a path, write data, a name.
        tail: []const u8,
        /// The kernel-stamped badge of the caller. The only source identity
        /// there is — no protocol defines a sender field — so per-client state
        /// is keyed on this.
        sender: u32,
        /// A capability the call carried: a subscriber's endpoint, a DMA
        /// region. Only the synchronous call path can move one.
        capability: ?usize,
    };
}

/// Where a provider's handler writes its answer. The `Status` in front of it is
/// the dispatcher's to stamp — a handler never writes its own.
pub fn Answer(comptime ReplyType: type) type {
    return struct {
        buffer: []u8,

        const fixed_size = @sizeOf(ReplyType);

        /// Write the fixed reply part. A `void` reply writes nothing.
        pub fn set(self: @This(), reply: ReplyType) void {
            if (fixed_size == 0) return;
            @memcpy(self.buffer[0..fixed_size], bytesOf(ReplyType, &reply));
        }

        /// Room for the variable tail; the handler returns how much of it it used.
        pub fn tail(self: @This()) []u8 {
            return self.buffer[fixed_size..];
        }
    };
}

fn refuse(reply: []u8, status: i32) usize {
    const head = Status{ .status = status, .len = 0 };
    @memcpy(reply[0..prefix_size], std.mem.asBytes(&head));
    return prefix_size;
}

fn succeed(reply: []u8, len: usize) usize {
    const head = Status{ .status = 0, .len = @intCast(len) };
    @memcpy(reply[0..prefix_size], std.mem.asBytes(&head));
    return prefix_size + len;
}

/// The widest `[prefix][fixed]` over a set of operations, on one side.
fn widest(comptime operations: []const OperationSpecification, comptime side: enum { request, reply }) usize {
    var found: usize = prefix_size;
    for (operations) |operation| {
        const size = switch (side) {
            .request => @sizeOf(operation.request),
            .reply => @sizeOf(operation.reply),
        };
        found = @max(found, prefix_size + size);
    }
    return found;
}

/// An enum over `specifications`, tagged by their `name` field, numbered from
/// `first_protocol_operation` in declaration order.
fn numbered(comptime specifications: anytype, comptime field: []const u8) type {
    var names: [specifications.len][]const u8 = undefined;
    var values: [specifications.len]u32 = undefined;
    for (specifications, 0..) |specification, index| {
        names[index] = @field(specification, field);
        values[index] = first_protocol_operation + index;
    }
    const frozen_names = names;
    const frozen_values = values;
    return @Enum(u32, .exhaustive, &frozen_names, &frozen_values);
}

/// A verb's position in its declaration list, from its wire number.
fn indexOf(comptime operation: u32) usize {
    return operation - first_protocol_operation;
}

// --- tests ------------------------------------------------------------------

const testing = std.testing;

const Produce = extern struct { count: u32, flags: u32 = 0 };
const Produced = extern struct { total: u64 };
const Changed = extern struct { kind: u32, value: u32 };

const Sample = Define(.{
    .name = "sample",
    .version = 3,
    .operations = &.{
        .{ .name = "produce", .request = Produce, .reply = Produced },
        .{ .name = "reset" },
    },
    .events = &.{
        .{ .name = "changed", .payload = Changed },
    },
});

test "the prefix is 16 bytes in both directions" {
    try testing.expectEqual(@as(usize, 16), @sizeOf(Header));
    try testing.expectEqual(@as(usize, 16), @sizeOf(Status));
    try testing.expectEqual(@as(usize, 16), prefix_size);
    try testing.expectEqual(@as(usize, 256), packet_maximum);
    try testing.expectEqual(@as(usize, 64), post_maximum);
}

test "verb numbering skips the reserved range" {
    try testing.expectEqual(@as(u32, 16), first_protocol_operation);
    try testing.expectEqual(@as(u32, 16), @intFromEnum(Sample.Operation.produce));
    try testing.expectEqual(@as(u32, 17), @intFromEnum(Sample.Operation.reset));
    // Events are numbered in their own space, so appending an operation can
    // never renumber a shipped event.
    try testing.expectEqual(@as(u32, 16), @intFromEnum(Sample.Event.changed));
    for ([_]u32{ operation_describe, operation_enumerate, operation_subscribe, operation_unsubscribe }) |reserved| {
        try testing.expect(reserved < first_protocol_operation);
    }
}

test "request round trip, header folded and tail carried" {
    var buffer: [packet_maximum]u8 = undefined;
    const packet = Sample.encodeRequest(.produce, 42, .{ .count = 7 }, "tail bytes", &buffer).?;
    try testing.expectEqual(prefix_size + @sizeOf(Produce) + "tail bytes".len, packet.len);

    const header = headerOf(packet).?;
    try testing.expectEqual(@as(u32, 16), header.operation);
    try testing.expectEqual(@as(u64, 42), header.target);
    try testing.expectEqual(Sample.Operation.produce, Sample.operationOf(packet).?);

    const request = Sample.decodeRequest(.produce, packet).?;
    try testing.expectEqual(@as(u32, 7), request.count);
    try testing.expectEqualStrings("tail bytes", Sample.requestTail(.produce, packet));
}

test "reply round trip" {
    var buffer: [packet_maximum]u8 = undefined;
    const packet = Sample.encodeReply(.produce, 0, .{ .total = 99 }, "more", &buffer).?;
    const status = statusOf(packet).?;
    try testing.expectEqual(@as(i32, 0), status.status);
    try testing.expectEqual(@as(u32, @sizeOf(Produced) + "more".len), status.len);
    try testing.expectEqual(@as(u64, 99), Sample.decodeReply(.produce, packet).?.total);
    try testing.expectEqualStrings("more", Sample.replyTail(.produce, packet));
}

test "a void request and reply carry nothing but the verb" {
    var buffer: [packet_maximum]u8 = undefined;
    const packet = Sample.encodeRequest(.reset, 0, {}, &.{}, &buffer).?;
    try testing.expectEqual(prefix_size, packet.len);
    try testing.expectEqual(Sample.Operation.reset, Sample.operationOf(packet).?);
    try testing.expectEqual(@as(usize, 0), Sample.requestTail(.reset, packet).len);
}

test "event round trip within the push floor" {
    var buffer: [post_maximum]u8 = undefined;
    const packet = Sample.encodeEvent(.changed, 0, .{ .kind = 1, .value = 2 }, &buffer).?;
    try testing.expectEqual(prefix_size + @sizeOf(Changed), packet.len);
    try testing.expect(packet.len <= post_maximum);
    try testing.expectEqual(Sample.Event.changed, Sample.eventOf(packet).?);
    try testing.expectEqual(@as(u32, 2), Sample.decodeEvent(.changed, packet).?.value);
}

// A provider over a trivial context, to drive the generated dispatch table.
const Counter = struct {
    total: u64 = 0,

    fn onProduce(self: *Counter, invocation: Invocation(Produce), answer: Answer(Produced)) isize {
        self.total += invocation.request.count;
        answer.set(.{ .total = self.total });
        const note = "counted";
        @memcpy(answer.tail()[0..note.len], note);
        return note.len;
    }
};

const CounterProvider = Sample.Provider(*Counter);

test "dispatch reaches a handler and stamps the status" {
    var counter = Counter{};
    const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };

    var request: [packet_maximum]u8 = undefined;
    const packet = Sample.encodeRequest(.produce, 0, .{ .count = 5 }, &.{}, &request).?;
    var reply: [packet_maximum]u8 = undefined;
    const len = CounterProvider.dispatch(&counter, handlers, packet, 3, null, &reply);

    const answered = reply[0..len];
    try testing.expectEqual(@as(i32, 0), statusOf(answered).?.status);
    try testing.expectEqual(@as(u64, 5), Sample.decodeReply(.produce, answered).?.total);
    try testing.expectEqualStrings("counted", Sample.replyTail(.produce, answered));
}

test "describe is answered by the envelope, not the provider" {
    var counter = Counter{};
    const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };

    var request: [packet_maximum]u8 = undefined;
    const packet = encodeDescribe(&request).?;
    var reply: [packet_maximum]u8 = undefined;
    const len = CounterProvider.dispatch(&counter, handlers, packet, 3, null, &reply);

    const described = decodeDescribe(reply[0..len]).?;
    try testing.expectEqualStrings("sample", described.name);
    try testing.expectEqual(@as(u32, 3), described.description.version);
    try testing.expectEqual(@as(u32, 2), described.description.operation_count);
    try testing.expectEqual(@as(u32, 1), described.description.event_count);
}

test "an unimplemented or unknown verb answers -ENOSYS" {
    var counter = Counter{};
    const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };
    var reply: [packet_maximum]u8 = undefined;

    // A verb this protocol declares but this provider left null.
    var request: [packet_maximum]u8 = undefined;
    const declared = Sample.encodeRequest(.reset, 0, {}, &.{}, &request).?;
    var len = CounterProvider.dispatch(&counter, handlers, declared, 3, null, &reply);
    try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status);

    // A number no verb of this protocol wears.
    const stranger = Header{ .operation = first_protocol_operation + 900 };
    len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&stranger), 3, null, &reply);
    try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status);

    // A reserved verb the provider does not implement answers the same way.
    const enumerate = Header{ .operation = operation_enumerate };
    len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&enumerate), 3, null, &reply);
    try testing.expectEqual(@as(i32, -ENOSYS), statusOf(reply[0..len]).?.status);
}

test "a truncated packet answers -EPROTO" {
    var counter = Counter{};
    const handlers = CounterProvider.Handlers{ .produce = Counter.onProduce };
    var reply: [packet_maximum]u8 = undefined;

    // Names `produce`, but stops before the request it promises.
    const header = Header{ .operation = @intFromEnum(Sample.Operation.produce) };
    const len = CounterProvider.dispatch(&counter, handlers, std.mem.asBytes(&header), 3, null, &reply);
    try testing.expectEqual(@as(i32, -EPROTO), statusOf(reply[0..len]).?.status);
}

// The size rule, exercised directly. `Define` turns exactly these predicates
// into compile errors, which a test cannot catch — so the predicate is what the
// test pins, and the boundary protocol below proves the compile-time half from
// the other side. The negative example, spelled out: giving `Define` an
// operation with `.request = extern struct { bytes: [241]u8 }`, or an event with
// `.payload = extern struct { bytes: [49]u8 }`, fails to compile with the
// protocol, the verb, and the two numbers named in the message.
test "a subscribe carries its interest mask in the reserved verb's tail" {
    var buffer: [packet_maximum]u8 = undefined;
    const packet = encodeSubscribe(0b101, &buffer).?;
    try testing.expectEqual(operation_subscribe, headerOf(packet).?.operation);
    try testing.expectEqual(@as(u32, 0b101), decodeSubscribe(packet[prefix_size..]).interest);
    // No body at all — and a body too short to be one — read as "every event",
    // which is what a subscriber that named nothing wants.
    try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).interest);
    try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{ 1, 2 }).interest);

    const bare = encodeUnsubscribe(&buffer).?;
    try testing.expectEqual(operation_unsubscribe, headerOf(bare).?.operation);
    try testing.expectEqual(prefix_size, bare.len);
}

test "the floor counts the header once, and the boundary is exact" {
    try testing.expect(fitsPacket(extern struct { bytes: [240]u8 }));
    try testing.expect(!fitsPacket(extern struct { bytes: [241]u8 }));
    try testing.expect(fitsPost(extern struct { bytes: [48]u8 }));
    try testing.expect(!fitsPost(extern struct { bytes: [49]u8 }));
    try testing.expect(fitsPacket(void));
    try testing.expect(fitsPost(void));
}

const WidestRequest = extern struct { bytes: [packet_maximum - prefix_size]u8 };
const WidestEvent = extern struct { bytes: [post_maximum - prefix_size]u8 };

// A protocol sitting exactly on both floors. That this compiles at all is the
// positive half of the compile-time check.
const Boundary = Define(.{
    .name = "boundary",
    .version = 1,
    .operations = &.{.{ .name = "fill", .request = WidestRequest, .reply = WidestRequest }},
    .events = &.{.{ .name = "filled", .payload = WidestEvent }},
});

test "a protocol may sit exactly on the floor" {
    try testing.expectEqual(packet_maximum, Boundary.request_maximum);
    try testing.expectEqual(packet_maximum, Boundary.reply_maximum);
    try testing.expectEqual(post_maximum, Boundary.event_maximum);

    var buffer: [packet_maximum]u8 = undefined;
    const packet = Boundary.encodeRequest(.fill, 0, .{ .bytes = @splat(0xAB) }, &.{}, &buffer).?;
    try testing.expectEqual(packet_maximum, packet.len);
    try testing.expectEqual(@as(u8, 0xAB), Boundary.decodeRequest(.fill, packet).?.bytes[239]);

    // One byte of tail past the floor is refused at run time, not truncated.
    try testing.expect(Boundary.encodeRequest(.fill, 0, .{ .bytes = @splat(0) }, "x", &buffer) == null);

    var post: [post_maximum]u8 = undefined;
    const event = Boundary.encodeEvent(.filled, 0, .{ .bytes = @splat(1) }, &post).?;
    try testing.expectEqual(post_maximum, event.len);
}

test "a protocol's own sizes are reported prefix-included" {
    try testing.expectEqual(prefix_size + @sizeOf(Produce), Sample.request_maximum);
    try testing.expectEqual(prefix_size + @sizeOf(Produced), Sample.reply_maximum);
    try testing.expectEqual(prefix_size + @sizeOf(Changed), Sample.event_maximum);
    try testing.expectEqual(packet_maximum, Sample.message_maximum);
    try testing.expectEqualStrings("sample", Sample.protocol_name);
}
