//! /system/services/acpi — the ACPI discovery service: the x86 firmware
//! interpreter, moved out of ring 0 (docs/discovery.md). Claims the
//! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port
//! grant, a broad irq window, the SCI), and runs the **shared AML module** in
//! ring 3 — the same parser and interpreter the kernel uses.
//!
//! It also owns the **event side** (M21): it registers the domain-named `.power`
//! service, binds the SCI (System Control Interrupt), and on a power-button
//! fixed event publishes `power_button` to subscribers — and on init's request
//! writes S5 to power the machine off. The device discovery (M20) and the event
//! handling both run in one `runtime.service.run` loop.

const std = @import("std");
const runtime = @import("runtime");
const aml = @import("aml");
const acpi_ids = @import("acpi-ids");
const device = runtime.device;
const protocol = runtime.device_manager_protocol;
const power = runtime.power_protocol;
/// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, …) — so the `_HID`
/// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md).
const opcodes = aml.opcodes;

// The claimed acpi-tables node and the resource index of its broad io_port
// window — the Hal routes every port access through this one claim.
var node_id: u64 = 0;
var io_resource_index: u64 = 0;
// The SCI's irq resource index on the node (the len-1 irq, distinct from the
// broad [0,256) window), for irqBind / irqAck.
var sci_resource_index: u64 = 0;
var has_sci = false;

// PM1 event/control and GPE register ports, read from the FADT copy the kernel
// publishes on the node (M21). Port 0 means absent.
var pm1a_evt: u16 = 0;
var pm1b_evt: u16 = 0;
var pm1_evt_len: u8 = 0;
var pm1a_cnt: u16 = 0;
var pm1b_cnt: u16 = 0;
var gpe0_blk: u16 = 0;
var gpe0_len: u8 = 0;
var gpe1_blk: u16 = 0;
var gpe1_len: u8 = 0;
var smi_cmd: u16 = 0;
var acpi_enable_value: u8 = 0;
var s5_slp_typ_a: u8 = 0;
var s5_slp_typ_b: u8 = 0;
var s5_valid = false;

// PM1 event-register bits (ACPI): PWRBTN in the status/enable word is bit 8;
// the control word's SCI_EN is bit 0; SLP_EN is bit 13.
const pwrbtn_bit: u16 = 1 << 8;
const sci_en_bit: u32 = 1 << 0;
const slp_en: u32 = 1 << 13;

// The `.power` subscribers: endpoints handed over as capabilities, each
// receiving events as buffered messages. Dropped on a failed send. The
// subscriber's task id is kept too — a shutdown request is honored only from a
// subscriber (init subscribes; a stray process does not), the soft gate that
// stands in for "only the system supervisor may power off" without hardcoding
// a pid the kernel's idle tasks would have taken.
const maximum_subscribers = 8;
var subscribers: [maximum_subscribers]?runtime.ipc.Handle = .{null} ** maximum_subscribers;
var subscriber_tasks: [maximum_subscribers]u32 = .{0} ** maximum_subscribers;

// Pass-1 registration record (see main): what pass 2 reports.
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
var registered: [64]Registered = undefined;
var registered_count: usize = 0;

// A scratch page returned for SystemMemory OperationRegion maps: the service
// cannot map arbitrary physical memory from ring 3, so such regions are
// unsupported and degrade to harmless zeros rather than faulting. The M20.2
// targets (ps2, the legacy devices) use SystemIO and static templates.
var mmio_scratch: [4096]u8 align(4096) = .{0} ** 4096;

fn halMapMmio(physical: u64, len: u64, writable: bool) u64 {
    _ = physical;
    _ = len;
    _ = writable;
    return @intFromPtr(&mmio_scratch);
}

fn halPioRead(width: u8, port: u16) u32 {
    return device.ioRead(node_id, io_resource_index, port, width) orelse 0;
}

fn halPioWrite(width: u8, port: u16, value: u32) void {
    _ = device.ioWrite(node_id, io_resource_index, port, width, value);
}

fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor {
    const total = device.enumerate(buffer);
    for (buffer[0..@min(total, buffer.len)]) |d| {
        if (d.class == @intFromEnum(device.DeviceClass.acpi_tables)) return d;
    }
    return null;
}

pub fn main(init: runtime.process.Init) void {
    // When the acpi-parse scenario spawns this directly, argv[1] is a device-count
    // *floor* to self-verify against. The kernel no longer parses AML, so there is
    // no exact count to match — proving the ring-3 parse found at least a floor of
    // devices is the check. Deterministic, no log-scraping.
    const floor: ?usize = if (init.arguments.get(1)) |a| (std.fmt.parseInt(usize, a, 10) catch null) else null;

    const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
        _ = runtime.system.write("/system/services/acpi: out of memory\n");
        return;
    };
    const node = findTablesNode(buffer) orelse {
        _ = runtime.system.write("/system/services/acpi: no acpi-tables node to claim\n");
        return;
    };
    node_id = node.id;
    if (!device.claim(node_id)) {
        _ = runtime.system.write("/system/services/acpi: unable to claim acpi-tables\n");
        return;
    }

    // Map the node's resources: the AML blobs (bytecode), the FADT (intact
    // "FACP" header — decision 3), the io_port grant, and the SCI irq.
    var blocks: [8][]const u8 = undefined;
    var block_count: usize = 0;
    var found_io = false;
    var fadt: ?[]const u8 = null;
    for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| {
        if (resource.kind == @intFromEnum(device.ResourceKind.io_port) and !found_io) {
            io_resource_index = index;
            found_io = true;
            continue;
        }
        if (resource.kind == @intFromEnum(device.ResourceKind.irq) and resource.len == 1) {
            sci_resource_index = index;
            has_sci = true;
            continue;
        }
        if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue;
        const base = device.mmioMap(node_id, index) orelse continue;
        const pointer: [*]const u8 = @ptrFromInt(base);
        const bytes = pointer[0..@intCast(resource.len)];
        if (bytes.len >= 4 and std.mem.eql(u8, bytes[0..4], "FACP")) {
            fadt = bytes;
            continue;
        }
        if (block_count == blocks.len) continue;
        blocks[block_count] = bytes;
        block_count += 1;
    }
    if (block_count == 0) {
        _ = runtime.system.write("/system/services/acpi: no AML blobs on the node\n");
        return;
    }

    const result = aml.parse(runtime.allocator(), blocks[0..block_count]) catch {
        _ = runtime.system.write("/system/services/acpi: AML parse failed\n");
        return;
    };
    var namespace = result.namespace;
    const devices = aml.deviceCount(&namespace);
    std.log.info("parsed {d} AML blob(s), {d} namespace devices", .{ block_count, devices });
    if (floor) |minimum| {
        if (devices >= minimum) {
            _ = runtime.system.write("acpi-parse: ok\n");
        } else {
            std.log.info("acpi-parse: too few (ring-3 {d} < floor {d})", .{ devices, minimum });
        }
        // Self-verify mode is standalone (no manager); stop before reporting.
        while (true) runtime.system.sleep(1000);
    }

    // Register + report the present _HID devices (M20), then set up the power
    // event side (M21), then serve — all in one harness loop. The interpreter
    // and namespace outlive this frame (static), so the harness callbacks can
    // reach them.
    interpreter_arena = std.heap.ArenaAllocator.init(runtime.allocator());
    persistent_namespace = namespace;
    global_interpreter = aml.Interpreter.init(&persistent_namespace, .{
        .mapMmio = halMapMmio,
        .pioRead = halPioRead,
        .pioWrite = halPioWrite,
    }, interpreter_arena.allocator());

    readFadt(fadt);
    s5_valid = readSleepS5(&persistent_namespace);

    runtime.service.run(power.message_maximum, .{
        .service = .power,
        .init = onInit,
        .on_message = onMessage,
        .on_notification = onNotification,
    });
}

// Static so the harness callbacks (which run after main's stack frame is gone)
// can reach the namespace and interpreter.
var persistent_namespace: aml.Namespace = undefined;
var global_interpreter: aml.Interpreter = undefined;
var interpreter_arena: std.heap.ArenaAllocator = undefined;

