//! The `WFLM` container + the film contract — mirrors the `WSCR` / `W4DS`
//! section layout: 4-byte magic, `u16` section count, a `(kind u8, off u32,
//! len u32)` table, then 8-byte-aligned payloads. Three sections: the
//! canonical-JSON contract, the embedded sealed world (a `WWLD` container,
//! verbatim — the film is content-addressed over the exact world bytes), and
//! the camera track.

use super::CamKey;
use crate::world::Fx;

pub const MAGIC_WFLM: &[u8; 4] = b"WFLM";
const SECTION_CONTRACT: u8 = 0x01;
const SECTION_WORLD: u8 = 0x02;
const SECTION_CAMERA: u8 = 0x03;

/// Bytes per camera keyframe on the wire: tick(8) + pos(3×8) + rot(4×8) +
/// fov(8) = 72.
pub(super) const CAMKEY_BYTES: usize = 72;

#[derive(Debug)]
pub enum FilmError {
    BadMagic,
    Truncated,
    MissingSection(u8),
    BadContract(String),
    BadCamera(String),
    /// The embedded world failed to parse or to replay against its seal.
    BadWorld(String),
    /// A frame samples the world past its sealed final tick — the film
    /// would render a state the seal never defined.
    FrameTickPastSeal { tick: u64, final_tick: u64 },
}

impl std::fmt::Display for FilmError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::BadMagic => write!(f, "payload is not a WFLM container"),
            Self::Truncated => write!(f, "byte stream truncated"),
            Self::MissingSection(k) => write!(f, "required section 0x{k:02x} missing"),
            Self::BadContract(why) => write!(f, "bad film contract: {why}"),
            Self::BadCamera(why) => write!(f, "bad camera track: {why}"),
            Self::BadWorld(why) => write!(f, "bad embedded world: {why}"),
            Self::FrameTickPastSeal { tick, final_tick } => {
                write!(f, "frame tick {tick} is past the world's sealed final tick {final_tick}")
            }
        }
    }
}
impl std::error::Error for FilmError {}

/// Film contract — canonical JSON (sorted keys, no whitespace), bound by the
/// film hash and carried verbatim. The frame→world-tick map is exact and
/// resolution-free: frame `f` reads the world at `start_tick + f *
/// ticks_per_frame`. `fps` is presentation (playback speed), not part of the
/// verified quantity, so it is not in the contract.
#[derive(Clone, Debug, PartialEq)]
pub struct FilmContract {
    pub frames: u64,
    pub numeric: String,
    pub start_tick: u64,
    pub ticks_per_frame: u64,
}

impl FilmContract {
    pub fn to_canonical_json(&self) -> Vec<u8> {
        format!(
            "{{\"frames\":{},\"numeric\":\"{}\",\"start_tick\":{},\"ticks_per_frame\":{}}}",
            self.frames, self.numeric, self.start_tick, self.ticks_per_frame
        )
        .into_bytes()
    }

    pub fn from_json(b: &[u8]) -> Result<FilmContract, FilmError> {
        let v: serde_json::Value =
            serde_json::from_slice(b).map_err(|e| FilmError::BadContract(e.to_string()))?;
        let o = v.as_object().ok_or_else(|| FilmError::BadContract("not an object".into()))?;
        let u = |k: &str| {
            o.get(k).and_then(|x| x.as_u64()).ok_or_else(|| FilmError::BadContract(format!("missing `{k}`")))
        };
        let frames = u("frames")?;
        let ticks_per_frame = u("ticks_per_frame")?;
        if frames == 0 {
            return Err(FilmError::BadContract("frames must be >= 1".into()));
        }
        if ticks_per_frame == 0 {
            return Err(FilmError::BadContract("ticks_per_frame must be >= 1".into()));
        }
        Ok(FilmContract {
            frames,
            numeric: o
                .get("numeric")
                .and_then(|x| x.as_str())
                .map(str::to_owned)
                .ok_or_else(|| FilmError::BadContract("missing `numeric`".into()))?,
            start_tick: last_tick(frames, u("start_tick")?, ticks_per_frame)?.0,
            ticks_per_frame,
        })
    }
}

/// Checks a frame schedule: at most [`crate::limits::RENDER_MAX_FRAMES`] frames
/// (`duration_too_long`), and a last tick, `start_tick + (frames - 1) × ticks_per_frame`,
/// that a `u64` holds. Returns `(start_tick, last tick)`.
pub(super) fn last_tick(frames: u64, start_tick: u64, ticks_per_frame: u64) -> Result<(u64, u64), FilmError> {
    if frames > crate::limits::RENDER_MAX_FRAMES {
        return Err(FilmError::BadContract(crate::limits::duration_too_long(
            "frames", frames, crate::limits::RENDER_MAX_FRAMES)));
    }
    let last = (frames.max(1) - 1)
        .checked_mul(ticks_per_frame)
        .and_then(|t| t.checked_add(start_tick))
        .ok_or_else(|| FilmError::BadContract("the frame schedule's last tick passes 2^64 - 1".into()))?;
    Ok((start_tick, last))
}

