use audiopus::coder::Encoder as OpusEncoder;
use audiopus::{
    Application as OpusApp, Bitrate as OpusBitrate, Channels as OpusChannels,
    SampleRate as OpusSampleRate,
};
use std::sync::atomic::{AtomicU32, Ordering};
use tokio::sync::mpsc;
use tracing::{info, warn};

use chanora_protocol::{AudioData, CodecType, OutAudio, OutPacket};

use crate::AudioError;

pub(crate) const MAX_OPUS_FRAME: usize = 1275;

const VOIP_BITRATE_BPS: i32 = 32_000;
const VOIP_COMPLEXITY: u8 = 10;
const VOIP_PACKET_LOSS_PERC: u8 = 5;

pub(crate) fn new_voip_encoder(context: &str) -> Result<OpusEncoder, AudioError> {
    let mut encoder = OpusEncoder::new(OpusSampleRate::Hz48000, OpusChannels::Mono, OpusApp::Voip)
        .map_err(|e| AudioError::Opus(format!("encoder new ({context}): {e}")))?;
    tune_voip_encoder(&mut encoder, context);
    Ok(encoder)
}

pub(crate) fn tune_voip_encoder(encoder: &mut OpusEncoder, context: &str) {
    if let Err(e) = encoder.set_bitrate(OpusBitrate::BitsPerSecond(VOIP_BITRATE_BPS)) {
        warn!(target: "chanora_audio", context = %context, error = %e, "opus set_bitrate failed");
    }
    if let Err(e) = encoder.set_complexity(VOIP_COMPLEXITY) {
        warn!(target: "chanora_audio", context = %context, error = %e, "opus set_complexity failed");
    }
    if let Err(e) = encoder.set_inband_fec(true) {
        warn!(target: "chanora_audio", context = %context, error = %e, "opus set_inband_fec failed");
    }
    if let Err(e) = encoder.set_packet_loss_perc(VOIP_PACKET_LOSS_PERC) {
        warn!(
            target: "chanora_audio",
            context = %context,
            error = %e,
            "opus set_packet_loss_perc failed"
        );
    }

    info!(
        target: "chanora_audio",
        context = %context,
        bitrate_bps = VOIP_BITRATE_BPS,
        complexity = VOIP_COMPLEXITY,
        inband_fec = true,
        packet_loss_perc = VOIP_PACKET_LOSS_PERC,
        "opus encoder tuned for VoIP"
    );
}

/// Encode-scope send helper for a freshly encoded Opus voice frame.
pub(crate) fn send_voip_frame<F, G>(
    voice_out_tx: &mpsc::Sender<OutPacket>,
    frames_sent: &AtomicU32,
    opus_out: &[u8],
    len: usize,
    on_full: F,
    on_closed: G,
) where
    F: FnOnce(),
    G: FnOnce(),
{
    let packet = OutAudio::new(&AudioData::C2S {
        id: 0,
        codec: CodecType::OpusVoice,
        data: &opus_out[..len],
    });
    match voice_out_tx.try_send(packet) {
        Ok(()) => {
            frames_sent.fetch_add(1, Ordering::Relaxed);
        }
        Err(mpsc::error::TrySendError::Full(_)) => on_full(),
        Err(mpsc::error::TrySendError::Closed(_)) => on_closed(),
    }
}