/// Startup under the harness: register + report the discovered devices to the
/// manager (M20), then enable ACPI mode and arm the power button (M21).
fn onInit(endpoint: runtime.ipc.Handle) bool {
    registered_count = 0;
    walkDevices(persistent_namespace.root, &global_interpreter);

    const manager = runtime.ipc.lookup(.device_manager);
    var i: usize = 0;
    while (i < registered_count) : (i += 1) {
        const entry = registered[i];
        const hid = entry.hid[0..entry.hid_len];
        const desc = acpi_ids.description(hid);
        if (desc.len != 0)
            std.log.info("reported {s} (device {d}, {d} resources) — {s}", .{ hid, entry.device_id, entry.resource_count, desc })
        else
            std.log.info("reported {s} (device {d}, {d} resources)", .{ hid, entry.device_id, entry.resource_count });
        if (manager) |h| {
            var report = protocol.ChildAdded{ .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id };
            @memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
            var reply: [protocol.message_maximum]u8 = undefined;
            _ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {};
        }
    }
    std.log.info("reported {d} device(s) to the manager", .{registered_count});

    armPowerButton(endpoint);
    return true;
}

// --- power event side (M21) ---------------------------------------------------

/// Read the PM1 event/control and GPE register ports plus the SMI enable pair
/// from the FADT copy on the node. Offsets are from the FADT table start (the
/// SDT header is the first 36 bytes). Prefers the 32-bit port fields; QEMU's
/// FADT populates them.
fn readFadt(fadt: ?[]const u8) void {
    const f = fadt orelse {
        _ = runtime.system.write("acpi: no FADT on the node — power events off\n");
        return;
    };
    smi_cmd = @truncate(rd32(f, 48));
    acpi_enable_value = f[52];
    pm1a_evt = @truncate(rd32(f, 56));
    pm1b_evt = @truncate(rd32(f, 60));
    pm1a_cnt = @truncate(rd32(f, 64));
    pm1b_cnt = @truncate(rd32(f, 68));
    gpe0_blk = @truncate(rd32(f, 80));
    gpe1_blk = @truncate(rd32(f, 84));
    pm1_evt_len = if (f.len > 88) f[88] else 4;
    gpe0_len = if (f.len > 92) f[92] else 0;
    gpe1_len = if (f.len > 93) f[93] else 0;
}

fn readSleepS5(ns: *aml.Namespace) bool {
    const st = aml.sleepState(ns, 5) orelse return false;
    s5_slp_typ_a = st.slp_typ_a;
    s5_slp_typ_b = st.slp_typ_b;
    return true;
}

/// Enable ACPI mode if the firmware isn't already in it, then bind the SCI and
/// set PWRBTN_EN so the power button raises an interrupt we can see.
fn armPowerButton(endpoint: runtime.ipc.Handle) void {
    if (pm1a_cnt != 0 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0 and smi_cmd != 0) {
        // Switch to ACPI mode: write ACPI_ENABLE to the SMI command port, then
        // spin (bounded) until SCI_EN latches.
        halPioWrite(1, smi_cmd, acpi_enable_value);
        var tries: u32 = 0;
        while (tries < 1000 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0) : (tries += 1) {
            runtime.system.sleep(1);
        }
    }
    if (!has_sci) {
        _ = runtime.system.write("acpi: no SCI resource — power button unavailable\n");
        return;
    }
    if (!device.irqBind(node_id, sci_resource_index, endpoint)) {
        _ = runtime.system.write("acpi: SCI irq_bind failed\n");
        return;
    }
    // PWRBTN_EN lives in the PM1 enable register at evt_blk + evt_len/2.
    if (pm1a_evt != 0) {
        const en_port = pm1a_evt + pm1_evt_len / 2;
        halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
    }
    if (pm1b_evt != 0) {
        const en_port = pm1b_evt + pm1_evt_len / 2;
        halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
    }
    _ = runtime.system.write("acpi: power button armed\n");
}

