//! Shared axis-label helpers for the chart exports, layered on top of
//! `text.zig` (bitmap-glyph stamping) and `format.fmtLargeNum` (compact
//! dollar formatting).
//!
//! Each renderer reserves margins and calls these to draw y-axis dollar
//! ticks and x-axis endpoint labels straight into the surface buffer.
//! Kept here (rather than copy-pasted into each chart) so there's one
//! source of truth for label formatting, spacing, and tick math.

const std = @import("std");
const z2d = @import("z2d");
const text = @import("text.zig");
const draw = @import("draw.zig");
const fmt = @import("../format.zig");

const Surface = z2d.Surface;

/// Pick a glyph scale from the surface height so labels stay legible on
/// large exports (1080p -> 3) and shrink on small surfaces (>= 1).
pub fn labelScale(height_px: i32) i32 {
    return @max(1, @divFloor(height_px, 360));
}

/// Glyph cell height in surface pixels at `scale`.
pub fn charHeight(scale: i32) f64 {
    return @floatFromInt(text.glyph_h * scale);
}

/// Gap (pixels) between the plot edge and the nearest label.
pub fn labelGap(scale: i32) f64 {
    return @floatFromInt(4 * scale);
}

/// Bottom margin to reserve so a full x-axis label row fits below the
/// plot without clipping: gap + glyph height + a little breathing room.
pub fn bottomMargin(scale: i32) f64 {
    return labelGap(scale) + charHeight(scale) + @as(f64, @floatFromInt(2 * scale));
}

/// Left/right margin (in pixels) to reserve for a column of dollar
/// labels at `scale`. Sized for the widest label we emit - an 8-glyph
/// comma'd sub-million value like "$999,999" - plus the gap to the plot.
pub fn yAxisMargin(scale: i32) f64 {
    return @as(f64, @floatFromInt(text.measureWidth("$999,999", scale))) + labelGap(scale) + @as(f64, @floatFromInt(2 * scale));
}

/// Format a dollar value for an axis tick. Below a million we use whole
/// dollars with thousands separators ("$887,889"); at or above a million
/// we switch to compact T/B/M suffixes via `format.fmtLargeNum`
/// ("$1.3M", "$370.2M") so large axes stay narrow. Negatives get a
/// leading "-". The sub-million path rounds to the nearest whole dollar.
pub fn fmtDollar(buf: []u8, value: f64) []const u8 {
    const sign = if (value < 0) "-" else "";
    const abs = @abs(value);
    if (abs >= 1_000_000) {
        const large = fmt.fmtLargeNum(abs);
        const trimmed = std.mem.trimEnd(u8, &large, " ");
        return std.fmt.bufPrint(buf, "{s}${s}", .{ sign, trimmed }) catch "$?";
    }
    var nbuf: [20]u8 = undefined;
    const whole: u64 = @intFromFloat(@round(abs));
    const commas = fmt.fmtIntCommas(&nbuf, whole);
    return std.fmt.bufPrint(buf, "{s}${s}", .{ sign, commas }) catch "$?";
}

/// Unit for `drawYTicks` y-axis labels. `dollars` and `percent`
/// delegate to the canonical formatters (`fmtDollar`,
/// `format.fmtPct`); `years` is a compact whole-year tick ("10y")
/// for the convergence chart - the codebase has no shared compact-
/// years helper (the verbose "N Year" lives in the projections view).
pub const TickUnit = enum { dollars, percent, years };

/// Draw `n + 1` left-aligned dollar labels evenly spaced from
/// `value_max` (at `top`) down to `value_min` (at `bottom`), placed in
/// the right margin a small pad to the right of `plot_right` (the plot's
/// right edge) and vertically centered on each level. Right-side y-axis
/// is the financial convention: the latest (rightmost) values sit next
/// to the scale as the series grows.
pub fn drawYDollarTicks(
    sfc: *Surface,
    scale: i32,
    color: [3]u8,
    plot_right: f64,
    top: f64,
    bottom: f64,
    value_min: f64,
    value_max: f64,
    n: usize,
) void {
    drawYTicks(sfc, scale, color, plot_right, top, bottom, value_min, value_max, n, .dollars);
}

