#![allow(dead_code)]

//! Helpers ported from `celt/modes.c`.
//!
//! The reference implementation builds the band layout dynamically depending on
//! the sampling rate and frame size.  The helpers in this module translate the
//! Bark-scale based construction so that future porting work can reuse the
//! generated tables without reaching back into the C code.

use alloc::vec;
use alloc::vec::Vec;
use core::convert::TryFrom;
use core::fmt;

use crate::celt::rate::compute_pulse_cache;
use crate::celt::static_mode_48000_960::{
    CACHE_BITS_50, CACHE_CAPS_50, CACHE_INDEX_50, LOG_N_400, STATIC_MDCT_48000_960,
};
use crate::celt::types::{
    CeltCoef, MdctLookup, OpusCustomMode, OpusInt16, OpusInt32, OpusVal16, PulseCache,
    PulseCacheData,
};
use crate::celt::window_48000_960::WINDOW_120;
use libm::sinf;

/// Precomputed 5 ms critical band edges used by CELT's reference configuration.
const EBAND_5MS: [i16; 22] = [
    0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 34, 40, 48, 60, 78, 100,
];

/// Number of bands represented by [`EBAND_5MS`].
const MAX_BANDS: usize = EBAND_5MS.len() - 1;

/// Number of allocation vectors precomputed by the reference implementation.
pub(crate) const BITALLOC_SIZE: usize = 11;

/// Bit allocation matrix copied from `celt/modes.c`.
const BAND_ALLOCATION: [u8; BITALLOC_SIZE * MAX_BANDS] = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 80, 75, 69, 63, 56, 49, 40,
    34, 29, 20, 18, 10, 0, 0, 0, 0, 0, 0, 0, 0, 110, 100, 90, 84, 78, 71, 65, 58, 51, 45, 39, 32,
    26, 20, 12, 0, 0, 0, 0, 0, 0, 118, 110, 103, 93, 86, 80, 75, 70, 65, 59, 53, 47, 40, 31, 23,
    15, 4, 0, 0, 0, 0, 126, 119, 112, 104, 95, 89, 83, 78, 72, 66, 60, 54, 47, 39, 32, 25, 17, 12,
    1, 0, 0, 134, 127, 120, 114, 103, 97, 91, 85, 78, 72, 66, 60, 54, 47, 41, 35, 29, 23, 16, 10,
    1, 144, 137, 130, 124, 113, 107, 101, 95, 88, 82, 76, 70, 64, 57, 51, 45, 39, 33, 26, 15, 1,
    152, 145, 138, 132, 123, 117, 111, 105, 98, 92, 86, 80, 74, 67, 61, 55, 49, 43, 36, 20, 1, 162,
    155, 148, 142, 133, 127, 121, 115, 108, 102, 96, 90, 84, 77, 71, 65, 59, 53, 46, 30, 1, 172,
    165, 158, 152, 143, 137, 131, 125, 118, 112, 106, 100, 94, 87, 81, 75, 69, 63, 56, 45, 20, 200,
    200, 200, 200, 200, 200, 200, 200, 198, 193, 188, 183, 178, 173, 168, 163, 158, 153, 148, 129,
    104,
];

/// Bark-scale breakpoints used when constructing the band layout for custom modes.
const BARK_FREQ: [i32; BARK_BANDS + 1] = [
    0, 100, 200, 300, 400, 510, 630, 770, 920, 1080, 1270, 1480, 1720, 2000, 2320, 2700, 3150,
    3700, 4400, 5300, 6400, 7700, 9500, 12000, 15500, 20000,
];

/// Number of Bark bands covered by the table in the reference implementation.
const BARK_BANDS: usize = 25;

/// Result of [`compute_ebands`], mirroring the layout generated by the C helper.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct EBandLayout {
    /// Band edges measured in frequency bins.
    pub bands: Vec<i16>,
    /// Number of bands represented by [`bands`].
    pub num_bands: usize,
}

impl EBandLayout {
    fn new(bands: Vec<i16>, num_bands: usize) -> Self {
        debug_assert_eq!(bands.len(), num_bands + 1);
        Self { bands, num_bands }
    }
}

/// Interpolated allocation vectors describing how many bits each band should receive.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AllocationTable {
    vectors: Vec<u8>,
    num_bands: usize,
}

