// SPDX-License-Identifier: Unlicense

#include "player.h"

#include "file.h"
#include "global.h"
#include "pcm.h"
#include "psarc.h"
#include "recorder.h"
#include "revorb.h"
#include "sfx.h"
#include "shred.h"
#include "sound.h"
#include "spotify.h"
#include "wem.h"

#include <chrono>
#include <thread>

static bool playNextTick = false;
static bool previewPlaying = false;

static u32 readWemFileIdFromBnkFile(const u8* data)
{
  const u32 lenBKHD = u32_le(&data[4]);

  ASSERT(lenBKHD == 28);

  const u32 offset = lenBKHD + 12;

  const u32 lenDIDX = u32_le(&data[offset]);

  ASSERT(lenDIDX == 12);
  ASSERT(offset == 40);

  const u32 fileId = u32_le(&data[offset + 4]);

  return fileId;
}

#ifdef SHR3D_AUDIO_WEBAUDIO
EM_ASYNC_JS(void*, getPcmFromOgg, (const unsigned char* oggData_in, const int oggData_inSize, int* sampleRate, int* length, float* duration, int* numChannels), {

  // decodeAudioData() accepts only an ArrayBuffer. We got a SheardArrayBuffer from emscripten.
  const oggDataView = HEAPU8.subarray(oggData_in, oggData_in + oggData_inSize);
  const ogg = new Uint8Array(oggDataView).buffer;

  const audioCtx = new AudioContext();
  const decodedBuffer = await audioCtx.decodeAudioData(ogg);

  // audio metadata
  HEAP32[sampleRate >> 2] = decodedBuffer.sampleRate;
  HEAP32[length >> 2] = decodedBuffer.length; // mono
  HEAPF32[duration >> 2] = decodedBuffer.duration;
  HEAP32[numChannels >> 2] = decodedBuffer.numberOfChannels;

  const totalSamples = decodedBuffer.length * decodedBuffer.numberOfChannels;

  // Allocate memory in WASM
  const pcmPtr = _malloc(totalSamples * 4); // float32 = 4 bytes
  const pcmHeap = HEAPF32.subarray(pcmPtr >> 2, (pcmPtr >> 2) + totalSamples);


  // Fast bulk copy per channel (planar layout)
  let offset = 0;
  for (let ch = 0; ch < decodedBuffer.numberOfChannels; ch++) {
    const channelData = decodedBuffer.getChannelData(ch);
    pcmHeap.set(channelData, offset);
    offset += decodedBuffer.length;
  }

  return pcmPtr;
  });
#endif // SHR3D_AUDIO_WEBAUDIO

#ifdef SHR3D_PSARC
static i32 findBnkTocEntry(const Psarc::Info& psarcInfo, bool preview)
{
  if (preview)
    for (i32 i = 0; i < i32(psarcInfo.tocEntries.size()); ++i)
      if (psarcInfo.tocEntries[i].name.ends_with("_preview.bnk"))
        return i;

  for (i32 i = 0; i < i32(psarcInfo.tocEntries.size()); ++i)
    if (psarcInfo.tocEntries[i].name.ends_with(".bnk") && !psarcInfo.tocEntries[i].name.ends_with("_preview.bnk"))
      return i;

  ASSERT(false);
  return -1;
}

static std::vector<u8> loadOggFromPsarc(const Psarc::Info& psarcInfo, bool preview)
{
  const i32 bnkTocEntryIndex = findBnkTocEntry(psarcInfo, preview);
  const Psarc::Info::TOCEntry& bnkTocEntry = psarcInfo.tocEntries[bnkTocEntryIndex];

  ASSERT(bnkTocEntry.content.size() > 48);
  const u32 wemFileId = readWemFileIdFromBnkFile(bnkTocEntry.content.data());

  char wemFileName[32];

  sprintf(reinterpret_cast<char*>(wemFileName), "%u.wem", wemFileId);

  for (const Psarc::Info::TOCEntry& wemTocEntry : psarcInfo.tocEntries)
  {
    if (!wemTocEntry.name.ends_with(wemFileName))
      continue;

    std::vector<u8> ogg = Wem::to_ogg(wemTocEntry.content.data(), wemTocEntry.length);

#ifdef SHR3D_REVORB
    ogg = Revorb::revorbOgg(ogg.data(), ogg.size());
#endif // SHR3D_REVORB

    return ogg;
  }

  ASSERT(false);
  return {};
}
#endif // SHR3D_PSARC

