//! The compositor's pure core: rectangle math and the three blitting primitives the
//! display service composes frames from — fill a rectangle of a surface, composite one
//! surface onto another clipped to a damage rectangle, and copy a client-supplied pixel
//! tile in. Deliberately free of any syscall or `runtime` dependency (it takes plain
//! pixel pointers), so it is host-tested under `zig build test`. The service
//! (system/services/display/display.zig) wires real mmap'd surfaces and the framebuffer
//! to it. Pixels are opaque native 32-bit values — v1 layers don't alpha-blend, and
//! channel order (rgbx/bgrx) is the caller's concern (see protocol.pack).

const std = @import("std");

/// An axis-aligned rectangle in pixels. Signed, so a surface partly off-screen (a layer
/// dragged past an edge) clips with plain arithmetic. Half-open: covers [x, x+w) × [y, y+h).
pub const Rect = struct {
    x: i32,
    y: i32,
    w: i32,
    h: i32,

    pub const empty = Rect{ .x = 0, .y = 0, .w = 0, .h = 0 };

    pub fn init(x: i32, y: i32, w: i32, h: i32) Rect {
        return .{ .x = x, .y = y, .w = w, .h = h };
    }

    pub fn isEmpty(r: Rect) bool {
        return r.w <= 0 or r.h <= 0;
    }

    pub fn right(r: Rect) i32 {
        return r.x + r.w;
    }
    pub fn bottom(r: Rect) i32 {
        return r.y + r.h;
    }

    /// The overlap of two rectangles, or an empty rectangle if they don't touch.
    pub fn intersect(a: Rect, b: Rect) Rect {
        const x0 = @max(a.x, b.x);
        const y0 = @max(a.y, b.y);
        const x1 = @min(a.right(), b.right());
        const y1 = @min(a.bottom(), b.bottom());
        return .{ .x = x0, .y = y0, .w = x1 - x0, .h = y1 - y0 };
    }

    /// The bounding box of two rectangles. An empty operand contributes nothing (returns
    /// the other), so folding damage rectangles with `unite` from `empty` yields their
    /// bounding box.
    pub fn unite(a: Rect, b: Rect) Rect {
        if (a.isEmpty()) return b;
        if (b.isEmpty()) return a;
        const x0 = @min(a.x, b.x);
        const y0 = @min(a.y, b.y);
        const x1 = @max(a.right(), b.right());
        const y1 = @max(a.bottom(), b.bottom());
        return .{ .x = x0, .y = y0, .w = x1 - x0, .h = y1 - y0 };
    }
};

/// The dirty screen regions accumulated between presents. Kept as a *list* of rectangles,
/// not one bounding box: when two small things move far apart — the cursor on one side of
/// the screen, an animating layer on the other — a single bounding box unites them into a
/// huge region, and presenting it streams megabytes to the framebuffer for a few thousand
/// changed pixels. The long copy widens the window in which scanout (or QEMU's display
/// refresh) samples a half-written frame — visible as tearing and cursor trails. Small
/// separate rectangles keep each copy, and that window, tight.
///
/// A new rectangle that overlaps an existing entry is united into it (repainting a modest
/// superset is harmless — compositing is idempotent); the grown entry is *not* re-merged
/// against the rest, so entries may overlap, which costs only a duplicate repaint. When
/// the table is full the newcomer folds into the last entry — degrading toward the old
/// bounding-box behaviour instead of dropping damage.
pub const DamageList = struct {
    pub const capacity = 16;

    rects: [capacity]Rect = [_]Rect{Rect.empty} ** capacity,
    count: usize = 0,

    pub fn add(self: *DamageList, r: Rect) void {
        if (r.isEmpty()) return;
        for (self.rects[0..self.count]) |*existing| {
            if (!existing.intersect(r).isEmpty()) {
                existing.* = existing.unite(r);
                return;
            }
        }
        if (self.count < capacity) {
            self.rects[self.count] = r;
            self.count += 1;
            return;
        }
        self.rects[capacity - 1] = self.rects[capacity - 1].unite(r);
    }

    pub fn isEmpty(self: *const DamageList) bool {
        return self.count == 0;
    }

    pub fn slice(self: *const DamageList) []const Rect {
        return self.rects[0..self.count];
    }

    pub fn clear(self: *DamageList) void {
        self.count = 0;
    }
};