static STATIC_MODE_48KHZ_960: OpusCustomMode<'static> = OpusCustomMode {
    sample_rate: 48_000,
    overlap: 120,
    num_ebands: MAX_BANDS,
    effective_ebands: MAX_BANDS,
    pre_emphasis: [0.850_006_1, 0.0, 1.0, 1.0],
    e_bands: &EBAND_5MS,
    max_lm: 3,
    num_short_mdcts: 8,
    short_mdct_size: 120,
    num_alloc_vectors: BITALLOC_SIZE,
    alloc_vectors: &BAND_ALLOCATION,
    log_n: &LOG_N_400,
    window: &WINDOW_120,
    mdct: &STATIC_MDCT_48000_960,
    cache: PulseCache {
        size: CACHE_BITS_50.len(),
        index: &CACHE_INDEX_50,
        bits: &CACHE_BITS_50,
        caps: &CACHE_CAPS_50,
    },
};

fn static_mode_matches(sample_rate: OpusInt32, frame_size: usize, base_frame_size: usize) -> bool {
    if sample_rate != 48_000 || frame_size == 0 {
        return false;
    }

    let mut candidate = frame_size;
    for _ in 0..4 {
        if candidate == base_frame_size {
            return true;
        }
        if candidate > base_frame_size {
            break;
        }
        candidate = candidate.saturating_mul(2);
    }

    false
}

impl AllocationTable {
    fn new(vectors: Vec<u8>, num_bands: usize) -> Self {
        debug_assert_eq!(vectors.len(), num_bands * BITALLOC_SIZE);
        Self { vectors, num_bands }
    }

    /// Returns the flattened allocation vectors with `num_bands` elements per row.
    #[must_use]
    pub fn vectors(&self) -> &[u8] {
        &self.vectors
    }

    /// Number of allocation bands represented by the table.
    #[must_use]
    pub fn num_bands(&self) -> usize {
        self.num_bands
    }

    /// Number of allocation vectors stored in the table.
    #[must_use]
    pub fn num_vectors(&self) -> usize {
        BITALLOC_SIZE
    }
}

/// Computes the Bark-derived energy band edges for a given configuration.
///
/// Ports `compute_ebands()` from `celt/modes.c`, adapting the allocation logic
/// to return an owned vector.  The helper reproduces the integer arithmetic of
/// the reference implementation so that the resulting band boundaries match bit
/// for bit.
#[must_use]
#[allow(clippy::too_many_lines)]
pub(crate) fn compute_ebands(
    sample_rate: OpusInt32,
    frame_size: usize,
    resolution: OpusInt32,
) -> EBandLayout {
    assert!(resolution > 0, "resolution must be strictly positive");
    assert!(frame_size > 0, "frame size must be non-zero");
    let frame_size_i32 = OpusInt32::try_from(frame_size).expect("frame size fits in 32 bits");

    if i64::from(sample_rate) == 400 * i64::from(frame_size_i32) {
        let bands = EBAND_5MS.to_vec();
        let num_bands = bands.len() - 1;
        return EBandLayout::new(bands, num_bands);
    }

    let mut n_bark = 1usize;
    while n_bark < BARK_BANDS {
        if i64::from(BARK_FREQ[n_bark + 1]) * 2 >= i64::from(sample_rate) {
            break;
        }
        n_bark += 1;
    }

    let mut lin = 0usize;
    while lin < n_bark {
        if BARK_FREQ[lin + 1] - BARK_FREQ[lin] >= resolution {
            break;
        }
        lin += 1;
    }

    let low = ((BARK_FREQ[lin] + resolution / 2) / resolution) as usize;
    let high = n_bark - lin;
    let mut num_bands = low + high;
    let mut bands = vec![0i32; num_bands + 2];

    for (i, slot) in bands.iter_mut().take(low).enumerate() {
        *slot = i as i32;
    }

    let mut offset = 0i32;
    if low > 0 {
        let previous = bands[low - 1];
        offset = previous * resolution - BARK_FREQ[lin.saturating_sub(1)];
    }

    for (i, slot) in bands.iter_mut().skip(low).take(high).enumerate() {
        let target = BARK_FREQ[lin + i];
        let value = ((target + offset / 2 + resolution) / (2 * resolution)) * 2;
        *slot = value;
        offset = value * resolution - target;
    }

    for (i, band) in bands.iter_mut().take(num_bands).enumerate() {
        let threshold = i as i32;
        if *band < threshold {
            *band = threshold;
        }
    }

    let mut end_band = ((BARK_FREQ[n_bark] + resolution) / (2 * resolution)) * 2;
    if end_band > frame_size_i32 {
        end_band = frame_size_i32;
    }
    bands[num_bands] = end_band;

    if num_bands > 1 {
        #[allow(clippy::needless_range_loop)]
        for i in 1..(num_bands - 1) {
            let prev = bands[i - 1];
            let curr = bands[i];
            let next = bands[i + 1];
            if next - curr < curr - prev {
                bands[i] -= (2 * curr - prev - next) / 2;
            }
        }
    }

    let mut j = 0usize;
    #[allow(clippy::needless_range_loop)]
    for i in 0..num_bands {
        if bands[i + 1] > bands[j] {
            j += 1;
            bands[j] = bands[i + 1];
        }
    }
    num_bands = j;
    bands.truncate(num_bands + 1);

    if num_bands >= 1 {
        let last_width = bands[num_bands] - bands[num_bands - 1];
        for i in 1..num_bands {
            let width = bands[i] - bands[i - 1];
            debug_assert!(width <= last_width);
            debug_assert!(bands[i + 1] - bands[i] <= 2 * width);
        }
    }

    let final_bands: Vec<i16> = bands
        .into_iter()
        .map(|value| i16::try_from(value).expect("band index fits in 16 bits"))
        .collect();

    EBandLayout::new(final_bands, num_bands)
}