static void loadAudio(const u8* oggData, const u64 oggDataSize, const i32 sampleRate)
{
  DEBUG_PRINT("loadAudio called\n");

#ifdef SHR3D_AUDIO_WEBAUDIO
  {
    int inSampleRate;
    int musicPlaybackLength;
    f32 duration;
    int numberOfChannels;

    Global::musicPlaybackBufferLR[0] = reinterpret_cast<f32*>(getPcmFromOgg(oggData, oggDataSize, &inSampleRate, &musicPlaybackLength, &duration, &numberOfChannels));
    Global::musicPlaybackLength = i64(musicPlaybackLength);
    Global::musicPlaybackBufferLR[1] = Global::musicPlaybackBufferLR[0] + Global::musicPlaybackLength;

    ASSERT(inSampleRate == 48000);
    ASSERT(Global::musicPlaybackLength > 0);
    ASSERT(duration > 0.0f);
    ASSERT(numberOfChannels == 2);
  }
#else // SHR3D_AUDIO_WEBAUDIO
  {
    f32* musicBufferInterleaved = nullptr;
    i64 musicPlaybackLength;
    const i32 inSampleRate = Pcm::decodeOgg(oggData, oggDataSize, musicBufferInterleaved, musicPlaybackLength);
    if (inSampleRate != sampleRate)
      Pcm::resample(musicBufferInterleaved, musicPlaybackLength, inSampleRate, sampleRate);

    ASSERT(Global::musicDoubleBufferStatus == MusicDoubleBufferStatus::inital || Global::musicDoubleBufferStatus == MusicDoubleBufferStatus::DataInNonStreched0 || Global::musicDoubleBufferStatus == MusicDoubleBufferStatus::DataInNonStreched1);
    const u8 activeIndex = Global::musicDoubleBufferStatus == MusicDoubleBufferStatus::DataInNonStreched0 ? 1 : 0;

    ASSERT(Global::musicDoubleBuffer[activeIndex] == nullptr);
    Global::musicDoubleBuffer[activeIndex] = reinterpret_cast<f32*>(malloc(musicPlaybackLength * 2 * sizeof(f32)));

    for (i32 i = 0; i < musicPlaybackLength; ++i)
    {
      Global::musicDoubleBuffer[activeIndex][i] = musicBufferInterleaved[i * 2];
      Global::musicDoubleBuffer[activeIndex][i + musicPlaybackLength] = musicBufferInterleaved[i * 2 + 1];
    }
    free(musicBufferInterleaved);
    Global::musicBufferLength = musicPlaybackLength;

    Global::musicPlaybackBufferLR[0] = &Global::musicDoubleBuffer[activeIndex][0];
    Global::musicPlaybackBufferLR[1] = &Global::musicDoubleBuffer[activeIndex][musicPlaybackLength];
    Global::musicPlaybackLength = musicPlaybackLength;

    switch (Global::musicDoubleBufferStatus)
    {
    case MusicDoubleBufferStatus::inital:
      Global::musicDoubleBufferStatus = MusicDoubleBufferStatus::DataInNonStreched0;
      break;
    case MusicDoubleBufferStatus::DataInNonStreched0:
      Global::musicDoubleBufferStatus = MusicDoubleBufferStatus::DataInNonStreched1Delete0;
      break;
    case MusicDoubleBufferStatus::DataInNonStreched1:
      Global::musicDoubleBufferStatus = MusicDoubleBufferStatus::DataInNonStreched0Delete1;
      break;
    default:
      unreachable();
    }
  }
#endif // SHR3D_AUDIO_WEBAUDIO
}

