// One-off asset-prep tool (not part of the game build): key the painted
// transparency checkerboard out of the provided particle art and emit a clean
// white RGBA sprite the engine tints per gem/combo colour at load time.
//
// Source: /Users/agra/Downloads/m3te_particle.png — 1254x1254 RGB (NO alpha):
// a white 4-point sparkle + soft radial glow painted over a gray transparency
// checkerboard. The foreground is strictly BRIGHTER than the checker, so the
// key is luminance-driven: an 8-connected flood fill from the border removes the
// edge-connected checker (same technique as the P4.1 gem / P4.2 cell keys), and
// the surviving glow's alpha ramps smoothly from the lightest checker shade up
// to pure white — preserving the soft falloff. RGB is forced white everywhere so
// a per-gem tint multiply yields a clean coloured glow.
//
// Output: assets/fx/particle.png — 256x256 RGBA (area-averaged downscale).
// Run from the repo root:  /Users/agra/projects/sx/zig-out/bin/sx run tools/key_particle.sx
//
// NOTE (sx codegen): a block-scoped local declared INSIDE a loop body leaks
// stack per iteration, so a megapixel loop overflows. Every working local here
// is therefore hoisted above its loop and only assigned inside. The game itself
// never hits this — its loops run over 64 board cells, not millions of pixels.
#import "modules/std.sx";
#import "modules/math";
#import "modules/stb.sx";

SRC_PATH :: "/Users/agra/Downloads/m3te_particle.png";
OUT_PATH :: "assets/fx/particle.png";
OUT_DIM  :: 256;

// Flood-fill predicate slack: a pixel is "checker" if it is near-neutral gray
// and no brighter than the lightest checker shade plus this margin. The glow
// core sits well above it, so the fill stops at the sparkle.
GRAY_TOL    :: 24;     // max channel spread still considered neutral gray
LUM_MARGIN  :: 4;      // lum headroom above the light checker shade

is_gray :: (r: u8, g: u8, b: u8) -> bool {
    hi := max(max(cast(s64) r, cast(s64) g), cast(s64) b);
    lo := min(min(cast(s64) r, cast(s64) g), cast(s64) b);
    hi - lo <= GRAY_TOL
}

// Mark pixel `i` as removed background and queue it, if it is unvisited checker
// (near-neutral gray no brighter than the light checker shade + margin).
fd_seed :: (i: s64, bg: [*]u8, lum: [*]s64, src: [*]u8, stack: [*]s64, sp: *s64, lim: s64) {
    if bg[i] != 0 { return; }
    p := i * 4;
    if lum[i] <= lim and is_gray(src[p], src[p+1], src[p+2]) {
        bg[i] = 1;
        stack[sp.*] = i;
        sp.* += 1;
    }
}

