//! The `WSCN` container — extension §2. Mirrors the WWLD section table.

use super::AudioError;
use crate::world::{AssetEntry, Fx};

pub const MAGIC_WSCN: &[u8; 4] = b"WSCN";
const SECTION_CONTRACT: u8 = 0x01;
const SECTION_OBJECTS: u8 = 0x02;
const SECTION_ASSETS: u8 = 0x03;
const SECTION_SEAL: u8 = 0x04;

#[derive(Clone, Debug, PartialEq)]
pub struct SceneContract {
    pub sample_rate: u32,
    pub numeric: String,
    pub pan_law: String,
    pub distance: String,
    pub duration: u64,
    pub floor_layout: String,
}

impl SceneContract {
    /// Canonical JSON: keys sorted (`distance, duration, floor_layout,
    /// numeric, pan_law, sample_rate`), no insignificant whitespace.
    pub fn to_canonical_json(&self) -> Vec<u8> {
        format!(
            "{{\"distance\":\"{}\",\"duration\":{},\"floor_layout\":\"{}\",\"numeric\":\"{}\",\"pan_law\":\"{}\",\"sample_rate\":{}}}",
            self.distance, self.duration, self.floor_layout, self.numeric, self.pan_law, self.sample_rate,
        )
        .into_bytes()
    }

    pub fn from_json(bytes: &[u8]) -> Result<SceneContract, AudioError> {
        let v: serde_json::Value =
            serde_json::from_slice(bytes).map_err(|e| AudioError::BadContract(e.to_string()))?;
        let o = v.as_object().ok_or_else(|| AudioError::BadContract("not an object".into()))?;
        let s = |k: &str| -> Result<String, AudioError> {
            o.get(k).and_then(|x| x.as_str()).map(str::to_owned)
                .ok_or_else(|| AudioError::BadContract(format!("missing `{k}`")))
        };
        let u = |k: &str| -> Result<u64, AudioError> {
            o.get(k).and_then(|x| x.as_u64()).ok_or_else(|| AudioError::BadContract(format!("missing `{k}`")))
        };
        let sample_rate = u("sample_rate")?;
        if sample_rate == 0 || sample_rate > u32::MAX as u64 {
            return Err(AudioError::BadContract("sample_rate out of range".into()));
        }
        // The mandatory floor is a stereo mixdown: `duration` samples on each of two
        // channels. Richer layouts are renders the sink chooses.
        let duration = u("duration")?;
        let samples = duration.saturating_mul(2);
        if samples > crate::limits::RENDER_MAX_SAMPLES {
            return Err(AudioError::BadContract(crate::limits::duration_too_long(
                "samples in its stereo floor (duration × 2)", samples, crate::limits::RENDER_MAX_SAMPLES)));
        }
        Ok(SceneContract {
            sample_rate: sample_rate as u32,
            numeric: s("numeric")?,
            pan_law: s("pan_law")?,
            distance: s("distance")?,
            duration,
            floor_layout: s("floor_layout")?,
        })
    }
}

/// One position keyframe (Fx metres, listener-relative).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PosKey {
    pub pos_sample: u64,
    pub x: Fx,
    pub y: Fx,
    pub z: Fx,
}