/// The alternative damage tracker: a **fixed tile grid**, the scheme browser compositors
/// and tile-based GPUs use. The screen is divided into `tile_size`-pixel tiles up front;
/// `add` marks the tiles a rectangle touches (a bit per tile — merging is free and exact,
/// no heuristics), and `collect` walks the grid turning runs of adjacent dirty tiles into
/// repaint rectangles (horizontal runs, then equal-span rows merged vertically, so
/// full-screen damage collapses back to a single rectangle).
///
/// Trade-off against `DamageList`: tracking is O(1) with a strictly bounded worst case
/// (never more than the dirty tiles), but repaints are quantized — a 1-pixel change
/// repaints a whole tile. Which wins depends on the workload; the display service has a
/// compile-time switch (`damage_mode`) to compare them.
pub const TileGrid = struct {
    pub const tile_size = 64;
    pub const maximum_columns = 128; // supports screens up to 8192 px wide…
    pub const maximum_rows = 128; // …and 8192 px tall (beyond that, edge tiles stretch)
    pub const maximum_tiles = maximum_columns * maximum_rows;
    /// The most rectangles `collect` produces; extras fold into the last (never dropped).
    pub const maximum_rects = 64;

    width: u32 = 0,
    height: u32 = 0,
    columns: u32 = 0,
    rows: u32 = 0,
    dirty_count: u32 = 0,
    dirty: [maximum_tiles]bool = [_]bool{false} ** maximum_tiles,

    /// Size the grid for a screen. Also clears it — callers reset on a geometry change,
    /// where the mode-set paths damage the whole new screen anyway.
    pub fn reset(self: *TileGrid, width: u32, height: u32) void {
        self.width = width;
        self.height = height;
        self.columns = @min((width + tile_size - 1) / tile_size, maximum_columns);
        self.rows = @min((height + tile_size - 1) / tile_size, maximum_rows);
        self.clear();
    }

    pub fn matches(self: *const TileGrid, width: u32, height: u32) bool {
        return self.width == width and self.height == height;
    }

    pub fn isEmpty(self: *const TileGrid) bool {
        return self.dirty_count == 0;
    }

    pub fn clear(self: *TileGrid) void {
        @memset(&self.dirty, false);
        self.dirty_count = 0;
    }

    /// Mark every tile `r` touches. Clips to the screen first, so out-of-range
    /// rectangles are harmless.
    pub fn add(self: *TileGrid, r: Rect) void {
        const screen = Rect{ .x = 0, .y = 0, .w = @intCast(self.width), .h = @intCast(self.height) };
        const c = r.intersect(screen);
        if (c.isEmpty()) return;
        const column_first: u32 = @intCast(@divTrunc(c.x, tile_size));
        const row_first: u32 = @intCast(@divTrunc(c.y, tile_size));
        const column_last: u32 = @min(@as(u32, @intCast(@divTrunc(c.right() - 1, tile_size))), self.columns - 1);
        const row_last: u32 = @min(@as(u32, @intCast(@divTrunc(c.bottom() - 1, tile_size))), self.rows - 1);
        var row = row_first;
        while (row <= row_last) : (row += 1) {
            var column = column_first;
            while (column <= column_last) : (column += 1) {
                const index = row * self.columns + column;
                if (!self.dirty[index]) {
                    self.dirty[index] = true;
                    self.dirty_count += 1;
                }
            }
        }
    }

    /// The screen rectangle covered by tiles [column_first, column_end) of `row`. Edge
    /// tiles clamp to the true screen size (the last column/row may be partial — or, on a
    /// screen wider than the grid supports, stretched to cover the remainder).
    fn tileSpanRect(self: *const TileGrid, column_first: u32, column_end: u32, row: u32) Rect {
        const x: i32 = @intCast(column_first * tile_size);
        const y: i32 = @intCast(row * tile_size);
        const right: i32 = if (column_end >= self.columns) @intCast(self.width) else @intCast(column_end * tile_size);
        const bottom: i32 = if (row + 1 >= self.rows) @intCast(self.height) else @intCast((row + 1) * tile_size);
        return .{ .x = x, .y = y, .w = right - x, .h = bottom - y };
    }

    /// Turn the dirty tiles into repaint rectangles in `out`: coalesce each row's runs of
    /// adjacent dirty tiles, then merge a run into the rectangle directly above it when
    /// the spans match — so a dirty block of tiles becomes one rectangle. Returns the
    /// filled prefix of `out`.
    pub fn collect(self: *const TileGrid, out: []Rect) []Rect {
        var count: usize = 0;
        var row: u32 = 0;
        while (row < self.rows) : (row += 1) {
            var column: u32 = 0;
            while (column < self.columns) {
                if (!self.dirty[row * self.columns + column]) {
                    column += 1;
                    continue;
                }
                var run_end = column + 1;
                while (run_end < self.columns and self.dirty[row * self.columns + run_end]) run_end += 1;
                const rect = self.tileSpanRect(column, run_end, row);
                column = run_end;

                var merged = false;
                for (out[0..count]) |*existing| {
                    if (existing.x == rect.x and existing.w == rect.w and existing.bottom() == rect.y) {
                        existing.h += rect.h;
                        merged = true;
                        break;
                    }
                }
                if (merged) continue;
                if (count < out.len) {
                    out[count] = rect;
                    count += 1;
                } else {
                    out[count - 1] = out[count - 1].unite(rect);
                }
            }
        }
        return out[0..count];
    }
};

