/*
 * Copyright (C) 2006-2019 Christopho, 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/MusicSystem.h"
#include "solarus/audio/Sound.h"
#include "solarus/audio/SoundBuffer.h"
#include "solarus/core/Arguments.h"
#include "solarus/core/Debug.h"
#include "solarus/core/PerfCounter.h"
#include "solarus/core/QuestFiles.h"
#include "solarus/core/ResourceProvider.h"
#include "solarus/lua/LuaContext.h"
#include <algorithm>
#ifdef SOLARUS_OPENAL_EXTENSIONS_RECONNECT
#  include <AL/alext.h>
#endif
#include <cstdlib>
#include <cstring>

namespace Solarus {

ALint Sound::max_send_count = 8; // default value for Solarus, will be updated at system init
bool Sound::audio_enabled = false;
ALCdevice* Sound::device = nullptr;
ALCcontext* Sound::context = nullptr;
float Sound::global_volume = 1.0;
bool Sound::pc_play = false;
uint32_t Sound::next_device_detection_date = 0;
bool Sound::paused_by_system = false;
ResourceProvider* Sound::resource_provider = nullptr;

/**
 * \brief Creates a new Ogg Vorbis sound.
 * \param data The loaded sound data ready to be played.
 */
Sound::Sound(SoundBuffer& data):
  data(data),
  source(AL_NONE),
  paused_by_script(false),
  volume(1.0),
  audio_context_id(AudioContext::MAIN) {
}

/**
 * \brief Destroys the sound.
 */
Sound::~Sound() {
  stop();
}

/**
 * \brief Creates a new Ogg Vorbis sound.
 * \param data The loaded sound data ready to be played.
 */
SoundPtr Sound::create(SoundBuffer& data) {
  Sound* sound = new Sound(data);
  return SoundPtr(sound);
}

/**
 * \brief Initializes the audio (music and sound) system.
 *
 * This method should be called when the application starts.
 * If the argument -no-audio is provided, this function has no effect and
 * there will be no sound.
 * If the argument -perf-sound-play is provided and is "yes", sound
 * playing will be accounted using a performance counter.
 *
 * \param args Command-line arguments.
 * \param resource_provider The resource provider from the main loop if any, or nullptr.
 */
void Sound::initialize(const Arguments& args, ResourceProvider* resource_provider) {

  // Check the -no-audio option.
  audio_enabled = !args.has_argument("-no-audio");
  if (!audio_enabled) {
    return;
  }

  // Check the -perf-sound-play option.
  pc_play = args.get_argument_value("-perf-sound-play") == "yes";

  Sound::resource_provider = resource_provider;
  
  // Initialize OpenAL.
  update_device_connection();
  if (device == nullptr) {
    return;
  }

  // Retrive max auxiliary sends value from device
  alcGetIntegerv(device, ALC_MAX_AUXILIARY_SENDS, 1, &Sound::max_send_count);

  // Initialize OpenAL EAX effects system
  ALWrapper::init_efx_system();

  ALWrapper::generate_buffers(0, nullptr, "<init>");  // Necessary on some systems to avoid errors with the first sound loaded.

  // initialize the audio contexts
  AudioContext::initialize();
  ALWrapper::set_listener_float(AL_GAIN, 0.7, "<init>");  // Reduce master volume, too loud by default.

  // initialize the music system
  MusicSystem::initialize();
}

/**
 * \brief Closes the audio (music and sound) system.
 *
 * This method should be called when exiting the application.
 */
void Sound::quit() {

  if (!is_initialized()) {
    return;
  }

  // stop all currently playing sounds
  AudioContext::quit();

  // uninitialize the music subsystem
  MusicSystem::quit();

  // uninitialize OpenAL
  alcMakeContextCurrent(nullptr);
  alcDestroyContext(context);
  context = nullptr;
  alcCloseDevice(device);
  device = nullptr;
  global_volume = 1.0;
  next_device_detection_date = 0;
  audio_enabled = false;
  resource_provider = nullptr;
}

/**
 * \brief Checks if the audio device is connected.
 */