static void quickRepeater()
{
  if (Global::inputQuickRepeater.pressed)
  {
    if (Global::quickRepeaterBeginTimeNS == I64::max || Global::musicTimeElapsedNS < Global::quickRepeaterBeginTimeNS)
    {
      Global::quickRepeaterBeginTimeNS = Global::musicTimeElapsedNS;
    }
    else
    {
      Global::quickRepeaterEndTimeNS = Global::musicTimeElapsedNS;
    }

    const TimeNS diff = Global::quickRepeaterEndTimeNS - Global::quickRepeaterBeginTimeNS;
    if (diff >= 0 && diff < 1_s) // reset quick repeat
    {
      Global::quickRepeaterBeginTimeNS = I64::max;
      Global::quickRepeaterEndTimeNS = I64::max;
    }
  }
  else if (Global::quickRepeaterEndTimeNS < Global::musicTimeElapsedNS || Global::inputQuickRepeaterJumpBegin.pressed)
  {
    if (Global::quickRepeaterBeginTimeNS != I64::max)
    {
      Global::musicTimeElapsedNS = Global::quickRepeaterBeginTimeNS;
      Global::musicPlaybackPosition = i64(musicTimeElapsedToPlaybackPositionNonStretched(Global::quickRepeaterBeginTimeNS, f32(sampleRate())) * Global::musicStretchRatio);
    }
    else
    {
      Global::musicTimeElapsedNS = -Global::songInfos[Global::selectedSongIndex].shredDelayBegin;
      Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(-Global::songInfos[Global::selectedSongIndex].shredDelayBegin) * f32(sampleRate()));
    }
  }
}

static void endOfSong()
{
  ASSERT(Global::selectedSongIndex >= 0);
  const Song::Info& songInfo = Global::songInfos[Global::selectedSongIndex];

#ifdef SHR3D_BENCHMARK_FPS
  Global::benchmarkScoreLast = Global::benchmarkScore;
  Global::benchmarkScore = 0;
  Global::benchmarkAvgFPS = Global::benchmarkScoreLast / TimeNS_To_Seconds(songInfo.songLength);
#endif // SHR3D_BENCHMARK_FPS

  if (Settings::applicationEndOfSong == EndOfSong::openMenu)
  {
    Global::selectedSongIndex = -1;
    Global::selectedArrangementIndex = -1;
    Global::musicTimeElapsedNS = -Global::songInfos[Global::selectedSongIndex].shredDelayBegin;
    Global::sfxToneAutoSwitchOffset = 0;
    //Global::inputHideMenu.toggled = false;
  }
  else if (Settings::applicationEndOfSong == EndOfSong::loopSong)
  {
    Global::musicTimeElapsedNS = -Global::songInfos[Global::selectedSongIndex].shredDelayBegin;
    Global::sfxToneAutoSwitchOffset = 0;
    Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(-Global::songInfos[Global::selectedSongIndex].shredDelayBegin) * f32(sampleRate()));
  }

#ifdef SHR3D_RECORDER
  if (Global::recorderBuffer.size() != 0)
  {
    const std::string wavPath = Recorder::generateWavPath(songInfo);
    const std::string wavFilepath = File::uniqueFilepathInDirectory(wavPath, ".wav");
    File::createDirectories(wavPath.c_str());
    Recorder::saveWavFile(wavFilepath.c_str(), sampleRate());
  }
#endif // SHR3D_RECORDER
}

