//! Minimaler WAV-Decoder (Blender-/DAW-Export) → Mono-Samples.
//!
//! Eingebettete Fassung des irl3d-Loaders (../irl3d/src/wav.rs), auf die
//! WDS-Loader-Regeln gebracht: reiner Decoder (Bytes → owned Daten),
//! `Result` statt Panic — Sounds sind Autorinnen-Content, ein kaputtes
//! File wird gemeldet und bleibt still, nie fatal.
//!
//! Akzeptiert ausschließlich PCM (`format = 1`), 1 Kanal, 16 bit, 44100 Hz,
//! little-endian — das feste Format des Mixers (render::audio). „Als WAV
//! mono 16-bit 44,1 kHz exportieren" ist die ganze Anleitung; alles andere
//! ist eine klare Fehlermeldung mit Pfad. Über unbekannte RIFF-Chunks
//! (`LIST`, `INFO`, `bext`, …) wird hinweggesprungen.

/// Sample-Rate des gesamten Audio-Pfads (Decoder-Kontrakt und Mixer-Takt).
pub const SAMPLE_RATE: u32 = 44100;

pub struct Wav {
    pub samples: Vec<i16>,
}

pub fn load(path: &str) -> Result<Wav, String> {
    let bytes = std::fs::read(path).map_err(|e| format!("wav load {path}: {e}"))?;
    parse(&bytes).map_err(|e| format!("wav {path}: {e}"))
}

pub fn parse(data: &[u8]) -> Result<Wav, String> {
    if data.len() < 12 || &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" {
        return Err("kein RIFF/WAVE".into());
    }

    let mut fmt_found = false;
    let mut samples: Option<Vec<i16>> = None;

    let mut i = 12;
    while i + 8 <= data.len() {
        let id = &data[i..i + 4];
        let size = u32::from_le_bytes(data[i + 4..i + 8].try_into().unwrap()) as usize;
        let body = i + 8;
        let end = body + size;
        if end > data.len() { return Err("Chunk länger als Datei".into()); }
        match id {
            b"fmt " => {
                if size < 16 { return Err("fmt-Chunk zu kurz".into()); }
                let format   = u16::from_le_bytes(data[body..body + 2].try_into().unwrap());
                let channels = u16::from_le_bytes(data[body + 2..body + 4].try_into().unwrap());
                let rate     = u32::from_le_bytes(data[body + 4..body + 8].try_into().unwrap());
                let bits     = u16::from_le_bytes(data[body + 14..body + 16].try_into().unwrap());
                if format != 1 || channels != 1 || bits != 16 || rate != SAMPLE_RATE {
                    return Err(format!(
                        "brauche PCM mono 16-bit {SAMPLE_RATE} Hz, ist format={format} \
                         channels={channels} bits={bits} rate={rate}"));
                }
                fmt_found = true;
            }
            b"data" => {
                if !fmt_found { return Err("data-Chunk vor fmt".into()); }
                samples = Some(data[body..end].chunks_exact(2)
                    .map(|c| i16::from_le_bytes([c[0], c[1]]))
                    .collect());
            }
            _ => {} // LIST, INFO, bext, … überspringen
        }
        // Chunks sind word-aligned (Padding-Byte bei ungerader Größe).
        i = end + (size & 1);
    }

    match samples {
        Some(samples) if !samples.is_empty() => Ok(Wav { samples }),
        Some(_) => Err("data-Chunk leer".into()),
        None    => Err("kein data-Chunk".into()),
    }
}

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

    /// Minimale WAV-Datei bauen, wie ein Exporter sie schriebe.
    fn wav_bytes(format: u16, channels: u16, rate: u32, bits: u16, samples: &[i16]) -> Vec<u8> {
        let data_len = samples.len() * 2;
        let mut out = Vec::new();
        out.extend(b"RIFF");
        out.extend(((4 + 8 + 16 + 8 + data_len) as u32).to_le_bytes());
        out.extend(b"WAVE");
        out.extend(b"fmt ");
        out.extend(16u32.to_le_bytes());
        out.extend(format.to_le_bytes());
        out.extend(channels.to_le_bytes());
        out.extend(rate.to_le_bytes());
        out.extend((rate * 2).to_le_bytes());        // byte rate
        out.extend(2u16.to_le_bytes());              // block align
        out.extend(bits.to_le_bytes());
        out.extend(b"data");
        out.extend((data_len as u32).to_le_bytes());
        for s in samples { out.extend(s.to_le_bytes()); }
        out
    }

    #[test]
    fn parses_pcm_mono_16bit() {
        let w = parse(&wav_bytes(1, 1, SAMPLE_RATE, 16, &[0, 1000, -1000])).unwrap();
        assert_eq!(w.samples, vec![0, 1000, -1000]);
    }

    #[test]
    fn skips_unknown_chunks() {
        // LIST-Chunk zwischen fmt und data.
        let base = wav_bytes(1, 1, SAMPLE_RATE, 16, &[7]);
        let mut out = base[..36].to_vec();           // bis inkl. fmt
        out.extend(b"LIST");
        out.extend(4u32.to_le_bytes());
        out.extend(b"INFO");
        out.extend(&base[36..]);                     // data-Chunk
        let len = (out.len() - 8) as u32;
        out[4..8].copy_from_slice(&len.to_le_bytes());
        assert_eq!(parse(&out).unwrap().samples, vec![7]);
    }

    #[test]
    fn rejects_wrong_formats() {
        assert!(parse(b"OggS...nope").is_err());
        assert!(parse(&wav_bytes(1, 2, SAMPLE_RATE, 16, &[0])).is_err(), "stereo");
        assert!(parse(&wav_bytes(1, 1, 22050, 16, &[0])).is_err(), "falsche Rate");
        assert!(parse(&wav_bytes(1, 1, SAMPLE_RATE, 8, &[0])).is_err(), "8 bit");
        assert!(parse(&wav_bytes(3, 1, SAMPLE_RATE, 16, &[0])).is_err(), "float-PCM");
    }

    #[test]
    fn load_missing_file_is_err() {
        assert!(load("/nonexistent/x.wav").is_err());
    }
}
