//! User-space display client: talk to the display service (query the mode, and — from D3
//! — create layers, draw, and present) without hand-rolling the IPC. The `runtime.block`
//! shape: a cached `.display` lookup with a boot-race retry, then extern-struct request/
//! reply marshalling. See system/services/display/ and docs/display.md.

const std = @import("std");
const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("display-protocol");

/// The display's current mode, as `info()` reports it.
pub const Info = struct {
    width: u32,
    height: u32,
    pitch: u32, // bytes per row (may exceed width*4; see docs/framebuffer.md)
    format: u32, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
};

/// The service endpoint, looked up once and cached.
var handle: ?ipc.Handle = null;

/// Look up the display service, retrying while it comes up (a client races its
/// registration at boot). Returns the endpoint, or null if it never appears.
fn service() ?ipc.Handle {
    if (handle) |h| return h;
    var attempts: usize = 0;
    while (attempts < 100) : (attempts += 1) {
        if (ipc.lookup(.display)) |h| {
            handle = h;
            return h;
        }
        system.sleep(50);
    }
    return null;
}

/// Send one request, receive its reply; true on a zero status. `out` receives the reply
/// so callers can read `info`/`layer` fields on success.
fn transact(request: protocol.Request, out: *protocol.Reply) bool {
    const h = service() orelse return false;
    var req = request;
    var reply: [protocol.reply_size]u8 = undefined;
    const len = ipc.call(h, std.mem.asBytes(&req), &reply) catch return false;
    if (len < protocol.reply_size) return false;
    out.* = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
    return out.status == 0;
}

/// The display's current mode, or null if the service never came up.
pub fn info() ?Info {
    var reply: protocol.Reply = undefined;
    if (!transact(.{ .operation = @intFromEnum(protocol.Operation.info) }, &reply)) return null;
    return .{ .width = reply.width, .height = reply.height, .pitch = reply.pitch, .format = reply.format };
}

/// Composite the dirty layers and flush the frame to the screen.
pub fn present() bool {
    var reply: protocol.Reply = undefined;
    return transact(.{ .operation = @intFromEnum(protocol.Operation.present) }, &reply);
}

/// One selectable display mode.
pub const Mode = protocol.Mode;

/// Fill `out` with the resolutions the display can switch to; returns how many were written
/// (zero on the GOP floor, or if the service never came up).
pub fn modes(out: []Mode) usize {
    const h = service() orelse return 0;
    var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.get_modes) };
    var reply: [protocol.modes_reply_size]u8 = undefined;
    const len = ipc.call(h, std.mem.asBytes(&request), &reply) catch return 0;
    if (len < protocol.modes_reply_size) return 0;
    const answer = std.mem.bytesToValue(protocol.ModesReply, reply[0..protocol.modes_reply_size]);
    if (answer.status != 0) return 0;
    const count = @min(@min(answer.count, protocol.max_modes), out.len);
    for (0..count) |i| out[i] = answer.modes[i];
    return count;
}

/// Change the display resolution. Only a native backend that supports mode-setting honours it
/// (on the GOP floor it returns false); on success the display's `info()` reports the new mode.
pub fn setMode(width: u32, height: u32) bool {
    var reply: protocol.Reply = undefined;
    const changed = transact(.{ .operation = @intFromEnum(protocol.Operation.set_mode), .width = width, .height = height }, &reply);
    if (changed) mode = null; // the cached mode is stale now
    return changed;
}

/// The mode, cached after the first `info()` so `color()` doesn't round-trip per pixel.
var mode: ?Info = null;

fn cachedInfo() ?Info {
    if (mode) |m| return m;
    const i = info() orelse return null;
    mode = i;
    return i;
}

/// The native pixel value for an 8-bit-per-channel colour, in the display's format. A
/// client packs colours through this so it never has to know the byte order itself.
pub fn color(r: u8, g: u8, b: u8) u32 {
    const format = if (cachedInfo()) |i| i.format else 0;
    return protocol.pack(format, r, g, b);
}

/// A handle to a server-owned layer: a positioned, z-ordered surface the client draws
/// into by command. Create with `createLayer`; drawing and moves take effect on the next
/// `present`. Coordinates are signed (a layer may sit partly off-screen).
pub const Layer = struct {
    id: u32,

    /// Fill a rectangle of this layer (layer-local coordinates) with a native `colour`.
    pub fn fill(self: Layer, x: i32, y: i32, w: u32, h: u32, colour: u32) bool {
        var reply: protocol.Reply = undefined;
        return transact(.{
            .operation = @intFromEnum(protocol.Operation.fill_rect),
            .layer = self.id,
            .x = @bitCast(x),
            .y = @bitCast(y),
            .width = w,
            .height = h,
            .colour = colour,
        }, &reply);
    }

    /// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
    /// layer at (`x`, `y`). The tile rides inline in the request, so `w*h*4` must fit
    /// `protocol.maximum_payload`.
    pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
        var request = protocol.Request{
            .operation = @intFromEnum(protocol.Operation.blit_tile),
            .layer = self.id,
            .x = @bitCast(x),
            .y = @bitCast(y),
            .width = w,
            .height = h,
        };
        const header = std.mem.asBytes(&request);
        if (header.len + pixels.len > protocol.message_maximum) return false;
        var buffer: [protocol.message_maximum]u8 = undefined;
        @memcpy(buffer[0..header.len], header);
        @memcpy(buffer[header.len..][0..pixels.len], pixels);
        const h_svc = service() orelse return false;
        var reply: [protocol.reply_size]u8 = undefined;
        const len = ipc.call(h_svc, buffer[0 .. header.len + pixels.len], &reply) catch return false;
        if (len < protocol.reply_size) return false;
        return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
    }

    /// Move / restack / show or hide the layer.
    pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
        var reply: protocol.Reply = undefined;
        return transact(.{
            .operation = @intFromEnum(protocol.Operation.configure_layer),
            .layer = self.id,
            .x = @bitCast(x),
            .y = @bitCast(y),
            .z = z,
            .visible = if (visible) 1 else 0,
        }, &reply);
    }

    /// Mark a rectangle of this layer (layer-local) dirty for the next present — for when
    /// the layer's pixels changed without a drawing call the compositor already tracked.
    pub fn damage(self: Layer, x: i32, y: i32, w: u32, h: u32) bool {
        var reply: protocol.Reply = undefined;
        return transact(.{
            .operation = @intFromEnum(protocol.Operation.damage),
            .layer = self.id,
            .x = @bitCast(x),
            .y = @bitCast(y),
            .width = w,
            .height = h,
        }, &reply);
    }

    /// Release the layer and its surface.
    pub fn destroy(self: Layer) bool {
        var reply: protocol.Reply = undefined;
        return transact(.{ .operation = @intFromEnum(protocol.Operation.destroy_layer), .layer = self.id }, &reply);
    }
};

/// Create a server-owned layer of `w`×`h` pixels at screen (`x`, `y`) with stacking order
/// `z` (higher is nearer the front), initially visible. Returns a handle, or null.
pub fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32) ?Layer {
    var reply: protocol.Reply = undefined;
    if (!transact(.{
        .operation = @intFromEnum(protocol.Operation.create_layer),
        .x = @bitCast(x),
        .y = @bitCast(y),
        .width = w,
        .height = h,
        .z = z,
        .visible = 1,
    }, &reply)) return null;
    return .{ .id = reply.layer };
}
