/*
 * This file is part of the Colobot: Gold Edition source code
 * Copyright (C) 2001-2023, Daniel Roux, EPSITEC SA & TerranovaTeam
 * http://epsitec.ch; http://colobot.info; http://github.com/colobot
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://gnu.org/licenses
 */


#include "sound/oalsound/alsound.h"

#include "common/make_unique.h"

#include <algorithm>
#include <iomanip>


CALSound::CALSound()
    : m_enabled(false),
      m_audioVolume(1.0f),
      m_musicVolume(1.0f),
      m_channelsLimit(2048),
      m_device{},
      m_context{}
{
}

CALSound::~CALSound()
{
    CleanUp();
}

void CALSound::CleanUp()
{
    if (m_enabled)
    {
        GetLogger()->Info("Unloading files and closing device...\n");
        Reset();

        alcDestroyContext(m_context);
        alcCloseDevice(m_device);
    }
}

bool CALSound::Create()
{
    CleanUp();

    if (m_enabled)
        return true;

    GetLogger()->Info("Opening audio device...\n");
    m_device = alcOpenDevice(nullptr);
    if (!m_device)
    {
        GetLogger()->Error("Could not open audio device!\n");
        return false;
    }

    m_context = alcCreateContext(m_device, nullptr);
    if (!m_context)
    {
        GetLogger()->Error("Could not create audio context!\n");
        return false;
    }
    alcMakeContextCurrent(m_context);
    alListenerf(AL_GAIN, m_audioVolume);
    alDistanceModel(AL_LINEAR_DISTANCE_CLAMPED);

    GetLogger()->Info("Done.\n");
    m_enabled = true;
    return true;
}

void CALSound::Reset()
{
    StopAll();
    StopMusic();

    m_channels.clear();

    m_currentMusic.reset();

    m_oldMusic.clear();

    m_previousMusic.music.reset();

    m_sounds.clear();

    m_music.clear();
}

bool CALSound::GetEnable()
{
    return m_enabled;
}

void CALSound::SetAudioVolume(int volume)
{
    m_audioVolume = static_cast<float>(volume) / MAXVOLUME;
}

int CALSound::GetAudioVolume()
{
    if ( !m_enabled )
        return 0;

    return m_audioVolume * MAXVOLUME;
}

void CALSound::SetMusicVolume(int volume)
{
    m_musicVolume = static_cast<float>(volume) / MAXVOLUME;
    if (m_currentMusic)
    {
        m_currentMusic->SetVolume(m_musicVolume);
    }
}

int CALSound::GetMusicVolume()
{
    if ( !m_enabled )
        return 0.0f;

    return m_musicVolume * MAXVOLUME;
}

bool CALSound::Cache(SoundType sound, const std::string &filename)
{
    auto buffer = MakeUnique<CBuffer>();
    if (buffer->LoadFromFile(filename, sound))
    {
        m_sounds[sound] = std::move(buffer);
        return true;
    }
    return false;
}

void CALSound::CacheMusic(const std::string &filename)
{
    m_thread.Start([this, filename]()
    {
        if (m_music.find(filename) == m_music.end())
        {
            auto buffer = MakeUnique<CBuffer>();
            if (buffer->LoadFromFile(filename, static_cast<SoundType>(-1)))
            {
                m_music[filename] = std::move(buffer);
            }
        }
    });
}

bool CALSound::IsCached(SoundType sound)
{
    return m_sounds.find(sound) != m_sounds.end();
}

bool CALSound::IsCachedMusic(const std::string &filename)
{
    return m_music.find(filename) != m_music.end();
}

int CALSound::GetPriority(SoundType sound)
{
    if ( sound == SOUND_FLYh   ||
        sound == SOUND_FLY    ||
        sound == SOUND_MOTORw ||
        sound == SOUND_MOTORt ||
        sound == SOUND_MOTORr ||
        sound == SOUND_MOTORs ||
        sound == SOUND_SLIDE  ||
        sound == SOUND_ERROR  )
    {
        return 30;
    }

    if ( sound == SOUND_CONVERT  ||
        sound == SOUND_ENERGY   ||
        sound == SOUND_DERRICK  ||
        sound == SOUND_STATION  ||
        sound == SOUND_REPAIR   ||
        sound == SOUND_RESEARCH ||
        sound == SOUND_BURN     ||
        sound == SOUND_BUILD    ||
        sound == SOUND_TREMBLE  ||
        sound == SOUND_NUCLEAR  ||
        sound == SOUND_EXPLO    ||
        sound == SOUND_EXPLOl   ||
        sound == SOUND_EXPLOlp  ||
        sound == SOUND_EXPLOp   ||
        sound == SOUND_EXPLOi   )
    {
        return 20;
    }

    if ( sound == SOUND_BLUP    ||
        sound == SOUND_INSECTs ||
        sound == SOUND_INSECTa ||
        sound == SOUND_INSECTb ||
        sound == SOUND_INSECTw ||
        sound == SOUND_INSECTm ||
        sound == SOUND_PSHHH   ||
        sound == SOUND_EGG     )
    {
        return 0;
    }

    return 10;
}

