/*
Copyright (c) 2009 Peter "Corsix" Cawley

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include "th_sound.h"

#include "config.h"

#include <SDL_mixer.h>
#include <SDL_rwops.h>

#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <new>

#include "th.h"

// https://github.com/CorsixTH/theme-hospital-spec/blob/master/format-specification.md#info
constexpr size_t archive_header_size = 234;
constexpr size_t archive_header_table_position_offset = 50;
constexpr size_t archive_header_table_length_offset = 58;
constexpr size_t sound_entry_size = 32;
constexpr size_t sound_entry_position_offset = 18;
constexpr size_t sound_entry_length_offset = 26;

bool sound_archive::load_from_th_file(const uint8_t* pData,
                                      size_t iDataLength) {
  if (iDataLength < sizeof(uint32_t) + archive_header_size) {
    return false;
  }

  // Last 4 bytes of the file is the position of the header
  uint32_t headerPosition =
      bytes_to_uint32_le(pData + iDataLength - sizeof(uint32_t));

  // safety check on header position
  if (static_cast<size_t>(headerPosition) >=
      iDataLength - archive_header_size) {
    return false;
  }

  // table position
  size_t tablePosition = static_cast<size_t>(bytes_to_uint32_le(
      pData + headerPosition + archive_header_table_position_offset));
  size_t tableLength = static_cast<size_t>(bytes_to_uint32_le(
      pData + headerPosition + archive_header_table_length_offset));

  data.resize(iDataLength);
  std::copy_n(pData, iDataLength, data.begin());

  size_t soundFileCount = tableLength / sound_entry_size;
  sound_files.clear();
  for (int i = 0; i < soundFileCount; ++i) {
    size_t offset = tablePosition + i * sound_entry_size;
    if (offset + sound_entry_size > iDataLength) {
      return false;  // Out of bounds
    }
    sound_dat_sound_info soundInfo{
        {},
        bytes_to_uint32_le(data.data() + offset + sound_entry_position_offset),
        bytes_to_uint32_le(data.data() + offset + sound_entry_length_offset)};
    std::copy_n(data.data() + offset, soundInfo.sound_name.size(),
                soundInfo.sound_name.begin());
    sound_files.push_back(soundInfo);
  }

  return true;
}

size_t sound_archive::get_number_of_sounds() const {
  return sound_files.size();
}

const char* sound_archive::get_sound_name(size_t iIndex) const {
  if (iIndex >= sound_files.size()) return nullptr;
  return sound_files[iIndex].sound_name.data();
}

constexpr uint32_t fourcc(const char c1, const char c2, const char c3,
                          const char c4) {
  return ((static_cast<uint32_t>(static_cast<uint8_t>(c1)) << 0) |
          (static_cast<uint32_t>(static_cast<uint8_t>(c2)) << 8) |
          (static_cast<uint32_t>(static_cast<uint8_t>(c3)) << 16) |
          (static_cast<uint32_t>(static_cast<uint8_t>(c4)) << 24));
}

namespace {

template <typename A, typename B>
inline uint64_t mul64(A a, B b) {
  return static_cast<uint64_t>(a) * static_cast<uint64_t>(b);
}

}  // namespace

size_t sound_archive::get_sound_duration(size_t iIndex) {
  SDL_RWops* pFile = load_sound(iIndex);
  if (!pFile) {
    return 0;
  }

  uint16_t iWaveAudioFormat = 0;
  uint16_t iWaveChannelCount = 0;
  uint32_t iWaveSampleRate = 0;
  uint32_t iWaveByteRate = 0;
  uint16_t iWaveBlockAlign = 0;
  uint16_t iWaveBitsPerSample = 0;
  uint32_t iWaveDataLength = 0;

  // This is a very crude RIFF parser, but it does the job.
  uint32_t iFourCC;
  uint32_t iChunkLength;
  for (;;) {
    if (SDL_RWread(pFile, &iFourCC, 4, 1) != 1) {
      break;
    }
    if (SDL_RWread(pFile, &iChunkLength, 4, 1) != 1) {
      break;
    }
    if (iFourCC == fourcc('R', 'I', 'F', 'F') ||
        iFourCC == fourcc('L', 'I', 'S', 'T')) {
      if (iChunkLength >= 4) {
        if (SDL_RWread(pFile, &iFourCC, 4, 1) != 1) {
          break;
        } else {
          continue;
        }
      }
    }
    if (iFourCC == fourcc('f', 'm', 't', ' ') && iChunkLength >= 16) {
      if (SDL_RWread(pFile, &iWaveAudioFormat, 2, 1) != 1) {
        break;
      }
      if (SDL_RWread(pFile, &iWaveChannelCount, 2, 1) != 1) {
        break;
      }
      if (SDL_RWread(pFile, &iWaveSampleRate, 4, 1) != 1) {
        break;
      }
      if (SDL_RWread(pFile, &iWaveByteRate, 4, 1) != 1) {
        break;
      }
      if (SDL_RWread(pFile, &iWaveBlockAlign, 2, 1) != 1) {
        break;
      }
      if (SDL_RWread(pFile, &iWaveBitsPerSample, 2, 1) != 1) {
        break;
      }
      iChunkLength -= 16;
    }
    // Finally:
    if (iFourCC == fourcc('d', 'a', 't', 'a')) {
      iWaveDataLength = iChunkLength;
      break;
    }
    if (SDL_RWseek(pFile, iChunkLength + (iChunkLength & 1), RW_SEEK_CUR) ==
        -1) {
      break;
    }
  }
  SDL_RWclose(pFile);
  if (iWaveAudioFormat != 1 || iWaveChannelCount == 0 || iWaveSampleRate == 0 ||
      iWaveDataLength == 0 || iWaveBitsPerSample == 0) {
    return 0;
  }

  return static_cast<size_t>(
      mul64(iWaveDataLength, 8000) /
      mul64(mul64(iWaveBitsPerSample, iWaveChannelCount), iWaveSampleRate));
}

SDL_RWops* sound_archive::load_sound(size_t iIndex) {
  if (iIndex >= sound_files.size()) {
    return nullptr;
  }

  sound_dat_sound_info pFile = sound_files[iIndex];
  return SDL_RWFromConstMem(data.data() + pFile.position,
                            static_cast<int>(pFile.length));
}

sound_player* sound_player::singleton = nullptr;

sound_player::sound_player()
    : sounds(nullptr),
      sound_count(0),
      camera_x(0),
      camera_y(0),
      camera_radius(1.0),
      master_volume(1.0),
      sound_effect_volume(0.5),
      positionless_volume(MIX_MAX_VOLUME),
      sound_effects_enabled(true) {
  singleton = this;
  Mix_AllocateChannels(number_of_channels);
  Mix_ChannelFinished(on_channel_finished);
  channels.fill(null_handle);

  // Avoid likely having the same handles in save games
  auto seconds = std::chrono::duration_cast<std::chrono::seconds>(
                     std::chrono::system_clock::now().time_since_epoch())
                     .count();
  next_playing_track_handle = static_cast<uint32_t>(seconds);
}

sound_player::~sound_player() {
  populate_from(nullptr);
  if (singleton == this) {
    singleton = nullptr;
  }
}

void sound_player::on_channel_finished(int iChannel) {
  sound_player* pThis = get_singleton();
  if (pThis == nullptr) return;

  pThis->release_channel(iChannel);
}

sound_player* sound_player::get_singleton() { return singleton; }

void sound_player::populate_from(sound_archive* pArchive) {
  for (size_t i = 0; i < sound_count; ++i) {
    Mix_FreeChunk(sounds[i]);
  }
  delete[] sounds;
  sounds = nullptr;
  sound_count = 0;

  if (pArchive == nullptr) return;

  sounds = new Mix_Chunk*[pArchive->get_number_of_sounds()];
  for (; sound_count < pArchive->get_number_of_sounds(); ++sound_count) {
    sounds[sound_count] = nullptr;
    if (SDL_RWops* pRwop = pArchive->load_sound(sound_count)) {
      sounds[sound_count] = Mix_LoadWAV_RW(pRwop, 1);
      if (sounds[sound_count]) {
        Mix_VolumeChunk(sounds[sound_count], MIX_MAX_VOLUME);
      }
    }
  }
}

uint32_t sound_player::play(size_t iIndex, double dVolume, int loops) {
  if (iIndex >= sound_count || !sounds[iIndex]) {
    return null_handle;
  }

  return play_raw(iIndex, static_cast<int>(positionless_volume * dVolume),
                  loops);
}

uint32_t sound_player::play_at(size_t iIndex, int iX, int iY, int loops) {
  if (sound_effects_enabled) {
    return play_at(iIndex, sound_effect_volume, iX, iY, loops);
  }
  return null_handle;
}

uint32_t sound_player::play_at(size_t iIndex, double dVolume, int iX, int iY,
                               int loops) {
  if (iIndex >= sound_count || !sounds[iIndex]) {
    return null_handle;
  }

  const double fDX = iX - camera_x;
  const double fDY = iY - camera_y;
  double fDistance = sqrt(fDX * fDX + fDY * fDY);
  if (fDistance > camera_radius) return null_handle;
  fDistance = fDistance / camera_radius;

  double fVolume =
      master_volume * (1.0 - fDistance * 0.8) * (MIX_MAX_VOLUME * dVolume);

  return play_raw(iIndex, static_cast<int>(std::lround(fVolume)), loops);
}

sound_player::toggle_pause_result sound_player::toggle_pause(uint32_t handle) {
  int channel = playing_channel_for_handle(handle);
  if (channel < 0) {
    return toggle_pause_result::error;
  }
  if (Mix_Paused(channel)) {
    Mix_Resume(channel);
    return toggle_pause_result::resumed;
  } else {
    Mix_Pause(channel);
    return toggle_pause_result::paused;
  }
}

void sound_player::stop(uint32_t handle) {
  int channel = playing_channel_for_handle(handle);
  if (channel < 0) {
    return;
  }
  Mix_HaltChannel(channel);
  release_channel(channel);
}

bool sound_player::is_playing(uint32_t handle) {
  return playing_channel_for_handle(handle) >= 0;
}

void sound_player::set_sound_effect_volume(double dVolume) {
  sound_effect_volume = dVolume;
}

void sound_player::set_sound_effects_enabled(bool bOn) {
  sound_effects_enabled = bOn;
}

int sound_player::reserve_channel() {
  std::scoped_lock lock(channel_mutex);
  for (size_t i = 0; i < channels.size(); ++i) {
    if (channels[i] == null_handle) {
      // Don't assign a null handle to a playing track
      if (next_playing_track_handle == null_handle) {
        next_playing_track_handle++;
      }
      channels[i] = next_playing_track_handle++;
      return static_cast<int>(i);
    }
  }

  return -1;
}

void sound_player::release_channel(int iChannel) {
  std::scoped_lock lock(channel_mutex);
  channels[iChannel] = null_handle;
}

uint32_t sound_player::play_raw(size_t iIndex, int iVolume, int loops) {
  int iChannel = reserve_channel();
  if (iChannel < 0) {
    return null_handle;
  }

  Mix_Volume(iChannel, iVolume);
  Mix_PlayChannel(iChannel, sounds[iIndex], loops);

  return channels[iChannel];
}

void sound_player::set_camera(int iX, int iY, int iRadius) {
  camera_x = iX;
  camera_y = iY;
  camera_radius = static_cast<double>(iRadius);
  if (camera_radius < 0.001) camera_radius = 0.001;
}

int sound_player::playing_channel_for_handle(uint32_t handle) {
  std::scoped_lock lock(channel_mutex);

  if (handle == null_handle) {
    return -1;
  }

  for (size_t i = 0; i < channels.size(); ++i) {
    if (channels[i] == handle) {
      return static_cast<int>(i);
    }
  }
  return -1;
}