main :: () -> s32 {
    w : s32 = 0;
    h : s32 = 0;
    ch : s32 = 0;
    src : [*]u8 = xx stbi_load(SRC_PATH, @w, @h, @ch, 4);
    if xx src == 0 {
        print("FATAL: could not load {}\n", SRC_PATH);
        return 1;
    }
    W := cast(s64) w;
    H := cast(s64) h;
    N := W * H;
    print("loaded {}x{} ({} src channels)\n", w, h, ch);

    // Hoisted working locals (see codegen note above).
    y : s64 = 0;
    x : s64 = 0;
    i : s64 = 0;
    p : s64 = 0;
    r : s64 = 0;
    g : s64 = 0;
    b : s64 = 0;
    l : s64 = 0;

    // Per-pixel luminance, plus the checker shades read off the border ring
    // (the border is pure checker — the glow never reaches the corners).
    lum : [*]s64 = xx context.allocator.alloc(N * size_of(s64));
    c_lo : s64 = 255;
    c_hi : s64 = 0;
    y = 0;
    while y < H {
        x = 0;
        while x < W {
            i = y * W + x;
            p = i * 4;
            r = xx src[p];
            g = xx src[p+1];
            b = xx src[p+2];
            l = (r + g + b) / 3;
            lum[i] = l;
            if x == 0 or y == 0 or x == W - 1 or y == H - 1 {
                if l < c_lo { c_lo = l; }
                if l > c_hi { c_hi = l; }
            }
            x += 1;
        }
        y += 1;
    }
    print("checker shades: lo={} hi={}\n", c_lo, c_hi);

    // 8-connected flood fill of the edge-connected checker, seeded from every
    // border pixel. `bg[i]==1` marks a removed (transparent) background pixel.
    bg : [*]u8 = xx context.allocator.alloc(N);
    memset(xx bg, 0, N);
    stack : [*]s64 = xx context.allocator.alloc(N * size_of(s64));
    sp : s64 = 0;
    checker_lim := c_hi + LUM_MARGIN;

    x = 0;
    while x < W {
        fd_seed(x, bg, lum, src, stack, @sp, checker_lim);
        fd_seed((H - 1) * W + x, bg, lum, src, stack, @sp, checker_lim);
        x += 1;
    }
    y = 0;
    while y < H {
        fd_seed(y * W, bg, lum, src, stack, @sp, checker_lim);
        fd_seed(y * W + (W - 1), bg, lum, src, stack, @sp, checker_lim);
        y += 1;
    }

    cx : s64 = 0;
    cy : s64 = 0;
    dx : s64 = 0;
    dy : s64 = 0;
    nx : s64 = 0;
    ny : s64 = 0;
    while sp > 0 {
        sp -= 1;
        i = stack[sp];
        cx = i % W;
        cy = i / W;
        dy = -1;
        while dy <= 1 {
            dx = -1;
            while dx <= 1 {
                if dx != 0 or dy != 0 {
                    nx = cx + dx;
                    ny = cy + dy;
                    if nx >= 0 and nx < W and ny >= 0 and ny < H {
                        fd_seed(ny * W + nx, bg, lum, src, stack, @sp, checker_lim);
                    }
                }
                dx += 1;
            }
            dy += 1;
        }
    }

    // Full-res alpha: background → 0; everything else ramps from the lightest
    // checker shade up to pure white, giving the glow its smooth falloff.
    denom := cast(f32) (255 - c_hi);
    if denom < 1.0 { denom = 1.0; }
    alpha : [*]f32 = xx context.allocator.alloc(N * size_of(f32));
    kept : s64 = 0;
    n_bg : s64 = 0;
    a : f32 = 0.0;
    i = 0;
    while i < N {
        if bg[i] == 1 {
            alpha[i] = 0.0;
            n_bg += 1;
        } else {
            a = (cast(f32) (lum[i] - c_hi)) / denom;
            a = clamp(a, 0.0, 1.0);
            alpha[i] = a;
            if a > 0.0 { kept += 1; }
        }
        i += 1;
    }
    print("flood removed {} bg px; kept {} glow px (of {})\n", n_bg, kept, N);

    // Area-averaged downscale to OUT_DIM. RGB stays white; only the averaged
    // alpha carries the sprite, so no premultiply is needed (white*cov == white).
    out_px : [*]u8 = xx context.allocator.alloc(OUT_DIM * OUT_DIM * 4);
    sxf := cast(f32) W / cast(f32) OUT_DIM;
    syf := cast(f32) H / cast(f32) OUT_DIM;
    max_a : f32 = 0.0;
    ty : s64 = 0;
    tx : s64 = 0;
    x0 : s64 = 0;
    x1 : s64 = 0;
    y0 : s64 = 0;
    y1 : s64 = 0;
    sum : f32 = 0.0;
    cnt : s64 = 0;
    sy : s64 = 0;
    sx : s64 = 0;
    av : f32 = 0.0;
    o : s64 = 0;
    ty = 0;
    while ty < OUT_DIM {
        tx = 0;
        while tx < OUT_DIM {
            x0 = cast(s64) (cast(f32) tx * sxf);
            x1 = cast(s64) (cast(f32) (tx + 1) * sxf);
            y0 = cast(s64) (cast(f32) ty * syf);
            y1 = cast(s64) (cast(f32) (ty + 1) * syf);
            if x1 <= x0 { x1 = x0 + 1; }
            if y1 <= y0 { y1 = y0 + 1; }
            sum = 0.0;
            cnt = 0;
            sy = y0;
            while sy < y1 {
                sx = x0;
                while sx < x1 {
                    sum += alpha[sy * W + sx];
                    cnt += 1;
                    sx += 1;
                }
                sy += 1;
            }
            av = sum / cast(f32) cnt;
            if av > max_a { max_a = av; }
            o = (ty * OUT_DIM + tx) * 4;
            out_px[o]   = 255;
            out_px[o+1] = 255;
            out_px[o+2] = 255;
            out_px[o+3] = cast(u8) (clamp(av, 0.0, 1.0) * 255.0);
            tx += 1;
        }
        ty += 1;
    }
    cc := (OUT_DIM / 2 * OUT_DIM + OUT_DIM / 2) * 4;
    print("downscaled max alpha={} centre alpha={}\n", max_a, out_px[cc + 3]);

    ok := stbi_write_png(OUT_PATH, OUT_DIM, OUT_DIM, 4, xx out_px, OUT_DIM * 4);
    if ok == 0 {
        print("FATAL: stbi_write_png failed for {}\n", OUT_PATH);
        return 1;
    }
    stbi_image_free(xx src);
    print("wrote {} ({}x{} RGBA)\n", OUT_PATH, OUT_DIM, OUT_DIM);
    return 0;
}