/// The SCI fired. Read PM1 status; a set PWRBTN_STS is the power button — clear
/// it (write-1), publish, log. Any other set status is cleared and logged
/// (GPE/Notify dispatch is M21.2). Always re-arm the line.
fn onSci() void {
    var handled = false;
    inline for (.{ pm1a_evt, pm1b_evt }) |evt_port| {
        if (evt_port != 0) {
            const sts: u16 = @truncate(halPioRead(2, evt_port));
            if (sts & pwrbtn_bit != 0) {
                halPioWrite(2, evt_port, pwrbtn_bit); // write-1-to-clear
                handled = true;
            } else if (sts != 0) {
                halPioWrite(2, evt_port, sts); // clear whatever else latched
            }
        }
    }
    if (handled) {
        _ = runtime.system.write("power: button pressed\n");
        publishButton();
    }
    handleGpe();
    _ = device.irqAck(node_id, sci_resource_index);
}

/// General-purpose events: for each set+enabled GPE bit, evaluate its `\_GPE`
/// handler method (`_Lxx` level / `_Exx` edge), drain the Notify queue the
/// method produced, and publish an event per notified device. Then clear the
/// status bit. QEMU raises no GPEs on this config, so this path is exercised by
/// host unit tests (docs/acpi.md — ACPI events); on real hardware it carries
/// battery/AC/lid. The embedded controller's `_Qxx` queries are out of scope.
fn handleGpe() void {
    handleGpeBlock(gpe0_blk, gpe0_len, 0);
    handleGpeBlock(gpe1_blk, gpe1_len, gpe0_len * 4);
}

fn handleGpeBlock(blk: u16, len: u8, gpe_base: u32) void {
    if (blk == 0 or len == 0) return;
    const status_bytes = len / 2; // status half, then enable half
    var byte_index: u8 = 0;
    while (byte_index < status_bytes) : (byte_index += 1) {
        const sts: u8 = @truncate(halPioRead(1, blk + byte_index));
        const en: u8 = @truncate(halPioRead(1, blk + status_bytes + byte_index));
        const active = sts & en;
        if (active == 0) continue;
        var bit: u3 = 0;
        while (true) : (bit += 1) {
            if (active & (@as(u8, 1) << bit) != 0) {
                dispatchGpe(gpe_base + @as(u32, byte_index) * 8 + bit);
            }
            if (bit == 7) break;
        }
        halPioWrite(1, blk + byte_index, active); // write-1-to-clear the serviced bits
    }
}

/// Evaluate the `\_GPE._L%02X` or `_E%02X` handler for GPE number `n`, then
/// publish an event for each device it notified.
fn dispatchGpe(n: u32) void {
    const gpe_scope = aml.Namespace.resolve(&persistent_namespace, persistent_namespace.root, true, 0, &.{seg4("_GPE")}) orelse return;
    var name: [4]u8 = .{ '_', 'L', 0, 0 };
    writeHex2(name[2..4], n);
    var method = aml.Namespace.childOf(gpe_scope, name);
    if (method == null) {
        name[1] = 'E';
        method = aml.Namespace.childOf(gpe_scope, name);
    }
    const m = method orelse return; // no handler — the status bit was already cleared
    _ = global_interpreter.evaluate(m, &.{}) catch return;
    for (global_interpreter.takeNotifications()) |event| publishNotify(event.node, event.code);
}

fn publishNotify(node: *aml.Node, code: u64) void {
    // Map the notified device's _HID to a domain event where we recognize it.
    var hid: [8]u8 = .{0} ** 8;
    if (readHid(node, &global_interpreter)) |h| hid = h;
    const which: power.Event = if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) .battery else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) .ac else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) .lid else .notify;
    var event = power.EventMessage{ .event = @intFromEnum(which), .code = @truncate(code) };
    event.hid = hid;
    std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
    publishEvent(std.mem.asBytes(&event));
}

/// Two lowercase hex digits of `n` into `out[0..2]`.
fn writeHex2(out: []u8, n: u32) void {
    const digits = "0123456789ABCDEF";
    out[0] = digits[(n >> 4) & 0xF];
    out[1] = digits[n & 0xF];
}

