//! Projection comparison chart: overlays two percentile-band
//! envelopes - a "then" projection (computed as of a past snapshot)
//! and a "now" projection - so the viewer can see how the forecast
//! envelope shifted between the two dates.
//!
//! Both projections are aligned at year 0 (each one's own start), so
//! the x-axis is "years from the projection's start" and the overlay
//! answers "how did my projected envelope move between then and now?".
//!
//! Each side draws a light p10-p90 fill plus a solid median line in
//! its own hue (then = `theme.info` / cyan, now = `theme.accent` /
//! purple), keyed by a small top-left "then"/"now" color legend.
//!
//! Visual layers (bottom to top):
//!   - Background
//!   - Horizontal grid lines
//!   - "then" envelope fill, then "now" envelope fill
//!   - "then" median, then "now" median (both on top of both fills)
//!   - Zero line (if visible)
//!   - Panel border
//!   - then/now color legend (top-left)
//!   - Axis labels (export only)

const std = @import("std");
const z2d = @import("z2d");
const theme = @import("../tui/theme.zig");
const projections = @import("../analytics/projections.zig");
const draw = @import("draw.zig");
const axis = @import("axis.zig");
const text = @import("text.zig");

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

const margin_left: f64 = 4;
const margin_right: f64 = 4;
const margin_top: f64 = 4;
const margin_bottom: f64 = 4;

/// Comparison chart render result (RGB for kitty graphics).
pub const CompareChartResult = struct {
    rgb_data: []const u8,
    width: u16,
    height: u16,
    value_min: f64,
    value_max: f64,
};

/// Owned by the caller - call `result.deinit(alloc)` when done. The
/// surface seam shared between RGB extraction (kitty) and PNG export.
/// Mirrors `projection_chart.RenderedProjection`.
pub const RenderedCompare = struct {
    surface: Surface,
    width: u16,
    height: u16,
    value_min: f64,
    value_max: f64,

    pub fn deinit(self: *RenderedCompare, alloc: std.mem.Allocator) void {
        self.surface.deinit(alloc);
        self.* = undefined;
    }

    pub fn extractRgb(self: *const RenderedCompare, alloc: std.mem.Allocator) ![]u8 {
        return draw.extractRgb(alloc, &self.surface);
    }
};

