//! 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 `/protocol/display` open with a boot-race retry, then extern-struct request/
//! reply marshalling. See system/services/display/ and docs/display.md.

const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc");
const time = @import("time");
const display_protocol = @import("display-protocol");

const Protocol = display_protocol.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;

/// Open `/protocol/display`, retrying while it comes up (a client races the
/// service's bind 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 (channel.openEndpoint("display")) |h| {
            handle = h;
            return h;
        }
        time.sleepMillis(50);
    }
    return null;
}

/// A reply the compositor answered with, kept whole so the caller can decode the
/// verb's own fixed part out of it.
const Answered = struct {
    packet: [display_protocol.message_maximum]u8,
    len: usize,

    fn bytes(self: *const Answered) []const u8 {
        return self.packet[0..self.len];
    }
};

/// Send one request (`target` addresses a layer, or 0 for the compositor itself)
/// and keep the reply. Null when the transport failed or the compositor refused.
fn transact(
    comptime operation: Protocol.Operation,
    target: u64,
    request: Protocol.RequestOf(operation),
    tail: []const u8,
) ?Answered {
    const h = service() orelse return null;
    var packet: [display_protocol.message_maximum]u8 = undefined;
    const framed = Protocol.encodeRequest(operation, target, request, tail, &packet) orelse return null;
    var answered: Answered = .{ .packet = undefined, .len = 0 };
    answered.len = ipc.call(h, framed, &answered.packet) catch return null;
    const status = envelope.statusOf(answered.bytes()) orelse return null;
    if (status.status != 0) return null;
    return answered;
}

/// The display's current mode, or null if the service never came up.
pub fn info() ?Info {
    const answered = transact(.info, 0, {}, &.{}) orelse return null;
    const reply = Protocol.decodeReply(.info, answered.bytes()) orelse 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 {
    return transact(.present, 0, {}, &.{}) != null;
}

/// One selectable display mode.
pub const Mode = display_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 answered = transact(.get_modes, 0, {}, &.{}) orelse return 0;
    const offered = Protocol.decodeReply(.get_modes, answered.bytes()) orelse return 0;
    const count = @min(@min(offered.count, display_protocol.max_modes), out.len);
    for (0..count) |i| out[i] = offered.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 {
    const changed = transact(.set_mode, 0, .{ .width = width, .height = height }, &.{}) != null;
    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 display_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).
///
/// The id is the packet header's `target` on every call below, so it is named once
/// per request rather than repeated inside one.
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 {
        return transact(.fill_rect, self.id, .{ .x = x, .y = y, .width = w, .height = h, .colour = colour }, &.{}) != null;
    }

    /// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
    /// layer at (`x`, `y`). The tile rides inline as the request's tail, so `w*h*4` must
    /// fit `display_protocol.maximum_payload` — the bound the protocol derives from this
    /// verb's own fixed part, so the check here can never drift from what fits.
    pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
        if (pixels.len > display_protocol.maximum_payload) return false;
        return transact(.blit_tile, self.id, .{ .x = x, .y = y, .width = w, .height = h }, pixels) != null;
    }

    /// Move / restack / show or hide the layer.
    pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
        return transact(.configure_layer, self.id, .{ .x = x, .y = y, .z = z, .visible = if (visible) 1 else 0 }, &.{}) != null;
    }

    /// 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 {
        return transact(.damage, self.id, .{ .x = x, .y = y, .width = w, .height = h }, &.{}) != null;
    }

    /// Release the layer and its surface.
    pub fn destroy(self: Layer) bool {
        return transact(.destroy_layer, self.id, {}, &.{}) != null;
    }
};

/// 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 {
    const answered = transact(.create_layer, 0, .{ .x = x, .y = y, .width = w, .height = h, .z = z, .visible = 1 }, &.{}) orelse return null;
    return .{ .id = (Protocol.decodeReply(.create_layer, answered.bytes()) orelse return null).layer };
}
