//! Conformance corpus for the score extension (`score-conformance/`).
//! Mixdown-equivalence: given the same `wai.audio.score`, a conforming
//! sink MUST render the identical canonical `i16` PCM via the reference
//! synth, hence the identical BLAKE3 — on every machine, no tolerance.
//! The music counterpart of spatial audio's mixdown-equivalence, with
//! the recorded objects replaced by note events.
//!
//! Each vector also states `pcm_sha256`, the SHA-256 of the same samples as
//! signed 16-bit little-endian integers (SPEC §7 *Digests*), so a verifier that
//! holds only SHA-256 checks the samples; and `notes/<name>.json` holds its notes
//! in the authoring form `wai score build` reads (score §2.3), every field an
//! integer, which `verify` requires to be exactly the vector's notes.
//!
//!   wai_score_vectors gen <dir>  |  verify <dir>

use std::path::Path;
use wai::score::{waveform, Note, Score, ScoreContract};
use wai::world::{Fx, NUMERIC_FIXED64};

const SR: u32 = 22050;

fn main() {
    let a: Vec<String> = std::env::args().collect();
    match (a.get(1).map(String::as_str), a.get(2)) {
        (Some("gen"), Some(d)) => generate(Path::new(d)),
        (Some("verify"), Some(d)) => verify(Path::new(d)),
        _ => {
            eprintln!("usage: wai_score_vectors <gen|verify> <corpus-dir>");
            std::process::exit(2);
        }
    }
}

fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

/// SHA-256 of the mixdown as signed 16-bit little-endian samples.
fn pcm_sha256(s: &Score) -> String {
    let pcm: Vec<u8> = s.mixdown().iter().flat_map(|x| x.to_le_bytes()).collect();
    wai::pin::sha256_hex(&pcm)
}

/// The authoring form of a score (score §2.3): the contract, then one note a line,
/// `phase_inc`, `gain` and `sustain` as their raw Q32.32 bits.
fn notes_json(s: &Score) -> String {
    let c = &s.contract;
    let mut out = format!(
        "{{\n  \"sample_rate\": {},\n  \"channels\": {},\n  \"duration\": {},\n  \"numeric\": \"{}\",\n  \"notes\": [\n",
        c.sample_rate, c.channels, c.duration, c.numeric
    );
    let rows: Vec<String> = s.notes.iter().map(|n| format!(
        "    {{\"start\": {}, \"dur\": {}, \"phase_inc\": {}, \"gain\": {}, \"waveform\": {}, \
         \"attack\": {}, \"decay\": {}, \"sustain\": {}, \"release\": {}}}",
        n.start, n.dur, n.phase_inc.to_bits(), n.gain.to_bits(), n.waveform,
        n.attack, n.decay, n.sustain.to_bits(), n.release
    )).collect();
    out.push_str(&rows.join(",\n"));
    out.push_str("\n  ]\n}\n");
    out
}
fn f(n: i32, d: i32) -> Fx {
    Fx::from_int(n) / Fx::from_int(d)
}

/// MIDI note → per-sample phase increment (`freq / SR`), authored in f64
/// off-line — the floor never sees the exponential.
fn inc(midi: i32) -> Fx {
    let freq = 440.0 * 2f64.powf((midi - 69) as f64 / 12.0);
    Fx::from_bits(((freq / SR as f64) * 4294967296.0).round() as i64)
}

fn note(start: u64, dur: u64, midi: i32, wf: u8, gain: Fx) -> Note {
    Note {
        start,
        dur,
        phase_inc: inc(midi),
        gain,
        waveform: wf,
        attack: 220,
        decay: 600,
        sustain: f(65, 100),
        release: 1500,
    }
}

fn score(notes: Vec<Note>, dur: u64) -> Score {
    let c = ScoreContract { sample_rate: SR, numeric: NUMERIC_FIXED64.into(), channels: 1, duration: dur };
    Score { contract_bytes: c.to_canonical_json(), contract: c, notes }
}