/// Render the "then" vs "now" comparison overlay into a `Surface`.
/// Both band slices are aligned at year 0; the x-axis spans the
/// longer of the two horizons. With `axis_labels`, reserves margins
/// and stamps y-axis dollar ticks + x-axis year endpoints (export
/// path); the kitty wrapper passes `false`.
pub fn renderToSurface(
    io: std.Io,
    alloc: std.mem.Allocator,
    then_bands: []const projections.YearPercentiles,
    now_bands: []const projections.YearPercentiles,
    width_px: u32,
    height_px: u32,
    th: theme.Theme,
    axis_labels: bool,
) !RenderedCompare {
    if (then_bands.len < 2 or now_bands.len < 2) return error.InsufficientData;

    const w: i32 = @intCast(width_px);
    const h: i32 = @intCast(height_px);
    var sfc = try Surface.init(.image_surface_rgb, alloc, w, h);
    errdefer sfc.deinit(alloc);

    var ctx = Context.init(io, alloc, &sfc);
    defer ctx.deinit();

    ctx.setAntiAliasingMode(.none);
    ctx.setOperator(.src);

    const bg = th.bg;
    const fwidth: f64 = @floatFromInt(width_px);
    const fheight: f64 = @floatFromInt(height_px);

    try draw.fillBackground(&ctx, fwidth, fheight, bg);

    // Chart area. With axis labels, reserve a right margin for the
    // y-axis dollar ticks and a bottom margin for the year endpoints.
    const label_scale: i32 = axis.labelScale(h);
    const label_char_h: f64 = axis.charHeight(label_scale);
    const m_left: f64 = if (axis_labels) label_char_h else margin_left;
    const m_right: f64 = if (axis_labels) axis.yAxisMargin(label_scale) else margin_right;
    const m_top: f64 = if (axis_labels) (label_char_h / 2 + 4) else margin_top;
    const m_bottom: f64 = if (axis_labels) axis.bottomMargin(label_scale) else margin_bottom;
    const chart_left = m_left;
    const chart_right = fwidth - m_right;
    const chart_w = chart_right - chart_left;
    const chart_top = m_top;
    const chart_bottom = fheight - m_bottom;

    // Value range across BOTH envelopes (p10 floor, p90 ceiling).
    var value_min: f64 = then_bands[0].p10;
    var value_max: f64 = then_bands[0].p90;
    for (then_bands) |bp| {
        if (bp.p10 < value_min) value_min = bp.p10;
        if (bp.p90 > value_max) value_max = bp.p90;
    }
    for (now_bands) |bp| {
        if (bp.p10 < value_min) value_min = bp.p10;
        if (bp.p90 > value_max) value_max = bp.p90;
    }
    const pad = (value_max - value_min) * 0.05;
    value_min -= pad;
    value_max += pad;
    if (value_min < 0) value_min = 0;

    // X step: align both at year 0; the longer horizon spans the full
    // width. `bands[i].year == i`, so index doubles as the year offset.
    const n = @max(then_bands.len, now_bands.len);
    const x_step = chart_w / @as(f64, @floatFromInt(n - 1));

    // Grid lines.
    try draw.drawHorizontalGridLines(&ctx, chart_left, chart_right, chart_top, chart_bottom, 5, draw.blendColor(th.text_muted, 40, bg));

    // Envelopes: draw BOTH light fills first, then BOTH medians on
    // top. Rendering uses the `.src` operator (replace, not blend), so
    // drawing a fill after a median would occlude that median - hence
    // the two-pass order. "now" fill goes on top of "then" fill.
    try drawEnvelopeFill(&ctx, then_bands, chart_left, x_step, value_min, value_max, chart_top, chart_bottom, th.info, bg);
    try drawEnvelopeFill(&ctx, now_bands, chart_left, x_step, value_min, value_max, chart_top, chart_bottom, th.accent, bg);
    try drawEnvelopeMedian(&ctx, then_bands, chart_left, x_step, value_min, value_max, chart_top, chart_bottom, th.info);
    try drawEnvelopeMedian(&ctx, now_bands, chart_left, x_step, value_min, value_max, chart_top, chart_bottom, th.accent);

    // Zero line (if visible).
    if (value_min <= 0 and value_max > 0) {
        const zero_y = draw.mapY(0, value_min, value_max, chart_top, chart_bottom);
        try draw.drawHLine(&ctx, chart_left, chart_right, zero_y, draw.blendColor(th.negative, 120, bg), 1.0);
    }

    // Panel border.
    try draw.drawRect(&ctx, chart_left, chart_top, chart_right, chart_bottom, draw.blendColor(th.border, 80, bg), 1.0);

    // Color legend (top-left): keys the two envelope hues. Two
    // unlabeled colored medians would otherwise be ambiguous.
    {
        const lgh: i32 = @intFromFloat(axis.charHeight(label_scale));
        const lx: i32 = @as(i32, @intFromFloat(chart_left)) + 4 * label_scale;
        const ly: i32 = @as(i32, @intFromFloat(chart_top)) + 2 * label_scale;
        text.drawText(&sfc, lx, ly, label_scale, th.info, "then");
        text.drawText(&sfc, lx, ly + lgh + 2 * label_scale, label_scale, th.accent, "now");
    }

    // Axis labels (export only): y dollar ticks + x year endpoints.
    if (axis_labels) {
        axis.drawYTicks(&sfc, label_scale, th.text_muted, chart_right, chart_top, chart_bottom, value_min, value_max, 5, .dollars);
        var fbuf: [8]u8 = undefined;
        var lbuf: [8]u8 = undefined;
        const first_s = std.fmt.bufPrint(&fbuf, "{d}", .{0}) catch "0";
        const last_s = std.fmt.bufPrint(&lbuf, "{d}", .{n - 1}) catch "";
        const yr_y = chart_bottom + axis.labelGap(label_scale);
        axis.drawXEndpoints(&sfc, label_scale, th.text_muted, chart_left, chart_right, yr_y, first_s, last_s);
    }

    return .{
        .surface = sfc,
        .width = @intCast(width_px),
        .height = @intCast(height_px),
        .value_min = value_min,
        .value_max = value_max,
    };
}

/// Thin RGB wrapper over `renderToSurface` for the inline kitty path:
/// renders without axis labels, extracts RGB, frees the surface.
pub fn renderCompareChart(
    io: std.Io,
    alloc: std.mem.Allocator,
    then_bands: []const projections.YearPercentiles,
    now_bands: []const projections.YearPercentiles,
    width_px: u32,
    height_px: u32,
    th: theme.Theme,
) !CompareChartResult {
    var rendered = try renderToSurface(io, alloc, then_bands, now_bands, width_px, height_px, th, false);
    defer rendered.deinit(alloc);
    return .{
        .rgb_data = try rendered.extractRgb(alloc),
        .width = rendered.width,
        .height = rendered.height,
        .value_min = rendered.value_min,
        .value_max = rendered.value_max,
    };
}