void Sound::update_device_connection() {

#if SOLARUS_OPENAL_EXTENSIONS_RECONNECT
#define SOLARUS_OPENAL_DEVICE_SPECIFIER ALC_ALL_DEVICES_SPECIFIER
#else
#define SOLARUS_OPENAL_DEVICE_SPECIFIER ALC_DEVICE_SPECIFIER
#endif

  if (device != nullptr) {
    // Check if the device is still connected.
    ALCint is_connected = 0;
#ifdef SOLARUS_OPENAL_EXTENSIONS_RECONNECT
    alcGetIntegerv(device, ALC_CONNECTED, 1, &is_connected);
#else
    is_connected = device != nullptr;
#endif
    if (!is_connected) {
      Logger::info("Lost connection to audio device");
    } else {
      if (System::now_ms() >= next_device_detection_date) {
        // Check if this device is still the default one.
        next_device_detection_date = System::now_ms() + 1000;

        const ALchar* current_device_name = alcGetString(device, SOLARUS_OPENAL_DEVICE_SPECIFIER);
        const ALchar* default_device_name = alcGetString(nullptr, SOLARUS_OPENAL_DEVICE_SPECIFIER);
        if (current_device_name != nullptr &&
            default_device_name != nullptr &&
            std::strcmp(current_device_name, default_device_name)) {
          // This device is no longer the default one.
          Logger::info(std::string("Disconnecting from audio device '") + current_device_name +
                       "' because the default device is now '" + default_device_name + "'");
          is_connected = false;
        }
      }
    }
    if (!is_connected) {
      ALboolean success = alcMakeContextCurrent(nullptr);
      if (!success) {
        Debug::error("Failed to unset OpenAL context");
      }
      alcDestroyContext(context);
      context = nullptr;
      alcCloseDevice(device);
      device = nullptr;
      next_device_detection_date = System::now_ms();
      if (resource_provider != nullptr) {
        AudioContext::notify_device_disconnected_to_all_contexts();
        resource_provider->notify_audio_device_disconnected();
      }
    }
  }

  if (device == nullptr) {
    if (System::now_ms() >= next_device_detection_date) {
      // Try to connect or reconnect to an audio device.
      device = alcOpenDevice(nullptr);
      if (device == nullptr) {
        Debug::error("Cannot open audio device");
      } else {
        context = alcCreateContext(device, nullptr);
        if (context == nullptr) {
          Debug::error("Cannot create audio context");
          alcCloseDevice(device);
          device = nullptr;
        } else if (!alcMakeContextCurrent(context)) {
          Debug::error("Cannot activate audio context");
          alcDestroyContext(context);
          context = nullptr;
          alcCloseDevice(device);
          device = nullptr;
        } else {
          const ALchar* current_device_name = alcGetString(device, SOLARUS_OPENAL_DEVICE_SPECIFIER);
          Logger::info(std::string("Connected to audio device '") + (current_device_name ? current_device_name : "") + "'");
          if (AudioContext::is_initialized()) {
            AudioContext::notify_device_reconnected_to_all_contexts();
          }
        }
      }
      if (device == nullptr) {
        // The attempt failed: try again later.
        next_device_detection_date = System::now_ms() + 1000;
      }
    }
  }
}

/**
 * \brief Notifies this sound that the audio device was disconnected.
 */
void Sound::notify_device_disconnected() {

  // All sources and buffers are already destroyed by OpenAL at this point.
  source = AL_NONE;
}

/**
 * \brief Notifies this sound that the audio device was reconnected.
 */
void Sound::notify_device_reconnected() {

  if (source != AL_NONE) {
    // This sound was not playing when the device got disconnected.
    return;
  }

  // The OpenAL buffer was destroyed together with the device: reload it.
  data.load();

  // Recreate the source and resume playback (looped sounds keep looping,
  // one-shot sounds simply restart from the beginning).
  start();
}

/**
 * \brief Returns whether the audio (music and sound) system is initialized.
 * \return \c true if the audio (music and sound) system is initilialized.
 */
bool Sound::is_initialized() {
  return audio_enabled;
}

/**
 * \brief Returns the id of this sound.
 * \return The sound id.
 */
const std::string& Sound::get_id() const {
  return data.get_id();
}

/**
 * \brief Returns whether a sound exists.
 * \param sound_id id of the sound to test
 * \return true if the sound exists
 */
bool Sound::exists(const std::string& sound_id) {

  std::ostringstream oss;
  oss << "sounds/" << sound_id << ".ogg";
  return QuestFiles::data_file_exists(oss.str());
}

/**
 * \brief Starts playing the specified sound.
 * \param sound_id Id of the sound to play. An empty string does nothing.
 * \deprecated Use play_on_map instead.
 */
