//! The compositor's **scanout backend** — how a finished frame reaches the panel
//! (docs/display-v2.md). The compositor composes its layer stack into the backend's
//! cacheable `surface()` and calls `present(damage)`; everything device-specific lives
//! here. Today there is one backend, `Gop` — the firmware framebuffer: a cacheable back
//! buffer streamed write-combining to the linear framebuffer. A native virtio-gpu backend
//! slots in beside it later (V4); the compositor never learns which is active.

const std = @import("std");
const runtime = @import("runtime");
const compositor = @import("compositor.zig");

const system = runtime.system;
const device = runtime.device;
const ipc = runtime.ipc;
const scanout_protocol = runtime.scanout_protocol;
const Rect = compositor.Rect;
const Surface = compositor.Surface;

/// The current display mode, as a backend reports it. `refresh_hz` is the panel's
/// refresh rate from EDID (0 = unknown) — the frame clock's pacing seed; without vblank
/// it fixes the rate, never the phase (docs/display-v2.md, "Fenced is not vsync").
pub const Info = struct { width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32 };

/// Enumeration scratch — a `DeviceDescriptor` is large, and only one scan is ever needed.
var device_table: [64]device.DeviceDescriptor = undefined;

/// The GOP framebuffer backend: claims the kernel-seeded `display` device, maps the linear
/// framebuffer write-combining as the front buffer, and keeps a cacheable back buffer of
/// the same geometry as the compose target. `present` streams the damaged rectangle from
/// the back buffer to the LFB (sequential WC writes; the LFB is never read). No mode-set,
/// no present fence — the portable floor (docs/display-v2.md).
pub const Gop = struct {
    device_id: u64,
    front: [*]volatile u8, // the LFB (write-combining)
    back: [*]u8, // cacheable compose target, same geometry
    width: u32,
    height: u32,
    pitch: u32,
    format: u32,
    refresh_hz: u32, // from the boot EDID via the display0 node (0 = unknown)

    /// The framebuffer's id and geometry, captured together. `findDisplay` reads these out of
    /// the enumeration table and returns them by value, so the caller never re-reads the table
    /// across later syscalls (`device_enumerate` writes the whole table straight into this
    /// process's memory; reading a descriptor's tail again after other syscalls have run is a
    /// window we simply avoid by copying the few fields we need up front).
    const Found = struct { id: u64, width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32 };

    /// The first `display`-class device with a *valid* (non-zero) geometry, or null. A zero
    /// geometry is treated as "not ready yet" so the caller retries — a real framebuffer always
    /// has a non-zero width, height, and pitch.
    fn findDisplay() ?Found {
        const total = device.enumerate(&device_table);
        const n = @min(total, device_table.len);
        for (device_table[0..n]) |*d| {
            if (d.class != @intFromEnum(device.DeviceClass.display)) continue;
            if (d.display.width == 0 or d.display.height == 0 or d.display.pitch == 0) continue;
            return .{ .id = d.id, .width = d.display.width, .height = d.display.height, .pitch = d.display.pitch, .format = d.display.format, .refresh_hz = d.display.refresh_hz };
        }
        return null;
    }

    /// Claim the framebuffer (retrying while discovery catches up), map the LFB, and
    /// allocate the back buffer. Null if there is no framebuffer or a mapping fails.
    pub fn init() ?Gop {
        var tries: u32 = 0;
        const found = while (tries < 100) : (tries += 1) {
            if (findDisplay()) |f| break f;
            system.sleep(50);
        } else {
            _ = system.write("display: no framebuffer device (headless?)\n");
            return null;
        };

        if (!device.claim(found.id)) {
            _ = system.write("display: could not claim the framebuffer\n");
            return null;
        }
        // Resource 0 is the framebuffer memory window; the kernel maps it write-combining
        // because the resource carries that flag (docs/display-plan.md D1).
        const front_base = device.mmioMap(found.id, 0) orelse {
            _ = system.write("display: could not map the framebuffer\n");
            return null;
        };
        const size = @as(usize, found.height) * found.pitch;
        const back_base = system.mmap(size, system.PROT_READ | system.PROT_WRITE);
        if (system.mmapFailed(back_base)) {
            _ = system.write("display: could not allocate the back buffer\n");
            return null;
        }
        return .{
            .device_id = found.id,
            .front = @ptrFromInt(front_base),
            .back = @ptrFromInt(back_base),
            .width = found.width,
            .height = found.height,
            .pitch = found.pitch,
            .format = found.format,
            .refresh_hz = found.refresh_hz,
        };
    }

    pub fn info(self: *const Gop) Info {
        return .{ .width = self.width, .height = self.height, .pitch = self.pitch, .format = self.format, .refresh_hz = self.refresh_hz };
    }

    /// The cacheable compose target (the back buffer).
    pub fn surface(self: *const Gop) Surface {
        return .{
            .pixels = @ptrCast(@alignCast(self.back)),
            .stride = self.pitch / 4, // pitch is bytes; a 32-bpp row is pitch/4 pixels
            .width = self.width,
            .height = self.height,
        };
    }

    /// Stream each damaged rectangle from the back buffer to the write-combining LFB, row
    /// by row (sequential writes — what WC memory wants; the LFB is never read). The rows
    /// are copied by `presentSpan` below, which widens the stores by hand: `volatile`
    /// keeps the compiler from eliding or reordering framebuffer writes, but it also
    /// forbids it from merging them, so a naive per-pixel loop is stuck at one 4-byte
    /// store per iteration. Keeping each copy small (the damage list) and each store wide
    /// shrinks the window in which scanout can sample a half-written frame.
    pub fn present(self: *const Gop, damage: []const Rect) void {
        const bounds = Rect{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) };
        for (damage) |rect| {
            const c = rect.intersect(bounds);
            if (c.isEmpty()) continue;
            const span: usize = @intCast(c.w);
            var y: i32 = c.y;
            while (y < c.bottom()) : (y += 1) {
                const offset = @as(usize, @intCast(y)) * self.pitch + @as(usize, @intCast(c.x)) * 4;
                const source: [*]const u32 = @ptrCast(@alignCast(self.back + offset));
                const front_row: [*]volatile u32 = @ptrCast(@alignCast(self.front + offset));
                presentSpan(front_row, source, span);
            }
        }
    }
};

