#!/usr/bin/env python3
"""Synthetic-only MIPS compiler/assembler reproducibility probe.

This tool never reads game inputs. It writes a fixed self-authored C fixture to a
new caller-selected output directory, compiles it twice with an explicit compiler
command, assembles both outputs with an explicit GNU assembler command, and
requires byte-identical objects. Its result is candidate-tool behavior only, not
an identification of the original game toolchain.
"""

from __future__ import annotations

import argparse
import hashlib
from pathlib import Path
import subprocess
import sys
from typing import Sequence


FIXTURE = """\
typedef unsigned int u32;

volatile u32 p3_global = 0x13579BDFu;
extern int p3_external(int value);

int p3_arith(int left, int right) {
    return (left * 3) + (right ^ 0x55);
}

int p3_stack(int value) {
    volatile int local = value + 9;
    return local - 4;
}

u32 p3_shift(u32 value) {
    return (value << 5) | (value >> 27);
}

int p3_call(int value) {
    return p3_external(p3_arith(value, 7));
}
"""


def _sha1(path: Path) -> str:
    digest = hashlib.sha1()
    with path.open("rb") as source:
        for chunk in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _run(command: list[str]) -> None:
    completed = subprocess.run(command, capture_output=True, text=True)
    if completed.returncode:
        diagnostic = (completed.stderr or completed.stdout).splitlines()
        message = diagnostic[0] if diagnostic else "no diagnostic"
        raise RuntimeError(f"command failed ({completed.returncode}): {message}")


def run_probe(clang: Path, assembler: Path, output: Path) -> None:
    if output.exists():
        raise ValueError("output directory already exists")
    if not clang.is_file() or not assembler.is_file():
        raise ValueError("compiler or assembler path is not a regular file")

    output.mkdir(parents=True)
    source = output / "fixture.c"
    source.write_text(FIXTURE, encoding="ascii")

    common_flags = [
        "--target=mipsel-none-elf",
        "-march=mips1",
        "-mno-abicalls",
        "-fno-pic",
        "-msoft-float",
        "-ffreestanding",
        "-fno-builtin",
        "-fno-addrsig",
        "-O2",
        "-G0",
    ]
    assembly_a = output / "candidate-a.s"
    assembly_b = output / "candidate-b.s"
    object_a = output / "candidate-a.o"
    object_b = output / "candidate-b.o"

    _run([str(clang), *common_flags, "-S", str(source), "-o", str(assembly_a)])
    _run([str(clang), *common_flags, "-S", str(source), "-o", str(assembly_b)])
    _run([str(assembler), "-march=r3000", "-G0", "-o", str(object_a), str(assembly_a)])
    _run([str(assembler), "-march=r3000", "-G0", "-o", str(object_b), str(assembly_b)])

    if object_a.read_bytes() != object_b.read_bytes():
        raise RuntimeError("two clean candidate objects differ")

    print("fixture=synthetic-c-call-stack-global-shift")
    print("compiler_target=mipsel-none-elf")
    print("compiler_flags=-march=mips1 -mno-abicalls -fno-pic -msoft-float -ffreestanding -fno-builtin -fno-addrsig -O2 -G0")
    print("assembler_flags=-march=r3000 -G0")
    print("object_reproducible=yes")
    print(f"object_size={object_a.stat().st_size}")
    print(f"object_sha1={_sha1(object_a)}")


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--clang", required=True, type=Path)
    parser.add_argument("--assembler", required=True, type=Path)
    parser.add_argument("--output", required=True, type=Path)
    args = parser.parse_args(argv)
    try:
        run_probe(args.clang, args.assembler, args.output)
    except (OSError, RuntimeError, ValueError) as exc:
        parser.error(str(exc))
    return 0


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