#!/usr/bin/env python3
"""What KIND of error is left at the edges after tone is accounted for?

    tools/port/edge-residual-kind [screen]        # default main_menu

`verify-capture`'s `diff` column thresholds at 25 % and so only sees GROSS
displacement. Fitting a per-level LUT removes everything a tone effect can
explain. What is left on the main menu is concentrated 3.2x on edge pixels
(DECISIONS.md, 2026-08-31) -- and three things produce that: a misregistration,
an antialiasing difference, or a genuinely misplaced element.

THE DISCRIMINATOR IS THE SIGN, and it is the Decoder's, from their reply on
2026-08-31: a shift gives a residual with a CONSISTENT DIRECTION along the edge,
an antialiasing difference does not. Made concrete:

  * shifted by (dx,dy):  residual ~ dx*d/dx + dy*d/dy   -- and the fitted SLOPE
                                                           IS THE SHIFT IN PIXELS
  * blurred/sharpened :  residual ~ -k * laplacian      -- symmetric, no direction

EXIT CODES. 0 the report is trustworthy, 2 A CONTROL FAILED so the numbers below
it mean nothing. There is no 1: this tool classifies, it does not judge. A
correlation this tool reports is worthless without the two controls above it,
which is why they are not optional and not a flag.
"""
import math, os, subprocess, sys, tempfile

CAPS = "docs/re/captures/title-builds"
SCREEN = sys.argv[1] if len(sys.argv) > 1 else "main_menu"
# The captures are a 1279x675 top-left crop of the 1280x720 guest surface, so the
# render is cropped to match and NOTHING IS SCALED -- resampling would manufacture
# exactly the edge signal this tool measures. See verify-capture, same reason.
W, H = 1279, 675
EDGE = 12          # |grad| above which a pixel is an edge
PASS_SHIFT, PASS_BLUR = 0.70, -0.70


def gray(png, out):
    subprocess.run(["convert", png, "-colorspace", "Gray", "-depth", "8",
                    "gray:" + out], check=True)
    return open(out, "rb").read()


def lutfit(a, b):
    tot = [0] * 256; cnt = [0] * 256
    for i in range(len(a)):
        tot[a[i]] += b[i]; cnt[a[i]] += 1
    return [(tot[v] // cnt[v]) if cnt[v] else v for v in range(256)]


def analyse(a, b):
    lut = lutfit(a, b)
    gx = []; gy = []; lp = []; rs = []
    for y in range(1, H - 1):
        o = y * W
        for x in range(1, W - 1):
            i = o + x
            ax = (a[i + 1] - a[i - 1]) * 0.5
            ay = (a[i + W] - a[i - W]) * 0.5
            if abs(ax) + abs(ay) < EDGE:
                continue
            gx.append(ax); gy.append(ay)
            lp.append(float(a[i + 1] + a[i - 1] + a[i + W] + a[i - W] - 4 * a[i]))
            rs.append(float(lut[a[i]] - b[i]))
    n = len(rs)
    if n < 1000:
        print(f"  🔴 only {n} edge pixels -- nothing to classify"); sys.exit(2)
    mr = sum(rs) / n

    def fit(u):
        mu = sum(u) / n
        suu = sum((v - mu) ** 2 for v in u)
        srr = sum((v - mr) ** 2 for v in rs)
        sur = sum((u[k] - mu) * (rs[k] - mr) for k in range(n))
        return (0.0, 0.0) if suu <= 0 or srr <= 0 else (sur / suu, sur / math.sqrt(suu * srr))
    return n, fit(gx), fit(gy), fit(lp)


def row(label, res):
    n, (sx, rx), (sy, ry), (sl, rl) = res
    print(f"  {label}  (n={n})")
    print(f"    horizontal shift : r={rx:+.3f}  slope={sx:+.3f} px")
    print(f"    vertical   shift : r={ry:+.3f}  slope={sy:+.3f} px")
    print(f"    blur / sharpness : r={rl:+.3f}  coef ={sl:+.3f}")
    return rx, ry, rl


def shifted(a, dx):
    out = bytearray(a)
    for y in range(H):
        for x in range(W):
            out[y * W + x] = a[y * W + min(W - 1, max(0, x - dx))]
    return bytes(out)


def blurred(a):
    out = bytearray(a)
    for y in range(1, H - 1):
        o = y * W
        for x in range(1, W - 1):
            i = o + x
            out[i] = (a[i] * 4 + a[i + 1] + a[i - 1] + a[i + W] + a[i - W]) // 8
    return bytes(out)


tmp = tempfile.mkdtemp()
cap_png = f"{CAPS}/live-{SCREEN.replace('_', '-')}.png"
if not os.path.exists(cap_png):
    print(f"  🔴 no capture: {cap_png}"); sys.exit(2)
render = os.environ.get("RENDER") or f"{tmp}/render.png"
if not os.path.exists(render):
    print(f"  🔴 no render at {render} -- set RENDER=<png>"); sys.exit(2)
subprocess.run(["convert", render, "-crop", f"{W}x{H}+0+0", "+repage",
                f"{tmp}/crop.png"], check=True)
r = gray(f"{tmp}/crop.png", f"{tmp}/r.gray")
c = gray(cap_png, f"{tmp}/c.gray")

print("CONTROLS -- the render against a deliberately damaged copy of itself.")
print("A correlation below is meaningless unless these two recover what was done.\n")
ra = analyse(r, shifted(r, 1))
rxa, _, rla = row("known +1 px HORIZONTAL shift", ra)
rb = analyse(r, blurred(r))
_, _, rlb = row("known BLUR, no shift", rb)
bad = []
if rxa < PASS_SHIFT: bad.append(f"shift control r={rxa:+.3f} < {PASS_SHIFT}")
if rlb > PASS_BLUR:  bad.append(f"blur control r={rlb:+.3f} > {PASS_BLUR}")
if bad:
    print("\n  🔴 CONTROL FAILED: " + "; ".join(bad))
    print("  The discriminator cannot see what it is for. Report suppressed.")
    sys.exit(2)
print(f"\n  ✅ controls pass -- a 1 px shift reads as {ra[1][0]:+.3f} px\n")
print(f"THE REAL PAIR -- {SCREEN}\n")
rx, ry, rl = row(f"{SCREEN} render vs oracle capture", analyse(r, c))
print()
if max(abs(rx), abs(ry)) < 0.15 and abs(rl) < 0.3:
    print("  => NEITHER a global shift NOR a uniform blur.")
    print("  ⚠️ REACH: this is a WHOLE-FRAME fit. One misplaced element is a small")
    print("     share of the edge pixels and would not move these numbers. This")
    print("     excludes a global translation; it does not exclude a local one.")
