//! Conformance corpus for the linear-film extension (`film-conformance/`).
//! **Film-equivalence**: given the same `wai.film.linear`, a conforming sink
//! MUST compute the identical per-frame identity — `BLAKE3(world state hash
//! at the frame's tick || the interpolated camera pose)` — hence the
//! identical per-frame hashes and the identical Merkle root over them, on
//! every machine, no tolerance. It composes the world's replay-equivalence
//! (already a corpus) with the `wai.det.fixed64` camera track.
//!
//!   wai_film_vectors gen <dir>  |  verify <dir>

use std::path::Path;
use wai::film::{CamKey, Film, FilmContract, Reel, Shot};
use wai::score::{waveform, Note, Score, ScoreContract};
use wai::world::receipt::merkle_root;
use wai::world::sim::{kin_mutation, pack_fx, KinNode, CAP_SIM_KINEMATIC0};
use wai::world::skeletal::{pack_bone, pack_fx as skel_pack_fx, skel_mutation, CAP_SIM_SKELETAL0};
use wai::world::{
    mint_node_id, CausalOp, Fx, Kinematic0, LiveWorld, Skeletal0, SimContract, WorldContainer,
    WorldState, NUMERIC_FIXED64, RESOLVE_LWW,
};

use ed25519_dalek::SigningKey;

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

fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}
fn key(s: u8) -> SigningKey {
    SigningKey::from_bytes(&[s; 32])
}
fn fxi(n: i32) -> Fx {
    Fx::from_int(n)
}
fn fxr(n: i32, d: i32) -> Fx {
    Fx::from_int(n) / Fx::from_int(d)
}

/// Build a sealed kinematic world from a set of subjects (each a CREATE at a
/// start position with a velocity) sealed at `final_tick`. Position integrates
/// as `pos += vel · (1/tick_rate)` per tick (tick_rate 60), so a velocity of N
/// traverses `N · final_tick / 60` units — chosen here to keep subjects framed.
fn sealed_world(subjects: &[(u8, [i32; 3], [i32; 3])], final_tick: u64) -> Vec<u8> {
    let c = SimContract {
        tick_rate: 60,
        numeric: NUMERIC_FIXED64.into(),
        rng_alg: "xoshiro256++".into(),
        rng_seed: [0x42; 32],
        sim: CAP_SIM_KINEMATIC0.into(),
        resolve_default: RESOLVE_LWW.into(),
        checkpoint_every: 100,
    };
    let mut container = WorldContainer {
        contract_bytes: c.to_canonical_json(),
        contract: c,
        graph_bytes: WorldState::<KinNode>::new().canonical_bytes(&Kinematic0),
        assets: vec![],
        ops: vec![],
        seal: None,
    };
    let mut live = LiveWorld::open(Kinematic0, container.clone()).expect("open");
    for &(seed, pos, vel) in subjects {
        let k = key(seed);
        let op = CausalOp::sign(
            &k,
            1,
            1,
            vec![],
            mint_node_id(&k.verifying_key().to_bytes(), 1),
            kin_mutation::CREATE,
            pack_fx(&[
                fxi(pos[0]), fxi(pos[1]), fxi(pos[2]),
                fxi(vel[0]), fxi(vel[1]), fxi(vel[2]),
                Fx::ZERO,
            ]),
        );
        live.ingest(op).expect("ingest");
    }
    let final_hash = live.advance_to(final_tick).expect("advance");
    container.ops = live.canonical_ops();
    container.seal(&key(9), final_tick, final_hash);
    container.to_bytes()
}

/// f64 → Q32.32 Fx (off-line; used only to author rig offsets + quaternions,
/// the floor itself never sees a float).
fn fxf(v: f64) -> Fx {
    Fx::from_bits((v * 4294967296.0).round() as i64)
}
fn v3(x: f64, y: f64, z: f64) -> [Fx; 3] {
    [fxf(x), fxf(y), fxf(z)]
}
/// Axis-angle → unit quaternion `(x,y,z,w)`, computed off-line.
fn quat(axis: [f64; 3], angle: f64) -> [Fx; 4] {
    let h = angle * 0.5;
    let s = h.sin();
    [fxf(axis[0] * s), fxf(axis[1] * s), fxf(axis[2] * s), fxf(h.cos())]
}