void Sound::play(const std::string& sound_id) {

  if (device == nullptr) {
    // Sound might be disabled.
    return;
  }

  if (sound_id.empty()) {
    return;
  }

  if (resource_provider == nullptr) {
    Debug::error("Cannot play sound '" + sound_id + "': missing resource provider");
    return;
  }
  if (pc_play) {
    PerfCounter::update("sound-play");
  }

  SoundBuffer& buffer = resource_provider->get_sound(sound_id);
  SoundPtr sound = Sound::create(buffer);
  sound->start();

  Debug::warning("Built in sound should be played using play_on_map.");
}

/**
 * \brief Starts playing the specified sound in the given built-in audio context.
 * \param sound_id Id of the sound to play. An empty string does nothing.
 * \param context_id The audio context to play the sound into.
 */
void Sound::play_in_context(const std::string& sound_id, const AudioContext::Identifier& context_id) {

  if (device == nullptr) {
    // Sound might be disabled.
    return;
  }

  if (sound_id.empty()) {
    return;
  }

  if (resource_provider == nullptr) {
    Debug::error("Cannot play sound '" + sound_id + "': missing resource provider");
  }
  if (pc_play) {
    PerfCounter::update("sound-play");
  }

  SoundBuffer& buffer = resource_provider->get_sound(sound_id);
  SoundPtr sound = Sound::create(buffer);
  auto audio_context = AudioContext::get_context(context_id);
  sound->set_audio_context_id(context_id);
  audio_context->play_sound(sound);
}

/**
 * \brief Starts playing the specified sound on the map audio context.
 * \param sound_id Id of the sound to play. An empty string does nothing.
 */
void Sound::play_on_map(const std::string& sound_id) {
  play_in_context(sound_id, AudioContext::MAP);
}

/**
 * \brief Starts playing the specified sound on the game audio context.
 *
 * Unlike play_on_map(), the sound is not stopped when the current map
 * finishes: only when the game itself finishes. Use this for built-in
 * sounds that are conceptually tied to the hero or to a transition between
 * maps rather than to the map being left (e.g. the teletransporter sound),
 * so they are not cut off by the map teardown they trigger.
 * \param sound_id Id of the sound to play. An empty string does nothing.
 */
void Sound::play_on_game(const std::string& sound_id) {
  play_in_context(sound_id, AudioContext::GAME);
}

/**
 * \brief Returns the default volume of sound effects.
 * \return The volume (0 to 100).
 */
int Sound::get_global_volume() {

  return static_cast<int>(global_volume * 100.0 + 0.5);
}

/**
 * \brief Sets the global volume of sound effects.
 * \param volume The new volume (0 to 100).
 */
void Sound::set_global_volume(int global_volume) {

  Sound::global_volume = std::min(100, std::max(0, global_volume)) / 100.0;
}

/**
 * \brief Returns the relative volume of this sound effect.
 *
 * This is relative to the global volume.
 *
 * \return The volume (0 to 100).
 */
int Sound::get_volume() const {

  return static_cast<int>(volume * 100.0 + 0.5);
}

/**
 * \brief Sets the relative volume of this sound effect.
 *
 * This is relative to the global volume.
 *
 * \return The volume (0 to 100).
 */
void Sound::set_volume(int volume) {

  this->volume = std::min(100, std::max(0, volume)) / 100.0;

  if (source != AL_NONE) {
    ALWrapper::set_source_float(source, AL_GAIN, get_actual_volume(), get_id());
  }
}

/**
 * \brief Returns the actual volume of this sound.
 *
 * This is <tt>get_volume() * get_global_volume()</tt>.
 *
 * \return The actual volume between 0.0 and 1.0.
 */
float Sound::get_actual_volume() const {

  return volume * global_volume;
}

/**
 * \brief Returns the current pan value of this sound.
 * 
 * 0.0 is the default value for center.
 * -1.0 is totally to the left.
 * 1.0 is totally to the right.
 * 
 * \return The pan value between -1.0 and 1.0.
 */
float Sound::get_pan() const {

  return pan;
}

/**
 * \brief Sets the pan value of this sound effect.
 *
 * This has no effect on stereo sounds.
 * 0.0 is the default value for center.
 * -1.0 is totally to the left.
 * 1.0 is totally to the right.
 *
 * \return The pan (-1.0 to 1.0).
 */
