//! Volumetric / 4D temporal Gaussian splats — `wai.splat.4d` (Phase 5).
//!
//! The frontier of "video": a volumetric scene that moves. A static
//! Gaussian splat (`wai.splat.spz`) is a cloud of anisotropic Gaussians;
//! a *temporal* splat gives each Gaussian a keyframed track — position,
//! scale, rotation, color, opacity over time — and the sink interpolates
//! the cloud at any frame and renders it with the EWA pipeline. This is
//! lever 3 (instructions-at-the-sink) for volumetric capture: the wire
//! carries the trajectories, the sink reconstructs the frame.
//!
//! Like haptics and spatial audio, the reconstruction is deterministic
//! in the `wai.det.fixed64` floor (Fx linear interpolation of the
//! keyframes), so conformance is **frame-state-equivalence**: the
//! Gaussian set at frame N hashes identically on every sink, no
//! tolerance parameter. The *render* of that set (EWA splatting,
//! sorting, blending) stays the renderer's business — exactly as the
//! static splat's rasterization does.
//!
//! Honest scope: volumetric is heavy (a cloud is MB-scale). The win here
//! is the same as static splats — content-addressing, dedup, one signed
//! envelope, and a verifiable per-frame state — NOT payload reduction.
//! The floor stays trig-free: rotations are quaternions, linearly
//! interpolated (nlerp without renormalization — deterministic; the sink
//! normalizes for rendering, which is presentation).

use crate::world::Fx;

mod container;
mod receipt;
pub use container::{Splat4dContract, Splat4dError};
pub use receipt::{FrameHash, Splat4dReceipt};

/// Domain separators (same discipline as the rest of WAI).
const DOMAIN_FRAME: &[u8] = b"wai:splat4d-frame\x01";
const DOMAIN_CLIP: &[u8] = b"wai:splat4d-clip\x01";

/// One keyframe of a Gaussian's track. All attributes are `Fx`; `color`
/// and `opacity` are in `[0, 1]`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GaussKey {
    pub frame: u64,
    pub pos: [Fx; 3],
    pub scale: [Fx; 3],
    /// Rotation quaternion `(x, y, z, w)`.
    pub rot: [Fx; 4],
    pub color: [Fx; 3],
    pub opacity: Fx,
}

/// The reconstructed state of one Gaussian at a frame — the canonical
/// hashed quantity, in field order `pos, scale, rot, color, opacity`
/// (14 `Fx`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GaussState {
    pub pos: [Fx; 3],
    pub scale: [Fx; 3],
    pub rot: [Fx; 4],
    pub color: [Fx; 3],
    pub opacity: Fx,
}

impl GaussState {
    /// Canonical bytes: the 14 `Fx` as `i64` little-endian, in field
    /// order. This IS the per-Gaussian normative state layout.
    fn write(&self, out: &mut Vec<u8>) {
        for v in self
            .pos
            .iter()
            .chain(self.scale.iter())
            .chain(self.rot.iter())
            .chain(self.color.iter())
            .chain(std::iter::once(&self.opacity))
        {
            out.extend_from_slice(&v.to_bits().to_le_bytes());
        }
    }
}

/// A temporal Gaussian-splat clip: per-Gaussian keyframe tracks.
#[derive(Clone, Debug, PartialEq)]
pub struct Splat4dClipData {
    pub contract: Splat4dContract,
    /// Exact contract bytes carried on the wire (preserved verbatim).
    pub contract_bytes: Vec<u8>,
    /// One keyframe track per Gaussian (each MUST be ascending by frame).
    pub gaussians: Vec<Vec<GaussKey>>,
}

/// Fixed-point frame index: `n` exactly, as Q32.32.
fn fx_frame(n: u64) -> Fx {
    Fx::from_bits((n as i64) << 32)
}

/// Linear interpolation `a + (b - a) · t` in the `Fx` floor.
fn lerp(a: Fx, b: Fx, t: Fx) -> Fx {
    a + (b - a) * t
}

/// Interpolate one Gaussian's attributes at `frame` from its keyframe
/// track. Before the first / after the last key, the endpoint holds.
fn reconstruct_one(track: &[GaussKey], frame: u64) -> GaussState {
    debug_assert!(!track.is_empty());
    let first = &track[0];
    let last = &track[track.len() - 1];
    if frame <= first.frame {
        return state_of(first);
    }
    if frame >= last.frame {
        return state_of(last);
    }
    // bracketing keys a.frame <= frame < b.frame
    let mut a = first;
    let mut b = last;
    for w in track.windows(2) {
        if w[0].frame <= frame && frame < w[1].frame {
            a = &w[0];
            b = &w[1];
            break;
        }
    }
    let t = fx_frame(frame - a.frame) / fx_frame(b.frame - a.frame);
    let lerp3 = |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 lerp4 = |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)]
    };
    GaussState {
        pos: lerp3(a.pos, b.pos),
        scale: lerp3(a.scale, b.scale),
        rot: lerp4(a.rot, b.rot),
        color: lerp3(a.color, b.color),
        opacity: lerp(a.opacity, b.opacity, t),
    }
}

