//! Device service: the kernel side of user-space driver access. At boot it
//! flattens the discovered device tree (src/device) into a stable, id-indexed
//! snapshot and a per-device claim table. User drivers enumerate the snapshot,
//! claim the device they own, and map its MMIO — the claim is the capability that
//! gates `mmio_map`/`irq_bind`, so a process can only ever touch hardware the
//! firmware-neutral device tree says it owns.
//!
//! The table is a **tree**: each entry carries its parent's id. Firmware discovery
//! seeds it, and a **bus driver** grows it — a process that has claimed a bus can
//! `register` children below it as it enumerates them (USB devices behind a hub, PCI
//! functions behind a bridge, comparators inside a timer block).
//!
//! Registration is where the capability model earns its keep. A `DeviceDescriptor` is, in
//! effect, a licence to map physical memory: whoever claims it may `mmio_map` its
//! `.memory` resources and `irq_bind` its `.irq` resources. If a bus driver could
//! invent arbitrary resources, it would invent one covering the kernel's RAM, claim
//! it, and map it. So `register` enforces **containment**: every resource of a child
//! must lie inside a resource of the same kind on its parent. A bus driver can only
//! ever subdivide what it was already given.

const std = @import("std");
const platform = @import("platform");
const device_abi = @import("device-abi");

const maximum_devices = 64;

/// Cap on children a single parent may have. A zero-resource child (legal — a USB
/// device is addressed through its controller, not by MMIO) sidesteps the containment
/// check, so without a bound a process that claimed one device could loop
/// `device_register` and exhaust the whole table, permanently denying it to every other
/// driver. This bounds the blast radius of one claim; a real quota (and a
/// `device_release` to reclaim on exit) is future work — see docs/driver-model.md.
const maximum_children_per_parent = 16;

var devices: [maximum_devices]device_abi.DeviceDescriptor = undefined;
var claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
var count: usize = 0;

/// The id of the seeded framebuffer node (`seedDisplay`), or null when the machine
/// handed over no framebuffer. Lets the process layer recognise the display claim
/// (to quiesce the bootstrap console) without threading the id through every caller.
var display_device: ?u64 = null;

/// Devices discovery found but the table had no room for. Non-zero means the machine
/// is bigger than `maximum_devices` and some hardware is simply invisible to drivers —
/// which would otherwise be an entirely silent failure. Logged at boot.
pub var dropped: usize = 0;

/// Snapshot the device tree into the flat table. Run once, right after discovery.
pub fn init(device_tree: *const platform.DeviceTree) void {
    count = 0;
    dropped = 0;
    display_device = null;
    for (&claimed) |*c| c.* = null;
    walk(device_tree.root, device_abi.no_parent);
}

/// Publish the loader's framebuffer as a `display` device — a root-level node with one
/// write-combining `memory` resource over the linear framebuffer and its geometry in
/// `.display`. The framebuffer is *not* firmware-discovered (it rides the
/// [[boot-handoff]], not the device tree), so it is seeded explicitly, after `init`.
/// Returns the new device id, or null when there is no framebuffer (headless) or the
/// table is full. Idempotent-ish: only ever call once per boot.
pub fn seedDisplay(base: u64, width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32) ?u64 {
    if (base == 0 or width == 0 or height == 0) return null; // headless
    if (count >= maximum_devices) {
        dropped += 1;
        return null;
    }
    var d = std.mem.zeroes(device_abi.DeviceDescriptor);
    d.id = count;
    d.parent = device_abi.no_parent;
    d.class = @intFromEnum(device_abi.DeviceClass.display);
    d.pci_class = device_abi.no_pci_class;
    d.resource_count = 1;
    d.resources[0] = .{
        .kind = @intFromEnum(device_abi.ResourceKind.memory),
        .start = base,
        .len = @as(u64, height) * pitch,
        .flags = device_abi.resource_flag_write_combining,
    };
    d.display = .{ .width = width, .height = height, .pitch = pitch, .format = format, .refresh_hz = refresh_hz };
    devices[count] = d;
    display_device = d.id;
    count += 1;
    return d.id;
}