static void tickToneSwitchAutomatic()
{
  static bool sfxChainWindowVisibleLastFrame = false;
  const bool sfxChainWindowVisible = !Global::inputHideMenu.toggled && Global::inputSfxChain.toggled;

  if (sfxChainWindowVisible)
  {
    if (!sfxChainWindowVisibleLastFrame)
    {
      Global::activeSfxToneIndex = ((Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank) + (Global::activeSfxToneIndex >= 0 ? Global::sfxToneAutoSwitchOffset : -Global::sfxToneAutoSwitchOffset);
      Global::sfxToneAutoSwitchOffset = 0;
    }
  }
  else
  {
    {
      static ArrangementIndex selectedArrangementIndexLastFrame = Global::selectedArrangementIndex;

      if (selectedArrangementIndexLastFrame != Global::selectedArrangementIndex)
      {
        const std::string& persistentId = Global::songInfos.at(Global::selectedSongIndex).arrangementInfos[Global::selectedArrangementIndex].persistentId;
        if (Global::songStats.contains(persistentId))
        {
          const SfxBankIndex sfxBankIndex = Global::songStats.at(persistentId).sfxBankIndex;
          if (sfxBankIndex != -1)
            Global::activeSfxToneIndex = sfxBankIndex * Const::sfxToneTonesPerBank;
        }

      }
      selectedArrangementIndexLastFrame = Global::selectedArrangementIndex;
    }

    if (sfxChainWindowVisibleLastFrame/* || Global::selectedArrangementIndex != selectedArrangementIndexLastFrame*/)
      Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank;

    if (Global::songTracks[Global::selectedArrangementIndex].tones.size() > 0)
    { // songTrack.tones starts with the first tone switch. Make sure Global::sfxToneAutoSwitchOffset is 0 at the start of the song.
      if (Global::songTracks[Global::selectedArrangementIndex].tones[0].timeNS > Global::musicTimeElapsedNS)
        Global::sfxToneAutoSwitchOffset = 0;
      else
      {
        for (i32 i = i32(Global::songTracks[Global::selectedArrangementIndex].tones.size()) - 1; i >= 0; --i)
        { // normal tone switches
          const Song::Tone& tone = Global::songTracks[Global::selectedArrangementIndex].tones[i];

          if (Global::musicTimeElapsedNS >= tone.timeNS)
          {
            Global::sfxToneAutoSwitchOffset = tone.id;
            break;
          }
        }
      }
    }
  }

  sfxChainWindowVisibleLastFrame = sfxChainWindowVisible;
}

static void tickToneSwitchManual()
{
  //ASSERT(Global::inputHideMenu.toggled || !Global::inputSfxChain.toggled);

#ifdef SHR3D_SFX
  static Instrument instrumentLastFrame = Settings::applicationInstrument;
  const i32 sfxTone = Global::activeSfxToneIndex + (Global::activeSfxToneIndex >= 0 ? Global::sfxToneAutoSwitchOffset : -Global::sfxToneAutoSwitchOffset);
  static i32 sfxToneLastFrame = sfxTone;

  if (instrumentLastFrame != Settings::applicationInstrument || sfxToneLastFrame != sfxTone)
  {
    Global::sfxToneTime = Global::time_;
    for (i32 i = 0; i < ARRAY_SIZE(Global::effectChain); ++i)
    {
      Global::effectChain[i].id = Global::sfxTone[sfxTone][i].id;
      Global::effectChain[i].state = Global::sfxTone[sfxTone][i].state;
      if (Global::effectChain[i].id.system != SfxSystem::empty)
      {
        i32 instance = 0;
        for (i32 j = 0; j < i; ++j)
          if (Global::effectChain[i].id.system == Global::effectChain[j].id.system && Global::effectChain[i].id.sfxIndex == Global::effectChain[j].id.sfxIndex)
            ++instance;

        Sfx::loadParameters(Global::effectChain[i].id, instance, Global::sfxParameters[sfxTone][i]);
      }
    }
  }

  instrumentLastFrame = Settings::applicationInstrument;
  sfxToneLastFrame = sfxTone;
#endif // SHR3D_SFX
}

static void stopPlayingSong()
{
  if (Global::musicPlaybackBufferLR[0] != nullptr)
  {
    Global::musicPlaybackPosition = I64::max;
    Global::musicPlaybackLength = 0;
    Global::musicPlaybackBufferLR[0] = nullptr;
    Global::musicPlaybackBufferLR[1] = nullptr;
  }
  Global::quickRepeaterBeginTimeNS = I64::max;
  Global::quickRepeaterEndTimeNS = I64::max;
}

void Player::tick()
{
  //if (Global::inputHideMenu.toggled || !Global::inputSfxChain.toggled)
  tickToneSwitchManual();

  if (!previewPlaying && Global::selectedSongIndex == -1)
    return;

  if (Global::inputFreezeHighway.toggled)
    return;

  Global::musicTimeElapsedNS += TimeNS(f64(Global::frameDelta) * (1.0 / f64(Global::musicStretchRatio)));
  if (Global::songTimePlayed != nullptr)
    *Global::songTimePlayed += Global::frameDelta;

  if (playNextTick)
  {
    Global::musicPlaybackSeekerTimeNS = 0;
    if (previewPlaying)
    {
      Global::musicPlaybackPosition = 0;
    }
    else
    {
      ASSERT(Global::selectedSongIndex >= 0);
      Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(-Global::songInfos[Global::selectedSongIndex].shredDelayBegin) * f32(sampleRate()));
      Global::musicTimeElapsedNS = -Global::songInfos[Global::selectedSongIndex].shredDelayBegin;
      Global::sfxToneAutoSwitchOffset = 0;
      Global::inputHideMenu.toggled = true;
    }
    playNextTick = false;

#ifdef SHR3D_BENCHMARK_FPS
    Global::benchmarkScore = 0;
#endif // SHR3D_BENCHMARK_FPS
  }

  if (Global::inputEject.pressed)
  {
    stopPlayingSong();
    Global::selectedSongIndex = -1;
#ifdef SHR3D_RECORDER
    Recorder::init();
#endif // SHR3D_RECORDER
    if (Global::inputHideMenu.toggled)
      Global::inputHideMenu.toggled = !Global::inputHideMenu.toggled;
    return;
  }

  if (previewPlaying)
    return;

  if (Global::musicTimeElapsedNS > (Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd))
    endOfSong();

  if (!previewPlaying)
    quickRepeater();

  if (Settings::applicationToneSwitch && Global::selectedSongArrangementToneBank != -1)
    tickToneSwitchAutomatic();
  else
    Global::sfxToneAutoSwitchOffset = 0;
}