fn publishButton() void {
    const event = power.EventMessage{ .event = @intFromEnum(power.Event.power_button) };
    publishEvent(std.mem.asBytes(&event));
}

fn publishEvent(bytes: []const u8) void {
    for (&subscribers) |*slot| {
        if (slot.*) |handle| {
            if (!runtime.ipc.send(handle, bytes)) slot.* = null;
        }
    }
}

fn isSubscriber(task: u32) bool {
    for (&subscribers, 0..) |*slot, si| {
        if (slot.* != null and subscriber_tasks[si] == task) return true;
    }
    return false;
}

/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
/// Mirrors the kernel's power.zig sleepValue. Only reached from a PID-1
/// shutdown request (M21.3).
fn enterS5() void {
    if (!s5_valid or pm1a_cnt == 0) {
        _ = runtime.system.write("power: S5 unavailable\n");
        return;
    }
    _ = runtime.system.write("power: entering S5\n");
    halPioWrite(2, pm1a_cnt, (@as(u32, s5_slp_typ_a & 0x7) << 10) | slp_en);
    if (pm1b_cnt != 0) halPioWrite(2, pm1b_cnt, (@as(u32, s5_slp_typ_b & 0x7) << 10) | slp_en);
    // If control returns, the write did not take — say so instead of hanging.
    runtime.system.sleep(500);
    _ = runtime.system.write("power: S5 write did not take\n");
}

// --- harness callbacks --------------------------------------------------------

fn onNotification(badge: u64) void {
    // The only notification the service binds is the SCI (an IRQ badge).
    _ = badge;
    onSci();
}

/// The `.power` protocol: subscribe (endpoint as the call's capability),
/// shutdown (PID 1 only). Device discovery uses a different endpoint (the
/// device manager's), so nothing here handles ChildAdded.
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
    if (message.len < 1) return 0;
    switch (message[0]) {
        @intFromEnum(power.Operation.subscribe) => {
            var status: i32 = -1;
            if (capability) |handle| {
                for (&subscribers, 0..) |*slot, si| {
                    if (slot.* == null) {
                        slot.* = handle;
                        subscriber_tasks[si] = sender;
                        status = 0;
                        break;
                    }
                }
            }
            const r = power.Reply{ .status = status };
            @memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r));
            return @sizeOf(power.Reply);
        },
        @intFromEnum(power.Operation.shutdown) => {
            // Honored only from a power subscriber — init, which has already run
            // the stop sequence over everything else. The power service is
            // mechanism (write S5); deciding *when* to shut down and stopping
            // the rest of the system first is init's policy.
            const allowed = isSubscriber(sender);
            const r = power.Reply{ .status = if (allowed) 0 else -1 };
            @memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r));
            if (allowed) enterS5();
            return @sizeOf(power.Reply);
        },
        else => return 0,
    }
}

/// Depth-first walk: register + report each present device with a _HID, then
/// descend. Scopes (\_SB, \_GPE …) are descended without producing a node.
fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter) void {
    var child = node.first_child;
    while (child) |c| : (child = c.next_sibling) {
        if (c.kind != .device) {
            walkDevices(c, interpreter);
            continue;
        }
        if (!devicePresent(interpreter, c)) continue; // absent: skip it and its subtree

        if (readHid(c, interpreter)) |hid| {
            // Skip PCI roots — pci-bus already reports PCI functions; ACPI adds
            // only the non-PCI _HID devices (docs/device-manager.md — matching). The two
            // roots are named through the shared registry, not bare _HID strings.
            const id = acpi_ids.HardwareId.fromHid(hid[0..7]);
            if (id != .pci_bus and id != .pci_express_root_bridge) {
                registerDevice(c, hid, interpreter);
            }
        }
        walkDevices(c, interpreter);
    }
}