/// Computes the bit allocation vectors for a given band layout.
///
/// Ports `compute_allocation_table()` from `celt/modes.c`. The helper interpolates the
/// 5 ms reference bit-allocation curves so that custom band layouts receive a consistent
/// distribution of coarse energy bits.
#[must_use]
pub(crate) fn compute_allocation_table(
    sample_rate: OpusInt32,
    short_mdct_size: usize,
    layout: &EBandLayout,
) -> AllocationTable {
    assert!(short_mdct_size > 0, "short MDCT size must be non-zero");
    let nb_bands = layout.num_bands;
    assert!(layout.bands.len() > nb_bands);

    let mut vectors = vec![0u8; BITALLOC_SIZE * nb_bands];
    let mdct_size_i64 = i64::try_from(short_mdct_size).expect("short MDCT size fits in 64 bits");
    if i64::from(sample_rate) == 400 * mdct_size_i64 {
        let count = BITALLOC_SIZE * nb_bands;
        vectors.copy_from_slice(&BAND_ALLOCATION[..count]);
        return AllocationTable::new(vectors, nb_bands);
    }

    let sample_rate_i64 = i64::from(sample_rate);

    for vec_idx in 0..BITALLOC_SIZE {
        for band in 0..nb_bands {
            let mut k = 0usize;
            let target = i64::from(layout.bands[band]) * sample_rate_i64 / mdct_size_i64;
            while k < MAX_BANDS {
                let threshold = 400 * i64::from(EBAND_5MS[k]);
                if threshold > target {
                    break;
                }
                k += 1;
            }

            let value = if k >= MAX_BANDS {
                BAND_ALLOCATION[vec_idx * MAX_BANDS + (MAX_BANDS - 1)]
            } else {
                let upper = k.max(1);
                let prev_freq = 400 * i64::from(EBAND_5MS[upper - 1]);
                let next_freq = 400 * i64::from(EBAND_5MS[upper]);
                let a1 = target - prev_freq;
                let a0 = next_freq - target;
                let numerator = a0 * i64::from(BAND_ALLOCATION[vec_idx * MAX_BANDS + (upper - 1)])
                    + a1 * i64::from(BAND_ALLOCATION[vec_idx * MAX_BANDS + upper]);
                (numerator / (a0 + a1)) as u8
            };

            vectors[vec_idx * nb_bands + band] = value;
        }
    }

    AllocationTable::new(vectors, nb_bands)
}