void Sound::set_pan(float pan) {
  this->pan = pan;

  if (source != AL_NONE) {
    ALWrapper::set_source_float(source, AL_ROLLOFF_FACTOR, 0.0f, get_id());
    ALWrapper::set_source_int(source, AL_SOURCE_RELATIVE, AL_TRUE, get_id());
    ALWrapper::set_source_vector3(
      source,
      AL_POSITION,
      pan,
      0,
      -sqrtf(1.0f - pan * pan),
      get_id()
    );
  }
}

/**
 * \brief Returns the current pitch value of this sound.
 * 
 * 1.0 is the default value for normal speed.
 * 
 * \return The pitch value between 0.5 and 2.0.
 */
float Sound::get_pitch() const {

  return pitch;
}

/**
 * \brief Sets the pitch value of this sound effect.
 * 
 * The value acts as a sample rate multiplier.
 *
 * \return The pitch (0.5 to 2.0).
 */
void Sound::set_pitch(float pitch) {
  this->pitch = pitch;

  if (source != AL_NONE) {
    ALWrapper::set_source_float(source, AL_PITCH, pitch, get_id());
  }
}

/**
 * \brief Gets the audio context id for this sound.
 * \return The audio context identifier
 */
const AudioContext::Identifier& Sound::get_audio_context_id() const {
  return audio_context_id;
}

/**
 * \brief Sets the audio context id for this sound.
 * \param id The audio context identifier
 */
void Sound::set_audio_context_id(const AudioContext::Identifier& id) {
  audio_context_id = id;
}

/**
 * \brief Sets the AL auxiliary effect slots to the source.
 * \param slot The AL effect slot vector to set
 */
void Sound::set_auxiliary_effect_slots(std::vector<ALuint> slots) {
  auxiliary_effect_slots = std::move(slots);
  apply_auxiliary_effect_slots();
}

/**
 * \brief Sets the AL direct filter on the source.
 * \param filter The AL filter handle to set, or AL_FILTER_NULL to remove.
 */
void Sound::set_direct_filter(ALuint filter) {
  direct_filter = filter;
  apply_direct_filter();
}

/**
 * \brief Applies the current direct filter to the source.
 *
 * No-op if the source is not yet created.
 */
void Sound::apply_direct_filter() {
  if (source == AL_NONE) {
    return;
  }
  ALWrapper::set_source_int(source, AL_DIRECT_FILTER, static_cast<ALint>(direct_filter), get_id());
}

/**
 * \brief Clears all sends then re-binds the current auxiliary effect slots to the source.
 *
 * Safe to call on a live source: clears all sends up to ALC_MAX_AUXILIARY_SENDS
 * before rebinding, so stale sends from a previous effect configuration cannot linger.
 * No-op if the source is not yet created.
 */
void Sound::apply_auxiliary_effect_slots() {
  if (source == AL_NONE) {
    return;
  }

  for (int i = 0; i < Sound::max_send_count; i++) {
    ALWrapper::set_source_3ints(
      source,
      AL_AUXILIARY_SEND_FILTER,
      AL_EFFECTSLOT_NULL, i, AL_FILTER_NULL,
      get_id()
    );
  }

  int send = 0;
  for (const ALuint slot : auxiliary_effect_slots) {
    ALWrapper::set_source_3ints(
      source,
      AL_AUXILIARY_SEND_FILTER,
      slot, send, AL_FILTER_NULL,
      get_id()
    );
    send++;
  }
}

/**
 * \brief Updates the audio (music and sound) system.
 *
 * This function is called repeatedly by the game.
 *
 * \param resource_provider The resource provider.
 */
void Sound::update() {

  if (!is_initialized()) {
    return;
  }

  update_device_connection();

  if (device != nullptr) {
    AudioContext::update_all();
  }
}

/**
 * \brief Updates this sound when it is playing.
 * \return true if the sound is still playing, false if it is finished.
 */
bool Sound::update_playing() {

  // See if this sound is still playing.
  if (source == AL_NONE) {
    return false;
  }

  ALint status;
  ALWrapper::get_source_int(source, AL_SOURCE_STATE, &status, get_id());

  if (status == AL_STOPPED) {  // The sound has just finished.
    stop();
  }

  return source != AL_NONE;
}

/**
 * \brief Plays the sound.
 * \return \c true if the sound was started successfully, \c false otherwise.
 */