/// The id of the seeded framebuffer device, or null when none was seeded.
pub fn displayDevice() ?u64 {
    return display_device;
}

/// Whether the framebuffer device is currently claimed by some process. The bootstrap
/// console uses this (via the process layer) to fall silent while a display service
/// owns the screen, and to resume if that service dies and its claim is released.
pub fn displayClaimed() bool {
    const id = display_device orelse return false;
    return ownerOf(id) != null;
}

/// Record `node` (unless it's the synthetic root) and recurse, threading the id we
/// assigned it down to its children as their parent.
fn walk(node: *platform.Device, parent_id: u64) void {
    const id = if (node.class == .root) device_abi.no_parent else record(node, parent_id);
    var child = node.first_child;
    while (child) |c| : (child = c.next_sibling) walk(c, id);
}

fn record(node: *platform.Device, parent_id: u64) u64 {
    if (count >= maximum_devices) {
        dropped += 1;
        return device_abi.no_parent; // children of a dropped node become roots, not orphans
    }
    var d = std.mem.zeroes(device_abi.DeviceDescriptor);
    d.id = count;
    d.parent = parent_id;
    d.class = @intFromEnum(node.class);
    d.pci_class = if (node.ids.pci_class) |code| code else device_abi.no_pci_class;
    const h = node.hid();
    d.hid_len = @min(h.len, d.hid.len);
    @memcpy(d.hid[0..d.hid_len], h[0..d.hid_len]);
    const rc = @min(node.resource_count, device_abi.maximum_device_resources);
    d.resource_count = rc;
    for (0..rc) |i| {
        const r = node.resources[i];
        d.resources[i] = .{ .kind = @intFromEnum(r.kind), .start = r.start, .len = r.len };
    }
    devices[count] = d;
    count += 1;
    return d.id;
}

/// Copy up to `out.len` device descriptors into `out`; returns the total count
/// available (which may exceed `out.len`).
pub fn enumerate(out: []device_abi.DeviceDescriptor) usize {
    const n = @min(count, out.len);
    @memcpy(out[0..n], devices[0..n]);
    return count;
}

/// Take exclusive ownership of device `id` for task `owner`. Fails if the id is
/// out of range or already claimed.
pub fn claim(id: u64, owner: u32) bool {
    if (id >= count) return false;
    if (claimed[@intCast(id)] != null) return false;
    claimed[@intCast(id)] = owner;
    return true;
}

/// The task that owns device `id`, or null.
pub fn ownerOf(id: u64) ?u32 {
    if (id >= count) return null;
    return claimed[@intCast(id)];
}

/// Release every claim held by `owner` — called by the process layer on every
/// path out of a process (exit, fault, kill), so a restarted driver can claim its
/// hardware again (docs/process-lifecycle.md iron rule 1: cleanup is the kernel's
/// job). The devices stay in the table — they describe hardware, which did not go
/// away — only their ownership clears.
pub fn releaseAllOwnedBy(owner: u32) void {
    for (claimed[0..count]) |*slot| {
        if (slot.*) |o| {
            if (o == owner) slot.* = null;
        }
    }
}

/// Resource `index` of device `id`, or null if out of range.
pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
    if (id >= count) return null;
    const d = &devices[@intCast(id)];
    if (index >= d.resource_count) return null;
    return d.resources[@intCast(index)];
}

