//! Safe, narrowly scoped access to the system `libopus` codec.
//!
//! The foreign ABI, raw handles, and error pointers are contained in this
//! crate. Callers can move an encoder or decoder between threads, but all
//! codec operations require exclusive access because libopus mutates them.

#![allow(unsafe_code)]

use std::ffi::{CStr, c_char, c_int};
use std::fmt;
use std::ptr::NonNull;

const OPUS_OK: c_int = 0;
const OPUS_BAD_ARG: c_int = -1;
const OPUS_ALLOC_FAIL: c_int = -7;
const OPUS_APPLICATION_VOIP: c_int = 2_048;
const MAX_PACKET_BYTES: usize = 1_275;

#[repr(C)]
struct OpusEncoder {
    _private: [u8; 0],
}

#[repr(C)]
struct OpusDecoder {
    _private: [u8; 0],
}

#[link(name = "opus")]
unsafe extern "C" {
    fn opus_encoder_create(
        sample_rate: c_int,
        channels: c_int,
        application: c_int,
        error: *mut c_int,
    ) -> *mut OpusEncoder;
    fn opus_encoder_destroy(encoder: *mut OpusEncoder);
    fn opus_encode(
        encoder: *mut OpusEncoder,
        pcm: *const i16,
        frame_size: c_int,
        output: *mut u8,
        max_output_bytes: c_int,
    ) -> c_int;
    fn opus_decoder_create(
        sample_rate: c_int,
        channels: c_int,
        error: *mut c_int,
    ) -> *mut OpusDecoder;
    fn opus_decoder_destroy(decoder: *mut OpusDecoder);
    fn opus_decode(
        decoder: *mut OpusDecoder,
        packet: *const u8,
        packet_bytes: c_int,
        pcm: *mut i16,
        frame_size: c_int,
        decode_fec: c_int,
    ) -> c_int;
    fn opus_strerror(error: c_int) -> *const c_char;
}

/// Channel layouts accepted by the Opus multirate API.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i32)]
pub enum Channels {
    Mono = 1,
    Stereo = 2,
}

impl Channels {
    const fn count(self) -> usize {
        self as usize
    }
}

/// Stable libopus error code returned by a codec operation.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Error(c_int);

impl Error {
    #[must_use]
    pub const fn code(self) -> i32 {
        self.0
    }
}

impl fmt::Display for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = unsafe {
            let pointer = opus_strerror(self.0);
            (!pointer.is_null()).then(|| CStr::from_ptr(pointer).to_string_lossy())
        };
        match message {
            Some(message) => write!(formatter, "libopus error {}: {message}", self.0),
            None => write!(formatter, "libopus error {}", self.0),
        }
    }
}

impl std::error::Error for Error {}

/// Owned Opus `VoIP` encoder.
pub struct Encoder {
    raw: NonNull<OpusEncoder>,
    channels: Channels,
    sample_rate: usize,
}

// libopus codec handles have no thread affinity. Exclusive method access
// prevents concurrent calls after the owned handle is moved to another thread.
unsafe impl Send for Encoder {}

impl Encoder {
    /// Creates a `VoIP` encoder for a supported Opus sample rate.
    ///
    /// # Errors
    ///
    /// Returns the native libopus error when the configuration is unsupported
    /// or the codec cannot be allocated.
    pub fn voip(sample_rate: u32, channels: Channels) -> Result<Self, Error> {
        let sample_rate = validate_sample_rate(sample_rate)?;
        let sample_rate_usize = usize::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))?;
        let mut error = OPUS_OK;
        let raw = unsafe {
            opus_encoder_create(
                sample_rate,
                channels as c_int,
                OPUS_APPLICATION_VOIP,
                &raw mut error,
            )
        };
        let raw = NonNull::new(raw).ok_or(Error(if error == OPUS_OK {
            OPUS_ALLOC_FAIL
        } else {
            error
        }))?;
        if error != OPUS_OK {
            unsafe { opus_encoder_destroy(raw.as_ptr()) };
            return Err(Error(error));
        }
        Ok(Self {
            raw,
            channels,
            sample_rate: sample_rate_usize,
        })
    }

    /// Encodes one complete Opus frame into a caller-owned packet buffer.
    ///
    /// # Errors
    ///
    /// Rejects malformed channel/frame lengths and oversized output buffers,
    /// and returns any native codec failure.
    pub fn encode(&mut self, pcm: &[i16], output: &mut [u8]) -> Result<usize, Error> {
        let channels = self.channels.count();
        if !pcm.len().is_multiple_of(channels) || output.is_empty() {
            return Err(Error(OPUS_BAD_ARG));
        }
        let frame_size = pcm.len() / channels;
        let base = self.sample_rate / 400;
        if ![base, base * 2, base * 4, base * 8, base * 16, base * 24].contains(&frame_size) {
            return Err(Error(OPUS_BAD_ARG));
        }
        let result = unsafe {
            opus_encode(
                self.raw.as_ptr(),
                pcm.as_ptr(),
                c_int::try_from(frame_size).map_err(|_| Error(OPUS_BAD_ARG))?,
                output.as_mut_ptr(),
                c_int::try_from(output.len()).map_err(|_| Error(OPUS_BAD_ARG))?,
            )
        };
        result_to_size(result)
    }
}

