//! Conformance corpus for the volumetric extension
//! (`volumetric-conformance/`). Frame-state-equivalence: given the same
//! temporal-splat clip, a conforming sink MUST reconstruct the identical
//! Gaussian set at every frame, hence the identical BLAKE3 — hash
//! equality, no tolerance. Clips are self-contained, so each vector is a
//! `.w4ds` file + its per-frame expected hashes.
//!
//!   wai_splat4d_vectors gen <dir>  |  verify <dir>

use std::path::Path;
use wai::splat4d::{GaussKey, Splat4dClipData, Splat4dContract};
use wai::world::{Fx, NUMERIC_FIXED64};

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_splat4d_vectors <gen|verify> <corpus-dir>");
            std::process::exit(2);
        }
    }
}

fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}
fn f(n: i32, d: i32) -> Fx {
    Fx::from_int(n) / Fx::from_int(d)
}
fn k(frame: u64, pos: [Fx; 3], scale: Fx, rot: [Fx; 4], color: [Fx; 3], opacity: Fx) -> GaussKey {
    GaussKey { frame, pos, scale: [scale, scale, scale], rot, color, opacity }
}
const IDENT: [Fx; 4] = [Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE];

fn clip(gaussians: Vec<Vec<GaussKey>>, n_frames: u64) -> Splat4dClipData {
    let c = Splat4dContract {
        frame_rate: 30,
        numeric: NUMERIC_FIXED64.into(),
        n_gaussians: gaussians.len() as u32,
        n_frames,
    };
    Splat4dClipData { contract_bytes: c.to_canonical_json(), contract: c, gaussians }
}

fn vectors() -> Vec<(&'static str, Splat4dClipData)> {
    let red = [Fx::ONE, Fx::ZERO, Fx::ZERO];
    let cyan = [Fx::ZERO, Fx::ONE, Fx::ONE];
    let s = f(15, 100);

    // orbit: two Gaussians swap x positions across 12 frames.
    let orbit = clip(
        vec![
            vec![
                k(0, [Fx::from_int(-1), Fx::ZERO, Fx::ZERO], s, IDENT, red, Fx::ONE),
                k(12, [Fx::from_int(1), Fx::ZERO, Fx::ZERO], s, IDENT, red, Fx::ONE),
            ],
            vec![
                k(0, [Fx::from_int(1), Fx::ZERO, Fx::ZERO], s, IDENT, cyan, Fx::ONE),
                k(12, [Fx::from_int(-1), Fx::ZERO, Fx::ZERO], s, IDENT, cyan, Fx::ONE),
            ],
        ],
        13,
    );

    // pulse: one Gaussian scales up + fades out across 10 frames.
    let pulse = clip(
        vec![vec![
            k(0, [Fx::ZERO; 3], f(8, 100), IDENT, red, Fx::ONE),
            k(10, [Fx::ZERO; 3], f(40, 100), IDENT, red, Fx::ZERO),
        ]],
        11,
    );

    // morph: one static Gaussian shifts color red -> cyan across 8 frames.
    let morph = clip(
        vec![vec![
            k(0, [Fx::ZERO; 3], s, IDENT, red, Fx::ONE),
            k(8, [Fx::ZERO; 3], s, IDENT, cyan, Fx::ONE),
        ]],
        9,
    );

    vec![("orbit", orbit), ("pulse", pulse), ("morph", morph)]
}

fn generate(dir: &Path) {
    let v = dir.join("vectors");
    std::fs::create_dir_all(&v).expect("mkdir");
    let mut manifest = serde_json::Map::new();
    for (name, clip) in vectors() {
        std::fs::write(v.join(format!("{name}.w4ds")), clip.to_bytes()).expect("write w4ds");
        let frames: Vec<serde_json::Value> = (0..clip.contract.n_frames)
            .map(|fr| serde_json::json!({ "frame": fr, "hash": hex(&clip.frame_hash(fr)) }))
            .collect();
        manifest.insert(
            format!("{name}.w4ds"),
            serde_json::json!({
                "n_gaussians": clip.contract.n_gaussians,
                "n_frames": clip.contract.n_frames,
                "clip_hash": hex(&clip.clip_hash()),
                "frames": frames,
            }),
        );
    }
    std::fs::write(
        dir.join("expected_hashes.json"),
        serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n",
    )
    .expect("manifest");
    println!("generated splat4d 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 clip = Splat4dClipData::from_bytes(&std::fs::read(v.join(file)).expect("w4ds")).expect("parse");
        let clip_ok = hex(&clip.clip_hash()) == spec["clip_hash"].as_str().unwrap();
        let frames_ok = spec["frames"].as_array().unwrap().iter().all(|fr| {
            let n = fr["frame"].as_u64().unwrap();
            hex(&clip.frame_hash(n)) == fr["hash"].as_str().unwrap()
        });
        let ok = clip_ok && frames_ok;
        println!("{} {file}", if ok { "PASS" } else { "FAIL" });
        if !ok {
            fails += 1;
        }
    }
    if fails > 0 {
        eprintln!("{fails} failed — NOT conformant");
        std::process::exit(1);
    }
    println!("all vectors pass: frame-state-equivalence holds");
}
