//! Linear film — `wai.film.linear`.
//!
//! "A film is a sealed, camera-on-rails world." A feature ships not as
//! pixels but as the **world** that generates it (a sealed
//! `wai.world.replay` — kilobytes of signed CausalOps) plus a **camera
//! track** (keyframed poses) and a **frame schedule** (which world tick each
//! frame samples). The sink renders the frames itself, at its own
//! resolution, frame rate, and quality — the wire carries the instructions,
//! not the result. This is the instructions-at-the-sink lever applied to the
//! largest captured-as-pixels category there is.
//!
//! Conformance is **film-equivalence**: for the same film, every sink
//! computes the identical per-frame identity — `BLAKE3(world_state_hash(tick)
//! || canonical camera pose)` — on every machine, no tolerance. It composes
//! two guarantees already proven: the world's *replay-equivalence* (every
//! sink agrees on the state hash at every tick) and the `wai.det.fixed64`
//! floor for the camera (interpolated trig-free, exactly like the
//! splat/skeletal tracks; renormalization of the quaternion for rendering is
//! presentation, not the verified quantity). The presentation render — the
//! actual pixels — stays the sink's business, exactly as the worlds renderer
//! or the audio-scene HRTF does.

use crate::world::{run_sampled_dispatch, run_sealed_dispatch, Fx, WorldContainer};

mod container;
pub use container::{FilmContract, FilmError};
mod receipt;
pub use receipt::{FilmReceipt, FrameHash};
mod reel;
pub use reel::{Reel, ReelError, Shot};

const DOMAIN_FRAME: &[u8] = b"wai:film-frame\x01";
const DOMAIN_FILM: &[u8] = b"wai:film\x01";

/// One camera keyframe, keyed by **world tick**. Orientation is a
/// quaternion; like the splat/skeletal tracks it is interpolated
/// component-wise and renormalized by the renderer — the verified quantity
/// is the exact interpolated pose, not a normalized one.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CamKey {
    pub tick: u64,
    pub pos: [Fx; 3],
    pub rot: [Fx; 4],
    pub fov: Fx,
}

/// A camera pose sampled at a frame (the interpolated track value).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CamPose {
    pub pos: [Fx; 3],
    pub rot: [Fx; 4],
    pub fov: Fx,
}

/// A linear film: a contract + an embedded sealed world + a camera track.
#[derive(Clone, Debug, PartialEq)]
pub struct Film {
    pub contract: FilmContract,
    /// Exact contract bytes on the wire (the film hash binds them).
    pub contract_bytes: Vec<u8>,
    /// The embedded sealed `WWLD` world, verbatim.
    pub world_bytes: Vec<u8>,
    pub camera: Vec<CamKey>,
}

fn fx_tick(n: u64) -> Fx {
    Fx::from_bits((n as i64) << 32)
}
fn lerp(a: Fx, b: Fx, t: Fx) -> Fx {
    a + (b - a) * t
}

/// The camera pose at a world `tick` from a keyframe track — lerp, clamped to
/// the endpoints outside the track (mirrors the splat/skeletal discipline).
/// Shared by [`Film`] and the reel's shots.
pub(crate) fn interpolate_camera(keys: &[CamKey], tick: u64) -> CamPose {
    let first = &keys[0];
    let last = &keys[keys.len() - 1];
    if tick <= first.tick {
        return CamPose { pos: first.pos, rot: first.rot, fov: first.fov };
    }
    if tick >= last.tick {
        return CamPose { pos: last.pos, rot: last.rot, fov: last.fov };
    }
    let (mut a, mut b) = (first, last);
    for w in keys.windows(2) {
        if w[0].tick <= tick && tick < w[1].tick {
            a = &w[0];
            b = &w[1];
            break;
        }
    }
    let t = fx_tick(tick - a.tick) / fx_tick(b.tick - a.tick);
    let l3 = |x: [Fx; 3], y: [Fx; 3]| [lerp(x[0], y[0], t), lerp(x[1], y[1], t), lerp(x[2], y[2], t)];
    let l4 = |x: [Fx; 4], y: [Fx; 4]| {
        [lerp(x[0], y[0], t), lerp(x[1], y[1], t), lerp(x[2], y[2], t), lerp(x[3], y[3], t)]
    };
    CamPose { pos: l3(a.pos, b.pos), rot: l4(a.rot, b.rot), fov: lerp(a.fov, b.fov, t) }
}

