//! # G.711 µ-law codec + 8 kHz <-> 24 kHz linear-interpolated resampling
//!
//! `G711Codec` is the boundary codec between Twilio Media Streams raw-audio
//! forks (8 kHz µ-law, base64-encoded JSON envelopes) and rutster's canonical
//! 24 kHz mono PCM `PcmFrame` (slice-1 spec §3.9 -- 480 samples / 20 ms).
//!
//! # Why in-core, not a `g711` crate dep
//!
//! µ-law is ~30 lines of table-driven code. The codec has been standard
//! since 1972 (ITU-T G.711); it has not changed. Pulling a dependency for a
//! constant-mapping table would be a FOB hygiene violation (ADR-0008 -- link
//! mature OSS only when the complexity is non-trivial; this is neither). The
//! implementation is also learner-facing per AGENTS.md "Code style (Rust)":
//! the µ-law companding formula is a piece of telephony history worth teaching.
//!
//! # The hot path
//!
//! [`G711Codec::decode_mulaw_to_pcm`] runs inside the WSS pump task for every
//! inbound Twilio "media" frame (one per 20 ms tick). [`G711Codec::encode_pcm_to_mulaw`]
//! runs in the WSS pump's outbound send loop for every brain reply the std
//! thread produces. Both call paths are pure-function over `&[u8]` -- no I/O
//! inside, no allocation beyond the result buffer's `Vec` capacity hint.
//!
//! # Why 3x upsample by linear interpolation (not `rubato` / speexdsp)
//!
//! 24 kHz / 8 kHz = exactly 3. Each input sample becomes 3 output samples:
//!
//! ```text
//!   out[3i]     = input[i]
//!   out[3i + 1] = (2*input[i] + input[i+1]) / 3
//!   out[3i + 2] = (input[i] + 2*input[i+1]) / 3
//! ```
//!
//! These weights place the two interpolated samples at 1/3 and 2/3 of the
//! 8 kHz sample interval -- a straight line between each pair of input
//! samples. Linear interpolation is the cheap correct-enough answer for the
//! spearhead MVP; the aliasing is below `LocalVadReflex`'s RMS threshold.
//!
//! Linear interpolation is the cheap correct-enough answer for the spearhead
//! MVP -- the artifacts are below the audibility threshold for `LocalVadReflex`'s
//! barge-in trigger, which only needs RMS energy above `VAD_RMS_THRESHOLD`
//! (slice-4 spec §3.4). A production-grade resampler (`rubato` or `speexdsp`)
//! would polish the high-frequency aliasing further, but doing so now would
//! pull in a transitive dep we don't need for the wedge claim ("the FOB reflex
//! loop works against real phone audio"). `rubato` lands in a post-spearhead
//! refinement if a downstream consumer needs it (slice-5 spec §6.6 / §8.1).
//!
//! # Resampling round-trip drift budget
//!
//! The encode -> decode round-trip energy drift is bounded by µ-law's intrinsic
//! segment quantization error (3 dB worst-case at segment transitions, well
//! under the 12% energy-drift budget in slice-5 spec §6.5). The drift is
//! verified by `tests::decode_then_encode_round_trips_a_loud_signal_within_12pct_energy_drift`.

use rutster_media::{PcmFrame, SAMPLES_PER_FRAME};

use crate::mulaw_decode_table::MULAW_TO_LINEAR;
use crate::mulaw_encode_table::LINEAR_TO_MULAW;

/// Zero-state codec. The methods are pure functions -- the codec holds no
/// per-session state because the µ-law encode/decode tables are global
/// `static` arrays (compile-time-generated, see `mulaw_decode_table.rs`
/// and `mulaw_encode_table.rs`). The struct exists so callers can build
/// router / handler signatures that explicitly thread the codec concept
/// (matching the slice-5 spec §3.1 surface we extend in later tasks).
pub struct G711Codec;