fn registerDevice(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter) void {
    if (registered_count >= registered.len) return;
    var descriptor = std.mem.zeroes(device.DeviceDescriptor);
    descriptor.class = @intFromEnum(device.DeviceClass.acpi_device);
    descriptor.pci_class = device.no_pci_class;
    const hid_len: u64 = std.mem.indexOfScalar(u8, &hid, 0) orelse hid.len;
    descriptor.hid_len = hid_len;
    @memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]);
    applyCrs(&descriptor, node, interpreter);

    const id = device.register(node_id, &descriptor) orelse {
        std.log.info("register refused for {s}", .{hid[0..@intCast(hid_len)]});
        return;
    };
    registered[registered_count] = .{ .hid = hid, .hid_len = @intCast(hid_len), .device_id = id, .resource_count = descriptor.resource_count };
    registered_count += 1;
}

/// _STA bit 0 (present); absent method or a failed evaluation is treated as
/// present, per the ACPI rules.
fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool {
    const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true;
    const obj = interpreter.evaluate(sta, &.{}) catch return true;
    const status = obj.asInteger() catch return true;
    return (status & 0x01) != 0;
}

/// The device's EISA-decoded _HID (e.g. "PNP0303"), or null.
fn readHid(node: *aml.Node, interpreter: *aml.Interpreter) ?[8]u8 {
    const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return null;
    var buffer: [8]u8 = .{0} ** 8;
    if (hid.kind == .method) {
        const obj = interpreter.evaluate(hid, &.{}) catch return null;
        switch (obj) {
            .integer => |n| {
                _ = eisaIdToStr(@truncate(n), &buffer);
                return buffer;
            },
            else => return null,
        }
    }
    if (hid.kind != .name or hid.value.len == 0) return null;
    const v = hid.value;
    switch (v[0]) {
        // A static _HID names an integer EISA id: Zero/One/Ones or a Byte/Word/DWord/
        // QWord integer prefix. Anything else is not an integer we can EISA-decode.
        opcodes.zero_opcode, opcodes.one_opcode, opcodes.ones_opcode, opcodes.byte_prefix, opcodes.word_prefix, opcodes.dword_prefix, opcodes.qword_prefix => {
            var p: usize = 0;
            const n = readIntObj(v, &p) orelse return null;
            _ = eisaIdToStr(@truncate(n), &buffer);
            return buffer;
        },
        else => return null,
    }
}

// --- _CRS resource-template decode (ported from the kernel's acpi.zig) --------

/// A resource template is a byte list of descriptors. Each starts with a tag byte whose
/// high bit picks the encoding: a *small* descriptor carries its type in bits [6:3] and
/// its length in bits [2:0]; a *large* descriptor is the whole tag byte, followed by a
/// 16-bit length. These are the descriptor types danos decodes into resources — named so
/// the walk below reads by descriptor, not by 0x04/0x85/… (docs/coding-standards.md).
const large_descriptor_bit: u8 = 0x80; // set in a tag byte => large descriptor
const small_length_mask: u8 = 0x07; // low 3 bits of a small tag = body length
const small_type_shift: u3 = 3; // small type sits in bits [6:3]

/// Small resource descriptor types (tag bits [6:3]). Non-exhaustive: an unhandled type
/// is skipped by its length, not misread.
const SmallResourceType = enum(u8) {
    irq = 0x04,
    io_port = 0x08,
    fixed_io_port = 0x09,
    end_tag = 0x0F,
    _,
};

/// Large resource descriptor types (the whole tag byte). Non-exhaustive for the same reason.
const LargeResourceType = enum(u8) {
    memory32 = 0x85,
    memory32_fixed = 0x86,
    extended_irq = 0x89,
    _,
};