/// Like `drawYDollarTicks` but renders the tick values in the given
/// `unit`, so non-dollar axes (percent returns, years-until-
/// retirement) reuse the same tick spacing and placement.
/// `drawYDollarTicks` is the dollar-specialized wrapper. Percent
/// ticks go through `format.fmtPct` (the canonical percent
/// formatter) rather than a local reimplementation.
pub fn drawYTicks(
    sfc: *Surface,
    scale: i32,
    color: [3]u8,
    plot_right: f64,
    top: f64,
    bottom: f64,
    value_min: f64,
    value_max: f64,
    n: usize,
    unit: TickUnit,
) void {
    const range = value_max - value_min;
    const span = bottom - top;
    const half_h = @divFloor(text.glyph_h * scale, 2);
    const pad: f64 = labelGap(scale);
    const lx: i32 = @intFromFloat(plot_right + pad);
    var i: usize = 0;
    while (i <= n) : (i += 1) {
        const frac = @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(n));
        const val = value_max - frac * range;
        const y = top + frac * span;
        var buf: [24]u8 = undefined;
        const label = switch (unit) {
            .dollars => fmtDollar(&buf, val),
            .percent => fmt.fmtPct(&buf, val, .{ .decimals = 0 }),
            .years => std.fmt.bufPrint(&buf, "{d}y", .{@as(i64, @intFromFloat(@round(val)))}) catch "?y",
        };
        const ly = @as(i32, @intFromFloat(y)) - half_h;
        text.drawText(sfc, lx, ly, scale, color, label);
    }
}

/// Draw two endpoint labels on baseline `y`: `left_label` left-aligned
/// at `left`, `right_label` right-aligned ending at `right`. Used for
/// x-axis start/end (dates for time series, offsets for projections).
pub fn drawXEndpoints(
    sfc: *Surface,
    scale: i32,
    color: [3]u8,
    left: f64,
    right: f64,
    y: f64,
    left_label: []const u8,
    right_label: []const u8,
) void {
    const yi: i32 = @intFromFloat(y);
    text.drawText(sfc, @as(i32, @intFromFloat(left)), yi, scale, color, left_label);
    const rw = text.measureWidth(right_label, scale);
    text.drawText(sfc, @as(i32, @intFromFloat(right)) - rw, yi, scale, color, right_label);
}

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

const testing = std.testing;

test "labelScale grows with height, floored at 1" {
    try testing.expectEqual(@as(i32, 1), labelScale(100));
    try testing.expectEqual(@as(i32, 1), labelScale(360));
    try testing.expectEqual(@as(i32, 3), labelScale(1080));
}

test "fmtDollar: M/B suffixes at/above a million, commas below, sign for negatives" {
    var buf: [24]u8 = undefined;
    try testing.expectEqualStrings("$1.3M", fmtDollar(&buf, 1_250_000));
    try testing.expectEqualStrings("$2.0B", fmtDollar(&buf, 2_000_000_000));
    try testing.expectEqualStrings("$950,000", fmtDollar(&buf, 950_000));
    try testing.expectEqualStrings("$887,889", fmtDollar(&buf, 887_889));
    try testing.expectEqualStrings("$313", fmtDollar(&buf, 313));
    try testing.expectEqualStrings("-$1.2M", fmtDollar(&buf, -1_200_000));
}

test "drawYTicks renders percent and years units" {
    const alloc = testing.allocator;
    const color = [3]u8{ 0xCC, 0xCC, 0xCC };

    // Percent ticks (decimal rates, via format.fmtPct) stamp glyphs.
    var sfc = try Surface.init(.image_surface_rgb, alloc, 300, 200);
    defer sfc.deinit(alloc);
    drawYTicks(&sfc, 2, color, 180, 10, 190, -0.05, 0.15, 5, .percent);
    try testing.expect(draw.countColor(&sfc, color) > 0);

    // Years ticks stamp glyphs too.
    var sfc2 = try Surface.init(.image_surface_rgb, alloc, 300, 200);
    defer sfc2.deinit(alloc);
    drawYTicks(&sfc2, 2, color, 180, 10, 190, 0, 12, 5, .years);
    try testing.expect(draw.countColor(&sfc2, color) > 0);
}

test "drawYDollarTicks stamps labels in the requested color" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 300, 200);
    defer sfc.deinit(alloc);
    const color = [3]u8{ 0xCC, 0xCC, 0xCC };
    try testing.expectEqual(@as(usize, 0), draw.countColor(&sfc, color));

    // plot_right=180 leaves room for the labels in the right margin of a
    // 300px-wide surface.
    drawYDollarTicks(&sfc, 2, color, 180, 10, 190, 1_000_000, 5_000_000, 5);
    try testing.expect(draw.countColor(&sfc, color) > 0);
}

test "drawXEndpoints draws both endpoint labels" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 300, 60);
    defer sfc.deinit(alloc);
    const color = [3]u8{ 0x40, 0x80, 0xC0 };
    drawXEndpoints(&sfc, 2, color, 10, 290, 20, "2024-01-01", "2026-12-31");
    // Both labels contribute pixels; expect a comfortably non-trivial count.
    try testing.expect(draw.countColor(&sfc, color) > 20);
}