/// A block of 32-bit pixels: `pixels` addressed row-major with `stride` pixels between
/// row starts (≥ width — the framebuffer's stride is pitch/4, a layer's is its width).
pub const Surface = struct {
    pixels: [*]u32,
    stride: u32, // pixels per row
    width: u32,
    height: u32,

    pub fn bounds(s: Surface) Rect {
        return .{ .x = 0, .y = 0, .w = @intCast(s.width), .h = @intCast(s.height) };
    }

    inline fn row(s: Surface, y: u32) [*]u32 {
        return s.pixels + @as(usize, y) * s.stride;
    }
};

/// Fill `rect` of `s` with the native pixel `colour`, clipped to `s`'s bounds. Each row is
/// one `@memset` over the clipped span, so the compiler vectorizes it and the bounds check
/// runs once per row, not once per pixel.
pub fn fillRect(s: Surface, rect: Rect, colour: u32) void {
    const c = rect.intersect(s.bounds());
    if (c.isEmpty()) return;
    const x0: usize = @intCast(c.x);
    const span: usize = @intCast(c.w);
    var y: i32 = c.y;
    while (y < c.bottom()) : (y += 1) {
        @memset((s.row(@intCast(y)) + x0)[0..span], colour);
    }
}

/// Composite the whole of `layer` onto `dst` with the layer's top-left at (`dx`, `dy`),
/// painting only the pixels that fall inside `clip` (a `dst`-space rectangle) and inside
/// `dst`. Opaque copy. This is the primitive `present` repeats over the visible layer
/// stack, bottom to top, for each damaged region.
pub fn composite(dst: Surface, dx: i32, dy: i32, layer: Surface, clip: Rect) void {
    const on_screen = Rect{ .x = dx, .y = dy, .w = @intCast(layer.width), .h = @intCast(layer.height) };
    const region = on_screen.intersect(clip).intersect(dst.bounds());
    if (region.isEmpty()) return;
    const span: usize = @intCast(region.w);
    const dst_x: usize = @intCast(region.x);
    const src_x: usize = @intCast(region.x - dx);
    var y: i32 = region.y;
    while (y < region.bottom()) : (y += 1) {
        const source_row = layer.row(@intCast(y - dy)) + src_x;
        const destination_row = dst.row(@intCast(y)) + dst_x;
        @memcpy(destination_row[0..span], source_row[0..span]);
    }
}

/// Copy a `w`×`h` tile of native pixels from `src` (raw little-endian bytes, row-major,
/// tightly packed) into `dst` at (`dx`, `dy`), clipped to `dst`'s bounds. `src` comes
/// straight out of an IPC message buffer and carries no alignment guarantee, so each
/// clipped row is a byte-wise `@memcpy` — which equals the old per-pixel little-endian
/// `readInt` on every danos target (all little-endian) without the alignment concern.
/// Returns without touching anything if `src` is short.
pub fn blitTile(dst: Surface, dx: i32, dy: i32, src: []const u8, w: u32, h: u32) void {
    if (src.len < @as(usize, w) * h * 4) return;
    const region = Rect.init(dx, dy, @intCast(w), @intCast(h)).intersect(dst.bounds());
    if (region.isEmpty()) return;
    const span: usize = @intCast(region.w);
    const tile_x: usize = @intCast(region.x - dx);
    const dst_x: usize = @intCast(region.x);
    var y: i32 = region.y;
    while (y < region.bottom()) : (y += 1) {
        const tile_y: usize = @intCast(y - dy);
        const offset = (tile_y * w + tile_x) * 4;
        const destination_row = dst.row(@intCast(y)) + dst_x;
        @memcpy(std.mem.sliceAsBytes(destination_row[0..span]), src[offset..][0 .. span * 4]);
    }
}