/// Returns the pre-emphasis filter coefficients for a given sampling rate.
///
/// Ports the float configuration of the `opus_custom_mode_create()` logic from
/// `celt/modes.c`. The reference implementation selects one of four
/// hard-coded responses depending on the sampling rate, approximating the
/// behaviour of the canonical 48 kHz filter at lower rates. The returned taps
/// are ordered to match the `mode->preemph` initialisation in the C source.
#[must_use]
pub(crate) fn compute_preemphasis(sample_rate: OpusInt32) -> [OpusVal16; 4] {
    if sample_rate < 12_000 {
        [0.350_006_1, -0.179_992_68, 0.271_996_8, 3.676_513_7]
    } else if sample_rate < 24_000 {
        [0.600_006_1, -0.179_992_68, 0.442_499_88, 2.259_887_7]
    } else if sample_rate < 40_000 {
        [0.779_998_8, -0.100_006_1, 0.749_977_1, 1.333_374]
    } else {
        [0.850_006_1, 0.0, 1.0, 1.0]
    }
}

/// Generates the sine-window used by CELT's MDCT overlap.
///
/// Ports the float configuration of the window initialisation performed inside
/// `opus_custom_mode_create()` in `celt/modes.c`.  The helper reproduces the
/// nested sine computation so that the resulting coefficients match the
/// bit-exact tables expected by the transform.
#[must_use]
pub(crate) fn compute_mdct_window(overlap: usize) -> Vec<CeltCoef> {
    assert!(overlap > 0, "overlap must be strictly positive");

    if overlap == WINDOW_120.len() {
        // Use the reference static table to preserve C's bit-exact coefficients for 48 kHz / 960.
        return WINDOW_120.to_vec();
    }

    let mut window = Vec::with_capacity(overlap);
    let scale = core::f32::consts::FRAC_PI_2;
    let denom = overlap as f32;

    for i in 0..overlap {
        let phase = scale * ((i as f32 + 0.5) / denom);
        let sin_phase = sinf(phase);
        let value = sinf(scale * sin_phase * sin_phase);
        window.push(value);
    }

    window
}

/// Computes the log-band widths used by CELT's allocation heuristics.
///
/// Mirrors the loop inside `opus_custom_mode_create()` that populates the
/// `mode->logN` array by applying the `log2_frac()` helper to each energy band.
/// The results are expressed with [`BITRES`] fractional bits, matching the
/// conservative rounding performed by the C reference implementation.
#[must_use]
pub(crate) fn compute_log_band_widths(layout: &EBandLayout) -> Vec<OpusInt16> {
    use crate::celt::cwrs::log2_frac;
    use crate::celt::entcode::BITRES;
    use crate::celt::types::OpusInt16;

    assert!(layout.bands.len() > layout.num_bands);

    let mut log_n = Vec::with_capacity(layout.num_bands);
    for band in 0..layout.num_bands {
        let start = layout.bands[band];
        let end = layout.bands[band + 1];
        let width = i32::from(end - start);
        assert!(width > 0, "band width must be positive");
        let value = log2_frac(width as u32, BITRES as i32) as OpusInt16;
        log_n.push(value);
    }

    log_n
}

/// Errors that can occur while constructing a custom CELT mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ModeError {
    /// The requested sampling rate is outside the supported range.
    BadSampleRate,
    /// The requested frame size violates the constraints from the reference implementation.
    BadFrameSize,
    /// The frame is shorter than the minimum 1 ms supported by CELT.
    FrameTooShort,
    /// The computed short MDCT exceeds the duration allowed by the format.
    ShortBlockTooLong,
    /// The largest energy band would exceed the PVQ lookup tables.
    BandTooWide,
}

impl fmt::Display for ModeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BadSampleRate => write!(f, "unsupported sampling rate"),
            Self::BadFrameSize => write!(f, "unsupported frame size"),
            Self::FrameTooShort => write!(f, "frame duration shorter than 1 ms"),
            Self::ShortBlockTooLong => write!(f, "short blocks exceed maximum duration"),
            Self::BandTooWide => write!(f, "energy band exceeds PVQ cache limits"),
        }
    }
}

/// Owned representation of a dynamically constructed CELT mode.
#[derive(Debug, Clone)]
pub(crate) struct OwnedOpusCustomMode {
    sample_rate: OpusInt32,
    frame_size: usize,
    overlap: usize,
    max_lm: usize,
    num_short_mdcts: usize,
    short_mdct_size: usize,
    pre_emphasis: [OpusVal16; 4],
    layout: EBandLayout,
    effective_ebands: usize,
    alloc_vectors: Vec<u8>,
    num_alloc_vectors: usize,
    window: Vec<CeltCoef>,
    log_n: Vec<OpusInt16>,
    mdct: MdctLookup,
    cache: PulseCacheData,
}

