/*
 * Copyright (C) 2006-2026 Metallizer, Solarus - http://www.solarus-games.org
 *
 * Solarus 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.
 *
 * Solarus 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://www.gnu.org/licenses/>.
 */

#include "solarus/audio/ALWrapper.h"
#include "solarus/audio/AudioContext.h"
#include "solarus/audio/effects/ChorusAudioEffect.h"
#include "solarus/audio/effects/ReverbAudioEffect.h"
#include "solarus/audio/filters/LowPassAudioFilter.h"
#include "solarus/audio/filters/HighPassAudioFilter.h"
#include "solarus/core/Debug.h"
#include "solarus/core/Logger.h"
#include "solarus/audio/Music.h"
#include "solarus/audio/Sound.h"
#include <vector>

namespace Solarus
{

std::map<AudioContext::Identifier, std::shared_ptr<AudioContext>> AudioContext::audio_contexts;

/**
 * \brief Initializes and create all contexts.
 */
void AudioContext::initialize() {
  audio_contexts[MAIN] = std::make_shared<AudioContext>(MAIN);
  audio_contexts[GAME] = std::make_shared<AudioContext>(GAME);
  audio_contexts[MAP] = std::make_shared<AudioContext>(MAP);
}

/**
 * \brief Destroys all contexts.
 */
void AudioContext::quit() {
  audio_contexts[MAIN].reset();
  audio_contexts[GAME].reset();
  audio_contexts[MAP].reset();
}

/**
 * \brief Returns whether the audio contexts are initialized.
 * \return \c true if the audio contexts are initialized.
 */
bool AudioContext::is_initialized() {
  return !audio_contexts.empty() && audio_contexts[MAIN] != nullptr;
}

/**
 * \brief Updates all contexts.
 */
void AudioContext::update_all() {
  audio_contexts[MAIN]->update();
  audio_contexts[GAME]->update();
  audio_contexts[MAP]->update();
}

/**
 * \brief Get a specific context with its identifier.
 * \param identifier The context identifier.
 * \return The context object.
 */
std::shared_ptr<AudioContext> AudioContext::get_context(const Identifier& identifier) {
  auto it = audio_contexts.find(identifier);
  if (it == audio_contexts.end()) {
    Debug::error("Audio context not found");
    return nullptr;
  }
  return it->second;
}

/**
 * \brief Notifies audio contexts that a game just finished.
 */
void AudioContext::notify_game_finished() {
  // Stop the common music even if it's on the main context
  // because the sol.audio api uses the main context to play musics.
  // This is the normal behavior for Solarus since the beginning.
  auto common_music = audio_contexts[MAIN]->current_common_music;
  if (common_music != nullptr) {
    common_music->stop();
    audio_contexts[MAIN]->current_common_music = nullptr;
  }

  // Stop all musics and sounds from game and map contexts.
  audio_contexts[GAME]->stop_all_musics();
  audio_contexts[GAME]->stop_all_sounds();
  audio_contexts[MAP]->stop_all_musics();
  audio_contexts[MAP]->stop_all_sounds();
}

/**
 * \brief Notifies audio contexts that a map has just finished.
 */
void AudioContext::notify_map_finished() {
  audio_contexts[MAP]->stop_all_musics();
  audio_contexts[MAP]->stop_all_sounds();
}

/**
 * \brief Notifies audio contexts that Solarus has been suspended or unsuspended.
 * \param is_suspended true if Solarus is suspended.
 */
void AudioContext::notify_suspended(bool is_suspended) {
  audio_contexts[MAIN]->set_pause_to_all_musics(is_suspended);
  audio_contexts[GAME]->set_pause_to_all_musics(is_suspended);
  audio_contexts[MAP]->set_pause_to_all_musics(is_suspended);
}

/**
 * \brief Notifies all audio contexts that the audio device has been disconnected.
 */
void AudioContext::notify_device_disconnected_to_all_contexts() {
  audio_contexts[MAIN]->notify_device_disconnected();
  audio_contexts[GAME]->notify_device_disconnected();
  audio_contexts[MAP]->notify_device_disconnected();
}

/**
 * \brief Notifies all audio contexts that the audio device has been reconnected.
 */
void AudioContext::notify_device_reconnected_to_all_contexts() {
  audio_contexts[MAIN]->notify_device_reconnected();
  audio_contexts[GAME]->notify_device_reconnected();
  audio_contexts[MAP]->notify_device_reconnected();
}

/**
 * \brief Default constructor.
 */
AudioContext::AudioContext()
  : identifier(MAIN) {
}

/**
 * \brief Constructor with a context identifier.
 * \param identifier The identifier of this context.
 */
AudioContext::AudioContext(Identifier identifier)
  : identifier(identifier) {
}

/**
 * \brief Updates sounds and musics if necessary.
 */
void AudioContext::update() {
  update_sounds();
  update_musics();
}

/**
 * \brief Returns the identifier for this context.
 */
const AudioContext::Identifier& AudioContext::get_identifier() const {
  return identifier;
}

/**
 * \brief Updates all sounds currently playing and removes the ones that are stopped.
 */
void AudioContext::update_sounds() {
  // update the playing sounds
  std::list<SoundPtr> sounds_to_remove;
  for (const SoundPtr& sound: current_sounds) {
    if (!sound->update_playing()) {
      sounds_to_remove.push_back(sound);
    }
  }

  for (const SoundPtr& sound: sounds_to_remove) {
    current_sounds.remove(sound);
  }
}

/**
 * \brief Updates all musics currently playing and removes the ones that are stopped.
 */
void AudioContext::update_musics() {
  // Update current music from sol.audio API
  if (current_common_music != nullptr) {
    bool playing = current_common_music->update_playing();
    if (!playing) {
      // Music is finished.
      ScopedLuaRef callback_ref = current_common_music->get_callback();
      current_common_music->stop();
      current_common_music = nullptr;
      callback_ref.call("music callback");
    }
  }

  // Update current musics from sol.music API
  std::list<MusicPtr> musics_to_remove;
  for (const MusicPtr& music: current_musics) {
    if (!music->update_playing()) {
      musics_to_remove.push_back(music);
    }
  }

  for (const MusicPtr& music: musics_to_remove) {
    current_musics.remove(music);
  }
}

/**
 * \brief Plays a sound in this context.
 * \param sound A pointer to the Sound to play.
 */
void AudioContext::play_sound(SoundPtr sound) {
  current_sounds.remove(sound);  // To avoid duplicates.
  current_sounds.push_back(sound);

  std::vector<ALuint> slots;
  for (const auto& effect : effects) {
    slots.push_back(effect.second->get_auxiliary_slot());
  }
  if (!slots.empty()) {
    sound->set_auxiliary_effect_slots(slots);
  }

  std::vector<ALuint> filter_handles;
  for (const auto& f : filters) {
    filter_handles.push_back(f.second->get_filter());
  }
  if (!filter_handles.empty()) {
    sound->set_direct_filter(filter_handles[0]);
  }

  sound->start();
}

/**
 * \brief Stops a sound in this context.
 * \param sound A pointer to the Sound to stop.
 */
void AudioContext::stop_sound(SoundPtr sound) {
  sound->stop();
  current_sounds.remove(sound);
}

/**
 * \brief Stops all the sounds currently playing in this context.
 */
void AudioContext::stop_all_sounds() {
  for (const auto& sound : current_sounds) {
    sound->stop();
  }
  current_sounds.clear();
}

/**
 * \brief Returns the music currently playing via the sol.audio API.
 * \return The current common music, or nullptr if none is playing.
 */
MusicPtr AudioContext::get_common_music() const {
  return current_common_music;
}

/**
 * \brief Plays a music via the sol.audio API, stopping any previously playing common music.
 * \param music The music to play.
 */
void AudioContext::play_common_music(MusicPtr music) {
  // Stops the current music if any
  if (current_common_music != nullptr) {
    current_common_music->stop();
    current_common_music = nullptr;
  }

  std::vector<ALuint> slots;
  for (const auto& effect : effects) {
    slots.push_back(effect.second->get_auxiliary_slot());
  }
  if (!slots.empty()) {
    music->set_auxiliary_effect_slots(slots);
  }

  std::vector<ALuint> filter_handles;
  for (const auto& f : filters) {
    filter_handles.push_back(f.second->get_filter());
  }
  if (!filter_handles.empty()) {
    music->set_direct_filter(filter_handles[0]);
  }

  current_common_music = music;
  current_common_music->start();
}

/**
 * \brief Plays a music via the sol.music API in this context.
 * \param music The music to play.
 */
void AudioContext::play_music(MusicPtr music) {
  current_musics.push_back(music);

  std::vector<ALuint> slots;
  for (const auto& effect : effects) {
    slots.push_back(effect.second->get_auxiliary_slot());
  }
  if (!slots.empty()) {
    music->set_auxiliary_effect_slots(slots);
  }

  std::vector<ALuint> filter_handles;
  for (const auto& f : filters) {
    filter_handles.push_back(f.second->get_filter());
  }
  if (!filter_handles.empty()) {
    music->set_direct_filter(filter_handles[0]);
  }

  music->start();
}

/**
 * \brief Stops the music currently playing via the sol.audio API.
 */
void AudioContext::stop_common_music() {
  if (current_common_music != nullptr) {
    current_common_music->stop();
    current_common_music = nullptr;
  }
}

/**
 * \brief Stops a specific music playing via the sol.music API in this context.
 * \param music The music to stop.
 */
void AudioContext::stop_music(MusicPtr music) {
  music->stop();
  current_musics.remove(music);
}

/**
 * \brief Stops all musics currently playing in this context, including the common music.
 */
void AudioContext::stop_all_musics() {
  if (current_common_music != nullptr) {
    current_common_music->stop();
    current_common_music = nullptr;
  }
  for (const auto& music : current_musics) {
    music->stop();
  }
  current_musics.clear();
}

/**
 * \brief Pauses or resumes all musics in this context.
 * \param is_paused \c true to pause all musics, \c false to resume them.
 */
void AudioContext::set_pause_to_all_musics(bool is_paused) {
  if (current_common_music != nullptr) {
    current_common_music->set_paused(is_paused);
  }
  for (const auto& music : current_musics) {
    music->set_paused(is_paused);
  }
}

bool AudioContext::is_effect_enabled(AudioEffectId id) const {
  return effects.count(id) > 0 || filters.count(id) > 0;
}

void AudioContext::add_reverb_effect(ReverbPreset preset) {
  effects.insert({ AudioEffectId::REVERB, std::make_shared<ReverbAudioEffect>(preset) });
}

void AudioContext::add_chorus_effect(ChorusPreset preset) {
  effects.insert({ AudioEffectId::CHORUS, std::make_shared<ChorusAudioEffect>(preset) });
}

void AudioContext::set_reverb_preset(ReverbPreset preset) {
  auto it = effects.find(AudioEffectId::REVERB);
  if (it != effects.end()) {
    static_cast<ReverbAudioEffect*>(it->second.get())->set_preset(preset);
  }
}

void AudioContext::set_chorus_preset(ChorusPreset preset) {
  auto it = effects.find(AudioEffectId::CHORUS);
  if (it != effects.end()) {
    static_cast<ChorusAudioEffect*>(it->second.get())->set_preset(preset);
  }
}

void AudioContext::add_echo_effect(EchoPreset preset) {
  effects.insert({ AudioEffectId::ECHO, std::make_shared<EchoAudioEffect>(preset) });
}

void AudioContext::set_echo_preset(EchoPreset preset) {
  auto it = effects.find(AudioEffectId::ECHO);
  if (it != effects.end()) {
    static_cast<EchoAudioEffect*>(it->second.get())->set_preset(preset);
  }
}

void AudioContext::add_flanger_effect(FlangerPreset preset) {
  effects.insert({ AudioEffectId::FLANGER, std::make_shared<FlangerAudioEffect>(preset) });
}

void AudioContext::set_flanger_preset(FlangerPreset preset) {
  auto it = effects.find(AudioEffectId::FLANGER);
  if (it != effects.end()) {
    static_cast<FlangerAudioEffect*>(it->second.get())->set_preset(preset);
  }
}

void AudioContext::add_low_pass_filter(float gain, float gainhf) {
  filters.insert({ AudioEffectId::LOW_PASS, std::make_shared<LowPassAudioFilter>(gain, gainhf) });
}

void AudioContext::set_low_pass_params(float gain, float gainhf) {
  auto it = filters.find(AudioEffectId::LOW_PASS);
  if (it != filters.end()) {
    static_cast<LowPassAudioFilter*>(it->second.get())->set_params(gain, gainhf);
  }
}

void AudioContext::add_high_pass_filter(float gain, float gainlf) {
  filters.insert({ AudioEffectId::HIGH_PASS, std::make_shared<HighPassAudioFilter>(gain, gainlf) });
}

void AudioContext::set_high_pass_params(float gain, float gainlf) {
  auto it = filters.find(AudioEffectId::HIGH_PASS);
  if (it != filters.end()) {
    static_cast<HighPassAudioFilter*>(it->second.get())->set_params(gain, gainlf);
  }
}

void AudioContext::remove_effect(AudioEffectId id) {
  effects.erase(id);
  filters.erase(id);
}

void AudioContext::remove_all_effects() {
  effects.clear();
  filters.clear();
}

/**
 * \brief Notifies all musics in this context that the global music volume has changed.
 */
void AudioContext::notify_music_global_volume_changed() {
  if (current_common_music != nullptr) {
    current_common_music->notify_global_volume_changed();
  }
  for (const auto& music: current_musics) {
    music->notify_global_volume_changed();
  }
}

/**
 * \brief Notifies all sounds in this context that the global sound volume has changed.
 */
void AudioContext::notify_sound_global_volume_changed() {
  // Sound cannot be impacted on global volume?
}

/**
 * \brief Notifies all musics and sounds in this context that the audio device has been disconnected.
 */
void AudioContext::notify_device_disconnected() {
  if (current_common_music != nullptr) {
    current_common_music->notify_device_disconnected();
  }
  for (const auto& music: current_musics) {
    music->notify_device_disconnected();
  }
  for (const auto& sound: current_sounds) {
    sound->notify_device_disconnected();
  }
}

/**
 * \brief Notifies all musics and sounds in this context that the audio device has been reconnected.
 */
void AudioContext::notify_device_reconnected() {
  if (current_common_music != nullptr) {
    current_common_music->notify_device_reconnected();
  }
  for (const auto& music: current_musics) {
    music->notify_device_reconnected();
  }
  for (const auto& sound: current_sounds) {
    sound->notify_device_reconnected();
  }
}

/**
 * \brief Destructor. Stops all sounds and musics before destruction.
 */
AudioContext::~AudioContext() {
  stop_all_sounds();
  stop_all_musics();
}

} // namespace Solarus