void Player::playSong(const SongIndex songIndex, const ArrangementIndex arrangementIndex)
{
  ASSERT(songIndex >= 0);
  ASSERT(arrangementIndex >= 0);
  DEBUG_PRINT("Player::playSong(%d)\n", songIndex);

  stopPlayingSong();

  previewPlaying = false;

  const Song::Info& songInfo = Global::songInfos[songIndex];

#ifdef SHR3D_SHRED_AND_PSARC
  if (File::type(songInfo.filepath.c_str()) == File::Type::shred)
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_SHRED
  {
    const Shred::Info& shredInfo = Global::shredInfos[songIndex];
    Global::songTracks = Song::ShredParser::loadTracks(shredInfo);
    Global::songVocals = Song::ShredParser::loadVocals(shredInfo);
  }
#endif // SHR3D_SHRED
#ifdef SHR3D_SHRED_AND_PSARC
  else
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_PSARC
  {
    ASSERT(File::type(songInfo.filepath.c_str()) == File::Type::psarc);
    Global::songTracks = Song::PsarcParser::loadTracks(Global::psarcInfos[songIndex], songInfo.arrangementInfos);
    Global::songVocals = Song::PsarcParser::loadVocals(Global::psarcInfos[songIndex]);
  }
#endif // SHR3D_PSARC

  Global::songLevels.clear();
  for (i32 i = 0; i < i32(Global::songTracks.size()); ++i)
  {
    Global::songLevels[i].overallLevel = 1.0f;
    std::vector<i32>& sectionLevels = Global::songLevels[i].sectionLevels;
    const std::vector<Song::LeveledSection>& leveledSections = Global::songTracks[i].leveledSections;
    sectionLevels.resize(leveledSections.size());
    for (i32 j = 0; j < i32(leveledSections.size()); ++j)
      sectionLevels[j] = leveledSections[j].maxLevel;

    Global::songTrackLevelAdjusted[i] = Song::loadTrackLevelAdjusted(Global::songTracks[i], sectionLevels);
  }

#ifdef SHR3D_SHRED_AND_PSARC
  if (File::type(songInfo.filepath.c_str()) == File::Type::shred)
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_SHRED
  {
    if (Global::shredInfos[songIndex].songOgg.size() > 0)
    {
      //Spotify::pauseSong();
      loadAudio(Global::shredInfos[songIndex].songOgg.data(), Global::shredInfos[songIndex].songOgg.size(), sampleRate());
    }
#ifdef SHR3D_SPOTIFY
    else if (songInfo.spotifyTrackId[0] != '\0')
    {
      Spotify::playSong(songInfo.spotifyTrackId);
    }
#endif // SHR3D_SPOTIFY
  }
#endif // SHR3D_SHRED
#ifdef SHR3D_SHRED_AND_PSARC
  else
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_PSARC
  {
    const std::vector<u8> ogg = loadOggFromPsarc(Global::psarcInfos[songIndex], false);
    loadAudio(ogg.data(), ogg.size(), sampleRate());
  }
#endif // SHR3D_PSARC

  Global::selectedSongIndex = songIndex;
  Global::selectedArrangementIndex = arrangementIndex;
  Global::selectedSongArrangementToneBank = -1;
  const Arrangement::Info& arrangementInfo = Global::songInfos[songIndex].arrangementInfos[arrangementIndex];
  if (Global::songStats.contains(arrangementInfo.persistentId) && Global::songStats.at(arrangementInfo.persistentId).sfxBankIndex != -1)
  {
    Global::selectedSongArrangementToneBank = Global::songStats.at(arrangementInfo.persistentId).sfxBankIndex;
    Global::activeSfxToneIndex = Global::selectedSongArrangementToneBank * Const::sfxToneTonesPerBank;
  }

#ifdef SHR3D_SFX_CORE_HEXFIN
  Global::a4ReferenceFrequency = 440.0f * exp2f(arrangementInfo.centOffset / 1200.0f);
#endif // SHR3D_SFX_CORE_HEXFIN

  SongStats& songStats = Global::songStats[songInfo.arrangementInfos[Global::selectedArrangementIndex].persistentId];
  songStats.lastPlayed = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
  Global::songTimePlayed = &songStats.timePlayed;

  playNextTick = true;

#ifdef SHR3D_RECORDER
  Recorder::init();
#endif // SHR3D_RECORDER
}

void Player::playPreview(SongIndex songIndex)
{
  previewPlaying = true;

  stopPlayingSong();

  const Song::Info& songInfo = Global::songInfos[songIndex];

#ifdef SHR3D_SHRED_AND_PSARC
  if (File::type(songInfo.filepath.c_str()) == File::Type::shred)
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_SHRED
  {
    if (Global::shredInfos[songIndex].previewOgg.size() > 0)
      loadAudio(Global::shredInfos[songIndex].previewOgg.data(), Global::shredInfos[songIndex].previewOgg.size(), sampleRate());
  }
#endif // SHR3D_SHRED
#ifdef SHR3D_SHRED_AND_PSARC
  else
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_PSARC
  {
    ASSERT(File::type(songInfo.filepath.c_str()) == File::Type::psarc);

    const std::vector<u8> ogg = loadOggFromPsarc(Global::psarcInfos[songIndex], true);
    loadAudio(ogg.data(), ogg.size(), sampleRate());
  }
#endif // SHR3D_PSARC

  playNextTick = true;
}
