#include "crAudio.h"

// single-TU stb_vorbis: the implementation lives here (not in the build files), so the config defines apply
#define STB_VORBIS_NO_PUSHDATA_API
#define STB_VORBIS_NO_STDIO

#pragma warning( push, 0 )
#if defined( __clang__ )
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-compare"   // stb_vorbis.c:1404 pointer-overflow guard — UB-reliant upstream code, kept as-is
#endif
extern "C" {
#include <stb/stb_vorbis.c>
}
#if defined( __clang__ )
#pragma clang diagnostic pop
#endif
#pragma warning( pop )

#include <stdlib.h>   // free — stb_vorbis decode output is plain malloc

#include "crMath.h"

bool crAudio::Init()
{
    if( SDL_InitSubSystem( SDL_INIT_AUDIO ) == false )
    {
        SDL_LogError( SDL_LOG_CATEGORY_AUDIO, "crAudio: audio subsystem init failed: %s", SDL_GetError() );
        return false;
    }

    SDL_AudioSpec spec = {};
    spec.format   = SDL_AUDIO_S16;
    spec.channels = 2;
    spec.freq     = SAMPLE_RATE;

    _device = SDL_OpenAudioDevice( SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec );
    if( _device == 0 )
    {
        SDL_LogError( SDL_LOG_CATEGORY_AUDIO, "crAudio: open device failed: %s", SDL_GetError() );
        return false;
    }

    for( int32_t i = 0; i < STREAM_COUNT_SFX; ++i )
    {
        _sfxStreams[ i ] = SDL_CreateAudioStream( &spec, &spec );
        SDL_BindAudioStream( _device, _sfxStreams[ i ] );   // bound streams are mixed by SDL
    }

    _bgmStream = SDL_CreateAudioStream( &spec, &spec );
    SDL_BindAudioStream( _device, _bgmStream );
    SDL_SetAudioStreamGain( _bgmStream, ( _masterVolume * _bgmVolume ) );

    SDL_LogTrace( SDL_LOG_CATEGORY_AUDIO, "crAudio: initted( voices:%d )", STREAM_COUNT_SFX );

    return true;
}
void crAudio::Cleanup()
{
    StopBgm();

    if( _bgmStream != nullptr )
    {
        SDL_DestroyAudioStream( _bgmStream );   // unbinds automatically
        _bgmStream = nullptr;
    }

    for( int32_t i = 0; i < STREAM_COUNT_SFX; ++i )
    {
        if( _sfxStreams[ i ] != nullptr )
        {
            SDL_DestroyAudioStream( _sfxStreams[ i ] );
            _sfxStreams[ i ] = nullptr;
        }
    }

    if( _device != 0 )
    {
        SDL_CloseAudioDevice( _device );
        _device = 0;
    }

    const int32_t sfxCount = _sfxClips.Size();
    for( int32_t s = 0; s < sfxCount; ++s )
        free( _sfxClips.At( s ).samples );
    _sfxClips.Clear();

    SDL_free( _sfxScratchBuffer );
    _sfxScratchBuffer = nullptr;
    _sfxScratchFrames = 0;

    SDL_QuitSubSystem( SDL_INIT_AUDIO );
}

void crAudio::Update()
{
    PumpBgm();
}

void crAudio::SetPaused( bool paused )
{
    if( _device == 0 )
        return;

    if( paused )
        SDL_PauseAudioDevice( _device );
    else
        SDL_ResumeAudioDevice( _device );
}

