// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.

#if AUDIO_API_OPENAL

#include "AudioBackendOAL.h"
#include "Engine/Platform/StringUtils.h"
#include "Engine/Core/Log.h"
#include "Engine/Tools/AudioTool/AudioTool.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Audio/Audio.h"
#include "Engine/Audio/AudioListener.h"
#include "Engine/Audio/AudioSource.h"
#include "Engine/Audio/AudioSettings.h"

// Include OpenAL library
// Source: https://github.com/kcat/openal-soft
//#define AL_LIBTYPE_STATIC
#include <OpenAL/al.h>
#include <OpenAL/alc.h>

#define FLAX_POS_TO_OAL(vec) -vec.X * 0.01f,  vec.Y *  0.01f, -vec.Z * 0.01f

namespace ALC
{
    ALCdevice* Device = nullptr;
    bool AL_EXT_float32 = false;
    Array<ALCcontext*, FixedAllocation<AUDIO_MAX_LISTENERS>> Contexts;

    bool IsExtensionSupported(const char* extension)
    {
        if (Device == nullptr)
            return false;

        const int32 length = StringUtils::Length(extension);
        if ((length > 2) && (StringUtils::Compare(extension, "ALC", 3) == 0))
            return alcIsExtensionPresent(Device, extension) != AL_FALSE;
        return alIsExtensionPresent(extension) != AL_FALSE;
    }

    ALCcontext* GetContext(const class AudioListener* listener)
    {
        const auto& listeners = Audio::Listeners;
        if (listeners.HasItems())
        {
            ASSERT(listeners.Count() == Contexts.Count());

            const int32 numContexts = Contexts.Count();
            for (int32 i = 0; i < numContexts; i++)
            {
                if (listeners[i] == listener)
                    return Contexts[i];
            }
        }

        ASSERT(Contexts.HasItems());
        return Contexts[0];
    }

    FORCE_INLINE const Array<ALCcontext*, FixedAllocation<AUDIO_MAX_LISTENERS>>& GetContexts()
    {
        return Contexts;
    }

    void ClearContexts()
    {
        alcMakeContextCurrent(nullptr);

        for (auto& context : Contexts)
            alcDestroyContext(context);
        Contexts.Clear();
    }

    namespace Listener
    {
        void Rebuild(AudioListener* listener)
        {
            AudioBackend::Listener::TransformChanged(listener);

            const auto& velocity = listener->GetVelocity();
            alListener3f(AL_VELOCITY, FLAX_POS_TO_OAL(velocity));
            alListenerf(AL_GAIN, Audio::GetVolume());
        }
    }

    namespace Source
    {
        void Rebuild(AudioSource* source)
        {
            ASSERT(source->SourceIDs.IsEmpty());

            auto& contexts = GetContexts();
            const int32 numContexts = contexts.Count();
            const bool is3D = source->Is3D();
            const bool loop = source->GetIsLooping() && !source->UseStreaming();
            const Vector3 position = is3D ? source->GetPosition() : Vector3::Zero;
            const Vector3 velocity = is3D ? source->GetVelocity() : Vector3::Zero;

            for (int32 i = 0; i < numContexts; i++)
            {
                if (numContexts > 1)
                    alcMakeContextCurrent(contexts[i]);

                uint32 sourceID = 0;
                alGenSources(1, &sourceID);

                source->SourceIDs.Add(sourceID);
            }

            for (int32 i = 0; i < numContexts; i++)
            {
                if (numContexts > 1)
                    alcMakeContextCurrent(contexts[i]);

                const uint32 sourceID = source->SourceIDs[i];

                alSourcef(sourceID, AL_GAIN, source->GetVolume());
                alSourcef(sourceID, AL_PITCH, source->GetPitch());
                alSourcef(sourceID, AL_REFERENCE_DISTANCE, source->GetMinDistance());
                alSourcef(sourceID, AL_ROLLOFF_FACTOR, source->GetAttenuation());
                alSourcei(sourceID, AL_LOOPING, loop);
                alSourcei(sourceID, AL_SOURCE_RELATIVE, !is3D);
                alSource3f(sourceID, AL_POSITION, FLAX_POS_TO_OAL(position));
                alSource3f(sourceID, AL_VELOCITY, FLAX_POS_TO_OAL(velocity));
            }

            // Restore state after Cleanup
            source->Restore();
        }
    }

