/*
 * 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/Sound.h"
#include "solarus/audio/Music.h"
#include "solarus/audio/MusicSystem.h"
#include "solarus/core/CurrentQuest.h"
#include "solarus/core/MainLoop.h"
#include "solarus/core/ResourceProvider.h"
#include "solarus/lua/LuaBind.h"
#include "solarus/lua/LuaContext.h"
#include "solarus/lua/LuaTools.h"
#include <lua.hpp>

namespace Solarus {

/**
 * Name of the Lua table representing the audio module.
 */
const std::string LuaContext::audio_module_name = "sol.audio";

/**
 * \brief Shows a lua error in the console about the incompatible audio context as first argument of audio functions.
 */
static void show_context_error(lua_State* l, const std::string& function_name) {
  std::ostringstream oss;
  oss << "(Thread " << l <<  ")";
  std::string message = "bad argument #1 to " + function_name + " (game, map or sol.main expected, got " +
      LuaTools::get_type_name(l, 1) + "), will use a default context (sol.main) instead";
  lua_pushcfunction(l, LuaContext::l_backtrace);
  lua_pushlstring(l, message.c_str(), message.size());
  LuaTools::call_function(l, 1, 1, "traceback");
  std::string backtrace = LuaTools::check_string(l, -1);
  lua_pop(l, 1);
  Debug::error(backtrace + "\n" + oss.str());
}

/**
 * \brief Returns the audio context identifier from a lua state if any.
 * \param l The lua state
 * \return The audio context id or nullopt if not main, game or map
 */
static std::optional<AudioContext::Identifier> get_context_id(lua_State* l) {
  if (LuaContext::is_main(l, 1)) {
    return AudioContext::Identifier::MAIN;
  } else if (LuaContext::is_game(l, 1)) {
    return AudioContext::Identifier::GAME;
  } else if (LuaContext::is_map(l, 1)) {
    return AudioContext::Identifier::MAP;
  }
  return std::nullopt;
}

/**
 * \brief Returns the audio context from a lua state if any.
 * \param l The lua state
 * \param default_context_id the identifier of the default context if the one from lua context was not found
 * \param invoker_function_name the name of the function that calls this one to inform the quest maker in case of error
 * \return A shared pointer to the context object
 */
static std::shared_ptr<AudioContext> get_context(
  lua_State* l,
  AudioContext::Identifier default_context_id,
  const std::string& invoker_function_name
) {
  AudioContext::Identifier context_id = default_context_id;

  if (CurrentQuest::is_format_at_least({ 2, 1 })) {
    if (lua_type(l, 1) != LUA_TSTRING) {
      // The first parameter is the context.
      auto id = get_context_id(l);
      if (id.has_value()) {
        context_id = id.value();
      } else {
        // The first parameter is an invalid context.
        // Show an error message without raising a Lua error
        // We can survive this by just using a default context as fallback.
        show_context_error(l, invoker_function_name);
      }
      // Remove the context from the argument stack so subsequent
      // argument reads (e.g. sound/music id at index 1) are correct.
      lua_remove(l, 1);
    }
  }

  return AudioContext::get_context(context_id);
}

/**
 * \brief Implementation of sol.audio.preload_sounds().
 * \param context The LuaContext that is calling this function.
 */
static void preload_sounds(LuaContext & context) {
  context.warning_deprecated(
      { 2, 0 },
      "sol.audio.preload_sounds()",
      "Sounds are always preloaded now."
  );
}

/**
 * \brief Implementation of sol.audio.play_sound().
 * \param l The lua state
 */
static void play_sound(LuaContext& lua_context) {
  lua_State* l = lua_context.get_internal_state();

  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "play_sound");
  
  const std::string& sound_id = LuaTools::check_string(l, 1);
  SoundBuffer& sound_buffer = lua_context.get_main_loop().get_resource_provider().get_sound(sound_id);
  SoundPtr sound = Sound::create(sound_buffer);
  
  sound->set_audio_context_id(audio_context->get_identifier());
  audio_context->play_sound(sound);
}

/**
 * \brief Implementation of sol.audio.play_music().
 * \param l The lua state
 */
static void play_music(lua_State* l) {
  const std::string& music_id = LuaTools::check_string(l, 1);
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "play_music");

  if (music_id == Music::unchanged) {
    // Special value "same": keep the current music as it is.
    return;
  }

  const MusicPtr& current_music = audio_context->get_common_music();
  if (current_music != nullptr && current_music->get_id() == music_id) {
    // This music is already playing: let it continue instead of restarting it.
    return;
  }

  if (music_id == Music::none) {
    audio_context->stop_common_music();
    return;
  }

  MusicPtr music = Music::create(music_id);
  music->set_audio_context_id(audio_context->get_identifier());
  audio_context->play_common_music(music);
}

/**
 * \brief Implementation of sol.audio.stop_music().
 * \param l The lua state
 */
static void stop_music(lua_State* l) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "stop_music");
  audio_context->stop_common_music();
}

/**
 * \brief Implementation of sol.audio.get_music().
 * \return The music id if music is playing, otherwise an empty optional.
 */
static std::optional<std::string> get_music(lua_State* l) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "get_music");
  MusicPtr music = audio_context->get_common_music();
  const std::string& music_id = music->get_id();

  if (music_id == Music::none) {
    return std::nullopt;
  } else {
    return std::make_optional(music_id);
  }
}