/// Is `child` wholly inside `parent`? For a range (memory, io_port, bus_range) that's
/// interval containment; for an irq it's equality, since an interrupt line is not
/// divisible. Zero-length child ranges are refused — an empty window is meaningless
/// and would otherwise vacuously "fit" anywhere.
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
    if (parent.kind != child.kind) return false;
    if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) {
        // Range containment: an interrupt line is still indivisible (a child owns
        // exactly one GSI), but a parent may own a *range* of lines so a broad
        // owner — the acpi-tables node, whose firmware names any legacy IRQ —
        // can contain its children's specific lines. A length-1 parent range is
        // exactly the old equality rule, so existing single-IRQ parents are
        // unaffected.
        const span = if (parent.len == 0) 1 else parent.len;
        return child.start >= parent.start and child.start < parent.start + span;
    }
    if (child.len == 0 or parent.len == 0) return false;
    // No overflow: a resource that wraps the address space is not containable.
    const child_end = std.math.add(u64, child.start, child.len) catch return false;
    const parent_end = std.math.add(u64, parent.start, parent.len) catch return false;
    return child.start >= parent.start and child_end <= parent_end;
}

pub const RegisterError = error{
    NoSpace, // the device table is full
    BadParent, // no such device, or not claimed by this task
    TooManyResources,
    TooManyChildren, // this parent is at maximum_children_per_parent
    NotContained, // a child resource escapes its parent's window
};

/// Number of devices currently recorded with `parent_id` as their parent.
fn childCount(parent_id: u64) usize {
    var n: usize = 0;
    for (devices[0..count]) |d| {
        if (d.parent == parent_id) n += 1;
    }
    return n;
}

/// Publish `descriptor` as a child of `parent_id`, on behalf of `owner`. Returns the new
/// device id. The child is left **unclaimed**, so another process (a class driver)
/// can claim it — that is how a bus hands a device to its driver.
///
/// `owner` must have claimed `parent_id`, and every resource in `descriptor` must be
/// contained in a parent resource of the same kind. A device with no resources is
/// fine and common: a USB device is addressed through its controller, not by MMIO.
pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.DeviceDescriptor) RegisterError!u64 {
    const parent_owner = ownerOf(parent_id) orelse return error.BadParent;
    if (parent_owner != owner) return error.BadParent;
    if (descriptor.resource_count > device_abi.maximum_device_resources) return error.TooManyResources;
    if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;
    if (count >= maximum_devices) return error.NoSpace;

    const parent = &devices[@intCast(parent_id)];
    for (0..@intCast(descriptor.resource_count)) |i| {
        const r = descriptor.resources[i];
        var ok = false;
        for (0..@intCast(parent.resource_count)) |j| {
            if (contains(parent.resources[j], r)) ok = true;
        }
        if (!ok) return error.NotContained;
    }

    // Idempotent on exact match (docs/device-manager.md): a restarted
    // registering bus re-registers what it rediscovers, and the table has no
    // unregister — an identical (class, identity, resources) child under the
    // same parent returns the existing id instead of appending a duplicate.
    for (devices[0..count]) |*existing| {
        if (existing.parent != parent_id) continue;
        if (existing.class != descriptor.class) continue;
        if (existing.pci_class != descriptor.pci_class) continue;
        if (existing.hid_len != descriptor.hid_len) continue;
        if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
        if (existing.resource_count != descriptor.resource_count) continue;
        var same = true;
        for (0..@intCast(descriptor.resource_count)) |i| {
            const a = existing.resources[i];
            const b = descriptor.resources[i];
            if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
        }
        if (same) return existing.id;
    }

    var d = std.mem.zeroes(device_abi.DeviceDescriptor);
    d.id = count;
    d.parent = parent_id;
    d.class = descriptor.class;
    d.pci_class = descriptor.pci_class;
    d.hid_len = @min(descriptor.hid_len, d.hid.len);
    @memcpy(d.hid[0..@intCast(d.hid_len)], descriptor.hid[0..@intCast(d.hid_len)]);
    d.resource_count = descriptor.resource_count;
    for (0..@intCast(descriptor.resource_count)) |i| d.resources[i] = descriptor.resources[i];

    devices[count] = d;
    count += 1;
    return d.id;
}