impl G711Codec {
    /// Decode a Twilio Media Streams "Media" frame payload (already
    /// base64-decoded bytes of µ-law samples at 8 kHz) into a 24 kHz
    /// `PcmFrame` (slice-1 canonical format). 3x linear upsample.
    ///
    /// # Frame size contract
    ///
    /// Twilio Media Streams delivers 160 µ-law bytes per 20 ms tick
    /// (8000 samples/sec * 0.020 sec = 160). 3x upsampled, that becomes
    /// 480 samples -- exactly one `PcmFrame` of slice-1 spec §3.9.
    ///
    /// # Hot-path policy
    ///
    /// A malformed frame (wrong byte count) does NOT crash the WSS pump
    /// task: `debug_assert!` surfaces protocol drift in test builds; in
    /// release the function returns a zeroed `PcmFrame` and lets the
    /// caller drop + observe (spec §3.1).
    pub fn decode_mulaw_to_pcm(mulaw: &[u8]) -> PcmFrame {
        // `debug_assert!` catches protocol drift in test builds; release
        // builds take the safe fallback so the WSS pump task never crashes
        // on a malformed envelope (hot-path policy, AGENTS.md).
        debug_assert_eq!(
            mulaw.len(),
            160,
            "expected 160 µ-law bytes per 20ms frame (got {})",
            mulaw.len()
        );

        // Malformed input is dropped on the hot path: return silence so the
        // loop tick continues safely. A correct Twilio Media Streams frame
        // always carries exactly 160 µ-law bytes per 20 ms tick.
        if mulaw.len() != 160 {
            return PcmFrame::zeroed();
        }

        // Decode the 160 µ-law bytes into a fixed-size 8 kHz linear buffer.
        // 160 * sizeof(i16) = 320 bytes -- small enough to live on the stack.
        let mut linear_8k = [0i16; 160];
        for (i, &byte) in mulaw.iter().enumerate() {
            linear_8k[i] = MULAW_TO_LINEAR[byte as usize];
        }

        let mut samples = [0i16; SAMPLES_PER_FRAME];
        for i in 0..160 {
            let current = linear_8k[i];
            // For the final 8 kHz sample there is no next sample to
            // interpolate toward, so hold the last value -- no extrapolation
            // past the frame boundary.
            let next = if i < 159 { linear_8k[i + 1] } else { current };

            let base = 3 * i;
            samples[base] = current;
            samples[base + 1] = ((2 * current as i32 + next as i32) / 3) as i16;
            samples[base + 2] = ((current as i32 + 2 * next as i32) / 3) as i16;
        }

        PcmFrame { samples }
    }