fn state_of(k: &GaussKey) -> GaussState {
    GaussState { pos: k.pos, scale: k.scale, rot: k.rot, color: k.color, opacity: k.opacity }
}

impl Splat4dClipData {
    pub fn n_gaussians(&self) -> usize {
        self.gaussians.len()
    }

    /// Reconstruct the full Gaussian set at `frame`.
    pub fn reconstruct_frame(&self, frame: u64) -> Vec<GaussState> {
        self.gaussians.iter().map(|t| reconstruct_one(t, frame)).collect()
    }

    /// Canonical frame bytes: `n_gaussians u32 | per-Gaussian 14×i64`.
    fn frame_bytes(&self, frame: u64) -> Vec<u8> {
        let states = self.reconstruct_frame(frame);
        let mut out = Vec::with_capacity(4 + states.len() * 14 * 8);
        out.extend_from_slice(&(states.len() as u32).to_le_bytes());
        for s in &states {
            s.write(&mut out);
        }
        out
    }

    /// **Frame-state-equivalence** quantity:
    /// `BLAKE3(DOMAIN_FRAME || frame_le || canonical frame bytes)`.
    /// Two sinks with the same clip MUST compute this identically at
    /// every frame.
    pub fn frame_hash(&self, frame: u64) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(DOMAIN_FRAME);
        h.update(&frame.to_le_bytes());
        h.update(&self.frame_bytes(frame));
        *h.finalize().as_bytes()
    }

    /// Clip identity: `BLAKE3(DOMAIN_CLIP || contract_bytes || tracks)`.
    /// Independent of any single frame — two copies of the same clip
    /// share it.
    pub fn clip_hash(&self) -> [u8; 32] {
        let mut h = blake3::Hasher::new();
        h.update(DOMAIN_CLIP);
        h.update(&self.contract_bytes);
        h.update(&container::tracks_bytes(&self.gaussians));
        *h.finalize().as_bytes()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::world::NUMERIC_FIXED64;

    fn f(n: i32, d: i32) -> Fx {
        Fx::from_int(n) / Fx::from_int(d)
    }

    fn clip() -> Splat4dClipData {
        // one Gaussian moving x: 0 -> 2 over frames 0..10, opacity 1->0
        let track = vec![
            GaussKey {
                frame: 0,
                pos: [Fx::ZERO, Fx::ZERO, Fx::ZERO],
                scale: [f(1, 10), f(1, 10), f(1, 10)],
                rot: [Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE],
                color: [Fx::ONE, Fx::ZERO, Fx::ZERO],
                opacity: Fx::ONE,
            },
            GaussKey {
                frame: 10,
                pos: [Fx::from_int(2), Fx::ZERO, Fx::ZERO],
                scale: [f(1, 10), f(1, 10), f(1, 10)],
                rot: [Fx::ZERO, Fx::ZERO, Fx::ZERO, Fx::ONE],
                color: [Fx::ONE, Fx::ZERO, Fx::ZERO],
                opacity: Fx::ZERO,
            },
        ];
        let c = Splat4dContract {
            frame_rate: 30,
            numeric: NUMERIC_FIXED64.into(),
            n_gaussians: 1,
            n_frames: 11,
        };
        Splat4dClipData { contract_bytes: c.to_canonical_json(), contract: c, gaussians: vec![track] }
    }

    #[test]
    fn interpolates_midframe() {
        let c = clip();
        let mid = c.reconstruct_frame(5);
        // halfway: x = 1, opacity = 0.5
        assert_eq!(mid[0].pos[0], Fx::from_int(1));
        assert_eq!(mid[0].opacity, f(1, 2));
    }

    #[test]
    fn holds_outside_range() {
        let c = clip();
        assert_eq!(c.reconstruct_frame(0)[0].pos[0], Fx::ZERO);
        assert_eq!(c.reconstruct_frame(10)[0].pos[0], Fx::from_int(2));
        // beyond last frame holds the last key
        assert_eq!(c.reconstruct_frame(99)[0].opacity, Fx::ZERO);
    }

    #[test]
    fn frame_hash_is_deterministic_and_moves() {
        let c = clip();
        assert_eq!(c.frame_hash(5), c.frame_hash(5));
        assert_ne!(c.frame_hash(3), c.frame_hash(7));
        // clip hash is frame-independent identity
        assert_eq!(c.clip_hash(), clip().clip_hash());
    }

    #[test]
    fn container_round_trips() {
        let c = clip();
        let bytes = c.to_bytes();
        let back = Splat4dClipData::from_bytes(&bytes).unwrap();
        assert_eq!(back, c);
        assert_eq!(back.frame_hash(5), c.frame_hash(5));
    }
}
