//! A tiny 5x7 bitmap font for drawing axis labels directly into a z2d
//! `image_surface_rgb` pixel buffer.
//!
//! Why a hand-rolled bitmap font instead of `z2d.text` + a TTF?
//!   1. The chart renderers in this directory deliberately draw with
//!      anti-aliasing OFF and the `.src` operator, pre-blending colors
//!      against the background to sidestep z2d's `src_over` compositor
//!      overflow on semi-transparent fills. Glyph outline rasterization
//!      needs alpha blending for its edges and would hit that same bug.
//!      Solid 1-bit pixels avoid it entirely.
//!   2. It keeps a ~hundreds-of-KB TTF (and its license) out of the repo.
//!
//! The glyph set is intentionally minimal - just what axis labels and
//! chart legends need: digits, `$`, `.`, `,`, `-`, `%`, the `T`/`B`/`M`
//! magnitude suffixes emitted by `format.fmtLargeNum`, and the
//! lowercase letters `t`/`h`/`e`/`n`/`o`/`w` (comparison-chart
//! "then"/"now" legend) plus `y` (convergence "years" axis ticks).
//! Space and unknown chars render blank.
//!
//! Coordinates are in surface pixels; `scale` multiplies the 5x7 cell
//! (so `scale = 3` renders 15x21 glyphs). Drawing is clipped to the
//! surface bounds. Callers own pixel layout (margins, alignment); this
//! module only stamps glyphs.

const std = @import("std");
const z2d = @import("z2d");
const draw = @import("draw.zig");

const Surface = z2d.Surface;
const RGB = z2d.pixel.RGB;

/// Glyph cell dimensions, in font pixels (pre-scale).
pub const glyph_w: i32 = 5;
pub const glyph_h: i32 = 7;
/// Horizontal advance per glyph including the 1px inter-glyph gap.
pub const advance: i32 = glyph_w + 1;

/// Each glyph is 7 rows; the low 5 bits of each row are the pixels,
/// bit 4 (0b10000) = leftmost column. Row 0 is the top.
const Glyph = [7]u8;

const blank: Glyph = .{ 0, 0, 0, 0, 0, 0, 0 };

const digits = [10]Glyph{
    .{ 0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E }, // 0
    .{ 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E }, // 1
    .{ 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F }, // 2
    .{ 0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E }, // 3
    .{ 0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02 }, // 4
    .{ 0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E }, // 5
    .{ 0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E }, // 6
    .{ 0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08 }, // 7
    .{ 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E }, // 8
    .{ 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C }, // 9
};

const glyph_dollar: Glyph = .{ 0x04, 0x0E, 0x14, 0x0E, 0x05, 0x0E, 0x04 };
const glyph_period: Glyph = .{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06 };
const glyph_comma: Glyph = .{ 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x08 };
const glyph_minus: Glyph = .{ 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00 };
const glyph_percent: Glyph = .{ 0x19, 0x1A, 0x02, 0x04, 0x08, 0x13, 0x03 };
const glyph_T: Glyph = .{ 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 };
const glyph_B: Glyph = .{ 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E };
const glyph_M: Glyph = .{ 0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11 };

// Lowercase letters: the comparison chart's "then"/"now" legend
// (t, h, e, n, o, w) plus `y` for the convergence "years" axis ticks.
// 5x7, low 5 bits per row. Named `lc_*` to avoid colliding with the
// `glyph_w`/`glyph_h` cell dims.
const lc_t: Glyph = .{ 0x08, 0x08, 0x1C, 0x08, 0x08, 0x08, 0x0C };
const lc_h: Glyph = .{ 0x10, 0x10, 0x10, 0x1E, 0x12, 0x12, 0x12 };
const lc_e: Glyph = .{ 0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E };
const lc_n: Glyph = .{ 0x00, 0x00, 0x1E, 0x12, 0x12, 0x12, 0x12 };
const lc_o: Glyph = .{ 0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E };
const lc_w: Glyph = .{ 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A };
const lc_y: Glyph = .{ 0x00, 0x00, 0x11, 0x11, 0x0E, 0x04, 0x08 };

/// Look up the bitmap for a character. Unknown characters (including
/// space) render blank.
fn glyphFor(ch: u8) Glyph {
    return switch (ch) {
        '0'...'9' => digits[ch - '0'],
        '$' => glyph_dollar,
        '.' => glyph_period,
        ',' => glyph_comma,
        '-' => glyph_minus,
        '%' => glyph_percent,
        'T' => glyph_T,
        'B' => glyph_B,
        'M' => glyph_M,
        't' => lc_t,
        'h' => lc_h,
        'e' => lc_e,
        'n' => lc_n,
        'o' => lc_o,
        'w' => lc_w,
        'y' => lc_y,
        else => blank,
    };
}

/// Width in surface pixels that `drawText` will occupy for `text` at
/// `scale` (excludes the trailing inter-glyph gap). Zero for empty text.
/// Useful for right-aligning a label against a known x.
pub fn measureWidth(text: []const u8, scale: i32) i32 {
    if (text.len == 0) return 0;
    const n: i32 = @intCast(text.len);
    // n full glyphs + (n-1) gaps = n*advance - 1, times scale.
    return (n * advance - 1) * scale;
}

