//! The backend-agnostic device model.
//!
//! Discovery backends (ACPI today, device-tree later) translate their native
//! hardware description into this one shape, so the rest of the kernel walks a
//! plain `Device` tree without knowing which firmware described the machine —
//! the same discipline `ps2-library.zig`'s `MemoryKind` applies to memory and `architecture`
//! applies to the CPU.
//!
//! This is deliberately minimal: enough to *describe* what was discovered (a
//! named node, its class, and its hardware resources) and where it sits in the
//! bus hierarchy. Driver matching, families, and probing are a later layer built
//! on top of this — nothing here presumes them.

const std = @import("std");
const device_abi = @import("device-abi");
const pci_class = @import("pci-class");
const acpi_ids = @import("acpi-ids");

/// The hardware primitives a discovery backend needs but can't express portably.
/// The kernel injects an implementation (the architecture VMM + port I/O), so the device
/// layer touches hardware without importing `architecture` — the same discipline that lets
/// it stay firmware-agnostic. `pioRead`/`pioWrite` take a width in bytes (1/2/4).
pub const Hal = struct {
    /// Map a physical MMIO range and return the virtual address to reach it at.
    /// The device layer dereferences the returned address and never learns how
    /// the kernel places it (identity, physmap, a window — the kernel's choice).
    mapMmio: *const fn (physical: u64, len: u64, writable: bool) u64,
    pioRead: *const fn (width: u8, port: u16) u32,
    pioWrite: *const fn (width: u8, port: u16, value: u32) void,
};

/// The kind of hardware resource a device occupies. Canonically defined by the
/// device ABI (system/devices/device-abi.zig) and re-exported here, so the kernel's
/// internal tree and the descriptors it hands user space share one enum.
pub const ResourceKind = device_abi.ResourceKind;

/// One hardware resource claimed by a device.
pub const Resource = struct {
    kind: ResourceKind,
    start: u64,
    len: u64,
};

/// A coarse classification of a device, independent of the describing firmware.
/// Canonically defined by the device ABI (system/devices/device-abi.zig) and
/// re-exported here — the enum a driver matches on and the one the kernel classifies
/// with are the same type. Kept small on purpose; refine as real drivers arrive.
pub const DeviceClass = device_abi.DeviceClass;

/// Firmware-independent identity. Each backend fills only the fields it knows;
/// the rest stay null. The generic layer never branches on *how* an id was
/// obtained, only on its value.
pub const Ids = struct {
    /// The device's ACPI hardware ID (`_HID`), EISA-encoded into 4 bytes, when applicable.
    acpi_hid: ?u32 = null,
    /// PCI configuration-space identity, when this node is a PCI function.
    pci_vendor: ?u16 = null,
    pci_device: ?u16 = null,
    /// PCI class/subclass/prog-if packed as 0xCCSSPP.
    pci_class: ?u24 = null,
    /// PCI bus/device/function packed as (bus << 8) | (device << 3) | function — the key
    /// the ACPI address (`_ADR`) merge uses to match a namespace device to this node.
    pci_bdf: ?u16 = null,
};

/// Upper bound on resources tracked per device (6 PCI BARs + a couple of IRQs is
/// the busy case). Stored inline so a device is a single allocation.
pub const maximum_resources = 8;

/// One node in the device tree. Nodes are individually heap-allocated and linked
/// intrusively (first-child / next-sibling), the classic device-tree layout —
/// no per-node dynamic arrays to manage.
pub const Device = struct {
    name_buffer: [24]u8 = undefined,
    name_len: u8 = 0,
    class: DeviceClass = .unknown,
    ids: Ids = .{},
    /// Human-readable hardware id (e.g. "PNP0A03"), when known. Backed inline like
    /// `name`; empty when unset. The generic layer stores/prints it without knowing
    /// how a backend encoded it.
    hid_buffer: [8]u8 = undefined,
    hid_len: u8 = 0,
    resources: [maximum_resources]Resource = undefined,
    resource_count: u8 = 0,

    parent: ?*Device = null,
    first_child: ?*Device = null,
    next_sibling: ?*Device = null,

    /// The device's short name (e.g. "cpu0", "pci0:00:1f.0"). Backed by an inline
    /// buffer, so it stays valid for the life of the node with no extra allocation.
    pub fn name(self: *const Device) []const u8 {
        return self.name_buffer[0..self.name_len];
    }

    fn setName(self: *Device, s: []const u8) void {
        const n: u8 = @intCast(@min(s.len, self.name_buffer.len));
        @memcpy(self.name_buffer[0..n], s[0..n]);
        self.name_len = n;
    }

    /// The device's hardware id string, or empty if none is set.
    pub fn hid(self: *const Device) []const u8 {
        return self.hid_buffer[0..self.hid_len];
    }

    pub fn setHid(self: *Device, s: []const u8) void {
        const n: u8 = @intCast(@min(s.len, self.hid_buffer.len));
        @memcpy(self.hid_buffer[0..n], s[0..n]);
        self.hid_len = n;
    }

    /// Record a resource. Silently drops beyond `maximum_resources` — discovery logs
    /// the truncation rather than failing the whole tree.
    pub fn addResource(self: *Device, kind: ResourceKind, start: u64, len: u64) bool {
        if (self.resource_count >= maximum_resources) return false;
        self.resources[self.resource_count] = .{ .kind = kind, .start = start, .len = len };
        self.resource_count += 1;
        return true;
    }

    /// The device's first resource of `kind`, or null — e.g. a timer's MMIO base.
    pub fn firstResource(self: *const Device, kind: ResourceKind) ?Resource {
        for (self.resources[0..self.resource_count]) |r| {
            if (r.kind == kind) return r;
        }
        return null;
    }
};