// --- tests ------------------------------------------------------------------

test "rect intersect: overlap and disjoint" {
    try std.testing.expectEqual(Rect.init(5, 5, 5, 5), Rect.init(0, 0, 10, 10).intersect(Rect.init(5, 5, 10, 10)));
    try std.testing.expect(Rect.init(0, 0, 10, 10).intersect(Rect.init(20, 20, 5, 5)).isEmpty());
}

test "rect unite: bounding box, empty is identity" {
    const a = Rect.init(2, 2, 4, 4);
    try std.testing.expectEqual(Rect.init(2, 1, 10, 5), a.unite(Rect.init(10, 1, 2, 2)));
    try std.testing.expectEqual(a, a.unite(Rect.empty));
    try std.testing.expectEqual(a, Rect.empty.unite(a));
}

test "fillRect clips to surface and honours stride padding" {
    // A 4×3 surface inside a 6-wide allocation (stride 6 > width 4), like pitch padding.
    var mem = [_]u32{0} ** (6 * 3);
    const s = Surface{ .pixels = &mem, .stride = 6, .width = 4, .height = 3 };
    fillRect(s, Rect.init(-1, -1, 3, 3), 0xAB); // straddles the top-left corner
    try std.testing.expectEqual(@as(u32, 0xAB), mem[0 * 6 + 0]);
    try std.testing.expectEqual(@as(u32, 0xAB), mem[1 * 6 + 1]);
    try std.testing.expectEqual(@as(u32, 0), mem[0 * 6 + 2]); // beyond the 2-wide fill
    try std.testing.expectEqual(@as(u32, 0), mem[2 * 6 + 0]); // row 2 untouched
    try std.testing.expectEqual(@as(u32, 0), mem[0 * 6 + 4]); // stride padding untouched
}

test "composite: overlap shows the top layer, clipped to damage" {
    var back = [_]u32{0} ** (8 * 8);
    const dst = Surface{ .pixels = &back, .stride = 8, .width = 8, .height = 8 };
    var lo = [_]u32{0x11} ** (4 * 4);
    var hi = [_]u32{0x22} ** (4 * 4);
    const low = Surface{ .pixels = &lo, .stride = 4, .width = 4, .height = 4 };
    const high = Surface{ .pixels = &hi, .stride = 4, .width = 4, .height = 4 };
    composite(dst, 0, 0, low, dst.bounds()); // bottom at (0,0)
    composite(dst, 2, 2, high, dst.bounds()); // top overlaps at (2,2)
    try std.testing.expectEqual(@as(u32, 0x11), back[0 * 8 + 0]); // bottom-only
    try std.testing.expectEqual(@as(u32, 0x22), back[3 * 8 + 3]); // overlap → top wins
    try std.testing.expectEqual(@as(u32, 0x22), back[5 * 8 + 5]); // top-only
    try std.testing.expectEqual(@as(u32, 0), back[7 * 8 + 7]); // neither
}

test "composite honours the damage rectangle" {
    var back = [_]u32{0} ** (8 * 8);
    const dst = Surface{ .pixels = &back, .stride = 8, .width = 8, .height = 8 };
    var fill = [_]u32{0x33} ** (8 * 8);
    const layer = Surface{ .pixels = &fill, .stride = 8, .width = 8, .height = 8 };
    composite(dst, 0, 0, layer, Rect.init(2, 2, 2, 2)); // only this damage region
    try std.testing.expectEqual(@as(u32, 0x33), back[2 * 8 + 2]);
    try std.testing.expectEqual(@as(u32, 0x33), back[3 * 8 + 3]);
    try std.testing.expectEqual(@as(u32, 0), back[1 * 8 + 1]); // outside damage
    try std.testing.expectEqual(@as(u32, 0), back[4 * 8 + 4]); // outside damage
}

test "damage list keeps disjoint rectangles separate and merges overlap" {
    var list = DamageList{};
    list.add(Rect.init(0, 0, 10, 10));
    list.add(Rect.init(100, 100, 10, 10)); // far away: its own entry
    try std.testing.expectEqual(@as(usize, 2), list.slice().len);
    list.add(Rect.init(5, 5, 10, 10)); // overlaps the first: united into it
    try std.testing.expectEqual(@as(usize, 2), list.slice().len);
    try std.testing.expectEqual(Rect.init(0, 0, 15, 15), list.slice()[0]);
    try std.testing.expect(!list.isEmpty());
    list.clear();
    try std.testing.expect(list.isEmpty());
}