/// Copy `count` pixels into the write-combining front buffer with 8-byte volatile stores
/// (plus a 4-byte head/tail where the span isn't 8-aligned — pixel spans are always
/// 4-aligned). The loads come from the cacheable back buffer and are assembled into a
/// `u64` in registers, so nothing here reads the front buffer.
fn presentSpan(destination: [*]volatile u32, source: [*]const u32, count: usize) void {
    var i: usize = 0;
    if (i < count and (@intFromPtr(destination) & 7) != 0) {
        destination[0] = source[0];
        i = 1;
    }
    while (i + 2 <= count) : (i += 2) {
        const pair = @as(u64, source[i]) | (@as(u64, source[i + 1]) << 32);
        const wide: *volatile u64 = @ptrCast(@alignCast(destination + i));
        wide.* = pair;
    }
    if (i < count) destination[i] = source[i];
}

/// A display mode the native backend can switch to.
pub const Mode = scanout_protocol.Mode;

/// The native virtio-gpu backend: the compositor composes into a **shared** scanout surface
/// (a shared-memory region the driver created and handed over) and `present` asks the driver to put
/// a frame on the panel over its `.scanout` endpoint. Unlike GOP there is no local copy — the
/// surface *is* the device's resource backing, so compositing writes land straight where the
/// driver transfers-and-flushes from (x86 DMA is cache-coherent, so the cacheable shared pages
/// need no explicit flush). Built by the display service when a driver announces (V4). The
/// surface is sized to the driver's largest mode, so `stride` (its row width) is fixed while
/// `width`/`height` — the active mode — change under `setMode` (V5).
pub const VirtioGpu = struct {
    pixels: [*]u32, // the shared scanout surface, mapped into the compositor
    stride: u32, // the surface's row stride in pixels (the driver's max mode width) — fixed
    width: u32, // the active mode
    height: u32,
    format: u32,
    refresh_hz: u32, // from the driver's EDID read, carried in the announce (0 = unknown)
    scanout: ipc.Handle, // the driver's present + mode channel (looked up on `.scanout`)

    pub fn info(self: *const VirtioGpu) Info {
        return .{ .width = self.width, .height = self.height, .pitch = self.stride * 4, .format = self.format, .refresh_hz = self.refresh_hz };
    }
    pub fn surface(self: *const VirtioGpu) Surface {
        return .{ .pixels = self.pixels, .stride = self.stride, .width = self.width, .height = self.height };
    }
    /// Ask the driver to present. The composited pixels are already in the shared surface, so
    /// this is a single request over `.scanout` regardless of how many damage rectangles
    /// accumulated; the driver transfers + fenced-flushes the whole frame.
    pub fn present(self: *const VirtioGpu, damage: []const Rect) void {
        _ = damage;
        var request = scanout_protocol.Request{
            .operation = @intFromEnum(scanout_protocol.Operation.present),
            .width = self.width,
            .height = self.height,
        };
        var reply: [scanout_protocol.reply_size]u8 = undefined;
        _ = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch {};
    }
    /// Fill `out` with the driver's offered modes; returns how many were written.
    pub fn modes(self: *const VirtioGpu, out: []Mode) usize {
        var request = scanout_protocol.Request{ .operation = @intFromEnum(scanout_protocol.Operation.get_modes) };
        var reply: [scanout_protocol.modes_reply_size]u8 = undefined;
        const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return 0;
        if (n < scanout_protocol.modes_reply_size) return 0;
        const answer = std.mem.bytesToValue(scanout_protocol.ModesReply, reply[0..scanout_protocol.modes_reply_size]);
        if (answer.status != 0) return 0;
        const count = @min(@min(answer.count, scanout_protocol.max_modes), out.len);
        for (0..count) |i| out[i] = answer.modes[i];
        return count;
    }
    /// Change the scanout resolution. On success the active `width`/`height` update (the shared
    /// surface — sized to the max mode — is unchanged, so `stride` stays put).
    pub fn setMode(self: *VirtioGpu, w: u32, h: u32) bool {
        if (w == 0 or h == 0 or w > self.stride) return false;
        var request = scanout_protocol.Request{
            .operation = @intFromEnum(scanout_protocol.Operation.set_mode),
            .width = w,
            .height = h,
        };
        var reply: [scanout_protocol.reply_size]u8 = undefined;
        const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return false;
        if (n < scanout_protocol.reply_size) return false;
        if (std.mem.bytesToValue(scanout_protocol.Reply, reply[0..scanout_protocol.reply_size]).status != 0) return false;
        self.width = w;
        self.height = h;
        return true;
    }
};