bool CALSound::SearchFreeBuffer(SoundType sound, int &channel, bool &alreadyLoaded)
{
    int priority = GetPriority(sound);

    // Seeks a channel used which sound is stopped.
    for (auto& it : m_channels)
    {
        if (it.second->IsPlaying())
        {
            continue;
        }
        if (it.second->GetSoundType() != sound)
        {
            continue;
        }

        it.second->SetPriority(priority);
        it.second->Reset();
        channel = it.first;
        alreadyLoaded = it.second->IsLoaded();
        return true;
    }

    // just add a new channel if we dont have any
    if (m_channels.size() == 0)
    {
        auto chn = MakeUnique<CChannel>();
        // check if we channel ready to play music, if not report error
        if (chn->IsReady())
        {
            chn->SetPriority(priority);
            chn->Reset();
            channel = 1;
            m_channels[channel] = std::move(chn);
            alreadyLoaded = false;
            return true;
        }
        GetLogger()->Error("Could not open channel to play sound!\n");
        return false;
    }

    // Assigns new channel within limit
    if (m_channels.size() < m_channelsLimit)
    {
        auto it = m_channels.end();
        it--;
        int i = (*it).first;
        while (++i)
        {
            if (m_channels.find(i) == m_channels.end())
            {
                auto chn = MakeUnique<CChannel>();
                // check if channel is ready to play music, if not destroy it and seek free one
                if (chn->IsReady())
                {
                    chn->SetPriority(priority);
                    chn->Reset();
                    m_channels[i] = std::move(chn);
                    channel = i;
                    alreadyLoaded = false;
                    return true;
                }
                GetLogger()->Debug("Could not open additional channel to play sound!\n");
                break;
            }
        }
    }

    int lowerOrEqual = -1;
    for (auto& it : m_channels)
    {
        if (it.second->GetPriority() < priority)
        {
            GetLogger()->Debug("Sound channel with lower priority will be reused.\n");
            channel = it.first;
            it.second->Reset();
            return true;
        }
        if (it.second->GetPriority() <= priority)
            lowerOrEqual = it.first;
    }

    if (lowerOrEqual != -1)
    {
        channel = lowerOrEqual;
        m_channels[channel]->Reset();
        GetLogger()->Debug("Sound channel with lower or equal priority will be reused.\n");
        return true;
    }

    GetLogger()->Debug("Could not find free buffer to use.\n");
    return false;
}

int CALSound::Play(SoundType sound, float amplitude, float frequency, bool loop)
{
    return Play(sound, Math::Vector{}, true, amplitude, frequency, loop);
}

int CALSound::Play(SoundType sound, const Math::Vector &pos, float amplitude, float frequency, bool loop)
{
    return Play(sound, pos, false, amplitude, frequency, loop);
}

int CALSound::Play(SoundType sound, const Math::Vector &pos, bool relativeToListener, float amplitude, float frequency, bool loop)
{
    if (!m_enabled)
    {
        return -1;
    }
    if (m_sounds.find(sound) == m_sounds.end())
    {
        GetLogger()->Debug("Sound %d was not loaded!\n", sound);
        return -1;
    }

    int channel;
    bool alreadyLoaded = false;
    if (!SearchFreeBuffer(sound, channel, alreadyLoaded))
    {
        return -1;
    }

    if (!alreadyLoaded)
    {
        if (!m_channels[channel]->SetBuffer(m_sounds[sound].get()))
        {
            m_channels[channel]->SetBuffer(nullptr);
            return -1;
        }
    }

    CChannel* chn = m_channels[channel].get();

    chn->SetPosition(pos, relativeToListener);
    chn->SetVolumeAtrib(1.0f);

    // setting initial values
    chn->SetStartAmplitude(amplitude);
    chn->SetStartFrequency(frequency);
    chn->SetChangeFrequency(1.0f);
    chn->ResetOper();
    chn->SetFrequency(frequency);
    chn->SetVolume(powf(amplitude * chn->GetVolumeAtrib(), 0.2f) * m_audioVolume);
    chn->SetLoop(loop);
    chn->Mute(false);

    if (!chn->Play())
    {
        m_channelsLimit = m_channels.size() - 1;
        GetLogger()->Debug("Changing channel limit to %u.\n", m_channelsLimit);
        m_channels.erase(channel);

        return -1;
    }

    return channel | ((chn->GetId() & 0xffff) << 16);
}