/// Camera-track bytes: `n_keys u32`, then per key `tick u64 | pos[3] i64 |
/// rot[4] i64 | fov i64` (all little-endian; the `Fx` values as raw bits).
pub fn camera_bytes(keys: &[CamKey]) -> Vec<u8> {
    let mut out = Vec::with_capacity(4 + keys.len() * CAMKEY_BYTES);
    out.extend_from_slice(&(keys.len() as u32).to_le_bytes());
    for k in keys {
        out.extend_from_slice(&k.tick.to_le_bytes());
        for c in k.pos {
            out.extend_from_slice(&c.to_bits().to_le_bytes());
        }
        for c in k.rot {
            out.extend_from_slice(&c.to_bits().to_le_bytes());
        }
        out.extend_from_slice(&k.fov.to_bits().to_le_bytes());
    }
    out
}

fn parse_camera(b: &[u8]) -> Result<Vec<CamKey>, FilmError> {
    if b.len() < 4 {
        return Err(FilmError::Truncated);
    }
    let n = u32::from_le_bytes(b[0..4].try_into().unwrap()) as usize;
    if b.len() != 4 + n * CAMKEY_BYTES {
        return Err(FilmError::BadCamera(format!(
            "camera section {} != 4 + {n}*{CAMKEY_BYTES}", b.len()
        )));
    }
    let fx = |s: &[u8]| Fx::from_bits(i64::from_le_bytes(s.try_into().unwrap()));
    let mut keys = Vec::with_capacity(n);
    let mut p = 4usize;
    let mut last_tick: Option<u64> = None;
    for _ in 0..n {
        let tick = u64::from_le_bytes(b[p..p + 8].try_into().unwrap());
        if let Some(prev) = last_tick
            && tick <= prev
        {
            return Err(FilmError::BadCamera("camera keys must be strictly ascending by tick".into()));
        }
        last_tick = Some(tick);
        let pos = [fx(&b[p + 8..p + 16]), fx(&b[p + 16..p + 24]), fx(&b[p + 24..p + 32])];
        let rot = [
            fx(&b[p + 32..p + 40]),
            fx(&b[p + 40..p + 48]),
            fx(&b[p + 48..p + 56]),
            fx(&b[p + 56..p + 64]),
        ];
        let fov = fx(&b[p + 64..p + 72]);
        keys.push(CamKey { tick, pos, rot, fov });
        p += CAMKEY_BYTES;
    }
    if keys.is_empty() {
        return Err(FilmError::BadCamera("camera track must carry at least one key".into()));
    }
    Ok(keys)
}

impl super::Film {
    pub fn to_bytes(&self) -> Vec<u8> {
        let cam = camera_bytes(&self.camera);
        let sections = [
            (SECTION_CONTRACT, &self.contract_bytes[..]),
            (SECTION_WORLD, &self.world_bytes[..]),
            (SECTION_CAMERA, &cam[..]),
        ];
        let mut out = Vec::new();
        out.extend_from_slice(MAGIC_WFLM);
        out.extend_from_slice(&(sections.len() as u16).to_le_bytes());
        let mut off = 0u32;
        let mut offs = Vec::new();
        for (_, b) in &sections {
            offs.push(off);
            off += b.len().div_ceil(8) as u32 * 8;
        }
        for ((kind, b), o) in sections.iter().zip(&offs) {
            out.push(*kind);
            out.extend_from_slice(&o.to_le_bytes());
            out.extend_from_slice(&(b.len() as u32).to_le_bytes());
        }
        for (_, b) in &sections {
            out.extend_from_slice(b);
            out.extend_from_slice(&[0u8; 8][..b.len().div_ceil(8) * 8 - b.len()]);
        }
        out
    }

    pub fn from_bytes(bytes: &[u8]) -> Result<super::Film, FilmError> {
        if bytes.len() < 6 {
            return Err(FilmError::Truncated);
        }
        if &bytes[..4] != MAGIC_WFLM {
            return Err(FilmError::BadMagic);
        }
        let n = u16::from_le_bytes(bytes[4..6].try_into().unwrap()) as usize;
        let table_end = 6 + n * 9;
        if bytes.len() < table_end {
            return Err(FilmError::Truncated);
        }
        let pay = &bytes[table_end..];
        let (mut contract_bytes, mut world_bytes, mut cam_bytes) = (None, None, None);
        for i in 0..n {
            let e = &bytes[6 + i * 9..6 + (i + 1) * 9];
            let off = u32::from_le_bytes(e[1..5].try_into().unwrap()) as usize;
            let len = u32::from_le_bytes(e[5..9].try_into().unwrap()) as usize;
            let end = off.checked_add(len).ok_or(FilmError::Truncated)?;
            if end > pay.len() {
                return Err(FilmError::Truncated);
            }
            match e[0] {
                SECTION_CONTRACT => contract_bytes = Some(pay[off..end].to_vec()),
                SECTION_WORLD => world_bytes = Some(pay[off..end].to_vec()),
                SECTION_CAMERA => cam_bytes = Some(&pay[off..end]),
                _ => {}
            }
        }
        let contract_bytes = contract_bytes.ok_or(FilmError::MissingSection(SECTION_CONTRACT))?;
        let world_bytes = world_bytes.ok_or(FilmError::MissingSection(SECTION_WORLD))?;
        let cam_bytes = cam_bytes.ok_or(FilmError::MissingSection(SECTION_CAMERA))?;
        let contract = FilmContract::from_json(&contract_bytes)?;
        let camera = parse_camera(cam_bytes)?;
        Ok(super::Film { contract, contract_bytes, world_bytes, camera })
    }
}