/// The pluggable scanout backend. A tagged union so the compositor holds one value and
/// dispatches without caring which is active; the `virtio` native backend joins `gop` at V4.
pub const Backend = union(enum) {
    gop: Gop,
    virtio: VirtioGpu,

    pub fn info(self: *const Backend) Info {
        return switch (self.*) {
            inline else => |*b| b.info(),
        };
    }
    pub fn surface(self: *const Backend) Surface {
        return switch (self.*) {
            inline else => |*b| b.surface(),
        };
    }
    pub fn present(self: *const Backend, damage: []const Rect) void {
        switch (self.*) {
            inline else => |*b| b.present(damage),
        }
    }
    /// The modes this backend can switch to (none for GOP); returns how many were written.
    pub fn modes(self: *const Backend, out: []Mode) usize {
        return switch (self.*) {
            .virtio => |*v| v.modes(out),
            .gop => 0,
        };
    }
    /// Change the resolution; false if this backend can't mode-set or the mode was refused.
    pub fn setMode(self: *Backend, w: u32, h: u32) bool {
        return switch (self.*) {
            .virtio => |*v| v.setMode(w, h),
            .gop => false,
        };
    }
    /// Whether this backend supports runtime mode-setting (GOP: no; virtio-gpu: yes, V5).
    pub fn canModeSet(self: *const Backend) bool {
        return switch (self.*) {
            .gop => false,
            .virtio => true,
        };
    }
    /// Whether this backend's present is **fenced** — it completes only once the device has
    /// consumed the frame (virtio-gpu: every flush carries a fence the used-ring ack waits on).
    /// A fence gives completion feedback and tear-free snapshot presents; it is *not* vblank —
    /// nothing paces presents to the display's refresh (base virtio-gpu 2D has no vblank event
    /// at all). True vsync needs a native driver's vblank interrupt. See docs/display-v2.md,
    /// "Fenced is not vsync".
    pub fn hasFencedPresent(self: *const Backend) bool {
        return switch (self.*) {
            .gop => false,
            .virtio => true,
        };
    }
};

/// Which backend to use. The pure selection *decision* is `chooseKind`; `select` below
/// binds it to the (syscall-bound) bring-up.
pub const Kind = enum { gop, virtio };

/// The selection decision, factored out of bring-up so it stays pure and host-testable:
/// prefer a native driver when one has announced itself (docs/display-v2.md V4), else the
/// GOP floor. Trivial today; it grows real inputs when native detection lands.
pub fn chooseKind(native_available: bool) Kind {
    return if (native_available) .virtio else .gop;
}

/// Pick and bring up the best available backend. Today the GOP framebuffer is the only one
/// (`chooseKind(false)` → `.gop`), so this is `Gop.init()`. V4 adds the native-if-present
/// branch, with GOP as the floor.
pub fn select() ?Backend {
    return switch (chooseKind(false)) {
        .gop => .{ .gop = Gop.init() orelse return null },
        .virtio => unreachable, // no native detection yet (V4)
    };
}

test "selection prefers native when present, else the gop floor" {
    try std.testing.expectEqual(Kind.gop, chooseKind(false));
    try std.testing.expectEqual(Kind.virtio, chooseKind(true));
}