bool CALSound::FlushEnvelope(int channel)
{
    if (!CheckChannel(channel))
    {
        return false;
    }

    m_channels[channel]->ResetOper();
    return true;
}

bool CALSound::AddEnvelope(int channel, float amplitude, float frequency, float time, SoundNext oper)
{
    if (!CheckChannel(channel))
    {
        return false;
    }

    SoundOper op;
    op.finalAmplitude = amplitude;
    op.finalFrequency = frequency;
    op.totalTime = time;
    op.nextOper = oper;
    op.currentTime = 0.0f;
    m_channels[channel]->AddOper(op);

    return true;
}

bool CALSound::Position(int channel, const Math::Vector &pos)
{
    if (!CheckChannel(channel))
    {
        return false;
    }

    m_channels[channel]->SetPosition(pos);
    return true;
}

bool CALSound::Frequency(int channel, float frequency)
{
    if (!CheckChannel(channel))
    {
        return false;
    }

    m_channels[channel]->SetFrequency(frequency * m_channels[channel]->GetInitFrequency());
    m_channels[channel]->SetChangeFrequency(frequency);
    return true;
}

bool CALSound::Stop(int channel)
{
    if (!CheckChannel(channel))
    {
        return false;
    }

    m_channels[channel]->Stop();
    m_channels[channel]->ResetOper();

    return true;
}

bool CALSound::StopAll()
{
    if (!m_enabled)
    {
        return false;
    }

    for (auto& channel : m_channels)
    {
        channel.second->Stop();
        channel.second->ResetOper();
    }

    return true;
}

bool CALSound::MuteAll(bool mute)
{
    if (!m_enabled)
    {
        return false;
    }

    for (auto& it : m_channels)
    {
        if (it.second->IsPlaying())
        {
            it.second->Mute(mute);
        }
    }

    return true;
}

void CALSound::FrameMove(float rTime)
{
    if (!m_enabled)
    {
        return;
    }

    float progress;
    float volume, frequency;
    for (auto& it : m_channels)
    {
        if (!it.second->IsPlaying())
        {
            continue;
        }
        if (it.second->IsMuted())
        {
            it.second->SetVolume(0.0f);
            continue;
        }

        if (!it.second->HasEnvelope())
            continue;

        SoundOper &oper = it.second->GetEnvelope();
        oper.currentTime += rTime;
        progress = oper.currentTime / oper.totalTime;
        progress = std::min(progress, 1.0f);

        // setting volume
        volume = progress * (oper.finalAmplitude - it.second->GetStartAmplitude());
        volume = volume + it.second->GetStartAmplitude();
        it.second->SetVolume(powf(volume * it.second->GetVolumeAtrib(), 0.2f) * m_audioVolume);

        // setting frequency
        frequency = progress;
        frequency *= oper.finalFrequency - it.second->GetStartFrequency();
        frequency += it.second->GetStartFrequency();
        frequency *= it.second->GetChangeFrequency();
        frequency = (frequency * it.second->GetInitFrequency());
        it.second->SetFrequency(frequency);

        if (oper.totalTime <= oper.currentTime)
        {
            if (oper.nextOper == SOPER_LOOP)
            {
                oper.currentTime = 0.0f;
                it.second->Play();
            }
            else
            {
                it.second->SetStartAmplitude(oper.finalAmplitude);
                it.second->SetStartFrequency(oper.finalFrequency);
                if (oper.nextOper == SOPER_STOP)
                {
                    it.second->Stop();
                }

                it.second->PopEnvelope();
            }
        }
    }

    auto it = m_oldMusic.begin();
    while (it != m_oldMusic.end())
    {
        if (it->currentTime >= it->fadeTime)
        {
            it = m_oldMusic.erase(it);
        }
        else
        {
            it->currentTime += rTime;
            it->music->SetVolume(((it->fadeTime-it->currentTime) / it->fadeTime) * m_musicVolume);
            ++it;
        }
    }

    if (m_previousMusic.fadeTime > 0.0f)
    {
        if (m_previousMusic.currentTime >= m_previousMusic.fadeTime)
        {
            m_previousMusic.music->Pause();
        }
        else
        {
            m_previousMusic.currentTime += rTime;
            m_previousMusic.music->SetVolume(((m_previousMusic.fadeTime-m_previousMusic.currentTime) / m_previousMusic.fadeTime) * m_musicVolume);
        }
    }
}