fn applyCrs(descriptor: *device.DeviceDescriptor, node: *aml.Node, interpreter: *aml.Interpreter) void {
    const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return;
    const obj = interpreter.evaluate(crs, &.{}) catch return;
    const bytes = switch (obj) {
        .buffer => |b| b,
        else => return,
    };
    var i: usize = 0;
    while (i < bytes.len) {
        const tag = bytes[i];
        if (tag & large_descriptor_bit == 0) {
            const len: usize = tag & small_length_mask;
            const body = i + 1;
            if (body + len > bytes.len) break;
            switch (@as(SmallResourceType, @enumFromInt((tag >> small_type_shift) & 0x0F))) {
                .irq => if (len >= 2) { // IRQ mask
                    const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8);
                    var b: usize = 0;
                    while (b < 16) : (b += 1) {
                        if (mask & (@as(u16, 1) << @intCast(b)) != 0) addResource(descriptor, .irq, b, 1);
                    }
                },
                .io_port => if (len >= 7) addResource(descriptor, .io_port, rd16(bytes, body + 1), bytes[body + 6]),
                .fixed_io_port => if (len >= 3) addResource(descriptor, .io_port, rd16(bytes, body), bytes[body + 2]),
                .end_tag => break,
                else => {},
            }
            i = body + len;
        } else {
            if (i + 3 > bytes.len) break;
            const len: usize = @intCast(rd16(bytes, i + 1));
            const body = i + 3;
            if (body + len > bytes.len) break;
            switch (@as(LargeResourceType, @enumFromInt(tag))) {
                .memory32 => if (len >= 17) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 13)),
                .memory32_fixed => if (len >= 9) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 5)),
                .extended_irq => if (len >= 2) {
                    const count = bytes[body + 1];
                    var k: usize = 0;
                    while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) {
                        addResource(descriptor, .irq, rd32(bytes, body + 2 + k * 4), 1);
                    }
                },
                else => {},
            }
            i = body + len;
        }
    }
}

fn addResource(descriptor: *device.DeviceDescriptor, kind: device.ResourceKind, start: u64, len: u64) void {
    if (descriptor.resource_count >= descriptor.resources.len) return;
    descriptor.resources[@intCast(descriptor.resource_count)] = .{ .kind = @intFromEnum(kind), .start = start, .len = len };
    descriptor.resource_count += 1;
}

// --- small helpers ported verbatim from the kernel's acpi.zig ----------------

fn seg4(comptime s: *const [4:0]u8) [4]u8 {
    return s[0..4].*;
}

fn hexDigit(n: u8) u8 {
    return if (n < 10) '0' + n else 'A' + (n - 10);
}

fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 {
    const b0: u16 = @intCast(id & 0xFF);
    const b1: u16 = @intCast((id >> 8) & 0xFF);
    const b2: u8 = @truncate(id >> 16);
    const b3: u8 = @truncate(id >> 24);
    const mfg = (b0 << 8) | b1;
    buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F));
    buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F));
    buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F));
    buffer[3] = hexDigit((b2 >> 4) & 0xF);
    buffer[4] = hexDigit(b2 & 0xF);
    buffer[5] = hexDigit((b3 >> 4) & 0xF);
    buffer[6] = hexDigit(b3 & 0xF);
    buffer[7] = 0;
    return buffer[0..7];
}

fn readIntObj(bytes: []const u8, p: *usize) ?u64 {
    if (p.* >= bytes.len) return null;
    const op = bytes[p.*];
    p.* += 1;
    switch (op) {
        opcodes.zero_opcode => return 0,
        opcodes.one_opcode => return 1,
        opcodes.ones_opcode => return 1,
        opcodes.byte_prefix => {
            if (p.* >= bytes.len) return null;
            const v = bytes[p.*];
            p.* += 1;
            return v;
        },
        opcodes.word_prefix => {
            if (p.* + 2 > bytes.len) return null;
            const v = rd16(bytes, p.*);
            p.* += 2;
            return v;
        },
        opcodes.dword_prefix => {
            if (p.* + 4 > bytes.len) return null;
            const v = rd32(bytes, p.*);
            p.* += 4;
            return v;
        },
        else => return null,
    }
}

fn rd16(bytes: []const u8, off: usize) u64 {
    return @as(u64, bytes[off]) | (@as(u64, bytes[off + 1]) << 8);
}

fn rd32(bytes: []const u8, off: usize) u64 {
    return rd16(bytes, off) | (rd16(bytes, off + 2) << 16);
}
