#pragma once

#include <opus.h>
#include <memory>
#include <vector>
#include <functional>
#include <stdexcept>

#include "Concepts/AudioAdapterConcept.h"
#include "Concepts/AudioEncderConcept.h"
#include "AudioAdapters/PortAudioAdapter.h"

namespace snoop {


template<AudioAdapterConcept TAudioAdapter>
class OpusEncoder {
    using TAudioCallback = std::function<void( const char* data, size_t size )>;

    std::shared_ptr<TAudioAdapter> m_audioAdapter;
    ::OpusEncoder* m_encoder;
    std::vector<char> m_encodedBuffer;
    size_t m_maxDataBytes;

public:
    OpusEncoder( int sampleRate, int channels, int framesPerBuffer, TAudioCallback audioCallback )
        : m_maxDataBytes( framesPerBuffer * sizeof( float ) * 2 ), m_encodedBuffer( framesPerBuffer * sizeof( float ) * 2 ) {

        int error;
        m_encoder = opus_encoder_create( sampleRate, channels, OPUS_APPLICATION_AUDIO, &error );
        if( error != OPUS_OK ) {
            throw std::runtime_error( "Failed to create Opus encoder: " + std::string( opus_strerror( error ) ) );
        }

        m_audioAdapter = std::make_shared<TAudioAdapter>( sampleRate, channels, framesPerBuffer,
                                                          [this, audioCallback]( const char* input, size_t size ) {
                                                              this->encode( reinterpret_cast<const float*>(input), static_cast<int>(size / sizeof( float )),
                                                                            audioCallback );
                                                          } );
    }

    ~OpusEncoder() {
        if( m_encoder ) {
            opus_encoder_destroy( m_encoder );
        }
    }

private:
    void encode( const float* input, int frameCount, const TAudioCallback& audioCallback ) {
        int numBytes = opus_encode_float( m_encoder, input, frameCount, reinterpret_cast<unsigned char*>(m_encodedBuffer.data()), m_maxDataBytes );

        if( numBytes < 0 ) {
            spdlog::error( "Opus encoding failed: {}", std::string( opus_strerror( numBytes ) ) );
            throw std::runtime_error( "Opus encoding failed: " + std::string( opus_strerror( numBytes ) ) );
        }

        audioCallback( m_encodedBuffer.data(), numBytes );
    }
};

static_assert( AudioEncoderConcept<OpusEncoder<PortAudioAdapter>>, "OpusEncoder does not satisfy AudioEncoderConcept" );


}