/// Canonical camera-pose bytes for hashing: `pos[3] | rot[4] | fov`, raw `Fx`
/// bits little-endian. Shared by [`Film`] and the reel.
pub(crate) fn pose_bytes(p: &CamPose) -> Vec<u8> {
    let mut o = Vec::with_capacity(64);
    for c in p.pos {
        o.extend_from_slice(&c.to_bits().to_le_bytes());
    }
    for c in p.rot {
        o.extend_from_slice(&c.to_bits().to_le_bytes());
    }
    o.extend_from_slice(&p.fov.to_bits().to_le_bytes());
    o
}

impl Film {
    /// The world tick frame `f` samples: `start_tick + f * ticks_per_frame`. Within the
    /// film's frames it cannot overflow: the contract's reader refuses a schedule whose
    /// last tick a `u64` does not hold. Past them it saturates rather than wrap to an
    /// earlier tick.
    pub fn frame_tick(&self, f: u64) -> u64 {
        f.saturating_mul(self.contract.ticks_per_frame).saturating_add(self.contract.start_tick)
    }

    /// The frame ticks, ascending — what the world is sampled at.
    pub fn frame_ticks(&self) -> Vec<u64> {
        (0..self.contract.frames).map(|f| self.frame_tick(f)).collect()
    }

    /// The camera pose at a world `tick` — keyframe lerp, clamped to the
    /// endpoints outside the track (mirrors the splat/skeletal discipline).
    pub fn camera_at(&self, tick: u64) -> CamPose {
        interpolate_camera(&self.camera, tick)
    }

    fn pose_bytes(p: &CamPose) -> Vec<u8> {
        let mut o = Vec::with_capacity(64);
        for c in p.pos {
            o.extend_from_slice(&c.to_bits().to_le_bytes());
        }
        for c in p.rot {
            o.extend_from_slice(&c.to_bits().to_le_bytes());
        }
        o.extend_from_slice(&p.fov.to_bits().to_le_bytes());
        o
    }

    /// Parse the embedded world container.
    pub fn world(&self) -> Result<WorldContainer, FilmError> {
        WorldContainer::from_bytes(&self.world_bytes).map_err(|e| FilmError::BadWorld(e.to_string()))
    }

    /// Validate the embedded world as a conformant sealed replay (the seal
    /// verifies and the log re-derives the sealed final hash) and return its
    /// sealed final tick.
    pub fn validate_world(&self) -> Result<u64, FilmError> {
        let world = self.world()?;
        run_sealed_dispatch(&world).map_err(|e| FilmError::BadWorld(e.to_string()))?;
        world
            .seal
            .map(|s| s.final_tick)
            .ok_or_else(|| FilmError::BadWorld("embedded world is not sealed".into()))
    }

    /// Per-frame reconstruction-block hashes — the film's objects. Each is
    /// `BLAKE3(DOMAIN_FRAME || frame_index || world_state_hash(frame_tick) ||
    /// camera_pose)`. Validates the embedded world first; fails if a frame
    /// samples past the sealed final tick.
    pub fn frame_hashes(&self) -> Result<Vec<FrameHash>, FilmError> {
        let final_tick = self.validate_world()?;
        let ticks = self.frame_ticks();
        if let Some(&max) = ticks.last()
            && max > final_tick
        {
            return Err(FilmError::FrameTickPastSeal { tick: max, final_tick });
        }
        let world = self.world()?;
        let checkpoints =
            run_sampled_dispatch(&world, &ticks).map_err(|e| FilmError::BadWorld(e.to_string()))?;
        if checkpoints.len() != ticks.len() {
            return Err(FilmError::BadWorld(format!(
                "sampler returned {} hashes for {} frame ticks", checkpoints.len(), ticks.len()
            )));
        }
        Ok(checkpoints
            .iter()
            .enumerate()
            .map(|(f, cp)| {
                let pose = self.camera_at(cp.tick);
                let mut h = blake3::Hasher::new();
                h.update(DOMAIN_FRAME);
                h.update(&(f as u64).to_le_bytes());
                h.update(&cp.hash);
                h.update(&Self::pose_bytes(&pose));
                FrameHash { index: f as u64, hash: *h.finalize().as_bytes() }
            })
            .collect())
    }