    void RebuildContexts(bool isChangingDevice)
    {
        LOG(Info, "Rebuild audio contexts");

        if (!isChangingDevice)
        {
            for (auto& source : Audio::Sources)
                source->Cleanup();
        }

        ClearContexts();

        if (Device == nullptr)
            return;

        const int32 numListeners = Audio::Listeners.Count();
        const int32 numContexts = numListeners > 1 ? numListeners : 1;

        for (int32 i = 0; i < numContexts; i++)
        {
            ALCcontext* context = alcCreateContext(Device, nullptr);
            Contexts.Add(context);
        }

        // If only one context is available keep it active as an optimization.
        // Audio listeners and sources will avoid excessive context switching in such case.
        alcMakeContextCurrent(Contexts[0]);

        for (auto& listener : Audio::Listeners)
            Listener::Rebuild(listener);

        for (auto& source : Audio::Sources)
            Source::Rebuild(source);
    }
}

ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth)
{
    // TODO: cache enum values in Init()??

    switch (bitDepth)
    {
    case 8:
    {
        switch (numChannels)
        {
        case 1:
            return AL_FORMAT_MONO8;
        case 2:
            return AL_FORMAT_STEREO8;
        case 4:
            return alGetEnumValue("AL_FORMAT_QUAD8");
        case 6:
            return alGetEnumValue("AL_FORMAT_51CHN8");
        case 7:
            return alGetEnumValue("AL_FORMAT_61CHN8");
        case 8:
            return alGetEnumValue("AL_FORMAT_71CHN8");
        default:
        CRASH;
            return 0;
        }
    }
    case 16:
    {
        switch (numChannels)
        {
        case 1:
            return AL_FORMAT_MONO16;
        case 2:
            return AL_FORMAT_STEREO16;
        case 4:
            return alGetEnumValue("AL_FORMAT_QUAD16");
        case 6:
            return alGetEnumValue("AL_FORMAT_51CHN16");
        case 7:
            return alGetEnumValue("AL_FORMAT_61CHN16");
        case 8:
            return alGetEnumValue("AL_FORMAT_71CHN16");
        default:
        CRASH;
            return 0;
        }
    }
    case 32:
    {
        switch (numChannels)
        {
        case 1:
            return alGetEnumValue("AL_FORMAT_MONO_FLOAT32");
        case 2:
            return alGetEnumValue("AL_FORMAT_STEREO_FLOAT32");
        case 4:
            return alGetEnumValue("AL_FORMAT_QUAD32");
        case 6:
            return alGetEnumValue("AL_FORMAT_51CHN32");
        case 7:
            return alGetEnumValue("AL_FORMAT_61CHN32");
        case 8:
            return alGetEnumValue("AL_FORMAT_71CHN32");
        default:
        CRASH;
            return 0;
        }
    }
    default:
    CRASH;
        return 0;
    }
}

void AudioBackendOAL::Listener_OnAdd(AudioListener* listener)
{
    ALC::RebuildContexts(false);
}

void AudioBackendOAL::Listener_OnRemove(AudioListener* listener)
{
    ALC::RebuildContexts(false);
}

void AudioBackendOAL::Listener_VelocityChanged(AudioListener* listener)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();

    if (numContexts > 1)
    {
        const auto context = ALC::GetContext(listener);
        alcMakeContextCurrent(context);
    }

    const auto& velocity = listener->GetVelocity();
    alListener3f(AL_VELOCITY, FLAX_POS_TO_OAL(velocity));
}