/// A sealed skeletal world: an 11-bone humanoid that waves both arms, turns
/// its head, and sways — proving a film embeds *any* sim, not just kinematic
/// points (here `wai.world.sim.skeletal0`, the avatars class). The sink skins
/// or draws the rig by the deterministic FK pose; that render is presentation.
fn sealed_character(final_tick: u64) -> Vec<u8> {
    let c = SimContract {
        tick_rate: 60,
        numeric: NUMERIC_FIXED64.into(),
        rng_alg: "xoshiro256++".into(),
        rng_seed: [0x42; 32],
        sim: CAP_SIM_SKELETAL0.into(),
        resolve_default: RESOLVE_LWW.into(),
        checkpoint_every: 100,
    };
    let mut container = WorldContainer {
        contract_bytes: c.to_canonical_json(),
        contract: c,
        graph_bytes: WorldState::<wai::world::Bone>::new().canonical_bytes(&Skeletal0),
        assets: vec![],
        ops: vec![],
        seal: None,
    };
    let k = key(1);
    let pk = k.verifying_key().to_bytes();
    let id = |l: u64| mint_node_id(&pk, l);
    let z16 = [0u8; 16];
    let ident = [Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE];
    let one = [Fx::ONE; 3];
    // (lamport, parent_lamport_or_root, local translation)
    let bones: [(u64, Option<u64>, [f64; 3]); 11] = [
        (1, None, [0.0, 1.0, 0.0]),       // pelvis (root, raised so feet ~y0)
        (2, Some(1), [0.0, 0.55, 0.0]),   // spine
        (3, Some(2), [0.0, 0.45, 0.0]),   // head
        (4, Some(2), [-0.35, 0.30, 0.0]), // l shoulder
        (5, Some(2), [0.35, 0.30, 0.0]),  // r shoulder
        (6, Some(4), [0.0, -0.70, 0.0]),  // l hand (arm hangs)
        (7, Some(5), [0.0, -0.70, 0.0]),  // r hand
        (8, Some(1), [-0.20, -0.05, 0.0]), // l hip
        (9, Some(1), [0.20, -0.05, 0.0]),  // r hip
        (10, Some(8), [0.0, -0.85, 0.0]),  // l foot
        (11, Some(9), [0.0, -0.85, 0.0]),  // r foot
    ];
    let mut ops = Vec::new();
    for (lam, parent, t) in bones {
        let p = parent.map(id).unwrap_or(z16);
        ops.push(CausalOp::sign(&k, lam, 1, vec![], id(lam), skel_mutation::CREATE, pack_bone(&p, &v3(t[0], t[1], t[2]), &ident, &one)));
    }
    // Animation: raise/wave both arms (shoulders about z, opposite signs),
    // turn the head (about y), sway the spine — a few keyframes over the reel.
    let mut lam = 12u64;
    let z = [0.0, 0.0, 1.0];
    let y = [0.0, 1.0, 0.0];
    let frames: [(u64, f64, f64, f64, f64); 4] = [
        (4, 2.0, -2.0, 0.35, 0.10),
        (10, 2.7, -2.7, -0.35, -0.10),
        (16, 2.0, -2.0, 0.35, 0.10),
        (22, 2.5, -2.5, 0.0, 0.0),
    ];
    for (tick, ls, rs, hd, sp) in frames {
        for (node, q) in [
            (4u64, quat(z, ls)),
            (5u64, quat(z, rs)),
            (3u64, quat(y, hd)),
            (2u64, quat(z, sp)),
        ] {
            ops.push(CausalOp::sign(&k, lam, tick, vec![], id(node), skel_mutation::SET_ROTATION, skel_pack_fx(&q)));
            lam += 1;
        }
    }
    let mut live = LiveWorld::open(Skeletal0, container.clone()).expect("open");
    for op in &ops {
        live.ingest(op.clone()).expect("ingest");
    }
    let final_hash = live.advance_to(final_tick).expect("advance");
    container.ops = live.canonical_ops();
    container.seal(&key(9), final_tick, final_hash);
    container.to_bytes()
}