    /// Film identity: `BLAKE3(DOMAIN_FILM || contract || world_bytes ||
    /// camera)`, independent of the rendered pixels — binds the exact inputs.
    pub fn film_hash(&self) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(DOMAIN_FILM);
        h.update(&self.contract_bytes);
        h.update(&self.world_bytes);
        h.update(&container::camera_bytes(&self.camera));
        *h.finalize().as_bytes()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::world::sim::{kin_mutation, pack_fx, KinNode, CAP_SIM_KINEMATIC0};
    use crate::world::{
        mint_node_id, CausalOp, Kinematic0, LiveWorld, SimContract, WorldContainer, WorldState,
        NUMERIC_FIXED64, RESOLVE_LWW,
    };
    use ed25519_dalek::SigningKey;

    fn key(s: u8) -> SigningKey {
        SigningKey::from_bytes(&[s; 32])
    }

    // A tiny sealed kinematic world: one node created at tick 1 with a
    // constant velocity (1,0,0), so its position advances deterministically
    // each tick — every frame's state hash differs.
    fn sealed_world() -> 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 a = key(1);
        let create = CausalOp::sign(
            &a,
            1,
            1,
            vec![],
            mint_node_id(&a.verifying_key().to_bytes(), 1),
            kin_mutation::CREATE,
            pack_fx(&[Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE, Fx::ZERO, Fx::ZERO, Fx::ZERO]),
        );
        let mut live = LiveWorld::open(Kinematic0, container.clone()).unwrap();
        live.ingest(create).unwrap();
        let final_tick = 16u64;
        let final_hash = live.advance_to(final_tick).unwrap();
        container.ops = live.canonical_ops();
        container.seal(&key(3), final_tick, final_hash);
        container.to_bytes()
    }

    fn cam_key(tick: u64, x: i32) -> CamKey {
        CamKey {
            tick,
            pos: [Fx::from_int(x), Fx::from_int(2), Fx::ZERO],
            rot: [Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE],
            fov: Fx::from_int(1) / Fx::from_int(2),
        }
    }

    fn film() -> Film {
        let contract = FilmContract {
            frames: 5,
            numeric: NUMERIC_FIXED64.into(),
            start_tick: 0,
            ticks_per_frame: 4,
        };
        Film {
            contract_bytes: contract.to_canonical_json(),
            contract,
            world_bytes: sealed_world(),
            camera: vec![cam_key(0, 0), cam_key(16, 8)],
        }
    }

    #[test]
    fn frame_ticks_map_exactly() {
        let f = film();
        assert_eq!(f.frame_ticks(), vec![0, 4, 8, 12, 16]);
    }

    #[test]
    fn camera_interpolates_and_clamps() {
        let f = film();
        // clamp before the first key
        assert_eq!(f.camera_at(0).pos[0], Fx::ZERO);
        // midpoint tick 8 of [0,16] → x = 4
        assert_eq!(f.camera_at(8).pos[0], Fx::from_int(4));
        // clamp after the last key
        assert_eq!(f.camera_at(99).pos[0], Fx::from_int(8));
    }

    #[test]
    fn frames_reconstruct_and_are_deterministic() {
        let f = film();
        let fh = f.frame_hashes().expect("frame hashes");
        assert_eq!(fh.len(), 5);
        // deterministic
        assert_eq!(f.frame_hashes().unwrap(), fh);
        // distinct frames differ (the world + camera both move)
        assert_ne!(fh[0].hash, fh[4].hash);
    }

    #[test]
    fn film_hash_binds_inputs() {
        let a = film();
        let mut b = film();
        b.camera[1] = cam_key(16, 9); // move the last camera key
        assert_ne!(a.film_hash(), b.film_hash());
        assert_ne!(a.frame_hashes().unwrap().last().unwrap().hash,
                   b.frame_hashes().unwrap().last().unwrap().hash);
    }

    #[test]
    fn frame_past_seal_is_rejected() {
        let mut f = film();
        f.contract.frames = 6; // frame 5 → tick 20 > sealed final 16
        f.contract_bytes = f.contract.to_canonical_json();
        assert!(matches!(f.frame_hashes(), Err(FilmError::FrameTickPastSeal { .. })));
    }

    #[test]
    fn container_round_trips() {
        let f = film();
        let bytes = f.to_bytes();
        let back = Film::from_bytes(&bytes).unwrap();
        assert_eq!(back, f);
        assert_eq!(back.film_hash(), f.film_hash());
        assert_eq!(back.frame_hashes().unwrap(), f.frame_hashes().unwrap());
    }
}
