#!/usr/bin/env python3
"""Independently parse generated MIDI and PCM WAV files; standard library only."""
from collections import Counter
import json
import math
from pathlib import Path
import struct
import sys
import wave
from array import array


def read_vlq(data, pos):
    value = 0
    for _ in range(4):
        b = data[pos]
        pos += 1
        value = value*128+(b & 127)
        if b < 128:
            return value, pos
    raise AssertionError("Invalid variable-length MIDI quantity")


def validate_midi(path):
    raw = path.read_bytes()
    assert raw[:4] == b"MThd"
    hlen, fmt, ntracks, ppq = struct.unpack(">IHHH", raw[4:14])
    assert hlen == 6 and fmt in (0, 1) and ppq == 960
    expected_ticks = (16 if "comparison" in path.name else 64)*4*ppq
    offset, results, tempos = 14, [], []
    for i in range(ntracks):
        assert raw[offset:offset+4] == b"MTrk"
        size = struct.unpack(">I", raw[offset+4:offset+8])[0]
        data = raw[offset+8:offset+8+size]
        assert len(data) == size
        offset += size+8
        pos, tick, running, name = 0, 0, None, ""
        active, pitches, total, eot = {}, [], 0, False
        channels, max_polyphony = set(), 0
        while pos < len(data):
            delta, pos = read_vlq(data, pos)
            tick += delta
            status = data[pos]
            if status >= 128:
                pos += 1
                if status < 240:
                    running = status
            else:
                assert running is not None
                status = running
            if status == 255:
                kind = data[pos]
                pos += 1
                length, pos = read_vlq(data, pos)
                payload = data[pos:pos+length]
                pos += length
                if kind == 3:
                    name = payload.decode("ascii")
                elif kind == 81:
                    tempos.append(int.from_bytes(payload, "big"))
                elif kind == 47:
                    assert length == 0 and pos == len(data)
                    assert tick == expected_ticks
                    eot = True
                continue
            kind, channel = status >> 4, status & 15
            assert 8 <= kind <= 14
            nbytes = 1 if kind in (12, 13) else 2
            values = data[pos:pos+nbytes]
            assert len(values) == nbytes and all(v < 128 for v in values)
            pos += nbytes
            if kind in (8, 9):
                pitch, velocity = values
                key = (channel, pitch)
                if kind == 9 and velocity:
                    assert key not in active, f"Same-pitch overlap: {path.name}, {name}, {key}, tick {tick}"
                    active[key] = tick
                    pitches.append(pitch)
                    channels.add(channel+1)
                    total += 1
                    max_polyphony = max(max_polyphony, len(active))
                else:
                    assert key in active, f"Unmatched note off: {path.name}/{name}/{key}"
                    assert tick > active.pop(key)
        assert eot and not active, f"Stuck notes or missing end: {path.name}/{name}"
        results.append({"name": name, "notes": total,
                        "pitch_range": [min(pitches), max(pitches)] if pitches else None,
                        "channels": sorted(channels), "max_polyphony": max_polyphony,
                        "end_tick": tick})
    assert offset == len(raw) and tempos and set(tempos) == {461538}
    return {"file": path.name, "format": fmt, "tracks": ntracks, "ppq": ppq,
            "tempo_microseconds_per_quarter": tempos[0],
            "seconds": expected_ticks/ppq*tempos[0]/1_000_000,
            "instrument_tracks": results}


def validate_wave(path):
    with wave.open(str(path), "rb") as wav:
        assert wav.getnchannels() == 2 and wav.getsampwidth() == 2
        sr, frames = wav.getframerate(), wav.getnframes()
        assert sr == 32000
        expected = round((16 if "comparison" in path.name else 64)*4*.461538*sr)
        assert frames == expected
        samples = array("h", wav.readframes(frames))
    if sys.byteorder != "little":
        samples.byteswap()
    peak = max(abs(s) for s in samples)
    assert 0 < peak < 32767
    square_sum = sum(s*s for s in samples)
    rms = math.sqrt(square_sum/len(samples))
    assert rms > 100
    assert any(samples[i] != samples[i+1] for i in range(0, len(samples), 2))
    assert all(s == 0 for s in samples[-2:])
    return {"file": path.name, "frames": frames, "sample_rate": sr,
            "seconds": frames/sr, "channels": 2, "bits": 16,
            "peak_dbfs": round(20*math.log10(peak/32768), 3),
            "rms_dbfs": round(20*math.log10(rms/32768), 3),
            "clipped_samples": sum(abs(s) >= 32767 for s in samples),
            "stereo_difference": True}


def main():
    out = Path(sys.argv[1] if len(sys.argv) > 1 else "midnight-circuit")
    midis = sorted(out.glob("*.mid"))
    wavs = sorted(out.glob("*.wav"))
    assert len(midis) == 8 and len(wavs) == 3
    report = {"midi": [validate_midi(p) for p in midis],
              "wav": [validate_wave(p) for p in wavs],
              "checks": ["All 8 MIDI files parsed independently", "Every note-on has one later note-off",
                         "No same-channel same-pitch overlaps", "Every track ends at the exact bar boundary",
                         "All 3 WAV files contain non-silent stereo audio", "No PCM clipping",
                         "WAV durations match MIDI tempo and bar count to the nearest sample"]}
    (out/"validation.json").write_text(json.dumps(report, indent=2)+"\n")
    print(json.dumps({"checks": report["checks"], "wav": report["wav"]}, indent=2))
    full = next(m for m in report["midi"] if m["file"] == "midnight-circuit-full.mid")
    print(json.dumps(full, indent=2))


if __name__ == "__main__":
    main()