fn cam(tick: u64, pos: [i32; 3], rot: [Fx; 4], fov: Fx) -> CamKey {
    CamKey { tick, pos: [fxi(pos[0]), fxi(pos[1]), fxi(pos[2])], rot, fov }
}

fn film(contract: FilmContract, world: Vec<u8>, camera: Vec<CamKey>) -> Film {
    Film { contract_bytes: contract.to_canonical_json(), contract, world_bytes: world, camera }
}

/// `(name, film)`. The subjects' displacement is `vel · ticks / 60`; cameras
/// are posed (at -z, looking toward +z where the action is) to keep them
/// composed — the camera move is the legible thing, the world the cargo.
fn vectors() -> Vec<(&'static str, Film)> {
    // A gentle downward pitch (~14°, x-axis quaternion) so the camera looks
    // down onto the action and the ground plane reads as a receding floor.
    let down = [fxr(12, 100), Fx::ZERO, Fx::ZERO, fxr(99, 100)];

    // "dolly": a subject crosses the frame left → right (start x=-6, vel +30
    // over 24 ticks → ends x=+6 at z=3); two static markers frame the space.
    // The camera dollies behind it, panning slightly. 9 frames every 3 ticks.
    let dolly = film(
        FilmContract { frames: 9, numeric: NUMERIC_FIXED64.into(), start_tick: 0, ticks_per_frame: 3 },
        sealed_world(
            &[
                (1, [-6, 0, 3], [30, 0, 0]),
                (2, [-7, -1, 6], [0, 0, 0]),
                (3, [7, 1, 6], [0, 0, 0]),
            ],
            24,
        ),
        vec![
            cam(0, [-4, 4, -7], down, fxr(11, 10)),
            cam(24, [4, 5, -6], down, fxr(11, 10)),
        ],
    );

    // "twoshot": two subjects part (±18 from the z=3 line, drifting +y) while
    // the camera holds wide, then pushes in. 9 frames every 4 ticks (36 ticks).
    let twoshot = film(
        FilmContract { frames: 9, numeric: NUMERIC_FIXED64.into(), start_tick: 0, ticks_per_frame: 4 },
        sealed_world(
            &[
                (1, [0, 0, 3], [-18, 6, 0]),
                (2, [0, 0, 3], [18, 6, 0]),
                (4, [0, -2, 7], [0, 0, 0]),
            ],
            36,
        ),
        vec![
            cam(0, [0, 5, -11], down, fxr(13, 10)),
            cam(18, [0, 5, -11], down, fxr(13, 10)),
            cam(36, [0, 6, -7], down, fxr(11, 10)),
        ],
    );

    // "rise": a subject climbs (start y=-4, vel +24 over 24 ticks → ends y≈5.6
    // at z=4); the camera cranes up tracking it and tilts toward the top of the
    // climb (a quaternion lerp from identity). 7 frames every 4 ticks (24).
    let rise = film(
        FilmContract { frames: 7, numeric: NUMERIC_FIXED64.into(), start_tick: 0, ticks_per_frame: 4 },
        sealed_world(
            &[
                (1, [0, -4, 4], [0, 24, 0]),
                (2, [-4, -3, 7], [0, 0, 0]),
                (3, [4, -3, 7], [0, 0, 0]),
            ],
            24,
        ),
        vec![
            cam(0, [0, 1, -8], down, fxr(11, 10)),
            // crane higher and tilt a touch more at the top of the climb
            cam(24, [0, 9, -8], [fxr(22, 100), Fx::ZERO, Fx::ZERO, fxr(97, 100)], fxr(11, 10)),
        ],
    );

    // "character": the embedded world is a skeletal humanoid (the avatars
    // sim) waving both arms — film composes with avatar. The camera frames the
    // figure from the front and cranes up a touch. 9 frames every 3 ticks.
    let character = film(
        FilmContract { frames: 9, numeric: NUMERIC_FIXED64.into(), start_tick: 0, ticks_per_frame: 3 },
        sealed_character(24),
        vec![
            cam(0, [0, 1, -4], down, fxr(11, 10)),
            cam(24, [0, 2, -4], down, fxr(11, 10)),
        ],
    );

    vec![("dolly", dolly), ("twoshot", twoshot), ("rise", rise), ("character", character)]
}