/// An audio object: a content-addressed source + gain + trajectory.
#[derive(Clone, Debug, PartialEq)]
pub struct AudioObject {
    pub source_hash: [u8; 32],
    pub gain: Fx,
    pub keys: Vec<PosKey>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SceneSeal {
    pub duration: u64,
    pub mixdown_hash: [u8; 32],
    pub signature: [u8; 64],
}

#[derive(Clone, Debug)]
pub struct SpatialScene {
    pub contract: SceneContract,
    pub contract_bytes: Vec<u8>,
    pub objects: Vec<AudioObject>,
    pub assets: Vec<AssetEntry>,
    pub seal: Option<SceneSeal>,
}

/// Canonical object-table bytes (also what `scene_hash` commits to).
pub fn object_table_bytes(objects: &[AudioObject]) -> Vec<u8> {
    let mut out = Vec::new();
    out.extend_from_slice(&(objects.len() as u32).to_le_bytes());
    for o in objects {
        out.extend_from_slice(&o.source_hash);
        out.extend_from_slice(&o.gain.to_bits().to_le_bytes());
        out.extend_from_slice(&(o.keys.len() as u16).to_le_bytes());
        for k in &o.keys {
            out.extend_from_slice(&k.pos_sample.to_le_bytes());
            out.extend_from_slice(&k.x.to_bits().to_le_bytes());
            out.extend_from_slice(&k.y.to_bits().to_le_bytes());
            out.extend_from_slice(&k.z.to_bits().to_le_bytes());
        }
    }
    out
}

fn parse_object_table(bytes: &[u8]) -> Result<Vec<AudioObject>, AudioError> {
    let mut c = Cur { b: bytes, p: 0 };
    let n = c.u32()? as usize;
    // Reserve no more entries than the bytes left could hold: the count is the input's,
    // and a reservation sized by it alone let a few bytes ask for gigabytes.
    let mut objs = Vec::with_capacity(n.min(bytes.len() / (32 + 8 + 2)));
    for _ in 0..n {
        let source_hash = c.arr::<32>()?;
        let gain = Fx::from_bits(c.i64()?);
        let nk = c.u16()? as usize;
        let mut keys = Vec::with_capacity(nk);
        for _ in 0..nk {
            keys.push(PosKey {
                pos_sample: c.u64()?,
                x: Fx::from_bits(c.i64()?),
                y: Fx::from_bits(c.i64()?),
                z: Fx::from_bits(c.i64()?),
            });
        }
        objs.push(AudioObject { source_hash, gain, keys });
    }
    Ok(objs)
}

fn encode_assets(a: &[AssetEntry]) -> Vec<u8> {
    let mut out = Vec::new();
    out.extend_from_slice(&(a.len() as u32).to_le_bytes());
    for e in a {
        out.extend_from_slice(&e.hash);
        out.push(e.role);
        out.extend_from_slice(&(e.name.len() as u32).to_le_bytes());
        out.extend_from_slice(e.name.as_bytes());
    }
    out
}

fn parse_assets(bytes: &[u8]) -> Result<Vec<AssetEntry>, AudioError> {
    let mut c = Cur { b: bytes, p: 0 };
    let n = c.u32()? as usize;
    // Reserve no more entries than the bytes left could hold: the count is the input's,
    // and a reservation sized by it alone let a few bytes ask for gigabytes.
    let mut out = Vec::with_capacity(n.min(bytes.len() / (32 + 1 + 4)));
    for _ in 0..n {
        let hash = c.arr::<32>()?;
        let role = c.u8()?;
        let nl = c.u32()? as usize;
        let name = String::from_utf8(c.take(nl)?.to_vec()).map_err(|_| AudioError::Truncated)?;
        out.push(AssetEntry { hash, role, name });
    }
    Ok(out)
}

impl SpatialScene {
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut sections: Vec<(u8, Vec<u8>)> = vec![
            (SECTION_CONTRACT, self.contract_bytes.clone()),
            (SECTION_OBJECTS, object_table_bytes(&self.objects)),
        ];
        if !self.assets.is_empty() {
            sections.push((SECTION_ASSETS, encode_assets(&self.assets)));
        }
        if let Some(s) = &self.seal {
            let mut b = Vec::with_capacity(104);
            b.extend_from_slice(&s.duration.to_le_bytes());
            b.extend_from_slice(&s.mixdown_hash);
            b.extend_from_slice(&s.signature);
            sections.push((SECTION_SEAL, b));
        }
        let mut out = Vec::new();
        out.extend_from_slice(MAGIC_WSCN);
        out.extend_from_slice(&(sections.len() as u16).to_le_bytes());
        let mut offsets = Vec::with_capacity(sections.len());
        let mut off = 0u32;
        for (_, body) in &sections {
            offsets.push(off);
            off += body.len().div_ceil(8) as u32 * 8;
        }
        for ((kind, body), o) in sections.iter().zip(&offsets) {
            out.push(*kind);
            out.extend_from_slice(&o.to_le_bytes());
            out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        }
        for (_, body) in &sections {
            out.extend_from_slice(body);
            let pad = body.len().div_ceil(8) * 8 - body.len();
            out.extend_from_slice(&[0u8; 8][..pad]);
        }
        out
    }

    pub fn from_bytes(bytes: &[u8]) -> Result<SpatialScene, AudioError> {
        if bytes.len() < 6 {
            return Err(AudioError::Truncated);
        }
        if &bytes[..4] != MAGIC_WSCN {
            return Err(AudioError::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(AudioError::Truncated);
        }
        let payloads = &bytes[table_end..];
        let (mut contract_bytes, mut objects, mut assets, mut seal) = (None, None, Vec::new(), None);
        for i in 0..n {
            let e = &bytes[6 + i * 9..6 + (i + 1) * 9];
            let kind = e[0];
            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(AudioError::Truncated)?;
            if end > payloads.len() {
                return Err(AudioError::Truncated);
            }
            let body = &payloads[off..end];
            match kind {
                SECTION_CONTRACT => contract_bytes = Some(body.to_vec()),
                SECTION_OBJECTS => objects = Some(parse_object_table(body)?),
                SECTION_ASSETS => assets = parse_assets(body)?,
                SECTION_SEAL => {
                    if body.len() != 8 + 32 + 64 {
                        return Err(AudioError::Truncated);
                    }
                    seal = Some(SceneSeal {
                        duration: u64::from_le_bytes(body[..8].try_into().unwrap()),
                        mixdown_hash: body[8..40].try_into().unwrap(),
                        signature: body[40..104].try_into().unwrap(),
                    });
                }
                _ => {}
            }
        }
        let contract_bytes = contract_bytes.ok_or(AudioError::MissingSection(SECTION_CONTRACT))?;
        let objects = objects.ok_or(AudioError::MissingSection(SECTION_OBJECTS))?;
        let contract = SceneContract::from_json(&contract_bytes)?;
        Ok(SpatialScene { contract, contract_bytes, objects, assets, seal })
    }
}

struct Cur<'a> {
    b: &'a [u8],
    p: usize,
}
impl<'a> Cur<'a> {
    fn take(&mut self, n: usize) -> Result<&'a [u8], AudioError> {
        let end = self.p.checked_add(n).ok_or(AudioError::Truncated)?;
        if end > self.b.len() {
            return Err(AudioError::Truncated);
        }
        let s = &self.b[self.p..end];
        self.p = end;
        Ok(s)
    }
    fn arr<const N: usize>(&mut self) -> Result<[u8; N], AudioError> {
        Ok(self.take(N)?.try_into().unwrap())
    }
    fn u8(&mut self) -> Result<u8, AudioError> {
        Ok(self.take(1)?[0])
    }
    fn u16(&mut self) -> Result<u16, AudioError> {
        Ok(u16::from_le_bytes(self.arr::<2>()?))
    }
    fn u32(&mut self) -> Result<u32, AudioError> {
        Ok(u32::from_le_bytes(self.arr::<4>()?))
    }
    fn u64(&mut self) -> Result<u64, AudioError> {
        Ok(u64::from_le_bytes(self.arr::<8>()?))
    }
    fn i64(&mut self) -> Result<i64, AudioError> {
        Ok(i64::from_le_bytes(self.arr::<8>()?))
    }
}
