//! MS-ADPCM → PCM16 software decoder.
//!
//! Entry point: [`decode_to_pcm16`].  Operates on a block-structured payload described by
//! an [`AdpcmWaveFormat`] header.  Supports 1- and 2-channel audio.
//!
//! The algorithm follows the Microsoft MS-ADPCM specification: each block starts with a
//! per-channel preamble (predictor index, delta, two history samples) followed by a stream
//! of 4-bit nibbles that encode sample deltas.

use crate::error::{Error, Result};
use crate::scd::track::AdpcmWaveFormat;

/// Delta step-size adaptation table (one entry per possible 4-bit nibble value).
const ADAPTATION_TABLE: [i32; 16] = [
    230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230,
];

/// Minimum allowed delta value; prevents the step size from collapsing to zero.
const MIN_DELTA: i32 = 16;

#[derive(Debug, Clone, Copy)]
struct ChannelState {
    delta: i32,
    sample1: i32,
    sample2: i32,
    coef1: i32,
    coef2: i32,
}

/// Decodes an MS-ADPCM payload to interleaved PCM16 samples.
///
/// `payload` must be a whole multiple of `fmt.block_align` bytes.
/// Returns [`Error::InvalidMsAdpcmBlock`] for malformed payloads or invalid header fields.
pub(crate) fn decode_to_pcm16(fmt: &AdpcmWaveFormat, payload: &[u8]) -> Result<Vec<i16>> {
    let channels = validate_channels(fmt.channels)?;
    let block_align = validate_block_align(fmt.block_align)?;

    let preamble_size = channels.checked_mul(7).ok_or(Error::InvalidMsAdpcmBlock {
        reason: "preamble size overflows usize",
    })?;
    if block_align < preamble_size {
        return Err(Error::InvalidMsAdpcmBlock {
            reason: "block_align smaller than per-channel preamble",
        });
    }

    let samples_per_block = usize::from(fmt.samples_per_block);
    if samples_per_block < 2 {
        return Err(Error::InvalidMsAdpcmBlock {
            reason: "samples_per_block must be >= 2 (preamble carries 2 samples per channel)",
        });
    }

    let post_samples = samples_per_block - 2;
    let nibble_bytes_required = match channels {
        1 => post_samples.div_ceil(2),
        2 => post_samples,
        _ => unreachable!("channels validated to 1..=2 above"),
    };
    let required_block_size =
        preamble_size
            .checked_add(nibble_bytes_required)
            .ok_or(Error::InvalidMsAdpcmBlock {
                reason: "block size overflows usize",
            })?;
    if block_align < required_block_size {
        return Err(Error::InvalidMsAdpcmBlock {
            reason: "block_align too small for declared samples_per_block",
        });
    }

    if !payload.is_empty() && payload.len() % block_align != 0 {
        return Err(Error::InvalidMsAdpcmBlock {
            reason: "payload length is not a whole multiple of block_align",
        });
    }

    let num_coef = usize::from(fmt.num_coef);
    if num_coef * 2 > fmt.coefficients.len() {
        return Err(Error::InvalidMsAdpcmBlock {
            reason: "num_coef larger than declared coefficient table",
        });
    }

    let block_count = payload.len() / block_align;
    let mut out = Vec::with_capacity(block_count * samples_per_block * channels);

    for block in payload.chunks_exact(block_align) {
        decode_block(
            channels,
            samples_per_block,
            num_coef,
            &fmt.coefficients,
            block,
            &mut out,
        )?;
    }

    Ok(out)
}

fn validate_channels(raw: i16) -> Result<usize> {
    let n = usize::from(u16::try_from(raw).map_err(|_| Error::InvalidMsAdpcmBlock {
        reason: "channel count negative",
    })?);
    if !(1..=2).contains(&n) {
        return Err(Error::InvalidMsAdpcmBlock {
            reason: "channel count must be 1 or 2",
        });
    }
    Ok(n)
}

fn validate_block_align(raw: i16) -> Result<usize> {
    let n = usize::from(u16::try_from(raw).map_err(|_| Error::InvalidMsAdpcmBlock {
        reason: "block_align negative",
    })?);
    if n == 0 {
        return Err(Error::InvalidMsAdpcmBlock {
            reason: "block_align is zero",
        });
    }
    Ok(n)
}