/// A short soundtrack for the reel — a rising four-note triangle motif, ~0.7 s
/// at 22.05 kHz, authored so its mixdown spreads evenly over the reel's frames.
fn soundtrack() -> Vec<u8> {
    let sr = 22050u32;
    let inc = |midi: i32| {
        let freq = 440.0 * 2f64.powf((midi - 69) as f64 / 12.0);
        Fx::from_bits(((freq / sr as f64) * 4294967296.0).round() as i64)
    };
    let note = |start: u64, midi: i32| Note {
        start,
        dur: 3200,
        phase_inc: inc(midi),
        gain: Fx::from_int(35) / Fx::from_int(100),
        waveform: waveform::TRIANGLE,
        attack: 220,
        decay: 600,
        sustain: Fx::from_int(6) / Fx::from_int(10),
        release: 1200,
    };
    // C E G C — one note per shot-beat, over 12 frames worth of audio.
    let notes = [60, 64, 67, 72].iter().enumerate().map(|(i, &m)| note(i as u64 * 4000, m)).collect();
    let c = ScoreContract { sample_rate: sr, numeric: NUMERIC_FIXED64.into(), channels: 1, duration: 16000 };
    Score { contract_bytes: c.to_canonical_json(), contract: c, notes }.to_bytes()
}

