//! Shared z2d drawing primitives for the chart renderers in
//! `src/charts/` (chart.zig, projection_chart.zig, forecast_chart.zig,
//! line_chart.zig).
//!
//! These helpers were copy-pasted verbatim across all four renderers
//! before this module existed; they're consolidated here so there is a
//! single source of truth. Everything here is a pure, stateless z2d
//! operation parameterized by pixel coordinates and pre-blended colors:
//! no theme, domain, or chart-shape knowledge lives here.
//!
//! The Surface/Context lifetime (the `Surface.init` + `errdefer`/`defer
//! deinit` dance and the AA/operator setup) deliberately stays with
//! each renderer - that ownership does not extract cleanly - so only
//! the stateless drawing belongs in this module.

const std = @import("std");
const z2d = @import("z2d");

const Surface = z2d.Surface;
const Context = z2d.Context;
const Pixel = z2d.Pixel;

/// Map a data value to a y pixel coordinate within `[top_px, bottom_px]`.
/// A larger value maps nearer `top_px` (screen space grows downward). A
/// degenerate (`min_val == max_val`) range maps to the vertical midpoint.
pub fn mapY(value: f64, min_val: f64, max_val: f64, top_px: f64, bottom_px: f64) f64 {
    if (max_val == min_val) return (top_px + bottom_px) / 2;
    const norm = (value - min_val) / (max_val - min_val);
    return bottom_px - norm * (bottom_px - top_px);
}

/// Pre-blend a foreground color with `alpha` against a background color
/// and return a fully opaque pixel. This sidesteps z2d's src_over
/// compositor (which overflows on semi-transparent fills); renderers
/// draw with the `.src` operator and pre-blend through here instead.
pub fn blendColor(fg: [3]u8, alpha: u8, bg_color: [3]u8) Pixel {
    const a = @as(f64, @floatFromInt(alpha)) / 255.0;
    const inv_a = 1.0 - a;
    return .{ .rgb = .{
        .r = @intFromFloat(@as(f64, @floatFromInt(fg[0])) * a + @as(f64, @floatFromInt(bg_color[0])) * inv_a),
        .g = @intFromFloat(@as(f64, @floatFromInt(fg[1])) * a + @as(f64, @floatFromInt(bg_color[1])) * inv_a),
        .b = @intFromFloat(@as(f64, @floatFromInt(fg[2])) * a + @as(f64, @floatFromInt(bg_color[2])) * inv_a),
    } };
}

/// Opaque pixel from an RGB triple.
pub fn opaqueColor(c: [3]u8) Pixel {
    return .{ .rgb = .{ .r = c[0], .g = c[1], .b = c[2] } };
}

/// Fill the whole `width` x `height` surface with an opaque background.
/// Mirrors the "Background" layer every renderer paints first.
pub fn fillBackground(ctx: *Context, width: f64, height: f64, bg: [3]u8) !void {
    ctx.setSourceToPixel(opaqueColor(bg));
    ctx.resetPath();
    try ctx.moveTo(0, 0);
    try ctx.lineTo(width, 0);
    try ctx.lineTo(width, height);
    try ctx.lineTo(0, height);
    try ctx.closePath();
    try ctx.fill();
}

/// Draw `n_lines - 1` evenly-spaced horizontal grid lines strictly
/// between `top` and `bottom` (the edges themselves are left to the
/// panel border). Restores the line width to 2.0 when done so callers
/// can keep drawing without re-setting it.
pub fn drawHorizontalGridLines(
    ctx: *Context,
    left: f64,
    right: f64,
    top: f64,
    bottom: f64,
    n_lines: usize,
    col: Pixel,
) !void {
    ctx.setSourceToPixel(col);
    ctx.setLineWidth(0.5);
    for (1..n_lines) |i| {
        const frac = @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(n_lines));
        const y = top + frac * (bottom - top);
        ctx.resetPath();
        try ctx.moveTo(left, y);
        try ctx.lineTo(right, y);
        try ctx.stroke();
    }
    ctx.setLineWidth(2.0);
}

/// Draw a horizontal line at `y` from `x1` to `x2`. Restores the line
/// width to 2.0 afterward.
pub fn drawHLine(ctx: *Context, x1: f64, x2: f64, y: f64, col: Pixel, line_w: f64) !void {
    ctx.setSourceToPixel(col);
    ctx.setLineWidth(line_w);
    ctx.resetPath();
    try ctx.moveTo(x1, y);
    try ctx.lineTo(x2, y);
    try ctx.stroke();
    ctx.setLineWidth(2.0);
}