void AudioBackendOAL::Listener_TransformChanged(AudioListener* listener)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    const Vector3& position = listener->GetPosition();
    const Quaternion& orientation = listener->GetOrientation();
    Vector3 alOrientation[2] =
    {
        // Forward
        orientation * Vector3::Forward,
        // Up
        orientation * Vector3::Up
    };

    // If only one context is available it is guaranteed it is always active, so we can avoid setting it
    if (numContexts > 1)
    {
        const auto context = ALC::GetContext(listener);
        alcMakeContextCurrent(context);
    }

    alListenerfv(AL_ORIENTATION, (float*)alOrientation);
    alListener3f(AL_POSITION, FLAX_POS_TO_OAL(position));
}

void AudioBackendOAL::Source_OnAdd(AudioSource* source)
{
    ALC::Source::Rebuild(source);
}

void AudioBackendOAL::Source_OnRemove(AudioSource* source)
{
    source->Cleanup();
}

void AudioBackendOAL::Source_VelocityChanged(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    const bool is3D = source->Is3D();
    const Vector3 velocity = is3D ? source->GetVelocity() : Vector3::Zero;

    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSource3f(sourceID, AL_VELOCITY, FLAX_POS_TO_OAL(velocity));
    }
}

void AudioBackendOAL::Source_TransformChanged(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    const bool is3D = source->Is3D();
    const Vector3 position = is3D ? source->GetPosition() : Vector3::Zero;

    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSource3f(sourceID, AL_POSITION, FLAX_POS_TO_OAL(position));
    }
}

void AudioBackendOAL::Source_VolumeChanged(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcef(sourceID, AL_GAIN, source->GetVolume());
    }
}

void AudioBackendOAL::Source_PitchChanged(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcef(sourceID, AL_PITCH, source->GetPitch());
    }
}

void AudioBackendOAL::Source_IsLoopingChanged(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcei(sourceID, AL_LOOPING, source->GetIsLooping());
    }
}

void AudioBackendOAL::Source_MinDistanceChanged(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcef(sourceID, AL_REFERENCE_DISTANCE, source->GetMinDistance());
    }
}

void AudioBackendOAL::Source_AttenuationChanged(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcef(sourceID, AL_ROLLOFF_FACTOR, source->GetAttenuation());
    }
}

void AudioBackendOAL::Source_ClipLoaded(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    const auto clip = source->Clip.Get();
    const bool is3D = source->Is3D();
    const bool isStreamable = clip->IsStreamable();
    const bool loop = source->GetIsLooping() && !isStreamable;

    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcei(sourceID, AL_SOURCE_RELATIVE, !is3D);
        alSourcei(sourceID, AL_LOOPING, loop);
    }
}

void AudioBackendOAL::Source_Cleanup(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcei(sourceID, AL_BUFFER, 0);
        alDeleteSources(1, &sourceID);
    }
}

void AudioBackendOAL::Source_Play(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        // Play
        alSourcePlay(sourceID);
    }
}

void AudioBackendOAL::Source_Pause(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        // Pause
        alSourcePause(sourceID);

        // Unset streaming buffers
        int32 numQueuedBuffers;
        alGetSourcei(sourceID, AL_BUFFERS_QUEUED, &numQueuedBuffers);
        uint32 buffer;
        for (int32 j = 0; j < numQueuedBuffers; j++)
            alSourceUnqueueBuffers(sourceID, 1, &buffer);
    }
}

void AudioBackendOAL::Source_Stop(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        // Stop and rewind
        alSourceStop(sourceID);
        alSourcef(sourceID, AL_SEC_OFFSET, 0.0f);

        // Unset streaming buffers
        int32 numQueuedBuffers;
        alGetSourcei(sourceID, AL_BUFFERS_QUEUED, &numQueuedBuffers);
        uint32 buffer;
        for (int32 j = 0; j < numQueuedBuffers; j++)
            alSourceUnqueueBuffers(sourceID, 1, &buffer);
    }
}

void AudioBackendOAL::Source_SetCurrentBufferTime(AudioSource* source, float value)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcef(sourceID, AL_SEC_OFFSET, value);
    }
}