fn parse_preamble(
    channels: usize,
    num_coef: usize,
    coefficients: &[i16; 14],
    block: &[u8],
) -> Result<[ChannelState; 2]> {
    let mut state = [ChannelState {
        delta: 0,
        sample1: 0,
        sample2: 0,
        coef1: 0,
        coef2: 0,
    }; 2];

    for ch in 0..channels {
        let idx = block[ch] as usize;
        if idx >= num_coef {
            return Err(Error::InvalidMsAdpcmBlock {
                reason: "block predictor index past coefficient table",
            });
        }
        state[ch].coef1 = i32::from(coefficients[idx * 2]);
        state[ch].coef2 = i32::from(coefficients[idx * 2 + 1]);
    }

    let delta_base = channels;
    for (ch, item) in state.iter_mut().enumerate().take(channels) {
        let lo = block[delta_base + ch * 2];
        let hi = block[delta_base + ch * 2 + 1];
        item.delta = i32::from(i16::from_le_bytes([lo, hi]));
    }

    let s1_base = channels * 3;
    for (ch, item) in state.iter_mut().enumerate().take(channels) {
        let lo = block[s1_base + ch * 2];
        let hi = block[s1_base + ch * 2 + 1];
        item.sample1 = i32::from(i16::from_le_bytes([lo, hi]));
    }

    let s2_base = channels * 5;
    for (ch, item) in state.iter_mut().enumerate().take(channels) {
        let lo = block[s2_base + ch * 2];
        let hi = block[s2_base + ch * 2 + 1];
        item.sample2 = i32::from(i16::from_le_bytes([lo, hi]));
    }

    Ok(state)
}

fn decode_block(
    channels: usize,
    samples_per_block: usize,
    num_coef: usize,
    coefficients: &[i16; 14],
    block: &[u8],
    out: &mut Vec<i16>,
) -> Result<()> {
    let mut state = parse_preamble(channels, num_coef, coefficients, block)?;

    if channels == 1 {
        out.push(clamp_i16(state[0].sample2));
        out.push(clamp_i16(state[0].sample1));
    } else {
        out.push(clamp_i16(state[0].sample2));
        out.push(clamp_i16(state[1].sample2));
        out.push(clamp_i16(state[0].sample1));
        out.push(clamp_i16(state[1].sample1));
    }

    let preamble_size = channels * 7;
    let nibble_stream = &block[preamble_size..];
    let post_samples = samples_per_block - 2;

    match channels {
        1 => walk_mono(nibble_stream, post_samples, &mut state[0], out),
        2 => walk_stereo(nibble_stream, post_samples, &mut state, out),
        _ => unreachable!("channels validated to 1..=2 above"),
    }
}

fn walk_mono(
    nibble_stream: &[u8],
    post_samples: usize,
    state: &mut ChannelState,
    out: &mut Vec<i16>,
) -> Result<()> {
    let mut byte_idx = 0usize;
    let mut emitted = 0usize;
    while emitted < post_samples {
        let b = *nibble_stream
            .get(byte_idx)
            .ok_or(Error::InvalidMsAdpcmBlock {
                reason: "nibble stream exhausted before sample count satisfied",
            })?;
        let hi = (b >> 4) & 0x0F;
        out.push(step_one(hi, state));
        emitted += 1;
        if emitted < post_samples {
            let lo = b & 0x0F;
            out.push(step_one(lo, state));
            emitted += 1;
        }
        byte_idx += 1;
    }
    Ok(())
}

fn walk_stereo(
    nibble_stream: &[u8],
    post_samples: usize,
    state: &mut [ChannelState; 2],
    out: &mut Vec<i16>,
) -> Result<()> {
    let mut byte_idx = 0usize;
    let mut emitted_frames = 0usize;
    while emitted_frames < post_samples {
        let b = *nibble_stream
            .get(byte_idx)
            .ok_or(Error::InvalidMsAdpcmBlock {
                reason: "nibble stream exhausted before sample count satisfied",
            })?;
        let hi = (b >> 4) & 0x0F;
        let lo = b & 0x0F;
        let l = step_one(hi, &mut state[0]);
        let r = step_one(lo, &mut state[1]);
        out.push(l);
        out.push(r);
        emitted_frames += 1;
        byte_idx += 1;
    }
    Ok(())
}

fn step_one(nibble: u8, state: &mut ChannelState) -> i16 {
    debug_assert!(nibble < 16, "MS-ADPCM step_one expects a 4-bit nibble");

    let signed_nibble = if nibble & 0x08 != 0 {
        i32::from(nibble) - 16
    } else {
        i32::from(nibble)
    };

    let predictor = (state.sample1 * state.coef1 + state.sample2 * state.coef2) >> 8;
    let unclamped = predictor + signed_nibble * state.delta;
    let new_sample = unclamped.clamp(i32::from(i16::MIN), i32::from(i16::MAX));

    let next_delta = (state.delta * ADAPTATION_TABLE[nibble as usize]) / 256;
    state.delta = next_delta.max(MIN_DELTA);

    state.sample2 = state.sample1;
    state.sample1 = new_sample;

    clamp_i16(new_sample)
}

#[allow(clippy::cast_possible_truncation)]
fn clamp_i16(v: i32) -> i16 {
    v.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16
}