fn vectors() -> Vec<(&'static str, Score)> {
    let g = f(4, 10);
    // C major scale ascending, saw — one note every 4000 samples.
    let scale = {
        let midis = [60, 62, 64, 65, 67, 69, 71, 72];
        let notes = midis.iter().enumerate().map(|(i, &m)| note(i as u64 * 4000, 3500, m, waveform::SAW, g)).collect();
        score(notes, 8 * 4000 + 2000)
    };
    // C major triad held together, triangle.
    let chord = score(
        vec![
            note(0, 8000, 60, waveform::TRIANGLE, g),
            note(0, 8000, 64, waveform::TRIANGLE, g),
            note(0, 8000, 67, waveform::TRIANGLE, g),
        ],
        10000,
    );
    // A short arpeggio that changes timbre across the phrase.
    let arp = {
        let steps = [(60, waveform::SQUARE), (64, waveform::SAW), (67, waveform::TRIANGLE), (72, waveform::PULSE25)];
        let notes = steps.iter().enumerate().map(|(i, &(m, w))| note(i as u64 * 2500, 2200, m, w, g)).collect();
        score(notes, 4 * 2500 + 2000)
    };
    vec![("scale", scale), ("chord", chord), ("arp", arp)]
}

fn generate(dir: &Path) {
    let v = dir.join("vectors");
    let nd = dir.join("notes");
    std::fs::create_dir_all(&v).expect("mkdir");
    std::fs::create_dir_all(&nd).expect("mkdir");
    let mut manifest = serde_json::Map::new();
    for (name, s) in vectors() {
        std::fs::write(v.join(format!("{name}.wscr")), s.to_bytes()).expect("write wscr");
        std::fs::write(nd.join(format!("{name}.json")), notes_json(&s)).expect("write notes");
        manifest.insert(
            format!("{name}.wscr"),
            serde_json::json!({
                "notes": s.notes.len(),
                "duration": s.contract.duration,
                "score_hash": hex(&s.score_hash()),
                "mixdown_hash": hex(&s.mixdown_hash()),
                "pcm_sha256": pcm_sha256(&s),
            }),
        );
    }
    std::fs::write(
        dir.join("expected_hashes.json"),
        serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n",
    )
    .expect("manifest");
    println!("generated score vectors into {}", dir.display());
}

fn verify(dir: &Path) {
    let m: serde_json::Value =
        serde_json::from_slice(&std::fs::read(dir.join("expected_hashes.json")).expect("manifest")).expect("parse");
    let v = dir.join("vectors");
    let mut fails = 0;
    for (file, spec) in m.as_object().expect("obj") {
        let s = Score::from_bytes(&std::fs::read(v.join(file)).expect("wscr")).expect("parse");
        let name = file.strip_suffix(".wscr").expect("a .wscr file");
        let notes = std::fs::read_to_string(dir.join("notes").join(format!("{name}.json"))).unwrap_or_default();
        let checks = [
            ("mixdown_hash", hex(&s.mixdown_hash()) == spec["mixdown_hash"].as_str().unwrap()),
            ("score_hash", hex(&s.score_hash()) == spec["score_hash"].as_str().unwrap()),
            ("pcm_sha256", spec["pcm_sha256"].as_str() == Some(pcm_sha256(&s).as_str())),
            ("notes/*.json", notes == notes_json(&s)),
        ];
        let failed: Vec<&str> = checks.iter().filter(|(_, ok)| !ok).map(|(k, _)| *k).collect();
        let ok = failed.is_empty();
        println!("{} {file}{}", if ok { "PASS" } else { "FAIL" },
                 if ok { String::new() } else { format!(" ({})", failed.join(", ")) });
        if !ok {
            fails += 1;
        }
    }
    if fails > 0 {
        eprintln!("{fails} failed — NOT conformant");
        std::process::exit(1);
    }
    println!("all vectors pass: mixdown-equivalence holds for scores (mixdown BLAKE3, PCM SHA-256, score hash, authoring form)");
}