float AudioBackendOAL::Source_GetCurrentBufferTime(const AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();

    if (numContexts > 1)
        alcMakeContextCurrent(contexts[0]);

#if 0
    float time;
    alGetSourcef(source->SourceIDs[0], AL_SEC_OFFSET, &time);
#else
    ASSERT(source->Clip && source->Clip->IsLoaded());
    const auto& clipInfo = source->Clip->AudioHeader.Info;
    ALint samplesPlayed;
    alGetSourcei(source->SourceIDs[0], AL_SAMPLE_OFFSET, &samplesPlayed);
    const uint32 totalSamples = clipInfo.NumSamples / clipInfo.NumChannels;
    const float time = (samplesPlayed % totalSamples) / static_cast<float>(Math::Max(1U, clipInfo.SampleRate));
#endif

    return time;
}

void AudioBackendOAL::Source_SetNonStreamingBuffer(AudioSource* source)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    const uint32 bufferId = source->Clip->Buffers[0];

    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        alSourcei(sourceID, AL_BUFFER, bufferId);
    }
}

void AudioBackendOAL::Source_GetProcessedBuffersCount(AudioSource* source, int32& processedBuffersCount)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    if (numContexts > 1)
        alcMakeContextCurrent(contexts[0]);

    // Check the first context only
    const uint32 sourceID = source->SourceIDs[0];
    alGetSourcei(sourceID, AL_BUFFERS_PROCESSED, &processedBuffersCount);
}

void AudioBackendOAL::Source_GetQueuedBuffersCount(AudioSource* source, int32& queuedBuffersCount)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    if (numContexts > 1)
        alcMakeContextCurrent(contexts[0]);

    // Check the first context only
    const uint32 sourceID = source->SourceIDs[0];
    alGetSourcei(sourceID, AL_BUFFERS_QUEUED, &queuedBuffersCount);
}

void AudioBackendOAL::Source_QueueBuffer(AudioSource* source, uint32 bufferId)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        // Queue new buffer
        alSourceQueueBuffers(sourceID, 1, &bufferId);
    }
}

void AudioBackendOAL::Source_DequeueProcessedBuffers(AudioSource* source)
{
    ALuint buffers[AUDIO_MAX_SOURCE_BUFFERS];

    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        const uint32 sourceID = source->SourceIDs[i];

        int32 numProcessedBuffers;
        alGetSourcei(sourceID, AL_BUFFERS_PROCESSED, &numProcessedBuffers);
        alSourceUnqueueBuffers(sourceID, numProcessedBuffers, buffers);
    }
}

void AudioBackendOAL::Buffer_Create(uint32& bufferId)
{
    alGenBuffers(1, &bufferId);
}

void AudioBackendOAL::Buffer_Delete(uint32& bufferId)
{
    alDeleteBuffers(1, &bufferId);
}