    /// Encode a 24 kHz `PcmFrame` into 8 kHz µ-law bytes for Twilio Media
    /// Streams. Inverse of [`decode_mulaw_to_pcm`]: 3x downsample by
    /// decimation (take every 3rd sample) plus a `LINEAR_TO_MULAW` table
    /// lookup. The result is a 160-byte `Vec<u8>`, one per 20 ms tick.
    ///
    /// # Why decimation (not averaging)
    ///
    /// Decimation simply drops the samples at positions 3i+1 and 3i+2. The
    /// aliasing energy this introduces is below `LocalVadReflex`'s RMS
    /// threshold (slice-4 spec §3.4); the brain never cares about
    /// frequencies above 4 kHz (its speech model is band-limited below
    /// 8 kHz Nyquist). `rubato` would deliver better quality at the cost
    /// of a transitive dep + slower hot path -- not warranted for the
    /// spearhead MVP (spec §6.6 + §8.1).
    pub fn encode_pcm_to_mulaw(frame: &PcmFrame) -> Vec<u8> {
        let mut mulaw = Vec::with_capacity(160);
        for i in 0..160 {
            // `i16 as u16` reinterprets the bit pattern -- i16 -1 -> u16 65535
            // -> table index 65535. This is the correct index for
            // LINEAR_TO_MULAW, which is laid out `linear_to_mulaw(i as i16)` for
            // all `i in 0..65536` -- the bit-pattern identity holds.
            let sample = frame.samples[3 * i];
            mulaw.push(LINEAR_TO_MULAW[(sample as u16) as usize]);
        }
        mulaw
    }
}

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

    #[test]
    fn decode_160_byte_frame_yields_480_samples() {
        // 160 µ-law bytes is one 20 ms Twilio Media Streams tick; decoding
        // must yield exactly the slice-1 canonical 480-sample PcmFrame.
        // (Input is `0xFF` = ITU-T G.711 silence per spec; decode is the
        // `mulaw_to_linear(0xFF)` value, repeated 480 times after upsample.)
        let mulaw = vec![0xFFu8; 160];
        let frame = G711Codec::decode_mulaw_to_pcm(&mulaw);
        assert_eq!(frame.samples.len(), SAMPLES_PER_FRAME);
    }

    #[test]
    fn decode_silence_yields_all_zero_pcm() {
        // ITU-T G.711 µ-law silence is the byte 0xFF. The reference decoder's
        // BIAS add + subtract on the positive branch yields exactly 0 PCM --
        // this is the "mid-tread zero" property that keeps silences silent.
        let mulaw = vec![0xFFu8; 160];
        let frame = G711Codec::decode_mulaw_to_pcm(&mulaw);
        assert!(
            frame.samples.iter().all(|&s| s == 0),
            "decoded µ-law silence (0xFF repeated) must be all-zero PCM; got non-zero"
        );
    }

    #[test]
    fn encode_pcm_to_mulaw_emits_160_bytes_per_frame() {
        let frame = PcmFrame::zeroed();
        let mulaw = G711Codec::encode_pcm_to_mulaw(&frame);
        assert_eq!(mulaw.len(), 160);
    }

    #[test]
    fn encode_silence_emits_mu_law_silence_0xff() {
        // Encoding a zero PCM frame must yield 160 bytes of 0xFF (the µ-law
        // silence encoding) -- inverse of `decode_silence_yields_all_zero_pcm`.
        let frame = PcmFrame::zeroed();
        let mulaw = G711Codec::encode_pcm_to_mulaw(&frame);
        assert!(
            mulaw.iter().all(|&b| b == 0xFF),
            "encoding zero PCM must yield 0xFF µ-law silence; got non-0xFF"
        );
    }

    #[test]
    fn decode_then_encode_round_trips_a_loud_signal_within_12pct_energy_drift() {
        // The wedge claim: a real PSTN caller's voice survives the
        // µ-law encode -> And the decode round trip with RMS energy preserved
        // within the spec's 12% budget (slice-5 spec §6.5 -- the barge-in
        // trigger only needs RMS energy above VAD_RMS_THRESHOLD; µ-law's
        // quantization error is well below that floor).
        //
        // A constant-amplitude loud signal (10_000 amplitude i16, ~30% of
        // full scale) is the harshest single-tone test case -- the µ-law
        // quantization step is most visible at flat amplitudes (segment
        // transitions are zero, so no dynamic range advantage accrues).
        let mut input_frame = PcmFrame::zeroed();
        for s in &mut input_frame.samples {
            *s = 10_000;
        }

        let mulaw = G711Codec::encode_pcm_to_mulaw(&input_frame);
        assert_eq!(mulaw.len(), 160, "encoded frame must be 160 µ-law bytes");

        let decoded = G711Codec::decode_mulaw_to_pcm(&mulaw);

        let orig_rms = rms(&input_frame.samples);
        let dec_rms = rms(&decoded.samples);
        let drift = (dec_rms - orig_rms).abs() / orig_rms.max(1.0);

        assert!(
            drift <= 0.12,
            "µ-law round-trip energy drift {:.2}% > 12% budget (spec §6.5); \
             orig_rms={}, dec_rms={}",
            drift * 100.0,
            orig_rms,
            dec_rms
        );
    }

    #[test]
    fn encode_then_decode_preserves_polarity() {
        // A loud negative sample (silent cues, DTMF, or echo-cancel tails)
        // must round-trip without sign flip. Verifies the XOR mask sign
        // convention (positive -> MSB 1; negative -> MSB 0) is consistent
        // across encode + decode.
        let mut input_frame = PcmFrame::zeroed();
        for s in &mut input_frame.samples {
            *s = -10_000;
        }
        let mulaw = G711Codec::encode_pcm_to_mulaw(&input_frame);
        let decoded = G711Codec::decode_mulaw_to_pcm(&mulaw);

        // All decoded samples must be negative (no polarity flip).
        assert!(
            decoded.samples.iter().all(|&s| s <= 0),
            "negative-amplitude input must decode to non-positive samples; \
             observed a positive sample (sign convention bug)"
        );
    }

    /// RMS energy over a sample buffer -- the same shape `LocalVadReflex`
    /// uses for its barge-in trigger (slice-4 spec §3.4). Local helper here
    /// to keep this test module self-contained + the assertion readable.
    fn rms(samples: &[i16]) -> f64 {
        let sum_sq: u64 = samples.iter().map(|&s| (s as i64 * s as i64) as u64).sum();
        (sum_sq as f64 / samples.len() as f64).sqrt()
    }
}
