//! /system/drivers/pci-bus — the PCI bus driver: enumeration moved out of ring 0
//! (docs/discovery.md). The device manager matches the `pci_host_bridge`
//! node and spawns one instance per bridge, the bridge's device id as argv[1] —
//! the same per-device contract as usb-xhci-bus.
//!
//! M19.1 (this increment): claim the bridge, map its ECAM window (resource 0;
//! the bus range and the MMIO apertures follow it), walk every
//! bus/device/function config header, and log what the walk finds — ending
//! with "/system/drivers/pci-bus: N functions found", which the `pci-scan` scenario compares
//! against the kernel's own enumeration. Registration and reports (M19.2), and
//! the kernel walk's retirement (M19.3), build on this proven-equivalent scan.

const std = @import("std");
const runtime = @import("runtime");
const protocol = runtime.device_manager_protocol;
const device = runtime.device;
const pci_class = @import("pci-class");

/// Log a discovered function with its (class / subclass / prog-IF) triple decoded
/// to human names — the boot-log breadcrumb that says *what* the hardware is, so
/// "class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
/// progif 0x01 (AHCI 1.0)" reads straight off the log when writing a new driver.
/// A dedicated wider buffer than `writeLine`'s, since the decoded names are long.
fn logFunction(bus: u64, dev: u64, function: u64, class_triple: u32) void {
    const cc = pci_class.ClassCode.unpack(@truncate(class_triple));
    const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
    var line: [200]u8 = undefined;
    const text = if (pif.len != 0)
        std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s})  subclass 0x{x:0>2} ({s})  progif 0x{x:0>2} ({s})\n", .{ bus, dev, function, 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(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s})  subclass 0x{x:0>2} ({s})  progif 0x{x:0>2}\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if }) catch return;
    _ = runtime.system.write(text);
}

var bridge_id: u64 = protocol.no_device;
var ecam_base: usize = 0;
var ecam_physical: u64 = 0;
var start_bus: u64 = 0;
var bus_count: u64 = 0;
var manager_handle: runtime.ipc.Handle = 0;

/// One aligned 32-bit read from a function's configuration space.
fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 {
    const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
    const register: *volatile u32 = @ptrFromInt(address);
    return register.*;
}

fn configWrite(bus: u64, dev: u64, function: u64, offset: u64, value: u32) void {
    const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
    const register: *volatile u32 = @ptrFromInt(address);
    register.* = value;
}

fn configRead16(bus: u64, dev: u64, function: u64, offset: u64) u16 {
    const word = configRead(bus, dev, function, offset & ~@as(u64, 3));
    return @truncate(word >> @intCast((offset & 3) * 8));
}

fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) void {
    const aligned = offset & ~@as(u64, 3);
    const shift: u5 = @intCast((offset & 3) * 8);
    const word = configRead(bus, dev, function, aligned);
    const mask = @as(u32, 0xFFFF) << shift;
    configWrite(bus, dev, function, aligned, (word & ~mask) | (@as(u32, value) << shift));
}

/// Claim the bridge, map the ECAM, hello the manager, then scan.
fn initialise(endpoint: runtime.ipc.Handle) bool {
    _ = endpoint;
    if (!device.claim(bridge_id)) {
        std.log.info("unable to claim bridge device {d}", .{bridge_id});
        return false;
    }
    const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
        _ = runtime.system.write("/system/drivers/pci-bus: out of memory\n");
        return false;
    };
    const total = device.enumerate(buffer);
    const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
        if (d.id == bridge_id) break d;
    } else {
        std.log.info("device {d} not in the device tree", .{bridge_id});
        return false;
    };
    // Resource 0 is the ECAM window (1 MiB of config space per bus); the bus
    // range rides beside it. The MMIO apertures (M19.0) come after both.
    if (descriptor.resource_count < 2 or descriptor.resources[0].kind != @intFromEnum(device.ResourceKind.memory)) {
        _ = runtime.system.write("/system/drivers/pci-bus: bridge has no ECAM window\n");
        return false;
    }
    const bus_range = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| {
        if (resource.kind == @intFromEnum(device.ResourceKind.bus_range)) break resource;
    } else {
        _ = runtime.system.write("/system/drivers/pci-bus: bridge has no bus range\n");
        return false;
    };
    start_bus = bus_range.start;
    bus_count = bus_range.len;
    ecam_physical = descriptor.resources[0].start;
    ecam_base = device.mmioMap(bridge_id, 0) orelse {
        _ = runtime.system.write("/system/drivers/pci-bus: ECAM mmio_map failed\n");
        return false;
    };

    // The handshake, then the scan (reports join in M19.2).
    var manager: ?runtime.ipc.Handle = null;
    var tries: u32 = 0;
    while (manager == null and tries < 100) : (tries += 1) {
        manager = runtime.ipc.lookup(.device_manager);
        if (manager == null) runtime.system.sleep(20);
    }
    const h = manager orelse {
        _ = runtime.system.write("/system/drivers/pci-bus: no device manager to hello\n");
        return false;
    };
    const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = bridge_id };
    var reply: [protocol.message_maximum]u8 = undefined;
    const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
        _ = runtime.system.write("/system/drivers/pci-bus: hello call failed\n");
        return false;
    };
    if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
        _ = runtime.system.write("/system/drivers/pci-bus: hello refused\n");
        return false;
    }
    manager_handle = h;

    scan();
    return true;
}