/// Draw a vertical line at `x` from `y1` to `y2`. Restores the line
/// width to 2.0 afterward.
pub fn drawVLine(ctx: *Context, x: f64, y1: f64, y2: f64, col: Pixel, line_w: f64) !void {
    ctx.setSourceToPixel(col);
    ctx.setLineWidth(line_w);
    ctx.resetPath();
    try ctx.moveTo(x, y1);
    try ctx.lineTo(x, y2);
    try ctx.stroke();
    ctx.setLineWidth(2.0);
}

/// Stroke an axis-aligned rectangle outline. Restores the line width to
/// 2.0 afterward.
pub fn drawRect(ctx: *Context, x1: f64, y1: f64, x2: f64, y2: f64, col: Pixel, line_w: f64) !void {
    ctx.setSourceToPixel(col);
    ctx.setLineWidth(line_w);
    ctx.resetPath();
    try ctx.moveTo(x1, y1);
    try ctx.lineTo(x2, y1);
    try ctx.lineTo(x2, y2);
    try ctx.lineTo(x1, y2);
    try ctx.closePath();
    try ctx.stroke();
    ctx.setLineWidth(2.0);
}

/// Extract a flat `[]u8` of R,G,B triplets from an `image_surface_rgb`.
/// Caller owns the returned slice; the surface is left intact. Every
/// renderer transmits/encodes pixels through this same shape (Kitty
/// graphics RGB and z2d PNG export both want tightly-packed RGB bytes).
pub fn extractRgb(alloc: std.mem.Allocator, sfc: *const Surface) ![]u8 {
    const rgb_buf = switch (sfc.*) {
        .image_surface_rgb => |s| s.buf,
        else => unreachable,
    };
    const raw = try alloc.alloc(u8, rgb_buf.len * 3);
    for (rgb_buf, 0..) |px, i| {
        raw[i * 3 + 0] = px.r;
        raw[i * 3 + 1] = px.g;
        raw[i * 3 + 2] = px.b;
    }
    return raw;
}

// ── Tests ─────────────────────────────────────────────────────────────

const testing = std.testing;

/// Build a fresh RGB drawing context backed by `sfc`, configured the way
/// every renderer configures it (AA off, `.src` operator). Caller owns
/// both and must `ctx.deinit()` / `sfc.deinit(alloc)`.
fn testContext(sfc: *Surface) Context {
    var ctx = Context.init(testing.io, testing.allocator, sfc);
    ctx.setAntiAliasingMode(.none);
    ctx.setOperator(.src);
    return ctx;
}

/// Count pixels in the surface that exactly match the given RGB. A
/// shared test helper for the chart modules in this directory (draw,
/// text, axis) that need to assert glyphs/lines actually landed in a
/// known color. `pub` only so the sibling test files can reuse it.
pub fn countColor(sfc: *const Surface, color: [3]u8) usize {
    const buf = switch (sfc.*) {
        .image_surface_rgb => |s| s.buf,
        else => unreachable,
    };
    var n: usize = 0;
    for (buf) |px| {
        if (px.r == color[0] and px.g == color[1] and px.b == color[2]) n += 1;
    }
    return n;
}

test "mapY maps value to pixel coordinate" {
    // value at min -> bottom
    try testing.expectEqual(@as(f64, 500.0), mapY(0, 0, 100, 100, 500));
    // value at max -> top
    try testing.expectEqual(@as(f64, 100.0), mapY(100, 0, 100, 100, 500));
    // value at midpoint -> midpoint
    try testing.expectEqual(@as(f64, 300.0), mapY(50, 0, 100, 100, 500));
    // flat range -> midpoint
    try testing.expectEqual(@as(f64, 300.0), mapY(42, 42, 42, 100, 500));
}

test "blendColor alpha blending" {
    const white = [3]u8{ 255, 255, 255 };
    const black = [3]u8{ 0, 0, 0 };

    // Full alpha -> foreground.
    const full = blendColor(white, 255, black);
    try testing.expectEqual(@as(u8, 255), full.rgb.r);
    try testing.expectEqual(@as(u8, 255), full.rgb.g);
    try testing.expectEqual(@as(u8, 255), full.rgb.b);

    // Zero alpha -> background.
    const zero = blendColor(white, 0, black);
    try testing.expectEqual(@as(u8, 0), zero.rgb.r);

    // Half alpha -> midpoint (255 * 128/255 ~= 128).
    const half = blendColor(white, 128, black);
    try testing.expect(half.rgb.r >= 127 and half.rgb.r <= 129);

    // Zero alpha blends toward a non-black background, not just black.
    const onto_gray = blendColor(white, 0, .{ 40, 50, 60 });
    try testing.expectEqual(@as(u8, 40), onto_gray.rgb.r);
    try testing.expectEqual(@as(u8, 50), onto_gray.rgb.g);
    try testing.expectEqual(@as(u8, 60), onto_gray.rgb.b);
}

