#pragma once

#include <portaudio.h>
#include <spdlog/spdlog.h>
#include <exception>
#include <stdexcept>
#include <functional>

#include "Concepts/AudioAdapterConcept.h"


namespace snoop {

class PortAudioAdapter {
    using TAudioCallback = std::function<void( const char* input, size_t size )>;

    PaStream* m_stream = nullptr;
    TAudioCallback m_audioCallback;

public:
    PortAudioAdapter( int sampleRate, int channels, int framesPerBuffer, TAudioCallback audioCallback, int deviceIndex = paNoDevice )
        : m_audioCallback( std::move( audioCallback ) ) {

        auto error = Pa_Initialize();
        if( error != paNoError ) {
            const char* errorText = Pa_GetErrorText( error );
            spdlog::error( "PortAudio initialization error: {}", errorText );
            throw std::runtime_error( std::string( "PortAudio initialization error: " ) + errorText );
        }

        spdlog::info( "PortAudio initialized successfully." );

        // Output information about available host APIs
        int numApis = Pa_GetHostApiCount();
        spdlog::info( "Number of Host APIs: {}", numApis );
        for( int i = 0; i < numApis; ++i ) {
            const PaHostApiInfo* apiInfo = Pa_GetHostApiInfo( i );
            spdlog::info( "Host API [{}]: {}", i, apiInfo->name );
        }

        // Output information about available devices
        int numDevices = Pa_GetDeviceCount();
        PaSampleFormat sampleFormats[] = {
            paFloat32, paInt32, paInt24, paInt16, paInt8, paUInt8
        };
        const char* formatNames[] = {
            "paFloat32", "paInt32", "paInt24", "paInt16", "paInt8", "paUInt8"
        };
        spdlog::info( "Number of Devices: {}", numDevices );
        for( int i = 0; i < numDevices; ++i ) {
            const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo( i );
            spdlog::info( "Device [{}]: {}", i, deviceInfo->name );
            spdlog::info( "  Max Input Channels: {}", deviceInfo->maxInputChannels );
            spdlog::info( "  Max Output Channels: {}", deviceInfo->maxOutputChannels );
            spdlog::info( "  Default Sample Rate: {}", deviceInfo->defaultSampleRate );
            for (int j = 0; j < 6; ++j) {
                PaStreamParameters inputParameters;
                inputParameters.device = i;
                inputParameters.channelCount = deviceInfo->maxInputChannels;
                inputParameters.sampleFormat = sampleFormats[j];
                inputParameters.suggestedLatency = deviceInfo->defaultLowInputLatency;
                inputParameters.hostApiSpecificStreamInfo = nullptr;

                PaError err = Pa_IsFormatSupported(&inputParameters, nullptr, deviceInfo->defaultSampleRate);
                if (err == paFormatIsSupported) {
                    spdlog::info("  Supported Sample Format: {}", formatNames[j]);
                } else {
                    spdlog::info("  Unsupported Sample Format: {}", formatNames[j]);
                }
            }
        }

        // Select the device (default or user-provided)
        const PaDeviceInfo* selectedDeviceInfo;
        if( deviceIndex == paNoDevice ) {
            // Use default device
            selectedDeviceInfo = Pa_GetDeviceInfo( Pa_GetDefaultInputDevice() );
            spdlog::info( "Using default input device: {}", selectedDeviceInfo->name );
        } else if( deviceIndex >= 0 && deviceIndex < numDevices ) {
            // Use specified device
            selectedDeviceInfo = Pa_GetDeviceInfo( deviceIndex );
            spdlog::info( "Using specified input device [{}]: {}", deviceIndex, selectedDeviceInfo->name );
        } else {
            spdlog::error( "Invalid device index: {}", deviceIndex );
            throw std::runtime_error( "Invalid device index" );
        }

        // Configure input parameters
        PaStreamParameters inputParameters;
        inputParameters.device = ( deviceIndex == paNoDevice ) ? Pa_GetDefaultInputDevice() : deviceIndex;
        inputParameters.channelCount = channels;
        inputParameters.sampleFormat = paFloat32;
        inputParameters.suggestedLatency = selectedDeviceInfo->defaultLowInputLatency;
        inputParameters.hostApiSpecificStreamInfo = nullptr;

        // Define the callback function
        auto portAudioCallback = []( const void* input, void* output,
                                     unsigned long frameCount,
                                     const PaStreamCallbackTimeInfo* timeInfo,
                                     PaStreamCallbackFlags statusFlags,
                                     void* userData ) -> int {
            auto* audioCallback = static_cast<TAudioCallback*>(userData);
            ( *audioCallback )( static_cast<const char*>(input), frameCount * sizeof( float ) );
            return paContinue;
        };

        // Open the stream with the chosen device and parameters
        error = Pa_OpenStream( &this->m_stream,
                               &inputParameters,
                               nullptr, // No output parameters
                               sampleRate,
                               framesPerBuffer,
                               paClipOff, // No output clipping
                               portAudioCallback,
                               &this->m_audioCallback );
        if( error != paNoError ) {
            const char* errorText = Pa_GetErrorText( error );
            spdlog::error( "PortAudio error while opening stream: {}", errorText );
            throw std::runtime_error( std::string( "PortAudio error while opening stream: " ) + errorText );
        }

        spdlog::info( "Stream opened successfully." );

        error = Pa_StartStream( this->m_stream );
        if( error != paNoError ) {
            const char* errorText = Pa_GetErrorText( error );
            spdlog::error( "PortAudio error while starting stream: {}", errorText );
            throw std::runtime_error( std::string( "PortAudio error while starting stream: " ) + errorText );
        }

        spdlog::info( "Stream started successfully." );
    }


    ~PortAudioAdapter() {
        Pa_StopStream( m_stream );
        Pa_CloseStream( m_stream );
        Pa_Terminate();

        spdlog::info( "Stream stopped and PortAudio terminated." );
    }
};

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

}