#!/usr/bin/env python3
"""Compare two UBER joeylog.txt files by per-op surface hash.

Used by the planar 68k rewrite (project_planar_68k_plan.md): IIgs
captures the golden reference, each 68k port re-runs UBER after a
primitive conversion, and this tool tells you which ops produced
different pixels. Without this, "looks right visually" misses the
subtle mismatches that cascade into hard-to-debug corruption.

Usage:
    tools/diff-uber-hashes <reference-log> <test-log>

Exit code:
    0 = all hashes match
    1 = at least one mismatch
    2 = usage error or missing file
"""

import re
import sys

# Match e.g.:
#   UBER: drawCircle r=80: 56 iters / 4 frames = 840 ops/sec | hash=A1B2C3D4
LINE_RE = re.compile(
    r"UBER:\s+(?P<op>[^:]+):\s+\d+\s+iters\s+/\s+\d+\s+frames\s+=\s+\d+\s+ops/sec\s+\|\s+hash=(?P<hash>[0-9A-Fa-f]+)"
)

# Non-timed correctness checks (Phase 0 harness). Match e.g.:
#   UBER-CHK: edge-drawRect: hash=A1B2C3D4
CHK_RE = re.compile(
    r"UBER-CHK:\s+(?P<op>[^:]+):\s+hash=(?P<hash>[0-9A-Fa-f]+)"
)


def parse_log(path):
    """Return ordered dict {op_name: hash} from a UBER log file.

    Timed-op hashes and UBER-CHK correctness hashes both count; the
    check names get a "chk:" prefix so they can never collide with a
    timed op name. Multiple runs may be concatenated in the same log
    (joeyLog appends) -- in that case the LAST hash for each op wins,
    matching the most recent run.
    """
    hashes = {}
    with open(path) as f:
        for line in f:
            m = LINE_RE.search(line)
            if m:
                hashes[m.group("op").strip()] = m.group("hash").upper()
                continue
            m = CHK_RE.search(line)
            if m:
                hashes["chk:" + m.group("op").strip()] = m.group("hash").upper()
    return hashes


def main(argv):
    if len(argv) != 3:
        sys.stderr.write(
            "usage: diff-uber-hashes <reference-log> <test-log>\n"
        )
        return 2

    try:
        ref = parse_log(argv[1])
        test = parse_log(argv[2])
    except OSError as e:
        sys.stderr.write(f"error: {e}\n")
        return 2

    if not ref:
        sys.stderr.write(f"error: no UBER hash lines found in {argv[1]}\n")
        return 2
    if not test:
        sys.stderr.write(f"error: no UBER hash lines found in {argv[2]}\n")
        return 2

    mismatches = 0
    matches = 0
    for op, ref_hash in ref.items():
        test_hash = test.get(op)
        if test_hash is None:
            print(f"  MISSING in test: {op}  (ref={ref_hash})")
            mismatches += 1
        elif test_hash != ref_hash:
            print(f"  MISMATCH {op}: ref={ref_hash}  test={test_hash}")
            mismatches += 1
        else:
            matches += 1

    extras = [op for op in test if op not in ref]
    for op in extras:
        print(f"  EXTRA in test: {op}  (test={test[op]})")

    total = len(ref) + len(extras)
    print()
    if mismatches == 0 and not extras:
        print(f"OK: {matches}/{total} ops match")
        return 0
    print(f"FAIL: {matches} match, {mismatches} mismatch, {len(extras)} extras")
    return 1


if __name__ == "__main__":
    sys.exit(main(sys.argv))
