//! The VFS wire protocol — the message format spoken between a client (via the file
//! API) and the user-space VFS server over IPC. A request is a fixed `Request` header
//! followed by an inline payload (a path, or write bytes); a reply is a fixed `Reply`
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
//!
//! This is a danos-native contract, so it uses danos names throughout. The client
//! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly.
//!
//! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes.
//! Shared by library/runtime/fs.zig (client) and system/services/vfs/vfs.zig (server).

pub const Operation = enum(u32) {
    open, // open(path) -> node id
    close, // close(node)
    read, // read(node, offset, len) -> bytes
    write, // write(node, offset, bytes) -> count
    status, // status(node) -> FileStatus
    // Appended for the mount router (M5). Values stay stable, so existing clients
    // and the flat-ramfs tests are unaffected.
    readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
    mount, // mount(prefix payload, capability = backend endpoint)
    unmount, // unmount(prefix payload)
    // Appended for filesystem mutation (Phase 2). Path-based (the path is the
    // payload); a mounted backend handles them, the flat ramfs refuses them.
    mkdir, // mkdir(path payload) -> status
    unlink, // unlink(path payload) -> status
    // rename: the payload is the old path, a single 0x00 separator, then the new
    // path. Same-directory rename only (the router requires both under one mount).
    rename, // rename(old\0new payload) -> status
};

/// The type of a filesystem node, aligned to the FSH file-type table
/// (docs/danos-file-system-hierarchy-FSH.md). Fills `FileStatus.kind` and
/// `DirectoryEntry.kind`; `regular = 0` keeps the historical hardcoded value.
pub const NodeKind = enum(u32) {
    regular = 0,
    directory = 1,
    character_device = 2,
    block_device = 3,
    symbolic_link = 4,
    fifo = 5,
    socket = 6,
};

/// One directory entry, returned by `readdir`: a fixed header followed inline in
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
pub const DirectoryEntry = extern struct {
    kind: u32, // a NodeKind
    name_len: u32,
    size: u64,
};

pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);

/// Request header. `node` is the server-side open-file id (from a prior open);
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
/// carry the read/write position and count.
pub const Request = extern struct {
    operation: Operation,
    node: u64,
    offset: u64,
    len: u32,
    flags: u32,
};

/// Reply header. `status` is 0 on success or a negative errno; `node` is the new
/// open-file id (for `open`); `len` is the payload length (bytes read, or the
/// FileStatus size).
pub const Reply = extern struct {
    status: i32,
    _padding: u32 = 0,
    node: u64 = 0,
    len: u32 = 0,
    _padding2: u32 = 0,
};

/// A file's metadata (the danos-native answer to a `status` request). The POSIX
/// layer maps this onto `struct stat`.
pub const FileStatus = extern struct {
    size: u64,
    kind: u32,
    _padding: u32 = 0,
    /// Modification time — Unix epoch seconds, UTC. 0 if the backend has none (the
    /// flat ramfs). Filled from the FAT directory entry's write date/time.
    mtime: u64 = 0,
};

pub const message_maximum: usize = 256;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
/// Largest inline payload that still fits one IPC message alongside a header.
pub const maximum_payload: usize = message_maximum - request_size;

/// Open flags (danos-native; `runtime.fs.OpenOptions` maps its booleans onto these).
pub const create: u32 = 1;
/// Open a directory (for readdir) rather than a file. A mounted backend uses
/// this to open a directory node; the flat ramfs ignores it.
pub const directory: u32 = 2;
/// Truncate the file to zero length on open (O_TRUNC): replace its contents rather
/// than overwriting in place, so a shorter new file leaves no stale tail. A mounted
/// backend frees the old cluster chain; the flat ramfs ignores it.
pub const truncate: u32 = 4;

test "protocol struct sizes and node kinds" {
    const std = @import("std");
    try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
    try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
    try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
    // The appended operations keep the original values.
    try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
    try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
    try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
}