void CALSound::SetListener(const Math::Vector &eye, const Math::Vector &lookat)
{
    m_eye = eye;
    m_lookat = lookat;
    Math::Vector forward = lookat - eye;
    forward.Normalize();
    float orientation[] = {forward.x, forward.y, forward.z, 0.f, -1.0f, 0.0f};

    alListener3f(AL_POSITION, eye.x, eye.y, eye.z);
    alListenerfv(AL_ORIENTATION, orientation);
}

void CALSound::PlayMusic(const std::string &filename, bool repeat, float fadeTime)
{
    if (!m_enabled)
    {
        return;
    }

    m_thread.Start([this, filename, repeat, fadeTime]()
    {
        CBuffer* buffer = nullptr;

        // check if we have music in cache
        if (m_music.find(filename) == m_music.end())
        {
            GetLogger()->Debug("Music %s was not cached!\n", filename.c_str());

            auto newBuffer = MakeUnique<CBuffer>();
            buffer = newBuffer.get();
            if (!newBuffer->LoadFromFile(filename, static_cast<SoundType>(-1)))
            {
                return;
            }
            m_music[filename] = std::move(newBuffer);
        }
        else
        {
            GetLogger()->Debug("Music loaded from cache\n");
            buffer = m_music[filename].get();
        }

        if (m_currentMusic)
        {
            OldMusic old;
            old.music = std::move(m_currentMusic);
            old.fadeTime = fadeTime;
            old.currentTime = 0.0f;
            m_oldMusic.push_back(std::move(old));
        }

        m_currentMusic = MakeUnique<CChannel>();
        m_currentMusic->SetBuffer(buffer);
        m_currentMusic->SetVolume(m_musicVolume);
        m_currentMusic->SetLoop(repeat);
        m_currentMusic->Play();
    });
}

void CALSound::PlayPauseMusic(const std::string &filename, bool repeat)
{
    if (m_previousMusic.fadeTime > 0.0f)
    {
        if (m_currentMusic != nullptr)
        {
            OldMusic old;
            old.music = std::move(m_currentMusic);
            old.fadeTime = 2.0f;
            old.currentTime = 0.0f;
            m_oldMusic.push_back(std::move(old));
        }
    }
    else
    {
        if (m_currentMusic != nullptr)
        {
            m_previousMusic.music = std::move(m_currentMusic);
            m_previousMusic.fadeTime = 2.0f;
            m_previousMusic.currentTime = 0.0f;
        }
    }
    PlayMusic(filename, repeat);
}

void CALSound::StopPauseMusic()
{
    if (m_previousMusic.fadeTime > 0.0f)
    {
        StopMusic();

        m_currentMusic = std::move(m_previousMusic.music);
        if (m_currentMusic != nullptr)
        {
            m_currentMusic->SetVolume(m_musicVolume);
            if (m_previousMusic.currentTime >= m_previousMusic.fadeTime)
            {
                m_currentMusic->Play();
            }
        }
        m_previousMusic.fadeTime = 0.0f;
    }
}

void CALSound::StopMusic(float fadeTime)
{
    if (!m_enabled || m_currentMusic == nullptr)
    {
        return;
    }

    OldMusic old;
    old.music = std::move(m_currentMusic);
    old.fadeTime = fadeTime;
    old.currentTime = 0.0f;
    m_oldMusic.push_back(std::move(old));
}

bool CALSound::IsPlayingMusic()
{
    if (!m_enabled || m_currentMusic == nullptr)
    {
        return false;
    }

    return m_currentMusic->IsPlaying();
}

bool CALSound::CheckChannel(int &channel)
{
    int id = (channel >> 16) & 0xffff;
    channel &= 0xffff;

    if (!m_enabled)
    {
        return false;
    }

    if (m_channels.find(channel) == m_channels.end())
    {
        return false;
    }

    if  (m_audioVolume == 0)
    {
        return false;
    }

    if (m_channels[channel]->GetId() != id)
    {
        return false;
    }

    return true;
}
