//! device-list — the `ps` analog for the device tree (docs/device-manager.md
//! M18.3): asks the device manager for the tree over IPC, prints it, then
//! subscribes and prints every published add/remove event. The manager is the
//! one answer to "what devices exist" for user space; nothing here touches a
//! device_* system call.

const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc");
const time = @import("time");
const logging = @import("logging");
const device_manager_protocol = @import("device-manager-protocol");

fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
    var line: [96]u8 = undefined;
    _ = logging.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}

pub fn main() void {
    var manager: ?ipc.Handle = null;
    var tries: u32 = 0;
    while (manager == null and tries < 200) : (tries += 1) {
        manager = channel.openEndpoint("device-manager");
        if (manager == null) time.sleepMillis(20);
    }
    const h = manager orelse {
        _ = logging.write("device-list: no device manager\n");
        return;
    };

    // The snapshot — polled briefly, because at boot the bus drivers may still
    // be scanning: an empty first answer usually just means "too early". This is
    // the envelope's reserved `enumerate` verb, so the request is nothing but a
    // header and the answer is one `ChildEntry` per record in the reply's tail —
    // how many arrived is the reply's own length, which is why no count header
    // says it a second time.
    const Entry = device_manager_protocol.ChildEntry;
    const enumerate = envelope.Header{ .operation = envelope.operation_enumerate };
    var reply: [device_manager_protocol.message_maximum]u8 = undefined;
    var count: usize = 0;
    tries = 0;
    while (tries < 20) : (tries += 1) {
        const length = ipc.call(h, std.mem.asBytes(&enumerate), &reply) catch 0;
        if (envelope.statusOf(reply[0..length])) |status| {
            if (status.status == 0) {
                const carried = @min(@as(usize, status.len), length - envelope.prefix_size);
                count = carried / @sizeOf(Entry);
                if (count != 0) break;
            }
        }
        time.sleepMillis(100);
    }
    writeLine("device-list: {d} devices\n", .{count});
    for (0..count) |index| {
        const entry = std.mem.bytesToValue(Entry, reply[envelope.prefix_size + index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]);
        writeLine("device-list: device {d} port {d} identity {d}\n", .{ entry.parent, entry.bus_address, entry.identity });
    }

    // The subscription — the reserved `subscribe` verb: our endpoint rides as
    // the call's capability, and events arrive as buffered packets carrying the
    // same structs the bus drivers send, under the events' own numbering.
    const endpoint = ipc.createIpcEndpoint() orelse {
        _ = logging.write("device-list: no endpoint\n");
        return;
    };
    const subscribe = envelope.Header{ .operation = envelope.operation_subscribe };
    _ = ipc.callCap(h, std.mem.asBytes(&subscribe), &reply, endpoint) catch {
        _ = logging.write("device-list: subscribe failed\n");
        return;
    };
    _ = logging.write("device-list: subscribed\n");

    var receive: [device_manager_protocol.message_maximum]u8 = undefined;
    while (true) {
        const got = ipc.replyWait(endpoint, &.{}, &receive, null);
        if (!got.isMessage()) continue;
        const packet = receive[0..got.len];
        const event = device_manager_protocol.Protocol.eventOf(packet) orelse continue;
        switch (event) {
            .child_added => {
                const added = device_manager_protocol.Protocol.decodeEvent(.child_added, packet) orelse continue;
                writeLine("device-list: added (device {d} port {d})\n", .{ added.parent, added.bus_address });
            },
            .child_removed => {
                const removed = device_manager_protocol.Protocol.decodeEvent(.child_removed, packet) orelse continue;
                writeLine("device-list: removed (device {d} port {d})\n", .{ removed.parent, removed.bus_address });
            },
        }
    }
}