impl OwnedOpusCustomMode {
    #[must_use]
    pub fn mode(&self) -> OpusCustomMode<'_> {
        OpusCustomMode {
            sample_rate: self.sample_rate,
            overlap: self.overlap,
            num_ebands: self.layout.num_bands,
            effective_ebands: self.effective_ebands,
            pre_emphasis: self.pre_emphasis,
            e_bands: &self.layout.bands,
            max_lm: self.max_lm,
            num_short_mdcts: self.num_short_mdcts,
            short_mdct_size: self.short_mdct_size,
            num_alloc_vectors: self.num_alloc_vectors,
            alloc_vectors: &self.alloc_vectors,
            log_n: &self.log_n,
            window: &self.window,
            mdct: &self.mdct,
            cache: self.cache.as_view(),
        }
    }

    #[must_use]
    pub fn layout(&self) -> &EBandLayout {
        &self.layout
    }

    #[must_use]
    pub fn frame_size(&self) -> usize {
        self.frame_size
    }
}

fn build_custom_mode(
    sample_rate: OpusInt32,
    frame_size: usize,
) -> Result<OwnedOpusCustomMode, ModeError> {
    if !(8_000..=96_000).contains(&sample_rate) {
        return Err(ModeError::BadSampleRate);
    }
    if !(40..=1024).contains(&frame_size) || !frame_size.is_multiple_of(2) {
        return Err(ModeError::BadFrameSize);
    }

    let frame_size_i32 = i32::try_from(frame_size).map_err(|_| ModeError::BadFrameSize)?;
    if frame_size_i32 * 1000 < sample_rate {
        return Err(ModeError::FrameTooShort);
    }

    let lm = if frame_size_i32 * 75 >= sample_rate && frame_size.is_multiple_of(16) {
        3usize
    } else if frame_size_i32 * 150 >= sample_rate && frame_size.is_multiple_of(8) {
        2
    } else if frame_size_i32 * 300 >= sample_rate && frame_size.is_multiple_of(4) {
        1
    } else {
        0
    };

    let short_mdct_size = frame_size >> lm;
    if (short_mdct_size * 300) as OpusInt32 > sample_rate {
        return Err(ModeError::ShortBlockTooLong);
    }

    let pre_emphasis = compute_preemphasis(sample_rate);
    let num_short_mdcts = 1 << lm;
    let overlap = (short_mdct_size >> 2) << 2;
    let resolution =
        (sample_rate + short_mdct_size as OpusInt32) / (2 * short_mdct_size as OpusInt32);

    let layout = compute_ebands(sample_rate, short_mdct_size, resolution);
    if layout.num_bands == 0 {
        return Err(ModeError::BadFrameSize);
    }

    let mut effective_ebands = layout.num_bands;
    while effective_ebands > 0
        && usize::try_from(layout.bands[effective_ebands]).expect("band boundary is non-negative")
            > short_mdct_size
    {
        effective_ebands -= 1;
    }

    #[allow(clippy::cast_possible_wrap)]
    let last_width = layout.bands[layout.num_bands] - layout.bands[layout.num_bands - 1];
    if (last_width as usize) << lm > 208 {
        return Err(ModeError::BandTooWide);
    }

    let allocation = compute_allocation_table(sample_rate, short_mdct_size, &layout);
    let alloc_vectors = allocation.vectors().to_vec();
    let num_alloc_vectors = allocation.num_vectors();
    let window = compute_mdct_window(overlap);
    let log_n = compute_log_band_widths(&layout);
    let cache = compute_pulse_cache(&layout.bands, &log_n, lm);
    let mdct_len = 2 * short_mdct_size * num_short_mdcts;
    let mdct = MdctLookup::new(mdct_len, lm);

    Ok(OwnedOpusCustomMode {
        sample_rate,
        frame_size,
        overlap,
        max_lm: lm,
        num_short_mdcts,
        short_mdct_size,
        pre_emphasis,
        layout,
        effective_ebands,
        alloc_vectors,
        num_alloc_vectors,
        window,
        log_n,
        mdct,
        cache,
    })
}