void AudioBackendOAL::Buffer_Write(uint32 bufferId, byte* samples, const AudioDataInfo& info)
{
    PROFILE_CPU();

    // TODO: maybe use temporary buffers per thread to reduce dynamic allocations when uploading data to OpenAL?

    // Mono or stereo
    if (info.NumChannels <= 2)
    {
        if (info.BitDepth > 16)
        {
            if (ALC::AL_EXT_float32)
            {
                const uint32 bufferSize = info.NumSamples * sizeof(float);
                float* sampleBufferFloat = (float*)Allocator::Allocate(bufferSize);

                AudioTool::ConvertToFloat(samples, info.BitDepth, sampleBufferFloat, info.NumSamples);

                const ALenum format = GetOpenALBufferFormat(info.NumChannels, info.BitDepth);
                alBufferData(bufferId, format, sampleBufferFloat, bufferSize, info.SampleRate);

                Allocator::Free(sampleBufferFloat);
            }
            else
            {
                LOG(Warning, "OpenAL doesn't support bit depth larger than 16. Your audio data will be truncated.");

                const uint32 bufferSize = info.NumSamples * 2;
                byte* sampleBuffer16 = (byte*)Allocator::Allocate(bufferSize);

                AudioTool::ConvertBitDepth(samples, info.BitDepth, sampleBuffer16, 16, info.NumSamples);

                const ALenum format = GetOpenALBufferFormat(info.NumChannels, 16);
                alBufferData(bufferId, format, sampleBuffer16, bufferSize, info.SampleRate);

                Allocator::Free(sampleBuffer16);
            }
        }
        else if (info.BitDepth == 8)
        {
            // OpenAL expects unsigned 8-bit data, but engine stores it as signed, so convert
            const uint32 bufferSize = info.NumSamples * (info.BitDepth / 8);
            byte* sampleBuffer = (byte*)Allocator::Allocate(bufferSize);

            for (uint32 i = 0; i < info.NumSamples; i++)
                sampleBuffer[i] = ((int8*)samples)[i] + 128;

            const ALenum format = GetOpenALBufferFormat(info.NumChannels, 16);
            alBufferData(bufferId, format, sampleBuffer, bufferSize, info.SampleRate);

            Allocator::Free(sampleBuffer);
        }
        else
        {
            const ALenum format = GetOpenALBufferFormat(info.NumChannels, info.BitDepth);
            alBufferData(bufferId, format, samples, info.NumSamples * (info.BitDepth / 8), info.SampleRate);
        }
    }
        // Multichannel
    else
    {
        // Note: Assuming AL_EXT_MCFORMATS is supported. If it's not, channels should be reduced to mono or stereo.

        // 24-bit not supported, convert to 32-bit
        if (info.BitDepth == 24)
        {
            const uint32 bufferSize = info.NumChannels * sizeof(int32);
            byte* sampleBuffer32 = (byte*)Allocator::Allocate(bufferSize);

            AudioTool::ConvertBitDepth(samples, info.BitDepth, sampleBuffer32, 32, info.NumSamples);

            const ALenum format = GetOpenALBufferFormat(info.NumChannels, 32);
            alBufferData(bufferId, format, sampleBuffer32, bufferSize, info.SampleRate);

            Allocator::Free(sampleBuffer32);
        }
        else if (info.BitDepth == 8)
        {
            // OpenAL expects unsigned 8-bit data, but engine stores it as signed, so convert
            const uint32 bufferSize = info.NumSamples * (info.BitDepth / 8);
            byte* sampleBuffer = (byte*)Allocator::Allocate(bufferSize);

            for (uint32 i = 0; i < info.NumSamples; i++)
                sampleBuffer[i] = ((int8*)samples)[i] + 128;

            const ALenum format = GetOpenALBufferFormat(info.NumChannels, 16);
            alBufferData(bufferId, format, sampleBuffer, bufferSize, info.SampleRate);

            Allocator::Free(sampleBuffer);
        }
        else
        {
            const ALenum format = GetOpenALBufferFormat(info.NumChannels, info.BitDepth);
            alBufferData(bufferId, format, samples, info.NumSamples * (info.BitDepth / 8), info.SampleRate);
        }
    }
}

const Char* AudioBackendOAL::Base_Name()
{
    return TEXT("OpenAL");
}

void AudioBackendOAL::Base_OnActiveDeviceChanged()
{
    // Cleanup
    for (auto& source : Audio::Sources)
        source->Cleanup();
    ALC::ClearContexts();
    if (ALC::Device != nullptr)
    {
        alcCloseDevice(ALC::Device);
        ALC::Device = nullptr;
    }

    // Open device
    const auto& name = Audio::GetActiveDevice()->InternalName;
    ALC::Device = alcOpenDevice(name.Get());
    if (ALC::Device == nullptr)
    {
        LOG(Fatal, "Failed to open OpenAL device ({0}).", String(name));
        return;
    }

    // Setup
    ALC::AL_EXT_float32 = ALC::IsExtensionSupported("AL_EXT_float32");
    ALC::RebuildContexts(true);
}