/// The brute-force walk the kernel does today, from ring 3: every bus in the
/// range, 32 devices, 8 functions; vendor id FFFFh means nothing decodes there,
/// and only multifunction devices get their functions 1..7 probed.
fn scan() void {
    var found: u32 = 0;
    var bus: u64 = start_bus;
    while (bus < start_bus + bus_count) : (bus += 1) {
        var dev: u64 = 0;
        while (dev < 32) : (dev += 1) {
            const first = configRead(bus, dev, 0, 0);
            if (first & 0xFFFF == 0xFFFF) continue;
            const multifunction = (configRead(bus, dev, 0, 0x0C) >> 16) & 0x80 != 0;
            var function: u64 = 0;
            while (function < 8) : (function += 1) {
                if (function != 0 and !multifunction) break;
                const vendor_device = configRead(bus, dev, function, 0);
                if (vendor_device & 0xFFFF == 0xFFFF) continue;
                const class_revision = configRead(bus, dev, function, 0x08);
                found += 1;
                logFunction(bus, dev, function, class_revision >> 8);
                registerAndReport(bus, dev, function, class_revision >> 8);
            }
        }
    }
    std.log.info("{d} functions found", .{found});
}

/// Register one function under the bridge and report it to the manager. The
/// descriptor mirrors the kernel's own recording byte for byte — config slice
/// as resource 0, then the sized BARs — so during coexistence the idempotent
/// device_register (M19.0) returns the kernel's existing node id rather than
/// growing a duplicate, and the report carries the id drivers already use.
fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void {
    var descriptor = std.mem.zeroes(device.DeviceDescriptor);
    descriptor.class = @intFromEnum(device.DeviceClass.pci_device);
    descriptor.pci_class = class_triple;
    descriptor.resources[0] = .{
        .kind = @intFromEnum(device.ResourceKind.memory),
        .start = ecam_physical + (((bus - start_bus) << 20) | (dev << 15) | (function << 12)),
        .len = 4096,
    };
    descriptor.resource_count = 1;

    // The standard BAR-sizing probe, exactly as the kernel does it: decode off,
    // write all-ones, read the writable mask back, restore. Header type 0 only.
    const header_type = (configRead(bus, dev, function, 0x0C) >> 16) & 0x7F;
    if (header_type == 0) {
        const command = configRead16(bus, dev, function, 0x04);
        configWrite16(bus, dev, function, 0x04, command & ~@as(u16, 0b11));
        var i: u64 = 0;
        while (i < 6) : (i += 1) {
            if (descriptor.resource_count >= 8) break;
            const off = 0x10 + i * 4;
            const original = configRead(bus, dev, function, off);
            if (original == 0) continue;
            const slot: usize = @intCast(descriptor.resource_count);
            if (original & 1 != 0) {
                configWrite(bus, dev, function, off, 0xFFFF_FFFF);
                const readback = configRead(bus, dev, function, off);
                configWrite(bus, dev, function, off, original);
                const mask = readback & 0xFFFF_FFFC;
                const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
                if (size == 0) continue; // unimplemented BAR — nothing to register
                descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.io_port), .start = original & 0xFFFF_FFFC, .len = size };
                descriptor.resource_count += 1;
            } else if ((original >> 1) & 0x3 == 2) {
                const original_high = configRead(bus, dev, function, off + 4);
                configWrite(bus, dev, function, off, 0xFFFF_FFFF);
                configWrite(bus, dev, function, off + 4, 0xFFFF_FFFF);
                const lo = configRead(bus, dev, function, off);
                const hi = configRead(bus, dev, function, off + 4);
                configWrite(bus, dev, function, off, original);
                configWrite(bus, dev, function, off + 4, original_high);
                const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
                const size: u64 = if (readback == 0) 0 else ~readback +% 1;
                i += 1; // consumed the high half regardless
                if (size == 0) continue;
                descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = (@as(u64, original_high) << 32) | (original & 0xFFFF_FFF0), .len = size };
                descriptor.resource_count += 1;
            } else {
                configWrite(bus, dev, function, off, 0xFFFF_FFFF);
                const readback = configRead(bus, dev, function, off);
                configWrite(bus, dev, function, off, original);
                const mask = readback & 0xFFFF_FFF0;
                const size: u32 = if (mask == 0) 0 else ~mask +% 1;
                if (size == 0) continue;
                descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = original & 0xFFFF_FFF0, .len = size };
                descriptor.resource_count += 1;
            }
        }
        configWrite16(bus, dev, function, 0x04, command);
    }

    const registered = device.register(bridge_id, &descriptor) orelse {
        std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function });
        return;
    };
    const report = protocol.ChildAdded{
        .parent = bridge_id,
        .bus_address = (bus << 8) | (dev << 3) | function,
        .identity = class_triple,
        .device_id = registered,
    };
    var reply: [protocol.message_maximum]u8 = undefined;
    _ = runtime.ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
        std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function });
    };
}

fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
    _ = message;
    _ = reply;
    _ = sender;
    _ = capability;
    return 0;
}

pub fn main(init: runtime.process.Init) void {
    const argument = init.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
    bridge_id = std.fmt.parseInt(u64, argument, 10) catch {
        std.log.info("malformed bridge device id '{s}'", .{argument});
        return;
    };
    runtime.service.run(protocol.message_maximum, .{
        .init = initialise,
        .on_message = onMessage,
    });
}