/// Returns the statically-defined CELT mode that matches the reference tables.
///
/// Mirrors the `static_mode_list` lookup from `celt/modes.c`, lazily
/// constructing the precomputed 48 kHz / 960 sample mode on first use so that
/// subsequent calls share the same allocation.
pub(crate) fn opus_custom_mode_find_static(
    sample_rate: OpusInt32,
    frame_size: usize,
) -> Option<OpusCustomMode<'static>> {
    if static_mode_matches(sample_rate, frame_size, 960) {
        return Some(STATIC_MODE_48KHZ_960);
    }

    None
}

/// Returns a shared reference to the statically-defined CELT mode matching the
/// reference lookup table.
pub(crate) fn opus_custom_mode_find_static_ref(
    sample_rate: OpusInt32,
    frame_size: usize,
) -> Option<&'static OpusCustomMode<'static>> {
    if static_mode_matches(sample_rate, frame_size, 960) {
        return Some(&STATIC_MODE_48KHZ_960);
    }

    None
}

/// Ports the custom-mode construction from `opus_custom_mode_create()`.
pub(crate) fn opus_custom_mode_create(
    sample_rate: OpusInt32,
    frame_size: usize,
) -> Result<OwnedOpusCustomMode, ModeError> {
    build_custom_mode(sample_rate, frame_size)
}

#[cfg(test)]
mod tests {
    use alloc::vec;
    use alloc::vec::Vec;

    use super::{
        BAND_ALLOCATION, BITALLOC_SIZE, EBAND_5MS, ModeError, compute_allocation_table,
        compute_ebands, compute_log_band_widths, compute_mdct_window, compute_preemphasis,
        opus_custom_mode_create, opus_custom_mode_find_static, opus_custom_mode_find_static_ref,
    };
    use crate::celt::cwrs::log2_frac;

    #[test]
    fn returns_standard_layout_for_5ms_frames() {
        let layout = compute_ebands(48_000, 120, 200);
        assert_eq!(layout.num_bands, EBAND_5MS.len() - 1);
        assert_eq!(layout.bands, EBAND_5MS.to_vec());
    }

    #[test]
    fn computes_layout_for_48k_10ms_frame() {
        let frame_size = 480usize;
        let resolution = (48_000 + frame_size as i32) / (2 * frame_size as i32);
        let layout = compute_ebands(48_000, frame_size, resolution);
        let expected: Vec<i16> = vec![
            0, 2, 4, 6, 8, 10, 12, 15, 18, 22, 26, 30, 34, 40, 46, 54, 64, 74, 88, 106, 128, 154,
            190, 240, 310, 400,
        ];
        assert_eq!(layout.num_bands, expected.len() - 1);
        assert_eq!(layout.bands, expected);
    }

    #[test]
    fn computes_layout_for_16k_frame() {
        let frame_size = 320usize;
        let resolution = (16_000 + frame_size as i32) / (2 * frame_size as i32);
        let layout = compute_ebands(16_000, frame_size, resolution);
        let expected: Vec<i16> = vec![
            0, 4, 8, 12, 16, 20, 26, 32, 38, 44, 52, 60, 70, 80, 92, 108, 126, 148, 176, 212, 256,
            308,
        ];
        assert_eq!(layout.num_bands, expected.len() - 1);
        assert_eq!(layout.bands, expected);
    }

    #[test]
    fn allocation_table_matches_reference_for_standard_mode() {
        let layout = super::EBandLayout::new(EBAND_5MS.to_vec(), EBAND_5MS.len() - 1);
        let table = compute_allocation_table(48_000, 120, &layout);
        assert_eq!(table.num_vectors(), BITALLOC_SIZE);
        assert_eq!(table.num_bands(), layout.num_bands);
        let expected = &BAND_ALLOCATION[..BITALLOC_SIZE * layout.num_bands];
        assert_eq!(table.vectors(), expected);
    }

