//! Cutscene-voice resolution: movie → sound-id → continuous-stream byte region.
//!
//! Reverse-engineered 2026-07-22 (Canary file-I/O trace, user-confirmed by ear).
//! The movie voices form **one continuous XMA stream**, physically chunked into
//! `<lang>\Movie\VOICE_*.slb` `sound.pak` TOC entries whose boundaries do **not**
//! align with the cutscene cues — a single cue routinely spans two `.slb` chunks,
//! so a `.slb` does not necessarily hold the track its name claims. Each cue's
//! audio ends at an inline **trailer descriptor** `(sound_id: u32be, 0x11, …)`
//! whose id repeats at +0x800. Cue N's audio = the stream bytes
//! `[descriptor(N-1) .. descriptor(N)]`, read continuously across chunk
//! boundaries. Verified: decoded voice length matches each movie within ~0.3 s,
//! including cross-boundary cues (RT01B/RT01C span `VOICE_ADV`→`VOICE_RT01A`→…).
//!
//! Resolution chain:
//!   1. movie → cue name (`ADVERTISE_MOVIE` manifest `VOICETRACK`, e.g. `VOICE_RT01A`)
//!   2. cue name → sound-id (master sound registry, [`registry_voice_ids`], → 1601)
//!   3. sound-id → `[start, end]` global offsets via [`find_descriptor`] over the
//!      `sound.pNN` stream (the caller supplies the bytes + the physical anchor).

use std::collections::HashMap;

/// Parse the master sound registry — the large per-language `tables.pak` IDXD
/// entry (schema `0x13cb84ba`) that carries the `<lang>\Movie\VOICE_*.slb` paths —
/// into `cue-name → sound-id` from its `NAME\0 ID\0` string pool
/// (e.g. `VOICE_ADV`→1600, `VOICE_RT01A`→1601).
pub fn registry_voice_ids(registry: &[u8]) -> HashMap<String, u32> {
    let mut toks: Vec<&[u8]> = Vec::new();
    let mut start = 0usize;
    for i in 0..registry.len() {
        if registry[i] == 0 {
            if i > start {
                toks.push(&registry[start..i]);
            }
            start = i + 1;
        }
    }
    let mut out = HashMap::new();
    for w in toks.windows(2) {
        let (name, num) = (w[0], w[1]);
        if name.starts_with(b"VOICE_")
            && !num.is_empty()
            && num.iter().all(u8::is_ascii_digit)
        {
            if let (Ok(n), Ok(id)) = (
                std::str::from_utf8(name),
                std::str::from_utf8(num).unwrap().parse::<u32>(),
            ) {
                out.entry(n.to_string()).or_insert(id);
            }
        }
    }
    out
}

/// The constant marker following a cue's sound-id in its inline trailer.
const DESC_MARK: u32 = 0x11;
/// The trailer repeats its id this many bytes later; used to reject a coincidental
/// `(id, 0x11)` pair inside XMA audio (probability of a false match is ~2⁻⁶⁴).
const DESC_REPEAT: usize = 0x800;

/// Byte offset (within `buf`, a slice of the continuous voice stream) of cue
/// `id`'s trailer descriptor: the first big-endian `(id, 0x11)` pair whose id
/// repeats at `+0x800`. `None` if absent in the slice.
pub fn find_descriptor(buf: &[u8], id: u32) -> Option<usize> {
    if buf.len() < DESC_REPEAT + 4 {
        return None;
    }
    let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]);
    let end = buf.len() - (DESC_REPEAT + 4);
    let mut o = 0;
    while o <= end {
        if be(o) == id && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id {
            return Some(o);
        }
        o += 4;
    }
    None
}

/// Plausible sound-id range for a trailer (all real cue ids are well under this).
const ID_MAX: u32 = 0x1_0000;

/// Offset of the nearest cue trailer strictly **before** `before` — any plausible
/// id. Used to bound a cue's START when its `id-1` predecessor doesn't exist
/// (the sound-id sequence has gaps, e.g. `VOICE_D_453`=7142 → `VOICE_D_454`=7188).
/// Scans downward and returns the first (largest-offset) valid `(id, 0x11)` with
/// the `+0x800` id-repeat. `None` if there is no trailer below `before`.
pub fn find_descriptor_before(buf: &[u8], before: usize) -> Option<usize> {
    if before < 4 {
        return None;
    }
    let cap = buf.len().checked_sub(DESC_REPEAT + 4)?;
    let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]);
    // Start strictly below `before` (4-aligned) so the target's own trailer is
    // never returned as its predecessor.
    let mut o = (before - 1).min(cap) & !3;
    loop {
        let id = be(o);
        if id >= 1 && id < ID_MAX && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id {
            return Some(o);
        }
        if o < 4 {
            return None;
        }
        o -= 4;
    }
}

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

    #[test]
    fn registry_parses_name_id_pairs() {
        let mut b = Vec::new();
        for tok in [
            b"VOICE_ADV".as_slice(),
            b"1600",
            b"VOICE_RT01A",
            b"1601",
            b"NOT_A_VOICE",
            b"9",
        ] {
            b.extend_from_slice(tok);
            b.push(0);
        }
        let ids = registry_voice_ids(&b);
        assert_eq!(ids.get("VOICE_ADV"), Some(&1600));
        assert_eq!(ids.get("VOICE_RT01A"), Some(&1601));
        assert_eq!(ids.get("NOT_A_VOICE"), None);
    }

    #[test]
    fn find_descriptor_matches_repeat() {
        let mut b = vec![0u8; 0x900];
        b[0..4].copy_from_slice(&1601u32.to_be_bytes());
        b[4..8].copy_from_slice(&0x11u32.to_be_bytes());
        b[DESC_REPEAT..DESC_REPEAT + 4].copy_from_slice(&1601u32.to_be_bytes());
        assert_eq!(find_descriptor(&b, 1601), Some(0));
        assert_eq!(find_descriptor(&b, 1600), None);
    }
}