/// Reel vectors — a film with cuts: one world, a sequence of shots.
fn reel_vectors() -> Vec<(&'static str, Reel)> {
    let down = [fxr(12, 100), Fx::ZERO, Fx::ZERO, fxr(99, 100)];
    let contract = b"{\"numeric\":\"wai.det.fixed64\",\"shots\":3}".to_vec();
    // "coverage": the character beat cut three ways — a wide establishing
    // shot, a cut to a low push-in, then a cut BACK in time to a high angle on
    // the same moment (shots may revisit world ticks). One ~1 KB world, three
    // camera tracks.
    let coverage = Reel {
        numeric: NUMERIC_FIXED64.into(),
        contract_bytes: contract,
        world_bytes: sealed_character(24),
        shots: vec![
            // wide establishing (ticks 0..12)
            Shot { frames: 4, start_tick: 0, ticks_per_frame: 4, camera: vec![cam(0, [0, 2, -6], down, fxr(12, 10)), cam(12, [0, 2, -6], down, fxr(12, 10))] },
            // cut to a low front push-in (ticks 12..24)
            Shot { frames: 4, start_tick: 12, ticks_per_frame: 4, camera: vec![cam(12, [0, 1, -4], down, fxr(10, 10)), cam(24, [0, 1, -3], down, fxr(9, 10))] },
            // cut BACK to the mid-beat from a high angle (ticks 8..20 again)
            Shot { frames: 4, start_tick: 8, ticks_per_frame: 4, camera: vec![cam(8, [0, 5, -3], [fxr(35, 100), Fx::ZERO, Fx::ZERO, fxr(94, 100)], fxr(11, 10)), cam(20, [0, 5, -3], [fxr(35, 100), Fx::ZERO, Fx::ZERO, fxr(94, 100)], fxr(11, 10))] },
        ],
        // one envelope, two cargo classes: the cut picture + an embedded score.
        audio_bytes: Some(soundtrack()),
    };
    vec![("coverage", coverage)]
}

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, f) in vectors() {
        std::fs::write(v.join(format!("{name}.wflm")), f.to_bytes()).expect("write wflm");
        let frames = f.frame_hashes().expect("frame hashes");
        let leaves: Vec<[u8; 32]> = frames.iter().map(|x| x.hash).collect();
        manifest.insert(
            name.to_string(),
            serde_json::json!({
                "kind": "film",
                "frames": f.contract.frames,
                "ticks_per_frame": f.contract.ticks_per_frame,
                "start_tick": f.contract.start_tick,
                "film_hash": hex(&f.film_hash()),
                "frames_root": hex(&merkle_root(&leaves)),
            }),
        );
    }
    for (name, r) in reel_vectors() {
        std::fs::write(v.join(format!("{name}.wrel")), r.to_bytes()).expect("write wrel");
        let frames = r.frame_hashes().expect("frame hashes");
        let leaves: Vec<[u8; 32]> = frames.iter().map(|x| x.hash).collect();
        let audio_mixdown = r.audio_score().map(|s| hex(&s.expect("score").mixdown_hash()));
        manifest.insert(
            name.to_string(),
            serde_json::json!({
                "kind": "reel",
                "frames": r.total_frames(),
                "shots": r.shots.len(),
                "reel_hash": hex(&r.reel_hash()),
                "frames_root": hex(&merkle_root(&leaves)),
                "audio_mixdown_hash": audio_mixdown,
            }),
        );
    }
    std::fs::write(
        dir.join("expected_hashes.json"),
        serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n",
    )
    .expect("manifest");
    println!("generated film + reel 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 kind = spec.get("kind").and_then(|k| k.as_str()).unwrap_or("film");
        let mut audio_ok = true;
        let (frames, ident_ok, root) = if kind == "reel" {
            let r = Reel::from_bytes(&std::fs::read(v.join(format!("{file}.wrel"))).expect("wrel")).expect("parse");
            let fh = r.frame_hashes().expect("frame hashes");
            let leaves: Vec<[u8; 32]> = fh.iter().map(|x| x.hash).collect();
            // an embedded soundtrack is verified by its own mixdown-equivalence
            if let Some(expect) = spec.get("audio_mixdown_hash").and_then(|x| x.as_str()) {
                let got = r.audio_score().map(|s| hex(&s.expect("score").mixdown_hash()));
                audio_ok = got.as_deref() == Some(expect);
            }
            (fh.len() as u64, hex(&r.reel_hash()) == spec["reel_hash"].as_str().unwrap(), hex(&merkle_root(&leaves)))
        } else {
            let f = Film::from_bytes(&std::fs::read(v.join(format!("{file}.wflm"))).expect("wflm")).expect("parse");
            let fh = f.frame_hashes().expect("frame hashes");
            let leaves: Vec<[u8; 32]> = fh.iter().map(|x| x.hash).collect();
            (fh.len() as u64, hex(&f.film_hash()) == spec["film_hash"].as_str().unwrap(), hex(&merkle_root(&leaves)))
        };
        let ok = ident_ok
            && audio_ok
            && root == spec["frames_root"].as_str().unwrap()
            && frames == spec["frames"].as_u64().unwrap();
        let snd = if spec.get("audio_mixdown_hash").and_then(|x| x.as_str()).is_some() { " +sound" } else { "" };
        println!("{} {file}  ({kind}, {frames} frames{snd})", if ok { "PASS" } else { "FAIL" });
        if !ok {
            fails += 1;
        }
    }
    if fails > 0 {
        eprintln!("{fails} failed — film/reel-equivalence does NOT hold here");
        std::process::exit(1);
    }
    println!("all vectors pass: film/reel-equivalence holds (world replay + camera tracks)");
}