    #[test]
    fn allocation_table_interpolates_for_custom_mode() {
        let short_mdct_size = 240usize;
        let resolution = (48_000 + short_mdct_size as i32) / (2 * short_mdct_size as i32);
        let layout = compute_ebands(48_000, short_mdct_size, resolution);
        let table = compute_allocation_table(48_000, short_mdct_size, &layout);
        assert_eq!(table.num_vectors(), BITALLOC_SIZE);
        assert_eq!(table.num_bands(), layout.num_bands);
        let expected: Vec<u8> = vec![
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 85, 80,
            77, 75, 72, 69, 63, 56, 49, 40, 34, 31, 29, 20, 18, 10, 2, 0, 0, 0, 0, 0, 0, 0, 110,
            105, 100, 95, 90, 87, 84, 78, 71, 65, 58, 51, 48, 45, 39, 32, 26, 21, 16, 3, 0, 0, 0,
            0, 0, 118, 114, 110, 106, 103, 98, 93, 86, 80, 75, 70, 65, 62, 59, 53, 47, 40, 33, 27,
            17, 7, 0, 0, 0, 0, 126, 122, 119, 115, 112, 108, 104, 95, 89, 83, 78, 72, 69, 66, 60,
            54, 47, 41, 35, 26, 19, 12, 1, 0, 0, 134, 130, 127, 123, 120, 117, 114, 103, 97, 91,
            85, 78, 75, 72, 66, 60, 54, 48, 44, 36, 31, 24, 16, 10, 1, 144, 140, 137, 133, 130,
            127, 124, 113, 107, 101, 95, 88, 85, 82, 76, 70, 64, 58, 54, 46, 41, 34, 26, 15, 1,
            152, 148, 145, 141, 138, 135, 132, 123, 117, 111, 105, 98, 95, 92, 86, 80, 74, 68, 64,
            56, 51, 44, 36, 20, 1, 162, 158, 155, 151, 148, 145, 142, 133, 127, 121, 115, 108, 105,
            102, 96, 90, 84, 78, 74, 66, 61, 54, 46, 30, 1, 172, 168, 165, 161, 158, 155, 152, 143,
            137, 131, 125, 118, 115, 112, 106, 100, 94, 88, 84, 76, 71, 64, 56, 45, 20, 200, 200,
            200, 200, 200, 200, 200, 200, 200, 200, 200, 198, 195, 193, 188, 183, 178, 174, 170,
            164, 159, 153, 148, 129, 104,
        ];
        assert_eq!(table.vectors(), expected.as_slice());
    }

    #[test]
    fn compute_preemphasis_matches_reference_thresholds() {
        let low = compute_preemphasis(8_000);
        assert_eq!(low, [0.350_006_1, -0.179_992_68, 0.271_996_8, 3.676_513_7]);

        let mid = compute_preemphasis(16_000);
        assert_eq!(mid, [0.600_006_1, -0.179_992_68, 0.442_499_88, 2.259_887_7]);

        let high = compute_preemphasis(32_000);
        assert_eq!(high, [0.779_998_8, -0.100_006_1, 0.749_977_1, 1.333_374]);

        let full = compute_preemphasis(48_000);
        assert_eq!(full, [0.850_006_1, 0.0, 1.0, 1.0]);
    }

    #[test]
    fn compute_mdct_window_matches_reference_formula() {
        let overlap = 16usize;
        let window = compute_mdct_window(overlap);
        assert_eq!(window.len(), overlap);

        for (i, value) in window.iter().enumerate() {
            let phase = core::f64::consts::FRAC_PI_2 * ((i as f64 + 0.5) / overlap as f64);
            let sin_phase = phase.sin();
            let expected = (core::f64::consts::FRAC_PI_2 * sin_phase * sin_phase).sin();
            assert!(
                (f64::from(*value) - expected).abs() < 1e-6,
                "window[{}] mismatch: {} vs {}",
                i,
                value,
                expected
            );
        }
    }

    #[test]
    fn compute_log_band_widths_matches_log2_frac() {
        let layout = compute_ebands(48_000, 120, 200);
        let log_n = compute_log_band_widths(&layout);
        assert_eq!(log_n.len(), layout.num_bands);

        for (band, &value) in log_n.iter().enumerate() {
            let width = i32::from(layout.bands[band + 1] - layout.bands[band]);
            let expected = log2_frac(width as u32, 3) as i16;
            assert_eq!(value, expected, "band {} mismatch", band);
        }
    }