impl Drop for Encoder {
    fn drop(&mut self) {
        unsafe { opus_encoder_destroy(self.raw.as_ptr()) };
    }
}

/// Owned Opus decoder.
pub struct Decoder {
    raw: NonNull<OpusDecoder>,
    channels: Channels,
    sample_rate: usize,
}

// See the Encoder safety argument above.
unsafe impl Send for Decoder {}

impl Decoder {
    /// Creates a decoder for a supported Opus sample rate.
    ///
    /// # Errors
    ///
    /// Returns the native libopus error when the configuration is unsupported
    /// or the codec cannot be allocated.
    pub fn new(sample_rate: u32, channels: Channels) -> Result<Self, Error> {
        let sample_rate = validate_sample_rate(sample_rate)?;
        let sample_rate_usize = usize::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))?;
        let mut error = OPUS_OK;
        let raw = unsafe { opus_decoder_create(sample_rate, channels as c_int, &raw mut error) };
        let raw = NonNull::new(raw).ok_or(Error(if error == OPUS_OK {
            OPUS_ALLOC_FAIL
        } else {
            error
        }))?;
        if error != OPUS_OK {
            unsafe { opus_decoder_destroy(raw.as_ptr()) };
            return Err(Error(error));
        }
        Ok(Self {
            raw,
            channels,
            sample_rate: sample_rate_usize,
        })
    }

    /// Decodes one Opus packet, or performs packet-loss concealment for `None`.
    ///
    /// # Errors
    ///
    /// Rejects invalid output alignment/length and packets too large for the
    /// libopus single-stream format, and returns any native codec failure.
    pub fn decode(
        &mut self,
        packet: Option<&[u8]>,
        pcm: &mut [i16],
        decode_fec: bool,
    ) -> Result<usize, Error> {
        let channels = self.channels.count();
        if !pcm.len().is_multiple_of(channels)
            || pcm.is_empty()
            || pcm.len() / channels > self.sample_rate * 120 / 1_000
            || packet.is_some_and(|value| value.is_empty() || value.len() > MAX_PACKET_BYTES)
        {
            return Err(Error(OPUS_BAD_ARG));
        }
        let frame_size = pcm.len() / channels;
        let (packet_pointer, packet_bytes) = packet.map_or((std::ptr::null(), 0), |value| {
            (
                value.as_ptr(),
                c_int::try_from(value.len()).unwrap_or(c_int::MAX),
            )
        });
        let result = unsafe {
            opus_decode(
                self.raw.as_ptr(),
                packet_pointer,
                packet_bytes,
                pcm.as_mut_ptr(),
                c_int::try_from(frame_size).map_err(|_| Error(OPUS_BAD_ARG))?,
                c_int::from(decode_fec),
            )
        };
        result_to_size(result)
    }
}

impl Drop for Decoder {
    fn drop(&mut self) {
        unsafe { opus_decoder_destroy(self.raw.as_ptr()) };
    }
}

fn validate_sample_rate(sample_rate: u32) -> Result<c_int, Error> {
    if matches!(sample_rate, 8_000 | 12_000 | 16_000 | 24_000 | 48_000) {
        c_int::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))
    } else {
        Err(Error(OPUS_BAD_ARG))
    }
}

fn result_to_size(result: c_int) -> Result<usize, Error> {
    usize::try_from(result).map_err(|_| Error(result))
}

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

    #[test]
    fn system_libopus_encodes_and_decodes_a_real_frame() {
        let mut encoder = Encoder::voip(48_000, Channels::Mono).unwrap();
        let mut decoder = Decoder::new(48_000, Channels::Mono).unwrap();
        let pcm = (0..960)
            .map(|index| if index % 40 < 20 { 8_000 } else { -8_000 })
            .collect::<Vec<_>>();
        let mut packet = [0_u8; MAX_PACKET_BYTES];
        let packet_bytes = encoder.encode(&pcm, &mut packet).unwrap();
        assert!(packet_bytes > 0);

        let mut output_pcm = [0_i16; 960];
        assert_eq!(
            decoder
                .decode(Some(&packet[..packet_bytes]), &mut output_pcm, false)
                .unwrap(),
            960
        );
        assert!(output_pcm.iter().any(|sample| *sample != 0));
    }

    #[test]
    fn safe_boundary_rejects_invalid_shapes() {
        assert!(Encoder::voip(44_100, Channels::Mono).is_err());
        let mut encoder = Encoder::voip(48_000, Channels::Mono).unwrap();
        assert!(encoder.encode(&[0; 100], &mut [0; 100]).is_err());
        let mut decoder = Decoder::new(48_000, Channels::Stereo).unwrap();
        assert!(decoder.decode(Some(&[1]), &mut [0; 3], false).is_err());
    }
}
