#!/usr/bin/env python3
"""031 — The Shape Belongs to the Rule: the re-runnable handle.

Reproduces the three browser streams and the two live meters with only the
standard library.  It also guards the page-side constants and formulas, so a
later edit cannot silently leave CI checking a different instrument.
"""
from pathlib import Path
import hashlib
import math
import re


N = 900
SEED = 20260828
U32 = 0xFFFFFFFF
PI_PREFIX = "31415926535897932384626433832795"
E_PREFIX = "27182818284590452353602874713526"
RANDOM_PREFIX = "46299884944255105342133945866182"
E_JS_SHA256 = "11823f41197921090ff119e17c27bec567407dcdb8448c63cae9583c6a27b46d"
PI_JS_SHA256 = "60f9fe202c27a80b9b668b176dea4b9651b56ad131066e35dab4baf0ed738d06"
RANDOM_JS_SHA256 = "7dc02abbba208507b8ad114e066c446631cef8865431f0d649a593d2c388130b"
GEOMETRY_JS_SHA256 = "969eaaf6e93cc81b8598c163e28880fc2c88b0390e41c7054af23f69a8ea6f29"
METERS_JS_SHA256 = "89cf99b52e7128dce3031dcc2b9c7c72f568f773bf8d99133e20131fbf7bf129"
MOTION_JS_SHA256 = "462d0efa38720bae8afe2cb32c964edaaeed1550c73cf19419851fb70127281e"


class PiSpigot:
    """Integer port of the page's Rabinowitz-Wagon spigot."""

    def __init__(self, n):
        self.length = 10 * n // 3 + 1
        self.a = [2] * self.length
        self.out = []
        self.nines = 0
        self.pre = 0
        self.first = True
        self.n = n

    def emit(self, digit):
        if self.first:
            self.first = False
            return
        self.out.append(digit)

    def run(self):
        for _ in range(self.n):
            q = 0
            for i in range(self.length - 1, -1, -1):
                x = 10 * self.a[i] + q * (i + 1)
                self.a[i] = x % (2 * i + 1)
                q = x // (2 * i + 1)
            self.a[0] = q % 10
            q //= 10
            if q == 9:
                self.nines += 1
            elif q == 10:
                self.emit(self.pre + 1)
                for _ in range(self.nines):
                    self.emit(0)
                self.pre = 0
                self.nines = 0
            else:
                self.emit(self.pre)
                self.pre = q
                for _ in range(self.nines):
                    self.emit(9)
                self.nines = 0
        return self.out


def e_digits(n):
    scale = 10 ** (n + 12)
    term = scale
    total = scale
    k = 1
    while term:
        term //= k
        total += term
        k += 1
    return [int(c) for c in str(total)[:n]]


def imul(a, b):
    """Low 32 bits of JavaScript Math.imul."""
    return ((a & U32) * (b & U32)) & U32