/// Owns the discovered device tree and the allocator its nodes came from.
pub const DeviceTree = struct {
    allocator: std.mem.Allocator,
    root: *Device,

    /// Create a tree with just the synthetic root node.
    pub fn init(allocator: std.mem.Allocator) !DeviceTree {
        const root = try allocator.create(Device);
        root.* = .{ .class = .root };
        root.setName("root");
        return .{ .allocator = allocator, .root = root };
    }

    /// The first device of `class` anywhere in the tree (depth-first), or null —
    /// how the kernel pulls e.g. the HPET or IOAPIC MMIO base out of discovery.
    pub fn firstOfClass(self: *const DeviceTree, class: DeviceClass) ?*Device {
        return firstOfClassIn(self.root, class);
    }

    /// Allocate a device and append it under `parent`, returning it so the caller
    /// can attach resources/ids. Appended at the tail so a dump reads in the order
    /// devices were discovered.
    pub fn addChild(
        self: *DeviceTree,
        parent: *Device,
        class: DeviceClass,
        device_name: []const u8,
    ) !*Device {
        const d = try self.allocator.create(Device);
        d.* = .{ .class = class, .parent = parent };
        d.setName(device_name);
        if (parent.first_child == null) {
            parent.first_child = d;
        } else {
            var current = parent.first_child.?;
            while (current.next_sibling) |sib| current = sib;
            current.next_sibling = d;
        }
        return d;
    }

    /// Walk the tree depth-first, emitting an indented, human-readable listing.
    /// `emit` is a raw byte sink (e.g. the serial `debugWrite`), so this stays
    /// independent of the kernel console.
    pub fn dump(self: *const DeviceTree, emit: *const fn ([]const u8) void) void {
        dumpNode(self.root, 0, emit);
    }
};

fn firstOfClassIn(node: *Device, class: DeviceClass) ?*Device {
    var child = node.first_child;
    while (child) |c| : (child = c.next_sibling) {
        if (c.class == class) return c;
        if (firstOfClassIn(c, class)) |found| return found;
    }
    return null;
}

fn dumpNode(device: *const Device, depth: usize, emit: *const fn ([]const u8) void) void {
    const indent = @min(depth * 2, 40);

    var buffer: [200]u8 = undefined;
    @memset(buffer[0..indent], ' ');
    const body = if (device.hid_len != 0) blk: {
        // Decode the _HID to a human name when it's a known standard PnP/ACPI id.
        const desc = acpi_ids.description(device.hid());
        break :blk if (desc.len != 0)
            std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s} ({s})\n", .{ device.name(), @tagName(device.class), device.hid(), desc }) catch return
        else
            std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s}\n", .{ device.name(), @tagName(device.class), device.hid() }) catch return;
    } else std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return;
    emit(buffer[0 .. indent + body.len]);

    // For a PCI function, decode its class code — the (class / subclass / prog-IF)
    // triple that says what it actually is, which the coarse `DeviceClass` can't.
    if (device.ids.pci_class) |packed_code| {
        const cc = pci_class.ClassCode.unpack(packed_code);
        var cbuf: [200]u8 = undefined;
        const pad = @min(indent + 2, 42);
        @memset(cbuf[0..pad], ' ');
        const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
        const cline = if (pif.len != 0)
            std.fmt.bufPrint(cbuf[pad..], "class 0x{x:0>2} ({s})  subclass 0x{x:0>2} ({s})  progif 0x{x:0>2} ({s})\n", .{ cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if, pif }) catch return
        else
            std.fmt.bufPrint(cbuf[pad..], "class 0x{x:0>2} ({s})  subclass 0x{x:0>2} ({s})  progif 0x{x:0>2}\n", .{ cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if }) catch return;
        emit(cbuf[0 .. pad + cline.len]);
    }

    for (device.resources[0..device.resource_count]) |r| {
        var rbuf: [200]u8 = undefined;
        const pad = @min(indent + 2, 42);
        @memset(rbuf[0..pad], ' ');
        const rline = std.fmt.bufPrint(
            rbuf[pad..],
            "- {s} 0x{x} len 0x{x}\n",
            .{ @tagName(r.kind), r.start, r.len },
        ) catch continue;
        emit(rbuf[0 .. pad + rline.len]);
    }

    var child = device.first_child;
    while (child) |c| : (child = c.next_sibling) dumpNode(c, depth + 1, emit);
}