/**
 * \brief Implementation of sol.audio.get_music_format().
 * \return The format name if music is playing, otherwise an empty optional.
 */
static std::optional<std::string> get_music_format(lua_State* l) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "get_music_format");
  MusicPtr music = audio_context->get_common_music();
  const Music::Format format = music->get_format();

  if (format == Music::FORMAT_NONE) {
    // No music is playing.
    return std::nullopt;
  } else {
    return std::make_optional(Music::format_names[format]);
  }
}

/**
 * \brief Implementation of sol.audio.get_music_num_channels().
 * \return If the currently playing music, return its number of channels,
 *   otherwise an empty optional.
 */
static std::optional<int> get_music_num_channels(lua_State* l) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "get_music_num_channels");
  MusicPtr music = audio_context->get_common_music();

  if (music->get_format() != Music::FORMAT_IT) {
    return std::nullopt;
  } else {
    return std::make_optional(music->get_num_channels());
  }
}

/**
 * \brief Implementation of sol.audio.get_music_channel_volume().
 * \param l The Lua context that is calling this function.
 * \param channel Index of a channel.
 * \return If the playing music has channels, volume of the channel,
 *   otherwise an empty optional.
 */
static std::optional<int> get_music_channel_volume(lua_State* l, int channel) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "get_music_channel_volume");
  MusicPtr music = audio_context->get_common_music();
  
  if (music->get_format() != Music::FORMAT_IT) {
    return std::nullopt;
  } else {
    if (CurrentQuest::is_format_at_most({ 1, 6 })) {
      // Offset channel number for 1.6 quests and lower
      channel++;
    }
    if (channel < 1 || channel > music->get_num_channels()) {
      LuaTools::arg_error(l, 1,
        "Invalid channel number: " + std::to_string(channel));
      return std::nullopt;
    } else {
      return std::make_optional(music->get_channel_volume(channel));
    }
  }
}

/**
 * \brief Implementation of sol.audio.set_music_channel_volume().
 * \param l The Lua context that is calling this function.
 * \param channel Index of a channel.
 * \param volume The new volume for the channel.
 * \return Whether or not the current music has channels.
 */
static bool set_music_channel_volume(lua_State* l, int channel, int volume) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "set_music_channel_volume");
  MusicPtr music = audio_context->get_common_music();

  if (music->get_format() != Music::FORMAT_IT) {
    return false;
  } else {
    if (CurrentQuest::is_format_at_most({ 1, 6 })) {
      // Offset channel number for 1.6 quests and lower
      channel++;
    }
    if (channel < 1 || channel > music->get_num_channels()) {
      LuaTools::arg_error(l, 1,
        "Invalid channel number: " + std::to_string(channel));
    } else {
      music->set_channel_volume(channel, volume);
    }
    return true;
  }
}

/**
 * \brief Implementation of sol.audio.get_music_tempo().
 * \return The tempo value of the current music if it supports it,
 *   otherwise an empty optional.
 */
static std::optional<int> get_music_tempo(lua_State* l) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "get_music_tempo");
  MusicPtr music = audio_context->get_common_music();

  if (music->get_format() != Music::FORMAT_IT) {
    return std::nullopt;
  } else {
    return std::make_optional(music->get_tempo());
  }
}

/**
 * \brief Implementation of sol.audio.set_music_tempo().
 * \param tempo The new tempo value for the current music.
 * \return Whether or not the current music supports tempo.
 */
static bool set_music_tempo(lua_State* l, int tempo) {
  auto audio_context = get_context(l, AudioContext::Identifier::MAIN, "set_music_channel_volume");
  MusicPtr music = audio_context->get_common_music();

  if (music->get_format() != Music::FORMAT_IT) {
    return false;
  } else {
    music->set_tempo(tempo);
    return true;
  }
}

/**
 * \brief Initializes the audio features provided to Lua.
 */
void LuaContext::register_audio_module() {
  // Functions of sol.audio.
  const std::vector<luaL_Reg> functions = {
    { "preload_sounds", LUA_TO_C_BIND(preload_sounds) },  
    { "play_sound", LUA_TO_C_BIND(play_sound) },
    { "get_sound_volume", LUA_TO_C_BIND(Sound::get_global_volume) },
    { "set_sound_volume", LUA_TO_C_BIND(Sound::set_global_volume) },
    { "play_music", LUA_TO_C_BIND(play_music) },
    { "stop_music", LUA_TO_C_BIND(stop_music) },
    { "get_music_volume", LUA_TO_C_BIND(MusicSystem::get_global_volume) },
    { "set_music_volume", LUA_TO_C_BIND(MusicSystem::set_global_volume) },
    { "get_music", LUA_TO_C_BIND(get_music) },
    { "get_music_format", LUA_TO_C_BIND(get_music_format) },
    { "get_music_num_channels", LUA_TO_C_BIND(get_music_num_channels) },
    { "get_music_channel_volume", LUA_TO_C_BIND(get_music_channel_volume) },
    { "set_music_channel_volume", LUA_TO_C_BIND(set_music_channel_volume) },
    { "get_music_tempo", LUA_TO_C_BIND(get_music_tempo) },
    { "set_music_tempo", LUA_TO_C_BIND(set_music_tempo) }
  };
  register_functions(audio_module_name, functions);
}

}