/// Draw one envelope's light p10-p90 fill in `hue`. Indices map to x
/// via `chart_left + i * x_step`.
fn drawEnvelopeFill(
    ctx: *Context,
    bands: []const projections.YearPercentiles,
    chart_left: f64,
    x_step: f64,
    value_min: f64,
    value_max: f64,
    chart_top: f64,
    chart_bottom: f64,
    hue: [3]u8,
    bg: [3]u8,
) !void {
    ctx.setSourceToPixel(draw.blendColor(hue, 22, bg));
    ctx.resetPath();
    for (bands, 0..) |bp, i| {
        const x = chart_left + @as(f64, @floatFromInt(i)) * x_step;
        const y = draw.mapY(bp.p90, value_min, value_max, chart_top, chart_bottom);
        if (i == 0) try ctx.moveTo(x, y) else try ctx.lineTo(x, y);
    }
    var j: usize = bands.len;
    while (j > 0) {
        j -= 1;
        const x = chart_left + @as(f64, @floatFromInt(j)) * x_step;
        const y = draw.mapY(bands[j].p10, value_min, value_max, chart_top, chart_bottom);
        try ctx.lineTo(x, y);
    }
    try ctx.closePath();
    try ctx.fill();
}

/// Draw one envelope's solid p50 median line in `hue`.
fn drawEnvelopeMedian(
    ctx: *Context,
    bands: []const projections.YearPercentiles,
    chart_left: f64,
    x_step: f64,
    value_min: f64,
    value_max: f64,
    chart_top: f64,
    chart_bottom: f64,
    hue: [3]u8,
) !void {
    ctx.setSourceToPixel(draw.opaqueColor(hue));
    ctx.setLineWidth(2.0);
    ctx.resetPath();
    for (bands, 0..) |bp, i| {
        const x = chart_left + @as(f64, @floatFromInt(i)) * x_step;
        const y = draw.mapY(bp.p50, value_min, value_max, chart_top, chart_bottom);
        if (i == 0) try ctx.moveTo(x, y) else try ctx.lineTo(x, y);
    }
    try ctx.stroke();
}

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

fn syntheticBands(buf: []projections.YearPercentiles, base: f64, growth: f64) []projections.YearPercentiles {
    for (buf, 0..) |*b, i| {
        const v = base * (1.0 + growth * @as(f64, @floatFromInt(i)));
        b.* = .{
            .year = @intCast(i),
            .p10 = v * 0.6,
            .p25 = v * 0.8,
            .p50 = v,
            .p75 = v * 1.2,
            .p90 = v * 1.5,
        };
    }
    return buf;
}

test "renderCompareChart produces valid RGB output for two envelopes" {
    const alloc = std.testing.allocator;
    var then_buf: [11]projections.YearPercentiles = undefined;
    var now_buf: [11]projections.YearPercentiles = undefined;
    const then_bands = syntheticBands(&then_buf, 1_000_000, 0.05);
    const now_bands = syntheticBands(&now_buf, 1_200_000, 0.06);

    const th = theme.default_theme;
    const result = try renderCompareChart(std.testing.io, alloc, then_bands, now_bands, 240, 120, th);
    defer alloc.free(result.rgb_data);

    try std.testing.expectEqual(@as(u16, 240), result.width);
    try std.testing.expectEqual(@as(u16, 120), result.height);
    try std.testing.expectEqual(@as(usize, 240 * 120 * 3), result.rgb_data.len);
    try std.testing.expect(result.value_max > result.value_min);
}

test "renderToSurface draws both envelope hues" {
    const alloc = std.testing.allocator;
    var then_buf: [11]projections.YearPercentiles = undefined;
    var now_buf: [11]projections.YearPercentiles = undefined;
    const then_bands = syntheticBands(&then_buf, 1_000_000, 0.03);
    const now_bands = syntheticBands(&now_buf, 1_500_000, 0.07);

    const th = theme.default_theme;
    var rendered = try renderToSurface(std.testing.io, alloc, then_bands, now_bands, 200, 100, th, false);
    defer rendered.deinit(alloc);

    // Both median hues should have painted opaque pixels.
    try std.testing.expect(draw.countColor(&rendered.surface, th.info) > 0);
    try std.testing.expect(draw.countColor(&rendered.surface, th.accent) > 0);
}

test "renderToSurface insufficient data on a single-year band" {
    const alloc = std.testing.allocator;
    var then_buf: [1]projections.YearPercentiles = undefined;
    var now_buf: [11]projections.YearPercentiles = undefined;
    const then_bands = syntheticBands(&then_buf, 1_000_000, 0.05);
    const now_bands = syntheticBands(&now_buf, 1_000_000, 0.05);

    const th = theme.default_theme;
    try std.testing.expectError(
        error.InsufficientData,
        renderToSurface(std.testing.io, alloc, then_bands, now_bands, 200, 100, th, false),
    );
}