test "opaqueColor wraps an RGB triple" {
    const px = opaqueColor(.{ 0x7f, 0xd8, 0x8f });
    try testing.expectEqual(@as(u8, 0x7f), px.rgb.r);
    try testing.expectEqual(@as(u8, 0xd8), px.rgb.g);
    try testing.expectEqual(@as(u8, 0x8f), px.rgb.b);
}

test "fillBackground paints every pixel the bg color" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 8, 6);
    defer sfc.deinit(alloc);
    var ctx = testContext(&sfc);
    defer ctx.deinit();

    try fillBackground(&ctx, 8, 6, .{ 0x11, 0x22, 0x33 });

    const buf = switch (sfc) {
        .image_surface_rgb => |s| s.buf,
        else => unreachable,
    };
    for (buf) |px| {
        try testing.expectEqual(@as(u8, 0x11), px.r);
        try testing.expectEqual(@as(u8, 0x22), px.g);
        try testing.expectEqual(@as(u8, 0x33), px.b);
    }
}

test "extractRgb yields 3 interleaved bytes per pixel" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 4, 4);
    defer sfc.deinit(alloc);
    var ctx = testContext(&sfc);
    defer ctx.deinit();
    try fillBackground(&ctx, 4, 4, .{ 0xde, 0xad, 0xbe });

    const raw = try extractRgb(alloc, &sfc);
    defer alloc.free(raw);

    const buf = switch (sfc) {
        .image_surface_rgb => |s| s.buf,
        else => unreachable,
    };
    try testing.expectEqual(buf.len * 3, raw.len);
    // First pixel round-trips as (R, G, B) at indices 0, 1, 2.
    try testing.expectEqual(@as(u8, 0xde), raw[0]);
    try testing.expectEqual(@as(u8, 0xad), raw[1]);
    try testing.expectEqual(@as(u8, 0xbe), raw[2]);
}

test "drawHLine strokes a line in the requested color" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 40, 24);
    defer sfc.deinit(alloc);
    var ctx = testContext(&sfc);
    defer ctx.deinit();
    try fillBackground(&ctx, 40, 24, .{ 0, 0, 0 });

    try drawHLine(&ctx, 2, 38, 12, opaqueColor(.{ 0xff, 0x00, 0x00 }), 1.0);
    try testing.expect(countColor(&sfc, .{ 0xff, 0x00, 0x00 }) > 0);
}

test "drawVLine strokes a line in the requested color" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 40, 24);
    defer sfc.deinit(alloc);
    var ctx = testContext(&sfc);
    defer ctx.deinit();
    try fillBackground(&ctx, 40, 24, .{ 0, 0, 0 });

    try drawVLine(&ctx, 20, 2, 22, opaqueColor(.{ 0x00, 0xff, 0x00 }), 1.0);
    try testing.expect(countColor(&sfc, .{ 0x00, 0xff, 0x00 }) > 0);
}

test "drawRect strokes a rectangle outline in the requested color" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 40, 24);
    defer sfc.deinit(alloc);
    var ctx = testContext(&sfc);
    defer ctx.deinit();
    try fillBackground(&ctx, 40, 24, .{ 0, 0, 0 });

    try drawRect(&ctx, 4, 4, 36, 20, opaqueColor(.{ 0x00, 0x00, 0xff }), 1.0);
    try testing.expect(countColor(&sfc, .{ 0x00, 0x00, 0xff }) > 0);
}

test "drawHorizontalGridLines strokes lines in the requested color" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 40, 40);
    defer sfc.deinit(alloc);
    var ctx = testContext(&sfc);
    defer ctx.deinit();
    try fillBackground(&ctx, 40, 40, .{ 0, 0, 0 });

    try drawHorizontalGridLines(&ctx, 2, 38, 2, 38, 5, opaqueColor(.{ 0x33, 0x66, 0x99 }));
    try testing.expect(countColor(&sfc, .{ 0x33, 0x66, 0x99 }) > 0);
}
