//! The `WSCR` container + the score contract — mirrors the `WHAP` /
//! `WSCN` / `W4DS` section layout: 4-byte magic, `u16` section count, a
//! `(kind u8, off u32, len u32)` table, then 8-byte-aligned payloads.
//! Two sections: the canonical-JSON contract and the note events.

use super::Note;
use crate::world::Fx;

pub const MAGIC_WSCR: &[u8; 4] = b"WSCR";
const SECTION_CONTRACT: u8 = 0x01;
const SECTION_NOTES: u8 = 0x02;

#[derive(Debug)]
pub enum ScoreError {
    BadMagic,
    Truncated,
    MissingSection(u8),
    BadContract(String),
}

impl std::fmt::Display for ScoreError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::BadMagic => write!(f, "payload is not a WSCR 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 score contract: {why}"),
        }
    }
}
impl std::error::Error for ScoreError {}

/// Score contract — canonical JSON (sorted keys, no whitespace), bound by
/// the score hash and carried verbatim.
#[derive(Clone, Debug, PartialEq)]
pub struct ScoreContract {
    pub sample_rate: u32,
    pub numeric: String,
    pub channels: u32,
    /// Total length of the rendered mixdown, in samples.
    pub duration: u64,
}

impl ScoreContract {
    pub fn to_canonical_json(&self) -> Vec<u8> {
        format!(
            "{{\"channels\":{},\"duration\":{},\"numeric\":\"{}\",\"sample_rate\":{}}}",
            self.channels, self.duration, self.numeric, self.sample_rate
        )
        .into_bytes()
    }

    pub fn from_json(b: &[u8]) -> Result<ScoreContract, ScoreError> {
        let v: serde_json::Value =
            serde_json::from_slice(b).map_err(|e| ScoreError::BadContract(e.to_string()))?;
        let o = v.as_object().ok_or_else(|| ScoreError::BadContract("not an object".into()))?;
        let u = |k: &str| {
            o.get(k).and_then(|x| x.as_u64()).ok_or_else(|| ScoreError::BadContract(format!("missing `{k}`")))
        };
        let sr = u("sample_rate")?;
        if sr == 0 || sr > u32::MAX as u64 {
            return Err(ScoreError::BadContract("sample_rate out of range".into()));
        }
        let ch = u("channels")?;
        let duration = u("duration")?;
        let samples = duration.saturating_mul(ch.max(1));
        if samples > crate::limits::RENDER_MAX_SAMPLES {
            return Err(ScoreError::BadContract(crate::limits::duration_too_long(
                "samples (duration × channels)", samples, crate::limits::RENDER_MAX_SAMPLES)));
        }
        Ok(ScoreContract {
            sample_rate: sr as u32,
            numeric: o
                .get("numeric")
                .and_then(|x| x.as_str())
                .map(str::to_owned)
                .ok_or_else(|| ScoreError::BadContract("missing `numeric`".into()))?,
            channels: ch as u32,
            duration,
        })
    }
}

/// Notes section bytes: `n_notes u32`, then per note
/// `start u64 | dur u64 | phase_inc i64 | gain i64 | waveform u8 |
///  attack u64 | decay u64 | sustain i64 | release u64`.
pub fn notes_bytes(notes: &[Note]) -> Vec<u8> {
    let mut out = Vec::with_capacity(4 + notes.len() * 65);
    out.extend_from_slice(&(notes.len() as u32).to_le_bytes());
    for n in notes {
        out.extend_from_slice(&n.start.to_le_bytes());
        out.extend_from_slice(&n.dur.to_le_bytes());
        out.extend_from_slice(&n.phase_inc.to_bits().to_le_bytes());
        out.extend_from_slice(&n.gain.to_bits().to_le_bytes());
        out.push(n.waveform);
        out.extend_from_slice(&n.attack.to_le_bytes());
        out.extend_from_slice(&n.decay.to_le_bytes());
        out.extend_from_slice(&n.sustain.to_bits().to_le_bytes());
        out.extend_from_slice(&n.release.to_le_bytes());
    }
    out
}

fn parse_notes(b: &[u8]) -> Result<Vec<Note>, ScoreError> {
    let rd = |p: usize, n: usize| -> Result<&[u8], ScoreError> {
        if p + n > b.len() {
            Err(ScoreError::Truncated)
        } else {
            Ok(&b[p..p + n])
        }
    };
    let u64a = |s: &[u8]| u64::from_le_bytes(s.try_into().unwrap());
    let i64a = |s: &[u8]| i64::from_le_bytes(s.try_into().unwrap());
    let nn = u32::from_le_bytes(rd(0, 4)?.try_into().unwrap()) as usize;
    let mut p = 4usize;
    // 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 notes = Vec::with_capacity(nn.min(b.len() / 65));
    for _ in 0..nn {
        let start = u64a(rd(p, 8)?);
        let dur = u64a(rd(p + 8, 8)?);
        let phase_inc = Fx::from_bits(i64a(rd(p + 16, 8)?));
        let gain = Fx::from_bits(i64a(rd(p + 24, 8)?));
        let waveform = rd(p + 32, 1)?[0];
        let attack = u64a(rd(p + 33, 8)?);
        let decay = u64a(rd(p + 41, 8)?);
        let sustain = Fx::from_bits(i64a(rd(p + 49, 8)?));
        let release = u64a(rd(p + 57, 8)?);
        p += 65;
        notes.push(Note { start, dur, phase_inc, gain, waveform, attack, decay, sustain, release });
    }
    Ok(notes)
}

impl super::Score {
    pub fn to_bytes(&self) -> Vec<u8> {
        let notes = notes_bytes(&self.notes);
        let sections = [(SECTION_CONTRACT, &self.contract_bytes[..]), (SECTION_NOTES, &notes[..])];
        let mut out = Vec::new();
        out.extend_from_slice(MAGIC_WSCR);
        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::Score, ScoreError> {
        if bytes.len() < 6 {
            return Err(ScoreError::Truncated);
        }
        if &bytes[..4] != MAGIC_WSCR {
            return Err(ScoreError::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(ScoreError::Truncated);
        }
        let pay = &bytes[table_end..];
        let (mut contract_bytes, mut notebytes) = (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(ScoreError::Truncated)?;
            if end > pay.len() {
                return Err(ScoreError::Truncated);
            }
            match e[0] {
                SECTION_CONTRACT => contract_bytes = Some(pay[off..end].to_vec()),
                SECTION_NOTES => notebytes = Some(&pay[off..end]),
                _ => {}
            }
        }
        let contract_bytes = contract_bytes.ok_or(ScoreError::MissingSection(SECTION_CONTRACT))?;
        let notebytes = notebytes.ok_or(ScoreError::MissingSection(SECTION_NOTES))?;
        let contract = ScoreContract::from_json(&contract_bytes)?;
        let notes = parse_notes(notebytes)?;
        Ok(super::Score { contract, contract_bytes, notes })
    }
}