    #[test]
    fn custom_mode_matches_reference_configuration() {
        let owned = opus_custom_mode_create(48_000, 960).expect("custom mode");
        assert_eq!(owned.frame_size(), 960);
        let mode = owned.mode();
        assert_eq!(mode.sample_rate, 48_000);
        assert_eq!(mode.overlap, 120);
        assert_eq!(mode.max_lm, 3);
        assert_eq!(mode.num_short_mdcts, 8);
        assert_eq!(mode.short_mdct_size, 120);
        assert_eq!(mode.num_ebands, EBAND_5MS.len() - 1);
        assert_eq!(mode.effective_ebands, mode.num_ebands);
        assert_eq!(mode.e_bands, EBAND_5MS);
        assert_eq!(mode.num_alloc_vectors, BITALLOC_SIZE);
        assert_eq!(
            mode.alloc_vectors,
            &BAND_ALLOCATION[..BITALLOC_SIZE * mode.num_ebands]
        );
        assert_eq!(mode.window.len(), mode.overlap);
        assert_eq!(mode.log_n.len(), mode.num_ebands);
        assert_eq!(mode.mdct.len(), 1_920);
        assert_eq!(mode.mdct.max_shift(), 3);
        assert_eq!(mode.cache.size, mode.cache.bits.len());
        assert!((mode.pre_emphasis[0] - 0.850_006_1).abs() < 1e-6);
        assert!((mode.pre_emphasis[2] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn custom_mode_validates_parameters() {
        assert_eq!(
            opus_custom_mode_create(4_000, 960).unwrap_err(),
            ModeError::BadSampleRate
        );
        assert_eq!(
            opus_custom_mode_create(48_000, 39).unwrap_err(),
            ModeError::BadFrameSize
        );
        assert_eq!(
            opus_custom_mode_create(48_000, 40).unwrap_err(),
            ModeError::FrameTooShort
        );
        assert_eq!(
            opus_custom_mode_create(96_000, 2048).unwrap_err(),
            ModeError::BadFrameSize
        );
    }

    #[test]
    fn static_mode_lookup_matches_reference_mode() {
        let static_mode = opus_custom_mode_find_static(48_000, 960).expect("static mode");
        let dynamic = opus_custom_mode_create(48_000, 960).expect("dynamic mode");
        let dynamic_mode = dynamic.mode();

        assert_eq!(static_mode.sample_rate, dynamic_mode.sample_rate);
        assert_eq!(static_mode.overlap, dynamic_mode.overlap);
        assert_eq!(static_mode.num_ebands, dynamic_mode.num_ebands);
        assert_eq!(static_mode.effective_ebands, dynamic_mode.effective_ebands);
        assert_eq!(static_mode.pre_emphasis, dynamic_mode.pre_emphasis);
        assert_eq!(static_mode.e_bands, dynamic_mode.e_bands);
        assert_eq!(static_mode.max_lm, dynamic_mode.max_lm);
        assert_eq!(static_mode.num_short_mdcts, dynamic_mode.num_short_mdcts);
        assert_eq!(static_mode.short_mdct_size, dynamic_mode.short_mdct_size);
        assert_eq!(
            static_mode.num_alloc_vectors,
            dynamic_mode.num_alloc_vectors
        );
        assert_eq!(static_mode.alloc_vectors, dynamic_mode.alloc_vectors);
        assert_eq!(static_mode.log_n, dynamic_mode.log_n);
        assert_eq!(static_mode.window, dynamic_mode.window);
        assert_eq!(static_mode.mdct.len(), dynamic_mode.mdct.len());
        assert_eq!(static_mode.mdct.max_shift(), dynamic_mode.mdct.max_shift());
        assert_eq!(static_mode.cache.size, dynamic_mode.cache.size);
        assert_eq!(static_mode.cache.index, dynamic_mode.cache.index);
        assert_eq!(static_mode.cache.bits, dynamic_mode.cache.bits);
        assert_eq!(static_mode.cache.caps, dynamic_mode.cache.caps);
    }

    #[test]
    fn static_mode_lookup_supports_shorter_frames() {
        let mode = opus_custom_mode_find_static(48_000, 480).expect("static mode");
        assert_eq!(mode.sample_rate, 48_000);
        assert_eq!(mode.short_mdct_size, 120);
        assert_eq!(mode.max_lm, 3);
    }

    #[test]
    fn static_mode_lookup_rejects_unknown_combinations() {
        assert!(opus_custom_mode_find_static(32_000, 960).is_none());
        assert!(opus_custom_mode_find_static(48_000, 1920).is_none());
    }

    #[test]
    fn static_mode_ref_lookup_reuses_cached_mode() {
        let first = opus_custom_mode_find_static_ref(48_000, 960).expect("static mode");
        let second = opus_custom_mode_find_static_ref(48_000, 960).expect("static mode");
        assert!(core::ptr::eq(first, second));
    }
}