bool Sound::start() {

  if (device == nullptr) {
    // Sound might be disabled.
    return false;
  }

  ALuint buffer = data.get_buffer();
  if (buffer == AL_NONE) {
    return false;
  }

  if (source == AL_NONE) {
    // create a source
    ALenum error = 0;
    error = ALWrapper::generate_sources(1, &source, get_id());
    error = ALWrapper::set_source_int(source, AL_BUFFER, buffer, get_id());
    error = ALWrapper::set_source_float(source, AL_GAIN, get_actual_volume(), get_id());
    error = ALWrapper::set_source_int(source, AL_LOOPING, looped ? AL_TRUE : AL_FALSE, get_id());

    if (error != AL_NO_ERROR) {
      ALWrapper::delete_sources(1, &source, get_id());
      source = AL_NONE;
      return false;
    }

    apply_auxiliary_effect_slots();
    apply_direct_filter();

    // update initial parameters
    set_pan(pan);
    set_pitch(pitch);
  }

  // plays the sound
  ALenum error = ALWrapper::play_source(source, get_id());
  if (error != AL_NO_ERROR) {
    return false;
  }

  return true;
}

/**
 * \brief Stops playing this sound, cleans the source and releases the buffer.
 */
void Sound::stop() {

  if (device == nullptr) {
    return;
  }

  if (source == AL_NONE) {
    return;
  }

  ALint status = 0;
  ALWrapper::get_source_int(source, AL_SOURCE_STATE, &status, get_id());
  if (status == AL_PLAYING || status == AL_PAUSED) {
    // Stops the source if still active.
    ALWrapper::stop_source(source, get_id());
  }
  ALWrapper::set_source_int(source, AL_DIRECT_FILTER, AL_FILTER_NULL, get_id());
  ALWrapper::set_source_int(source, AL_BUFFER, 0, get_id());  // This allows the buffer to get deleted later.
  ALWrapper::delete_sources(1, &source, get_id());

  source = AL_NONE;
}

/**
 * \brief Returns whether the sound is currently playing.
 * \return \c true if the sound is playing.
 */
bool Sound::is_playing() const {
  if (device == nullptr || source == AL_NONE) {
    return false;
  }

  ALint status;
  ALWrapper::get_source_int(source, AL_SOURCE_STATE, &status, get_id());
  return status == AL_PLAYING;
}

/**
 * \brief Returns whether the sound is currently paused.
 * \return \c true if the sound is paused.
 */
bool Sound::is_paused() const {

  if (device == nullptr || source == AL_NONE) {
    return false;
  }

  ALint status;
  ALWrapper::get_source_int(source, AL_SOURCE_STATE, &status, get_id());
  return status == AL_PAUSED;
}

/**
 * \brief Pauses or resumes this sound.
 * \param paused \c true to pause the source, \c false to resume it.
 */
void Sound::set_paused(bool paused) {

  if (device == nullptr || source == AL_NONE) {
    return;
  }

  ALint status;
  ALWrapper::get_source_int(source, AL_SOURCE_STATE, &status, get_id());

  if (paused) {
    if (status == AL_PLAYING) {
      ALWrapper::pause_source(source, get_id());
    }
  }
  else {
    if (status == AL_PAUSED) {
      ALWrapper::play_source(source, get_id());
    }
  }
}

/**
 * \brief Returns whether the sound is currently paused by a script.
 * \return \c true if the sound is paused by a script.
 */
bool Sound::is_paused_by_script() const {
  return paused_by_script;
}

/**
 * \brief Pauses or resumes this sound from a script perspective.
 *
 * This is independent of the automatic pause/resume that can happen when
 * the window loses focus.
 *
 * \param paused \c true to pause the sound, \c false to resume it.
 */
void Sound::set_paused_by_script(bool paused_by_script) {

  this->paused_by_script = paused_by_script;
  update_paused();
}

/**
 * \brief Returns whether the sound is looped.
 * \return true if the sound is looped.
 */
bool Sound::is_looped() const {
  return looped;
}

/**
 * \brief Sets if the sound should be played in a loop.
 * \param looped true if the sound is looped.
 */
void Sound::set_looped(bool looped) {
  this->looped = looped;
}

/**
 * \brief Pauses or resumes the sound depending on the current pause state.
 */
void Sound::update_paused() {

  set_paused(paused_by_script || paused_by_system);
}

/**
 * \brief Returns the name identifying this type in Lua.
 * \return The name identifying this type in Lua.
 */
const std::string& Sound::get_lua_type_name() const {
  return LuaContext::sound_module_name;
}

}