def random_digits(n, seed):
    """Bit-exact unsigned emulation of the page's mulberry32 stream."""
    t = seed & U32
    out = []
    for _ in range(n):
        t = (t + 0x6D2B79F5) & U32
        r = t
        r = imul(r ^ (r >> 15), r | 1)
        r ^= (r + imul(r ^ (r >> 7), r | 61)) & U32
        r &= U32
        u = (r ^ (r >> 14)) & U32
        out.append((u * 10) // 4294967296)
    return out


def directions():
    golden_angle = math.pi * (3 - math.sqrt(5))
    result = []
    for i in range(10):
        y = 1 - 2 * i / 9
        radius = math.sqrt(max(0, 1 - y * y))
        theta = golden_angle * i
        result.append((math.cos(theta) * radius, y, math.sin(theta) * radius))
    return result


def meters(digits, dirs):
    point = [0.0, 0.0, 0.0]
    hist = [0] * 10
    for digit in digits[:N]:
        vector = dirs[digit]
        point = [point[j] + vector[j] for j in range(3)]
        hist[digit] += 1
    distance = math.sqrt(sum(v * v for v in point))
    expected = N / 10
    chi2 = sum((count - expected) ** 2 / expected for count in hist)
    return distance, distance / math.sqrt(N), chi2, hist


def main():
    failures = []

    def check(ok, label, got, want):
        print("  %-4s %-52s got=%s want=%s" % ("ok" if ok else "FAIL", label, got, want))
        if not ok:
            failures.append(label)

    page = Path(__file__).with_name("index.html").read_text(encoding="utf-8")
    print("031 — The Shape Belongs to the Rule · handle\n")
    print("1. the CI handle and the browser still describe one instrument")
    for fragment in (
        "var N = 900;",
        "digits:rndDigits(N+6, 20260828)",
        "var exp = n/10",
        "dist/Math.sqrt(w.n)",
        "y = 1 - (i/(K-1))*2;",
        "if (d <= 1) return null;",
        "var running = !reduced, acc = 0;",
        "if (!reduced || running || dragging) scheduleFrame();",
        "if (reduced) piGen.step(piGen.n);",
        'cv.addEventListener("pointercancel", endDrag);',
        'cv.addEventListener("lostpointercapture", endDrag);',
        'class="lab blind" id="lab"',
        'id="blind" aria-pressed="true"',
        ".lab.blind .meters,.lab.blind .ribbons,.lab.blind .stream-toggle{visibility:hidden}",
        "ctx.strokeStyle = blind ? BLIND_COL : COL[s];",
        'blind = true;',
        "if (!blind) return;",
        "elBlind.disabled = true;",
        'el.textContent = digits.length ? digits[0] : "";',
        ".wrap{max-width:1080px",
        ".intro{max-width:780px",
        ".why p{max-width:780px",
        ".guide{max-width:780px",
        "if (reduced && running && walks[0].n >= N && walks[1].n >= N && walks[2].n >= N)",
    ):
        check(fragment in page, "page contract: %s" % fragment, fragment in page, True)
    browser_contracts = (
        ("e", r"function eDigits\(n\)\{.*?\n\}(?=\nfunction PiSpigot)", E_JS_SHA256),
        ("pi", r"function PiSpigot\(n\)\{.*?PiSpigot\.prototype\.step = function\(count\)\{.*?\n\};(?=\n/\* mulberry32)", PI_JS_SHA256),
        ("control", r"function rndDigits\(n, seed\)\{.*?\n\}(?=\n\nvar piGen)", RANDOM_JS_SHA256),
        ("geometry", r"function tenDirections\(\)\{.*?function advance\(w, d\)\{.*?\n\}", GEOMETRY_JS_SHA256),
        ("meters", r"function chi2\(h, n\)\{.*?function meters\(\)\{.*?\n\}", METERS_JS_SHA256),
        ("motion", r"var running = !reduced, acc = 0;.*?scheduleFrame\(\);(?=\n\}\)\(\);)", MOTION_JS_SHA256),
    )
    for name, pattern, expected_hash in browser_contracts:
        implementation = re.search(pattern, page, re.S)
        implementation_hash = (
            hashlib.sha256(implementation.group(0).encode()).hexdigest()
            if implementation else None
        )
        check(implementation_hash == expected_hash,
              "browser %s implementation hash" % name,
              implementation_hash, expected_hash)

    streams = (
        ("pi", PiSpigot(N + 6).run()),
        ("e", e_digits(N + 6)),
        ("rnd", random_digits(N + 6, SEED)),
    )

    print("\n2. the named streams reproduce their independently known openings")
    check("".join(map(str, streams[0][1][:32])) == PI_PREFIX,
          "pi first 32 digits", "".join(map(str, streams[0][1][:32])), PI_PREFIX)
    check("".join(map(str, streams[1][1][:32])) == E_PREFIX,
          "e first 32 digits", "".join(map(str, streams[1][1][:32])), E_PREFIX)
    random_prefix = "".join(map(str, streams[2][1][:32]))
    check(random_prefix == RANDOM_PREFIX,
          "seeded control first 32 digits", random_prefix, RANDOM_PREFIX)

    dirs = directions()
    print("\n3. all ten digit directions have unit length")
    for i, vector in enumerate(dirs):
        length = math.sqrt(sum(v * v for v in vector))
        check(abs(length - 1) < 1e-12, "direction %d" % i, "%.12f" % length, "1.000000000000")

    print("\n4. the page's two live witnesses, re-derived at n=900")
    for name, digits in streams:
        distance, scaled, chi2, hist = meters(digits, dirs)
        print("  %-3s |d|=%7.3f  |d|/sqrt(n)=%5.3f  chi2=%5.2f  hist=%s" %
              (name, distance, scaled, chi2, hist))
        check(sum(hist) == N, "%s histogram total" % name, sum(hist), N)
        check(chi2 < 16.9, "%s chi2 below stated 5%% guide" % name, "%.2f" % chi2, "<16.9")

    print("\n%s — %d checks failed" % ("FAILED" if failures else "ALL HANDLES HOLD", len(failures)))
    return 1 if failures else 0


if __name__ == "__main__":
    raise SystemExit(main())