int32_t crAudio::LoadSfx( const char* baseName )
{
    const int32_t n = _sfxClips.Size();
    for( int32_t i = 0; i < n; ++i )
    {
        if( SDL_strcmp( _sfxClips.At( i ).name, baseName ) == 0 )
            return i;
    }

    SfxEntry entry = {};
    if( SDL_strlcpy( entry.name, baseName, sizeof( entry.name ) ) >= sizeof( entry.name ) )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_AUDIO, "crAudio: sfx name too long( %s ) — max %d chars", baseName, static_cast<int32_t>( sizeof( entry.name ) - 1 ) );
        SDL_assert( false );
    }

    char path[ 256 ];
    SDL_snprintf( path, sizeof( path ), "crassets/audio/sfx/%s.ogg", baseName );

    size_t len  = 0;
    void*  data = SDL_LoadFile( path, &len );
    if( data == nullptr )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_AUDIO, "crAudio: sfx load failed( %s ): %s", path, SDL_GetError() );
    }
    else
    {
        int    channels = 0;
        int    rate     = 0;
        short* samples  = nullptr;
        const int32_t frames = stb_vorbis_decode_memory( static_cast<const unsigned char*>( data ), static_cast<int>( len ), &channels, &rate, &samples );
        SDL_free( data );

        if( ( frames <= 0 ) || ( samples == nullptr ) )
        {
            SDL_LogError( CR_LOG_CATEGORY_ASSET_AUDIO, "crAudio: sfx decode failed( %s )", path );
        }
        else if( channels != 2 )   // the pipeline emits stereo sfx; a mismatch would mis-interleave and read out of bounds
        {
            SDL_LogError( CR_LOG_CATEGORY_ASSET_AUDIO, "crAudio: sfx not stereo( %s, ch:%d ) — dropped", path, channels );
            free( samples );
        }
        else
        {
            entry.samples    = samples;
            entry.frameCount = frames;
            entry.sampleRate = rate;
        }
    }

    if( entry.frameCount > _sfxScratchFrames )
    {
        _sfxScratchBuffer = static_cast<int16_t*>( SDL_realloc( _sfxScratchBuffer, static_cast<size_t>( entry.frameCount ) * 2 * sizeof( int16_t ) ) );
        _sfxScratchFrames = entry.frameCount;
    }

    _sfxClips.Add( entry );

    return ( _sfxClips.Size() - 1 );
}

void crAudio::PlaySfx( int32_t sfx, float volume /*= 1.0f*/, float pan /*= 0.0f*/ )
{
    SDL_assert( ( sfx >= 0 ) && ( sfx < _sfxClips.Size() ) );

    if( ( sfx < 0 ) || ( sfx >= _sfxClips.Size() ) )
        return;

    const SfxEntry& clip = _sfxClips.At( sfx );
    if( clip.samples == nullptr )
        return;   // failed to load — stay silent

    SDL_AudioStream* voice = nullptr;
    for( int32_t i = 0; i < STREAM_COUNT_SFX; ++i )
    {
        if( ( SDL_GetAudioStreamQueued( _sfxStreams[ i ] ) == 0 ) &&
            ( SDL_GetAudioStreamAvailable( _sfxStreams[ i ] ) == 0 ) )
        {
            voice = _sfxStreams[ i ];
            break;
        }
    }
    if( voice == nullptr )
    {
        SDL_LogTrace( SDL_LOG_CATEGORY_AUDIO, "crAudio: all sfxStreams are busy - drop this play( %s )", clip.name );
        return;
    }

    SDL_AudioSpec src = {};
    src.format   = SDL_AUDIO_S16;
    src.channels = 2;
    src.freq     = clip.sampleRate;
    SDL_SetAudioStreamFormat( voice, &src, nullptr );
    SDL_SetAudioStreamGain( voice, ( _masterVolume * _sfxVolume * crMath::Clamp01( volume ) ) );

    // balance baked at play start: center(0) passes through unchanged; +pan attenuates L, -pan attenuates R
    const float panClamped = crMath::Clamp( pan, -1.0f, 1.0f );
    const float gainL      = ( panClamped > 0.0f ) ? ( 1.0f - panClamped ) : 1.0f;
    const float gainR      = ( panClamped < 0.0f ) ? ( 1.0f + panClamped ) : 1.0f;

    const int32_t frames = clip.frameCount;
    for( int32_t f = 0; f < frames; ++f )
    {
        _sfxScratchBuffer[ ( f * 2 ) + 0 ] = static_cast<int16_t>( static_cast<float>( clip.samples[ ( f * 2 ) + 0 ] ) * gainL );
        _sfxScratchBuffer[ ( f * 2 ) + 1 ] = static_cast<int16_t>( static_cast<float>( clip.samples[ ( f * 2 ) + 1 ] ) * gainR );
    }

    SDL_PutAudioStreamData( voice, _sfxScratchBuffer, frames * 2 * static_cast<int32_t>( sizeof( int16_t ) ) );
    SDL_FlushAudioStream( voice );   // marks clip end so the converter tail drains fully
}

