#!/usr/bin/env python3
"""Build the Note's synthetic SMF fixtures and optionally record a historical mid run.

Python 3, standard library only. No audio is rendered and no input files are edited.
Example:
  python3 reproduce.py --out /tmp/refusal-fixtures --mid /path/to/mid
Use the binary built from e2b1b257fcb7e63a138e90e4353c48cf40172104.
--before-mid optionally records the preceding commit's overflow behaviour.
"""

import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess

AFTER = "e2b1b257fcb7e63a138e90e4353c48cf40172104"
BEFORE = "a236fae82f6ebe1c1bab4e3920977b79f86f6b51"
PPQ = 480
END = 4320
NOTES = [
    {"id": str(i + 1), "start": start, "duration": 120, "pitch": pitch}
    for i, (start, pitch) in enumerate(zip([0, 1440, 1920, 2880, 3360, 3840], [60, 62, 64, 65, 67, 69]))
]


def vlq(value):
    if not 0 <= value <= 0x0FFFFFFF:
        raise ValueError("delta must fit the SMF variable-length quantity")
    data = [value & 127]
    while value >> 7:
        value >>= 7
        data.insert(0, (value & 127) | 128)
    return bytes(data)


def smf(track):
    return b"MThd\x00\x00\x00\x06\x00\x00\x00\x01" + PPQ.to_bytes(2, "big") + b"MTrk" + len(track).to_bytes(4, "big") + track


def passage(meter):
    events = [(0, b"\xff\x51\x03\x07\xa1\x20")]
    if meter:
        events.append((0, bytes([255, 88, 4, meter, 2, 24, 8])))
    for note in NOTES:
        events.extend([
            (note["start"], bytes([144, note["pitch"], 80])),
            (note["start"] + note["duration"], bytes([128, note["pitch"], 0])),
        ])
    events.append((END, b"\xff\x2f\x00"))
    track, previous = b"", 0
    for tick, event in sorted(events, key=lambda item: item[0]):
        track += vlq(tick - previous) + event
        previous = tick
    return smf(track)


def overflow(count):
    delta = vlq(0x0FFFFFFF)
    return smf((delta + b"\xff\x01\x01x") * (count - 1) + delta + b"\xff\x2f\x00")


def run(binary, out, args):
    env = dict(os.environ)
    env.pop("RUST_BACKTRACE", None)
    result = subprocess.run([str(Path(binary).resolve()), *args], cwd=out, env=env, capture_output=True, text=True, timeout=30)
    return {"args": args, "exit": result.returncode, "stdout": result.stdout, "stderr": result.stderr}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--out", type=Path, required=True)
    parser.add_argument("--mid", help="binary built from the stated after commit; the script cannot infer a binary's commit")
    parser.add_argument("--before-mid", help="optional release binary built from the stated preceding commit")
    args = parser.parse_args()
    if args.before_mid and not args.mid:
        parser.error("--before-mid requires --mid")
    args.out.mkdir(parents=True, exist_ok=True)
    fixtures = {"unstated.mid": passage(None), "four-four.mid": passage(4), "three-four.mid": passage(3),
                "sixteen-deltas.mid": overflow(16), "seventeen-deltas.mid": overflow(17)}
    for name, data in fixtures.items():
        (args.out / name).write_bytes(data)
    record = {"kind": "Synthetic fixtures; historical CLI reconstruction, not a recorded creative session",
              "source_commit": AFTER, "ppq": PPQ, "end": END, "notes": NOTES,
              "sha256": {name: hashlib.sha256(data).hexdigest() for name, data in fixtures.items()}}
    if args.mid:
        commands = [["inspect", name, "--bars", "2:2", "--json"] for name in list(fixtures)[:3]]
        commands += [["inspect", "unstated.mid", "--json"], ["info", "sixteen-deltas.mid", "--json"], ["info", "seventeen-deltas.mid", "--json"]]
        record["runs"] = [run(args.mid, args.out, command) for command in commands]
        if [item["exit"] for item in record["runs"]] != [1, 0, 0, 0, 0, 1]:
            raise RuntimeError("Unexpected exit codes; do not publish this as a matching reconstruction")
        for index, expected in [(1, [1920, 2880, 3360]), (2, [1440, 1920])]:
            output = json.loads(record["runs"][index]["stdout"])
            notes = output if isinstance(output, list) else output["notes"]
            if [note["start"] for note in notes] != expected:
                raise RuntimeError("Bar selection does not match the Note")
        if [note["start"] for note in json.loads(record["runs"][3]["stdout"])] != [note["start"] for note in NOTES]:
            raise RuntimeError("Whole-file inspection did not preserve the six notes")
        if json.loads(record["runs"][4]["stdout"])["length_ticks"] != 4294967280:
            raise RuntimeError("The 16-gap control has an incorrect length")
        if args.before_mid:
            record["before_commit"] = BEFORE
            record["before_release"] = run(args.before_mid, args.out, ["info", "seventeen-deltas.mid", "--json"])
            before = record["before_release"]
            if before["exit"] != 0 or json.loads(before["stdout"])["length_ticks"] != 268435439:
                raise RuntimeError("Pre-fix result does not match the reported overflow")
        (args.out / "recorded-runs.json").write_text(json.dumps(record, indent=2) + "\n")
    print(f"Generated {len(fixtures)} fixtures" + (" and verified CLI results" if args.mid else "") + f" in {args.out}")


if __name__ == "__main__":
    main()