void AudioBackendOAL::Base_SetDopplerFactor(float value)
{
    alDopplerFactor(value);
}

void AudioBackendOAL::Base_SetVolume(float value)
{
    auto& contexts = ALC::GetContexts();
    const int32 numContexts = contexts.Count();
    for (int32 i = 0; i < numContexts; i++)
    {
        if (numContexts > 1)
            alcMakeContextCurrent(contexts[i]);

        alListenerf(AL_GAIN, value);
    }
}

bool AudioBackendOAL::Base_Init()
{
    auto& devices = Audio::Devices;

    // Initialization (use the preferred device)
    int32 activeDeviceIndex;
    ALC::Device = alcOpenDevice(nullptr);
    if (ALC::Device == nullptr)
    {
        activeDeviceIndex = -1;
        const auto err = alGetError();
        LOG(Warning, "Failed to open default OpenAL device. Error: 0x{0:X}", err);
    }
    else
    {
        activeDeviceIndex = 0;
    }

    // Get audio devices
#if ALC_ENUMERATE_ALL_EXT
    const ALCchar* defaultDevice = alcGetString(nullptr, ALC_DEFAULT_ALL_DEVICES_SPECIFIER);
    if (ALC::IsExtensionSupported("ALC_ENUMERATE_ALL_EXT") && defaultDevice != nullptr)
    {
        const ALCchar* devicesStr = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);

        const StringAnsi defaultDeviceName(defaultDevice);

        devices.Clear();
        devices.EnsureCapacity(8);

        activeDeviceIndex = -1;
        while (devicesStr && *devicesStr)
        {
            const int32 i = devices.Count();
            devices.Resize(i + 1);
            auto& device = devices[i];

            device.InternalName = devicesStr;
            device.Name = String(device.InternalName).TrimTrailing();
            device.Name.Replace(TEXT("OpenAL Soft on "), TEXT(""));

            if (device.InternalName == defaultDeviceName)
            {
                activeDeviceIndex = i;
            }

            devicesStr += (device.InternalName.Length() + 1) * sizeof(ALCchar);
        }

        if (activeDeviceIndex == -1)
        {
            LOG(Warning, "Failed to pick a default device");
            LOG_STR(Warning, String(defaultDeviceName));
            for (int32 i = 0; i < devices.Count(); i++)
                LOG_STR(Warning, devices[i].Name);
            if (devices.IsEmpty())
                return true;
            LOG(Warning, "Using the first audio device");
            activeDeviceIndex = 0;
        }

        // Open default device
        if (ALC::Device)
            alcCloseDevice(ALC::Device);
        const auto& name = devices[activeDeviceIndex].InternalName;
        ALC::Device = alcOpenDevice(name.Get());
        if (ALC::Device == nullptr)
        {
            LOG(Warning, "Failed to open OpenAL device ({0}).", String(name));
            return true;
        }
    }
    else
#endif
    {
        if (ALC::Device)
        {
            // Single device
            devices.Resize(1);
            devices[0].Name = TEXT("Default device");
        }
        else
        {
            // No device
            devices.Resize(0);
        }
    }

    // Log service info
    LOG(Info, "OpenAL version 1.19.1");
    for (int32 i = 0; i < devices.Count(); i++)
    {
        LOG(Info, "{0}{1}", i == activeDeviceIndex ? TEXT("[active] ") : TEXT(""), devices[i].Name);
    }

    // Init
    ALC::AL_EXT_float32 = ALC::IsExtensionSupported("AL_EXT_float32");
    SetDopplerFactor(AudioSettings::Get()->DopplerFactor);
    ALC::RebuildContexts(true);
    Audio::SetActiveDeviceIndex(activeDeviceIndex);

    return false;
}

void AudioBackendOAL::Base_Update()
{
}

void AudioBackendOAL::Base_Dispose()
{
    if (ALC::Device != nullptr)
    {
        alcCloseDevice(ALC::Device);
        ALC::Device = nullptr;
    }
}

#endif