void crAudio::PlayBgm( const char* baseName, bool loop )
{
    if( baseName == nullptr ||
        SDL_strlen( baseName ) <= 0 )
    {
        return;
    }
    if( _bgmStream == nullptr )
    {
        return;
    }

    StopBgm();

    char path[ 256 ];
    SDL_snprintf( path, sizeof( path ), "crassets/audio/bgm/%s.ogg", baseName );

    size_t len  = 0;
    void*  data = SDL_LoadFile( path, &len );
    if( data == nullptr )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_AUDIO, "crAudio: bgm load failed( %s ): %s", path, SDL_GetError() );
        return;
    }

    int err = 0;
    _bgmVorbis = stb_vorbis_open_memory( static_cast<const unsigned char*>( data ), static_cast<int>( len ), &err, nullptr );
    if( _bgmVorbis == nullptr )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_AUDIO, "crAudio: vorbis open failed( %s, err:%d )", path, err );
        SDL_free( data );
        return;
    }

    const stb_vorbis_info info = stb_vorbis_get_info( _bgmVorbis );
    if( ( info.channels < 1 ) || ( info.channels > 2 ) )
    {
        SDL_LogError( CR_LOG_CATEGORY_ASSET_AUDIO, "crAudio: bgm channels unsupported( %s, ch:%d )", path, info.channels );
        stb_vorbis_close( _bgmVorbis );
        _bgmVorbis = nullptr;
        SDL_free( data );
        return;
    }

    _bgmOgg      = data;
    _bgmChannels = info.channels;
    _bgmLoop     = loop;

    SDL_AudioSpec src = {};
    src.format   = SDL_AUDIO_S16;
    src.channels = info.channels;
    src.freq     = static_cast<int>( info.sample_rate );
    SDL_SetAudioStreamFormat( _bgmStream, &src, nullptr );

    PumpBgm();
}
void crAudio::StopBgm()
{
    if( _bgmStream != nullptr )
        SDL_ClearAudioStream( _bgmStream );

    CloseBgmDecoder();
}

void crAudio::SetMasterVolume( float volume )
{
    _masterVolume = crMath::Clamp01( volume );
    SDL_SetAudioStreamGain( _bgmStream, ( _masterVolume * _bgmVolume ) );   // sfx voices pick the new master up on their next play
}
void crAudio::SetBgmVolume( float volume )
{
    _bgmVolume = crMath::Clamp01( volume );
    SDL_SetAudioStreamGain( _bgmStream, ( _masterVolume * _bgmVolume ) );
}
void crAudio::SetSfxVolume( float volume )
{
    _sfxVolume = crMath::Clamp01( volume );
}

void crAudio::PumpBgm()
{
    if( ( _bgmVorbis == nullptr ) || ( _bgmStream == nullptr ) )
        return;

    bool restarted = false;
    while( SDL_GetAudioStreamQueued( _bgmStream ) < BGM_TARGET_QUEUED_BYTES )
    {
        const int32_t frames = stb_vorbis_get_samples_short_interleaved( _bgmVorbis, _bgmChannels, _bgmDecodeChunk, BGM_CHUNK_FRAMES * _bgmChannels );
        if( frames > 0 )
        {
            SDL_PutAudioStreamData( _bgmStream, _bgmDecodeChunk, frames * _bgmChannels * static_cast<int32_t>( sizeof( int16_t ) ) );
            restarted = false;
            continue;
        }

        if( _bgmLoop && ( restarted == false ) )   // end of file — rewind once; a second empty read means a broken file
        {
            stb_vorbis_seek_start( _bgmVorbis );
            restarted = true;
            continue;
        }

        CloseBgmDecoder();   // non-loop end (or broken file): stop decoding, let the queued tail drain
        break;
    }
}
void crAudio::CloseBgmDecoder()
{
    if( _bgmVorbis != nullptr )
    {
        stb_vorbis_close( _bgmVorbis );
        _bgmVorbis = nullptr;
    }

    SDL_free( _bgmOgg );
    _bgmOgg = nullptr;
}
