#include "codec/opus_codec.h"

namespace voicecat::codec {

#ifdef VOICECAT_HAS_OPUS

// Map an intended channel/capture sample rate (Hz) to the Opus max-bandwidth constant. The
// codec always runs at 48 kHz internally (docs/voice.md §3); this caps the bandwidth the
// encoder will select so a channel can request narrowband/wideband audio for low-bitrate rooms.
// 0 (unset) and >= 48000 map to FULLBAND, which is the encoder default (i.e. a no-op cap).
static opus_int32 opus_max_bandwidth_for(uint32_t rate_hz) {
    if (rate_hz == 0)     return OPUS_BANDWIDTH_FULLBAND;
    if (rate_hz <= 8000)  return OPUS_BANDWIDTH_NARROWBAND;    // ~4 kHz audio
    if (rate_hz <= 12000) return OPUS_BANDWIDTH_MEDIUMBAND;    // ~6 kHz
    if (rate_hz <= 16000) return OPUS_BANDWIDTH_WIDEBAND;      // ~8 kHz
    if (rate_hz <= 24000) return OPUS_BANDWIDTH_SUPERWIDEBAND; // ~12 kHz
    return OPUS_BANDWIDTH_FULLBAND;                            // ~20 kHz
}

// ── OpusEncoder ──────────────────────────────────────────────────────────────

bool OpusEncoder::init(const OpusParams& p) {
    destroy();
    channels_      = p.stereo ? 2 : 1;
    frame_samples_ = opus_frame_samples(p);

    int opus_application = OPUS_APPLICATION_VOIP;
    switch (p.application) {
        case OpusApplication::Audio:    opus_application = OPUS_APPLICATION_AUDIO; break;
        case OpusApplication::LowDelay: opus_application = OPUS_APPLICATION_RESTRICTED_LOWDELAY; break;
        case OpusApplication::Voip:     default: opus_application = OPUS_APPLICATION_VOIP; break;
    }

    int err = 0;
    enc_ = opus_encoder_create(static_cast<opus_int32>(p.sample_rate), channels_,
                               opus_application, &err);
    if (err != OPUS_OK || !enc_) {
        err_ = opus_strerror(err);
        return false;
    }

    opus_encoder_ctl(enc_, OPUS_SET_BITRATE(static_cast<opus_int32>(p.bitrate_bps)));
    opus_encoder_ctl(enc_, OPUS_SET_MAX_BANDWIDTH(opus_max_bandwidth_for(p.max_bandwidth_hz)));
    opus_encoder_ctl(enc_, OPUS_SET_COMPLEXITY(static_cast<opus_int32>(p.complexity)));
    opus_encoder_ctl(enc_, OPUS_SET_INBAND_FEC(p.fec ? 1 : 0));
    opus_encoder_ctl(enc_, OPUS_SET_DTX(p.dtx ? 1 : 0));
    opus_encoder_ctl(enc_, OPUS_SET_PACKET_LOSS_PERC(
        static_cast<opus_int32>(p.expected_packet_loss)));
    // DRED: embed enough redundancy to cover one full previous frame at any frame size.
    // OPUS_SET_DRED_DURATION takes units of 10ms; ceil(frame_ms/10) ensures one complete frame
    // of ML-reconstructed redundancy regardless of whether the channel runs at 10/20/40/60 ms.
    // At 10ms frames this yields 1 unit (one frame back); a burst-loss floor of 2 ensures
    // two consecutive 10ms frames can be recovered. Cost: ~800 bps per 10ms unit.
    uint32_t dred_units = std::max(2u, (p.frame_ms + 9u) / 10u);
    opus_encoder_ctl(enc_, OPUS_SET_DRED_DURATION(p.dred ? static_cast<opus_int32>(dred_units) : 0));
    return true;
}

int OpusEncoder::encode(const int16_t* pcm, int frame_samples, uint8_t* out_buf, int out_cap) {
    if (!enc_) return -1;
    int n = opus_encode(enc_, pcm, frame_samples, out_buf, out_cap);
    if (n < 0) { err_ = opus_strerror(n); return -1; }
    return n;
}

void OpusEncoder::destroy() {
    if (enc_) { opus_encoder_destroy(enc_); enc_ = nullptr; }
}

// ── OpusDecoder ──────────────────────────────────────────────────────────────

bool OpusDecoder::init(const OpusParams& p) {
    destroy();
    channels_      = p.stereo ? 2 : 1;
    frame_samples_ = opus_frame_samples(p);

    int err = 0;
    dec_ = opus_decoder_create(static_cast<opus_int32>(p.sample_rate), channels_, &err);
    if (err != OPUS_OK || !dec_) {
        err_ = opus_strerror(err);
        return false;
    }
    return true;
}

int OpusDecoder::decode(const uint8_t* opus_data, int len, int16_t* out_pcm, int max_samples,
                        bool fec) {
    if (!dec_) return -1;
    int n = opus_decode(dec_, opus_data, len, out_pcm, max_samples, fec ? 1 : 0);
    if (n < 0) { err_ = opus_strerror(n); return -1; }
    return n;
}

int OpusDecoder::decode_dred(::OpusDRED* dred, int32_t dred_offset,
                             int16_t* out_pcm, int max_samples) {
    if (!dec_ || !dred) return -1;
    int n = opus_decoder_dred_decode(dec_, dred, dred_offset, out_pcm, max_samples);
    if (n < 0) { err_ = opus_strerror(n); return -1; }
    return n;
}

void OpusDecoder::destroy() {
    if (dec_) { opus_decoder_destroy(dec_); dec_ = nullptr; }
}

#else  // !VOICECAT_HAS_OPUS — stubs

bool OpusEncoder::init(const OpusParams&)            { err_ = "OPUS not compiled in"; return false; }
int  OpusEncoder::encode(const int16_t*, int, uint8_t*, int) { return -1; }
void OpusEncoder::destroy()                          {}

bool OpusDecoder::init(const OpusParams&)            { err_ = "OPUS not compiled in"; return false; }
int  OpusDecoder::decode(const uint8_t*, int, int16_t*, int, bool) { return -1; }
void OpusDecoder::destroy()                          {}

#endif  // VOICECAT_HAS_OPUS

}  // namespace voicecat::codec
