//! Conformance corpus generator + verifier for the spatial-audio
//! extension (`audio-conformance/`). The mixdown-equivalence analog of
//! `wai_world_vectors`: a conforming sink mixes each scene to the
//! declared layout and MUST reproduce the expected canonical PCM hash —
//! pass is hash equality, no tolerance parameter.
//!
//!   wai_audio_vectors gen <dir>      regenerate the vectors
//!   wai_audio_vectors verify <dir>   mix every scene, compare hashes
//!
//! Sources here are deterministic synthetic tones (no clock, no
//! entropy). In a real sink they'd be decoded `wai.audio.*` assets;
//! the corpus provides the decoded mono PCM so the test isolates the
//! one new thing — the deterministic mixdown.

use std::collections::HashMap;
use std::path::Path;
use wai::audio_scene::{
    AudioObject, PosKey, SpatialScene, DIST_INVERSE_CLAMPED, PAN_SQRT_CONSTPOWER,
};
use wai::audio_scene::SceneContract;
use wai::world::{AssetEntry, Fx};

type Vector = (&'static str, SpatialScene, HashMap<[u8; 32], Vec<i16>>);

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

fn tone(n: usize, period: usize, amp: i16) -> Vec<i16> {
    (0..n).map(|i| if (i / period).is_multiple_of(2) { amp } else { -amp }).collect()
}

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

/// (scene, sources) builders — shared by gen and (re)verify.
fn vectors() -> Vec<Vector> {
    let mut out = Vec::new();

    // 1. static stereo pair: one hard-left, one hard-right.
    {
        let dur = 256u64;
        let mut src = HashMap::new();
        src.insert([1u8; 32], tone(dur as usize, 8, 9000));
        src.insert([2u8; 32], tone(dur as usize, 11, 7000));
        let scene = build(
            dur,
            vec![
                static_obj([1u8; 32], 1, -5, 0),
                static_obj([2u8; 32], 1, 5, 0),
            ],
        );
        out.push(("stereo_pair", scene, src));
    }

    // 2. distance: same source near vs far (two objects, different z).
    {
        let dur = 256u64;
        let mut src = HashMap::new();
        src.insert([3u8; 32], tone(dur as usize, 6, 10000));
        let scene = build(
            dur,
            vec![static_obj([3u8; 32], 1, -2, 1), static_obj([3u8; 32], 1, 6, 8)],
        );
        out.push(("distance", scene, src));
    }

    // 3. moving source: sweeps left→right over the duration.
    {
        let dur = 512u64;
        let mut src = HashMap::new();
        src.insert([4u8; 32], tone(dur as usize, 7, 8000));
        let moving = AudioObject {
            source_hash: [4u8; 32],
            gain: Fx::ONE,
            keys: vec![
                PosKey { pos_sample: 0, x: Fx::from_int(-6), y: Fx::ZERO, z: Fx::ONE },
                PosKey { pos_sample: dur, x: Fx::from_int(6), y: Fx::ZERO, z: Fx::ONE },
            ],
        };
        out.push(("moving", build(dur, vec![moving]), src));
    }

    out
}

fn static_obj(hash: [u8; 32], gain: i32, x: i32, z: i32) -> AudioObject {
    AudioObject {
        source_hash: hash,
        gain: Fx::from_int(gain),
        keys: vec![PosKey { pos_sample: 0, x: Fx::from_int(x), y: Fx::ZERO, z: Fx::from_int(z) }],
    }
}

fn build(dur: u64, objects: Vec<AudioObject>) -> SpatialScene {
    let c = SceneContract {
        sample_rate: 48000,
        numeric: wai::world::NUMERIC_FIXED64.into(),
        pan_law: PAN_SQRT_CONSTPOWER.into(),
        distance: DIST_INVERSE_CLAMPED.into(),
        duration: dur,
        floor_layout: "stereo".into(),
    };
    // name the sources in the asset table (role 0), for completeness.
    let mut assets = Vec::new();
    let mut seen = std::collections::BTreeSet::new();
    for o in &objects {
        if seen.insert(o.source_hash) {
            assets.push(AssetEntry { hash: o.source_hash, role: 0, name: "tone".into() });
        }
    }
    SpatialScene {
        contract_bytes: c.to_canonical_json(),
        contract: c,
        objects,
        assets,
        seal: None,
    }
}

fn write_sources(path: &Path, src: &HashMap<[u8; 32], Vec<i16>>) {
    let mut out = Vec::new();
    out.extend_from_slice(&(src.len() as u32).to_le_bytes());
    // deterministic order: by hash
    let mut keys: Vec<_> = src.keys().collect();
    keys.sort();
    for k in keys {
        let pcm = &src[k];
        out.extend_from_slice(k);
        out.extend_from_slice(&(pcm.len() as u32).to_le_bytes());
        for s in pcm {
            out.extend_from_slice(&s.to_le_bytes());
        }
    }
    std::fs::write(path, out).expect("write sources");
}

fn read_sources(path: &Path) -> HashMap<[u8; 32], Vec<i16>> {
    let b = std::fs::read(path).expect("read sources");
    let mut p = 4usize;
    let n = u32::from_le_bytes(b[..4].try_into().unwrap()) as usize;
    let mut map = HashMap::new();
    for _ in 0..n {
        let hash: [u8; 32] = b[p..p + 32].try_into().unwrap();
        p += 32;
        let len = u32::from_le_bytes(b[p..p + 4].try_into().unwrap()) as usize;
        p += 4;
        let pcm: Vec<i16> = b[p..p + len * 2]
            .chunks_exact(2)
            .map(|c| i16::from_le_bytes([c[0], c[1]]))
            .collect();
        p += len * 2;
        map.insert(hash, pcm);
    }
    map
}

fn generate(dir: &Path) {
    let vdir = dir.join("vectors");
    std::fs::create_dir_all(&vdir).expect("mkdir");
    let mut manifest = serde_json::Map::new();
    for (name, scene, src) in vectors() {
        std::fs::write(vdir.join(format!("{name}.wscn")), scene.to_bytes()).expect("write scene");
        write_sources(&vdir.join(format!("{name}.sources.bin")), &src);
        let mh = scene.mixdown_hash(&src, "stereo").expect("mix");
        manifest.insert(
            format!("{name}.wscn"),
            serde_json::json!({
                "sources": format!("{name}.sources.bin"),
                "layout": "stereo",
                "scene_hash": hex(&scene.scene_hash()),
                "mixdown_hash": hex(&mh),
            }),
        );
    }
    std::fs::write(
        dir.join("expected_hashes.json"),
        serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n",
    )
    .expect("write manifest");
    println!("generated audio vectors into {}", dir.display());
}

fn verify(dir: &Path) {
    let manifest: serde_json::Value =
        serde_json::from_slice(&std::fs::read(dir.join("expected_hashes.json")).expect("manifest"))
            .expect("parse");
    let vdir = dir.join("vectors");
    let mut failures = 0;
    for (file, spec) in manifest.as_object().expect("obj") {
        let scene = SpatialScene::from_bytes(&std::fs::read(vdir.join(file)).expect("scene"))
            .expect("parse scene");
        let src = read_sources(&vdir.join(spec["sources"].as_str().unwrap()));
        let layout = spec["layout"].as_str().unwrap();
        let got = scene.mixdown_hash(&src, layout).expect("mix");
        let ok = hex(&got) == spec["mixdown_hash"].as_str().unwrap()
            && hex(&scene.scene_hash()) == spec["scene_hash"].as_str().unwrap();
        println!("{} {file}", if ok { "PASS" } else { "FAIL" });
        if !ok {
            failures += 1;
        }
    }
    if failures > 0 {
        eprintln!("{failures} vector(s) failed — NOT conformant");
        std::process::exit(1);
    }
    println!("all vectors pass: mixdown-equivalence holds");
}
