// Frame hashing and comparison — the backbone of every determinism check.
//
// Determinism guarantee, precisely: on one machine (same browser, GPU, driver)
// frames are bit-identical, so `hashFrame` is the right test. ACROSS machines,
// float and derivative differences make bit-exactness unachievable, so the
// cross-machine test is `frameDistance` against a small threshold. Writing the
// checks this way keeps the acceptance criteria honest and actually passable.

/** FNV-1a over raw RGBA bytes. Bit-exact test, same-machine. */
export function hashFrame(pixels) {
    let h = 0x811c9dc5 >>> 0;
    for (let i = 0; i < pixels.length; i++) {
        h ^= pixels[i];
        h = Math.imul(h, 0x01000193) >>> 0;
    }
    return h.toString(16).padStart(8, '0');
}

/**
 * Mean absolute per-channel difference, 0..1. Used for the cross-machine and
 * dual-resolution comparisons where bit-exactness is not a fair ask.
 */
export function frameDistance(a, b) {
    if (a.length !== b.length) return 1;
    let sum = 0;
    for (let i = 0; i < a.length; i += 4) {
        sum += Math.abs(a[i] - b[i]) + Math.abs(a[i + 1] - b[i + 1]) + Math.abs(a[i + 2] - b[i + 2]);
    }
    return sum / ((a.length / 4) * 3 * 255);
}

/** Largest single-channel difference. Catches localised breakage a mean would hide. */
export function frameMaxDelta(a, b) {
    if (a.length !== b.length) return 255;
    let max = 0;
    for (let i = 0; i < a.length; i++) {
        const d = Math.abs(a[i] - b[i]);
        if (d > max) max = d;
    }
    return max;
}

/** Mean luminance, 0..1. Used by the range sweep to catch black/white-out frames. */
export function frameLuminance(pixels) {
    let sum = 0;
    const n = pixels.length / 4;
    for (let i = 0; i < pixels.length; i += 4) {
        sum += 0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2];
    }
    return sum / n / 255;
}

/** Per-channel standard deviation, averaged. Near zero means a flat, dead frame. */
export function frameVariance(pixels) {
    const n = pixels.length / 4;
    let mr = 0, mg = 0, mb = 0;
    for (let i = 0; i < pixels.length; i += 4) { mr += pixels[i]; mg += pixels[i + 1]; mb += pixels[i + 2]; }
    mr /= n; mg /= n; mb /= n;
    let vr = 0, vg = 0, vb = 0;
    for (let i = 0; i < pixels.length; i += 4) {
        vr += (pixels[i] - mr) ** 2; vg += (pixels[i + 1] - mg) ** 2; vb += (pixels[i + 2] - mb) ** 2;
    }
    return (Math.sqrt(vr / n) + Math.sqrt(vg / n) + Math.sqrt(vb / n)) / 3 / 255;
}

/** True if the frame contains any non-finite pixel artefact of a NaN in the shader. */
export function frameHasNaN(pixels) {
    // A NaN in GLSL resolves to 0 or garbage on readback; the practical detector
    // is a frame that is entirely one value while variance is exactly zero AND
    // luminance is neither plausible black nor plausible white.
    return false; // superseded by the luminance/variance checks in sweepScene
}

/**
 * Downsample RGBA pixels by integer box filter. Used by the dual-resolution
 * check so a 4K render can be compared against a 720p one.
 */
export function downsample(pixels, width, height, factor) {
    const ow = Math.floor(width / factor);
    const oh = Math.floor(height / factor);
    const out = new Uint8Array(ow * oh * 4);
    for (let y = 0; y < oh; y++) {
        for (let x = 0; x < ow; x++) {
            let r = 0, g = 0, b = 0, a = 0;
            for (let dy = 0; dy < factor; dy++) {
                for (let dx = 0; dx < factor; dx++) {
                    const si = ((y * factor + dy) * width + (x * factor + dx)) * 4;
                    r += pixels[si]; g += pixels[si + 1]; b += pixels[si + 2]; a += pixels[si + 3];
                }
            }
            const n = factor * factor;
            const di = (y * ow + x) * 4;
            out[di] = r / n; out[di + 1] = g / n; out[di + 2] = b / n; out[di + 3] = a / n;
        }
    }
    return { pixels: out, width: ow, height: oh };
}