test "damage list folds overflow into the last entry instead of dropping it" {
    var list = DamageList{};
    var i: i32 = 0;
    while (i < DamageList.capacity) : (i += 1) {
        list.add(Rect.init(i * 100, 0, 10, 10)); // disjoint: fills every slot
    }
    try std.testing.expectEqual(@as(usize, DamageList.capacity), list.slice().len);
    const overflow = Rect.init(0, 5000, 10, 10);
    list.add(overflow);
    try std.testing.expectEqual(@as(usize, DamageList.capacity), list.slice().len);
    const last = list.slice()[DamageList.capacity - 1];
    try std.testing.expect(!last.intersect(overflow).isEmpty()); // still covered
}

test "damage list ignores empty rectangles" {
    var list = DamageList{};
    list.add(Rect.empty);
    try std.testing.expect(list.isEmpty());
}

test "tile grid coalesces a run of adjacent tiles into one rectangle" {
    var grid = TileGrid{};
    grid.reset(256, 128); // 4×2 tiles of 64 px
    grid.add(Rect.init(10, 10, 100, 10)); // spans tiles (0,0) and (1,0)
    var scratch: [TileGrid.maximum_rects]Rect = undefined;
    const rects = grid.collect(&scratch);
    try std.testing.expectEqual(@as(usize, 1), rects.len);
    try std.testing.expectEqual(Rect.init(0, 0, 128, 64), rects[0]);
}

test "tile grid: full-screen damage collapses back to a single rectangle" {
    var grid = TileGrid{};
    grid.reset(1280, 720); // 20×12 tiles; the bottom row is partial (720 = 11*64 + 16)
    grid.add(Rect.init(0, 0, 1280, 720));
    var scratch: [TileGrid.maximum_rects]Rect = undefined;
    const rects = grid.collect(&scratch);
    try std.testing.expectEqual(@as(usize, 1), rects.len);
    try std.testing.expectEqual(Rect.init(0, 0, 1280, 720), rects[0]);
}

test "tile grid keeps far-apart damage as separate rectangles" {
    var grid = TileGrid{};
    grid.reset(1280, 720);
    grid.add(Rect.init(0, 0, 10, 10)); // top-left tile
    grid.add(Rect.init(1000, 600, 10, 10)); // a far-away tile
    var scratch: [TileGrid.maximum_rects]Rect = undefined;
    const rects = grid.collect(&scratch);
    try std.testing.expectEqual(@as(usize, 2), rects.len);
}

test "tile grid clamps edge tiles to the true screen size" {
    var grid = TileGrid{};
    grid.reset(100, 100); // 2×2 tiles, both partial in each axis
    grid.add(Rect.init(0, 0, 100, 100));
    var scratch: [TileGrid.maximum_rects]Rect = undefined;
    const rects = grid.collect(&scratch);
    try std.testing.expectEqual(@as(usize, 1), rects.len);
    try std.testing.expectEqual(Rect.init(0, 0, 100, 100), rects[0]);
}

test "tile grid clear empties it and reset resizes it" {
    var grid = TileGrid{};
    grid.reset(256, 256);
    grid.add(Rect.init(0, 0, 256, 256));
    try std.testing.expect(!grid.isEmpty());
    grid.clear();
    try std.testing.expect(grid.isEmpty());
    try std.testing.expect(grid.matches(256, 256));
    grid.reset(512, 512);
    try std.testing.expect(!grid.matches(256, 256));
    try std.testing.expect(grid.isEmpty());
}

test "blitTile copies a packed tile, clipping and reading unaligned bytes" {
    var back = [_]u32{0} ** (4 * 4);
    const dst = Surface{ .pixels = &back, .stride = 4, .width = 4, .height = 4 };
    // A 2×2 tile in a byte buffer offset by one byte, so reads are unaligned.
    var raw = [_]u8{0} ** (1 + 2 * 2 * 4);
    const tile = raw[1..];
    for (0..4) |i| std.mem.writeInt(u32, tile[i * 4 ..][0..4], @intCast(0xA0 + i), .little);
    blitTile(dst, 3, 3, tile, 2, 2); // bottom-right corner; only (3,3) lands on-surface
    try std.testing.expectEqual(@as(u32, 0xA0), back[3 * 4 + 3]);
    try std.testing.expectEqual(@as(u32, 0), back[0]); // nothing else touched
}