/// Stamp `text` onto the surface's RGB buffer with its top-left at
/// `(x, y)`, each font pixel drawn as a `scale` x `scale` solid block in
/// `color`. No-op for non-RGB surfaces. Pixels outside the surface are
/// clipped.
pub fn drawText(sfc: *Surface, x: i32, y: i32, scale: i32, color: [3]u8, text: []const u8) void {
    if (scale <= 0) return;
    const img = switch (sfc.*) {
        .image_surface_rgb => |s| s,
        else => return,
    };
    const w = img.width;
    const h = img.height;
    const px = RGB{ .r = color[0], .g = color[1], .b = color[2] };

    var cursor_x = x;
    for (text) |ch| {
        const glyph = glyphFor(ch);
        for (0..@intCast(glyph_h)) |gr| {
            const bits = glyph[gr];
            for (0..@intCast(glyph_w)) |gc| {
                const mask: u8 = @as(u8, 1) << @intCast(glyph_w - 1 - @as(i32, @intCast(gc)));
                if (bits & mask == 0) continue;
                fillBlock(
                    img.buf,
                    w,
                    h,
                    cursor_x + @as(i32, @intCast(gc)) * scale,
                    y + @as(i32, @intCast(gr)) * scale,
                    scale,
                    px,
                );
            }
        }
        cursor_x += advance * scale;
    }
}

/// Fill a `size` x `size` block of pixels at `(bx, by)`, clipped to the
/// `[0,w) x [0,h)` surface bounds.
fn fillBlock(buf: []RGB, w: i32, h: i32, bx: i32, by: i32, size: i32, px: RGB) void {
    var dy: i32 = 0;
    while (dy < size) : (dy += 1) {
        const yy = by + dy;
        if (yy < 0 or yy >= h) continue;
        var dx: i32 = 0;
        while (dx < size) : (dx += 1) {
            const xx = bx + dx;
            if (xx < 0 or xx >= w) continue;
            buf[@intCast(yy * w + xx)] = px;
        }
    }
}

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

const testing = std.testing;

test "measureWidth: empty is zero, scales with length" {
    try testing.expectEqual(@as(i32, 0), measureWidth("", 3));
    // One glyph = glyph_w * scale (no trailing gap).
    try testing.expectEqual(@as(i32, glyph_w * 2), measureWidth("1", 2));
    // Two glyphs = (2*advance - 1) * scale = (2*6 - 1)*2 = 22.
    try testing.expectEqual(@as(i32, 22), measureWidth("12", 2));
}

test "drawText stamps glyph pixels in the requested color" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 64, 16);
    defer sfc.deinit(alloc);

    // Surface starts zeroed (black). Draw white text.
    const white = [3]u8{ 0xFF, 0xFF, 0xFF };
    try testing.expectEqual(@as(usize, 0), draw.countColor(&sfc, white));

    drawText(&sfc, 1, 1, 1, white, "1");
    // '1' has 10 set pixels in the 5x7 bitmap; at scale 1 that's 10 white px.
    try testing.expectEqual(@as(usize, 10), draw.countColor(&sfc, white));
}

test "drawText scale multiplies the stamped pixel count" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 64, 32);
    defer sfc.deinit(alloc);

    const c = [3]u8{ 0x10, 0x20, 0x30 };
    drawText(&sfc, 0, 0, 2, c, "1");
    // 10 set font-pixels, each a 2x2 block -> 10 * 4 = 40 colored px.
    try testing.expectEqual(@as(usize, 40), draw.countColor(&sfc, c));
}

test "drawText clips out-of-bounds without writing past the buffer" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 8, 8);
    defer sfc.deinit(alloc);

    const c = [3]u8{ 0xAA, 0xBB, 0xCC };
    // Draw partly off the right/bottom edge and fully off-screen; must
    // not crash or panic (bounds-checked writes).
    drawText(&sfc, 6, 6, 3, c, "8");
    drawText(&sfc, -50, -50, 4, c, "8");
    drawText(&sfc, 100, 100, 4, c, "8");
    // Some pixels of the first (partly-visible) glyph may have landed.
    try testing.expect(draw.countColor(&sfc, c) <= 8 * 9);
}

test "drawText ignores unknown glyphs (renders blank)" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 64, 16);
    defer sfc.deinit(alloc);
    const white = [3]u8{ 0xFF, 0xFF, 0xFF };
    // '?' and space are not in the set -> nothing drawn.
    drawText(&sfc, 1, 1, 2, white, " ?");
    try testing.expectEqual(@as(usize, 0), draw.countColor(&sfc, white));
}

test "drawText renders the comma glyph (so thousands separators show)" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 16, 16);
    defer sfc.deinit(alloc);
    const white = [3]u8{ 0xFF, 0xFF, 0xFF };
    drawText(&sfc, 1, 1, 1, white, ",");
    // The comma bitmap (rows 0x06,0x06,0x08) has 5 set pixels.
    try testing.expectEqual(@as(usize, 5), draw.countColor(&sfc, white));
}

test "drawText renders the lowercase glyphs (then/now legend + years axis)" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 64, 16);
    defer sfc.deinit(alloc);
    const white = [3]u8{ 0xFF, 0xFF, 0xFF };
    // "thenowy" exercises all seven lowercase glyphs; none may be blank.
    drawText(&sfc, 1, 1, 1, white, "thenowy");
    try testing.expect(draw.countColor(&sfc, white) > 30);
}

test "drawText renders the percent glyph (for return-rate axis labels)" {
    const alloc = testing.allocator;
    var sfc = try Surface.init(.image_surface_rgb, alloc, 16, 16);
    defer sfc.deinit(alloc);
    const white = [3]u8{ 0xFF, 0xFF, 0xFF };
    drawText(&sfc, 1, 1, 1, white, "%");
    try testing.expect(draw.countColor(&sfc, white) > 0);
}
