// SPDX-License-Identifier: Unlicense

#include "collection.h"

#include "file.h"
#include "global.h"
#include "ini.h"
#include "player.h"
#include "psarc.h"
#include "shred.h"
#include "song.h"
#include "string_.h"
#include "texture.h"

#ifdef PLATFORM_EMSCRIPTEN
#include <emscripten.h>
#include <emscripten/fetch.h>
#endif // PLATFORM_EMSCRIPTEN

#include <thread>

static std::map<std::u8string, std::u8string> generateArrangementDetails(const Arrangement::Info& arrangementInfo)
{
  std::map<std::u8string, std::u8string> arrangementDetails;

  //if (!(arrangementInfo.tuning.string[0] == 0 && arrangementInfo.tuning.string[1] == 0 && arrangementInfo.tuning.string[2] == 0 && arrangementInfo.tuning.string[3] == 0 && arrangementInfo.tuning.string[4] == 0 && arrangementInfo.tuning.string[5] == 0))
  {
    char8_t tuningStr[32];
    i32 pos = 0;
    for (i32 i = 0; i < Const::highwayInstrumentGuitarStringCount; ++i)
    {
      if (arrangementInfo.tuning.string[i] == -128)
        break;

      pos += sprintf(reinterpret_cast<char*>(&tuningStr[pos]), "%d,", arrangementInfo.tuning.string[i]);
    }
    if (pos >= 1)
    {
      tuningStr[pos - 1] = '\0';
      arrangementDetails.insert({ u8"Tuning", tuningStr });
    }
  }
  if (arrangementInfo.capoFret != 0)
  {
    char8_t capoFret[5];
    sprintf(reinterpret_cast<char*>(capoFret), "%d", arrangementInfo.capoFret);
    arrangementDetails.insert({ u8"CapoFret", capoFret });
  }
  if (arrangementInfo.bassPick)
    arrangementDetails.insert({ u8"BassPick", u8"1" });
  if (arrangementInfo.centOffset != 0.0)
  {
    char8_t centOffsetStr[12];
    sprintf(reinterpret_cast<char*>(centOffsetStr), "%g", arrangementInfo.centOffset);
    arrangementDetails.insert({ u8"CentOffset", centOffsetStr });
  }

  return arrangementDetails;
}

static void saveCacheSongInfo(const char8_t* filename, const Song::Info& songInfo, const u8* albumCoverData, u64 albumCoverSize, const char8_t* albumCoverExtension)
{
  { // cache SongInfo
    std::vector<Ini::SectionKeyValue> iniContent;

    {
      std::map<std::u8string, std::u8string> keyValues;
      if (songInfo.albumName.size() != 0)
        keyValues[u8"Album"] = songInfo.albumName;
      if (songInfo.artistName.size() != 0)
        keyValues[u8"Artist"] = songInfo.artistName;
      keyValues[u8"Length"] = TimeNS_To_String(songInfo.songLength);
      keyValues[u8"Length"] = TimeNS_To_String(songInfo.songLength);
      keyValues[u8"Song"] = songInfo.songName;
      if (songInfo.spotifyTrackId[0] != '\0')
        keyValues[u8"SpotifyTrackId"] = reinterpret_cast<const char8_t*>(songInfo.spotifyTrackId);
      if (songInfo.track != 0)
      {
        char8_t trackStr[8];
        sprintf(reinterpret_cast<char*>(trackStr), "%d", songInfo.track);
        keyValues[u8"Track"] = trackStr;
      }
      if (songInfo.songYear != 0)
      {
        char8_t songYearStr[8];
        sprintf(reinterpret_cast<char*>(songYearStr), "%d", songInfo.songYear);
        keyValues[u8"Year"] = songYearStr;
      }
      if (songInfo.shredDelayBegin != 0)
        keyValues[u8"DelayBegin"] = TimeNS_To_String(songInfo.shredDelayBegin);
      if (songInfo.shredDelayEnd != 0)
        keyValues[u8"DelayEnd"] = TimeNS_To_String(songInfo.shredDelayEnd);
      iniContent.push_back(Ini::SectionKeyValue{ u8"Song", keyValues });
    }

    for (i32 i = 0; i < i32(songInfo.arrangementInfos.size()); ++i)
    {
      const Arrangement::Info& arrangementInfo = songInfo.arrangementInfos[i];

      if (arrangementInfo.arrangementName == u8"Lead" || arrangementInfo.arrangementName == u8"Rhythm" || arrangementInfo.arrangementName == u8"Bass")
      {
        iniContent.push_back(
          {
            arrangementInfo.arrangementName,
            {
              generateArrangementDetails(arrangementInfo)
            }
          }
        );
      }
      else if (arrangementInfo.arrangementName == u8"Combo") // some psarc file contain a Combo arrangement. Only add it to cache if there is no Lead.
      {
        bool hasALeadArrangement = false;
        for (const Arrangement::Info& checkForLead : songInfo.arrangementInfos)
        {
          if (checkForLead.arrangementName == u8"Lead")
          {
            hasALeadArrangement = true;
            break;
          }
        }
        if (!hasALeadArrangement)
        {
          iniContent.push_back(
            {
              arrangementInfo.arrangementName,
              {
                generateArrangementDetails(arrangementInfo)
              }
            }
          );
        }
      }
    }

    ASSERT(iniContent.size() >= 1);

    const char8_t* filename_ = File::filename(filename);
    const std::u8string iniFilename = File::replaceExtension(filename_, u8".ini");
    const std::u8string cachedIniFilename = Settings::pathCache + iniFilename;

    Ini::saveIniFile_keepSectionOrder(cachedIniFilename.c_str(), iniContent);
  }

  if (albumCoverData != nullptr)
  { // cache albumCover
    const char8_t* filename_ = File::filename(filename);
    const std::u8string albumCoverFilename = File::replaceExtension(filename_, albumCoverExtension);
    const std::u8string cachedAlbumCoverFilename = Settings::pathCache + albumCoverFilename;

    File::write(cachedAlbumCoverFilename.c_str(), albumCoverData, albumCoverSize);
  }
}

static Song::Info loadCacheSongInfo(const std::map<std::u8string, std::map<std::u8string, std::u8string>>& iniContent, const char8_t* filename)
{
  Song::Info songInfo = Song::loadSongIni(iniContent, filename);
  songInfo.loadState = Song::LoadState::cache;

  return songInfo;
}

//static void addToFatCache(std::vector<Ini::SectionKeyValue>& fatCacheIni, const char8_t* filename_, const Song::Info& songInfo)
//{
//  {
//    std::map<std::u8string, std::u8string> keyValues;
//    if (songInfo.albumName.size() != 0)
//      keyValues[u8"Album"] = songInfo.albumName;
//    if (songInfo.artistName.size() != 0)
//      keyValues[u8"Artist"] = songInfo.artistName;
//    keyValues[u8"Length"] = TimeNS_To_String(songInfo.songLength);
//    keyValues[u8"Length"] = TimeNS_To_String(songInfo.songLength);
//    keyValues[u8"Song"] = songInfo.songName;
//    if (songInfo.spotifyTrackId[0] != '\0')
//      keyValues[u8"SpotifyTrackId"] = reinterpret_cast<const char8_t*>(songInfo.spotifyTrackId);
//    if (songInfo.track != 0)
//    {
//      char8_t trackStr[8];
//      sprintf(reinterpret_cast<char*>(trackStr), "%d", songInfo.track);
//      keyValues[u8"Track"] = trackStr;
//    }
//    if (songInfo.songYear != 0)
//    {
//      char8_t songYearStr[8];
//      sprintf(reinterpret_cast<char*>(songYearStr), "%d", songInfo.songYear);
//      keyValues[u8"Year"] = songYearStr;
//    }
//    if (songInfo.shredDelayBegin != 0)
//      keyValues[u8"DelayBegin"] = TimeNS_To_String(songInfo.shredDelayBegin);
//    if (songInfo.shredDelayEnd != 0)
//      keyValues[u8"DelayEnd"] = TimeNS_To_String(songInfo.shredDelayEnd);
//    fatCacheIni.push_back(Ini::SectionKeyValue{ filename_, keyValues });
//  }
//
//  for (i32 i = 0; i < songInfo.arrangementInfos.size(); ++i)
//  {
//    const Arrangement::Info& arrangementInfo = songInfo.arrangementInfos[i];
//
//    if (arrangementInfo.arrangementName == u8"Lead" || arrangementInfo.arrangementName == u8"Rhythm" || arrangementInfo.arrangementName == u8"Bass")
//    {
//      fatCacheIni.push_back(
//        {
//          std::u8string(filename_) + u8'|' + arrangementInfo.arrangementName,
//          {
//            generateArrangementDetails(arrangementInfo)
//          }
//        }
//      );
//    }
//    else if (arrangementInfo.arrangementName == u8"Combo") // some psarc file contain a Combo arrangement. Only add it to cache if there is no Lead.
//    {
//      bool hasALeadArrangement = false;
//      for (const Arrangement::Info& checkForLead : songInfo.arrangementInfos)
//      {
//        if (checkForLead.arrangementName == u8"Lead")
//        {
//          hasALeadArrangement = true;
//          break;
//        }
//      }
//      if (!hasALeadArrangement)
//      {
//        fatCacheIni.push_back(
//          {
//            std::u8string(filename_) + u8'|' + arrangementInfo.arrangementName,
//            {
//              generateArrangementDetails(arrangementInfo)
//            }
//          }
//        );
//      }
//    }
//  }
//}
//
//static void writeFatCache(const std::vector<Ini::SectionKeyValue>& fatCacheIni)
//{
//  Ini::saveIniFile_keepSectionOrder(u8"fatcache.ini", fatCacheIni);
//}

static void fillCollection()
{
  //std::vector<Ini::SectionKeyValue> fatCacheIni;

#ifdef SHR3D_SHRED
  for (const std::u8string& filepath : File::filesInDirectory(Settings::pathShred.c_str()))
  {
    if (File::type(filepath.c_str()) != File::Type::shred)
      continue;

    const char8_t* filename_ = File::filename(filepath.c_str());
    const std::u8string iniPath = File::replaceExtension(filename_, u8".ini");

    const std::u8string cachedIni = Settings::pathCache + iniPath;
    if (File::exists(cachedIni.c_str()))
    {
      const std::vector<u8> cachedIniData = File::read(cachedIni.c_str());
      const std::map<std::u8string, std::map<std::u8string, std::u8string>> iniContent = Ini::loadIniContent(reinterpret_cast<const char8_t*>(cachedIniData.data()), cachedIniData.size());
      Song::Info songInfo = loadCacheSongInfo(iniContent, reinterpret_cast<const char8_t*>(filename_));

      ASSERT(songInfo.arrangementInfos.size() != 0);

      {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
        const std::unique_lock lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
        const SongIndex songIndex = SongIndex(Global::songFilePath.size());
        Global::songFilePath[songIndex] = filepath.c_str();
        Global::songInfos[songIndex] = std::move(songInfo);
      }
    }
    else
    {
      const std::vector<u8> shredData = File::read(filepath.c_str());
      const Shred::Info shredInfo = Shred::parse(shredData.data(), shredData.size());
      Song::Info songInfo = Song::ShredParser::loadSongInfo(filepath.c_str(), shredInfo);

      if (Global::installMode == InstallMode::installed)
      {
        saveCacheSongInfo(filename_, songInfo, shredInfo.albumPng.data(), shredInfo.albumPng.size(), u8".png");
        //addToFatCache(fatCacheIni, filename_, songInfo);
      }

      SongIndex songIndex;
      {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
        const std::unique_lock lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
        songIndex = SongIndex(Global::songFilePath.size());
        Global::songFilePath[songIndex] = filepath;
        Global::songInfos[songIndex] = std::move(songInfo);
      }
    }
  }
#endif // SHR3D_SHRED

#ifdef SHR3D_PSARC
  for (const std::u8string& filepath : File::filesInDirectory(Settings::pathPsarc.c_str()))
  {
    if (File::type(filepath.c_str()) != File::Type::psarc)
      continue;
    if (File::filename(filepath.c_str()) == std::u8string(u8"rs1compatibilitydlc_p.psarc")) // TODO: check what this file contains
      continue;

    const char8_t* filename_ = File::filename(filepath.c_str());
    const std::u8string iniPath = File::replaceExtension(filename_, u8".ini");

    const std::u8string cachedIni = Settings::pathCache + iniPath;
    if (File::exists(cachedIni.c_str()))
    {
      const std::vector<u8> cachedIniData = File::read(cachedIni.c_str());
      const std::map<std::u8string, std::map<std::u8string, std::u8string>> iniContent = Ini::loadIniContent(reinterpret_cast<const char8_t*>(cachedIniData.data()), cachedIniData.size());
      Song::Info songInfo = loadCacheSongInfo(iniContent, reinterpret_cast<const char8_t*>(filename_));

      ASSERT(songInfo.arrangementInfos.size() != 0);

      {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
        const std::unique_lock lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
        const SongIndex songIndex = SongIndex(Global::songFilePath.size());
        Global::songFilePath[songIndex] = filepath.c_str();
        Global::songInfos[songIndex] = std::move(songInfo);
      }
    }
    else
    {
      const std::vector<u8> psarcData = File::read(filepath.c_str());
      //const std::vector<u8> psarcData = Psarc::readPsarcData(filepath.c_str());
      const Psarc::Info psarcInfo = Psarc::parse(psarcData.data(), psarcData.size());
      Song::Info songInfo = Song::PsarcParser::loadSongInfo(psarcInfo, File::filename(filepath.c_str()));

      if (songInfo.arrangementInfos.size() == 0)
        continue;

      if (Global::installMode == InstallMode::installed)
      {
        saveCacheSongInfo(filename_, songInfo, psarcInfo.tocEntries[songInfo.psarcAlbumCover256_tocIndex].content.data(), psarcInfo.tocEntries[songInfo.psarcAlbumCover256_tocIndex].content.size(), u8".dds");
        //addToFatCache(fatCacheIni, filename_, songInfo);
      }

      SongIndex songIndex;
      {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
        const std::unique_lock lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
        songIndex = SongIndex(Global::songFilePath.size());
        Global::songFilePath[songIndex] = filepath.c_str();
        Global::songInfos[songIndex] = std::move(songInfo);
      }
    }
  }
#endif // SHR3D_PSARC

  //if (Global::installMode == InstallMode::installed)
  //{
  //  writeFatCache(fatCacheIni);
  //} 
}

static void initFromFile()
{
  if (Global::installMode != InstallMode::showInstaller)
  {
    if (Global::installMode == InstallMode::installed)
      File::createDirectories(Settings::pathCache.c_str());

#ifdef SHR3D_COLLECTION_WORKER_THREAD
    //#if 1 // keeping a pointer to a value can become unstable when new values are inserted into the map.
    //    Global::songFilePath.reserve(10000);
    //    Global::songInfos.reserve(10000);
    //#endif
    static std::thread wokerThread(fillCollection);
#else // SHR3D_COLLECTION_WORKER_THREAD
    fillCollection();
#endif // SHR3D_COLLECTION_WORKER_THREAD
  }
}

#ifdef PLATFORM_EMSCRIPTEN
static void downloadErrorFunc(emscripten_fetch_t* fetch)
{
  DEBUG_PRINT("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);

  emscripten_fetch_close(fetch); // Also free data on failure.
}

static void fetchFileFromWebserverNoProgress(const char* url, void (*successFunc)(emscripten_fetch_t*), void (*errorFunc)(emscripten_fetch_t*) = nullptr)
{
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = successFunc;
  attr.onerror = errorFunc != nullptr ? errorFunc : downloadErrorFunc;
  emscripten_fetch(&attr, url);
}

static void successFetchCachePngFunc(emscripten_fetch_t* fetch)
{
  DEBUG_PRINT("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);

  // not optimal to seach a hashmap by value.
  for (const auto& [songIndex, filePath] : Global::albumCoverUrl)
  {
    if (reinterpret_cast<const char8_t*>(fetch->url) == filePath)
    {
      const GLuint textureId = Texture::openGlLoadTexturePng((const u8*)fetch->data, fetch->numBytes);
      {
        const std::unique_lock lock(Global::songInfosMutex);
        Global::albumCoverTexture[songIndex] = textureId;
      }
      break;
    }
  }

  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

static void successFetchCacheDdsFunc(emscripten_fetch_t* fetch)
{
  DEBUG_PRINT("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);

  // not optimal to seach a hashmap by value.
  for (const auto& [songIndex, filePath] : Global::albumCoverUrl)
  {
    if (reinterpret_cast<const char8_t*>(fetch->url) == filePath)
    {
      const GLuint textureId = Texture::openGlLoadTextureDds((const u8*)fetch->data, fetch->numBytes);
      {
        const std::unique_lock lock(Global::songInfosMutex);
        Global::albumCoverTexture[songIndex] = textureId;
      }
      break;
    }
  }

  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

static void successFetchCacheIniFunc(emscripten_fetch_t* fetch)
{
  DEBUG_PRINT("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);

  for (const auto& [songIndex, filePath] : Global::iniUrl)
  {
    if (reinterpret_cast<const char8_t*>(fetch->url) == filePath)
    {
      const std::map<std::u8string, std::map<std::u8string, std::u8string>> iniContent = Ini::loadIniContent(reinterpret_cast<const char8_t*>(fetch->data), fetch->numBytes);
      Song::Info songInfo = loadCacheSongInfo(iniContent, File::filename(reinterpret_cast<const char8_t*>(fetch->url)));
      {
        const std::unique_lock lock(Global::songInfosMutex);
        Global::songInfos[songIndex] = std::move(songInfo);
      }
      break;
    }
  }

  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

static void downloadSuccessFunc(emscripten_fetch_t* fetch)
{
  switch (File::type_cacheIpfsCid(reinterpret_cast<const char8_t*>(fetch->url), reinterpret_cast<const u8*>(fetch->data)))
  {
  case File::Type::shred:
  {
    if (Global::downloadSongIndexInProgress == -1)
      Global::downloadSongIndexInProgress = Global::songFilePath.size();

    DEBUG_PRINT("downloadSuccessFunc(): Finished downloading shred %llu bytes from URL %s, Global::downloadSongIndexInProgress=%d.\n", fetch->numBytes, fetch->url, Global::downloadSongIndexInProgress);
    ASSERT(Global::downloadSongIndexInProgress >= 0);

    const std::u8string songFilePath = reinterpret_cast<const char8_t*>(fetch->url);

    Shred::Info shredInfo = Shred::parse(reinterpret_cast<const u8*>(fetch->data), fetch->numBytes);
    Song::Info songInfo = Song::ShredParser::loadSongInfo(reinterpret_cast<const char8_t*>(fetch->url), shredInfo);
    Song::ShredParser::loadSongTones(Global::downloadSongIndexInProgress, songInfo, shredInfo);

    {
      const std::unique_lock lock(Global::songInfosMutex);
      Global::songFilePath[Global::downloadSongIndexInProgress] = songFilePath;
      Global::shredInfos[Global::downloadSongIndexInProgress] = std::move(shredInfo);
      Global::songInfos[Global::downloadSongIndexInProgress] = std::move(songInfo);
    }

    emscripten_fetch_close(fetch); // Free data associated with the fetch.

    Global::downloadSongIndexFinished = Global::downloadSongIndexInProgress;
    Global::downloadSongIndexInProgress = -1;
    Global::downloadBytesFinished = 0;
    Global::downloadBytesInProgress = 0;
  }
  break;
  case File::Type::psarc:
  {
    if (Global::downloadSongIndexInProgress == -1)
      Global::downloadSongIndexInProgress = Global::songFilePath.size();

    DEBUG_PRINT("downloadSuccessFunc(): Finished downloading psarc %llu bytes from URL %s, Global::downloadSongIndexInProgress=%d.\n", fetch->numBytes, fetch->url, Global::downloadSongIndexInProgress);
    ASSERT(Global::downloadSongIndexInProgress >= 0);

    const std::u8string songFilePath = reinterpret_cast<const char8_t*>(fetch->url);

    Psarc::Info psarcInfo = Psarc::parse(reinterpret_cast<const u8*>(fetch->data), fetch->numBytes);
    Song::Info songInfo = Song::PsarcParser::loadSongInfo(psarcInfo, File::filename(reinterpret_cast<const char8_t*>(fetch->url)));

    {
      const std::unique_lock lock(Global::songInfosMutex);
      Global::songFilePath[Global::downloadSongIndexInProgress] = songFilePath;
      Global::psarcInfos[Global::downloadSongIndexInProgress] = std::move(psarcInfo);
      Global::songInfos[Global::downloadSongIndexInProgress] = std::move(songInfo);
    }

    emscripten_fetch_close(fetch); // Free data associated with the fetch.

    Global::downloadSongIndexFinished = Global::downloadSongIndexInProgress;
    Global::downloadSongIndexInProgress = -1;
    Global::downloadBytesFinished = 0;
    Global::downloadBytesInProgress = 0;
  }
  break;
  case File::Type::unknown:
  {
    DEBUG_PRINT("downloadSuccessFunc(): Finished downloading unknown %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);

    const std::u8string urlsTxt(fetch->data, fetch->data + fetch->numBytes - 1);

    emscripten_fetch_close(fetch); // Free data associated with the fetch.
    fetch = nullptr;

    const std::vector<std::u8string> lines = String::split(urlsTxt, u8'\n');

    for (const std::u8string& line : lines)
    {
      const std::vector<std::u8string> urls = String::split(line, u8'|');

      ASSERT(urls.size() >= 1);

      if (urls.size() >= 1)
      {
        const std::unique_lock lock(Global::songInfosMutex);
        const SongIndex songIndex = SongIndex(Global::songFilePath.size());
        Global::songFilePath[songIndex] = urls[0];
        if (urls.size() >= 2)
          Global::iniUrl[songIndex] = urls[1];
        if (urls.size() >= 3)
          Global::albumCoverUrl[songIndex] = urls[2];
      }
      if (urls.size() >= 2)
      {
        fetchFileFromWebserverNoProgress(reinterpret_cast<const char*>(urls[1].c_str()), successFetchCacheIniFunc);
      }
      if (urls.size() >= 3)
      {
        if (File::extension(urls[2].c_str()) == std::u8string(u8".png"))
        {
          DEBUG_PRINT("png\n");
          fetchFileFromWebserverNoProgress(reinterpret_cast<const char*>(urls[2].c_str()), successFetchCachePngFunc);
        }
        else
        {
          DEBUG_PRINT("dds\n");
          fetchFileFromWebserverNoProgress(reinterpret_cast<const char*>(urls[2].c_str()), successFetchCacheDdsFunc);
        }
      }
    }
  }
  break;
  default:

    unreachable();
  }
}

static void downloadProgressFunc(emscripten_fetch_t* fetch)
{
  Global::downloadBytesFinished = fetch->totalBytes;
  Global::downloadBytesInProgress = fetch->dataOffset;
}

void Collection::downloadFullSongWithProgress(const SongIndex songIndex)
{
  DEBUG_PRINT("downloadFullSongWithProgress()\n");

  Global::downloadSongIndexInProgress = songIndex;
  const std::u8string filepath = Global::songFilePath[Global::downloadSongIndexInProgress];

  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = downloadSuccessFunc;
  attr.onprogress = downloadProgressFunc;
  attr.onerror = downloadErrorFunc;
  emscripten_fetch(&attr, reinterpret_cast<const char*>(filepath.c_str()));
}
#endif // PLATFORM_EMSCRIPTEN

void Collection::init()
{
#ifdef PLATFORM_EMSCRIPTEN
  fetchFileFromWebserverNoProgress(reinterpret_cast<const char*>(Global::autoFetchUrl.c_str()), &downloadSuccessFunc);
#else // PLATFORM_EMSCRIPTEN
  if (!Global::skipFetchingCollection)
    initFromFile();
#endif // PLATFORM_EMSCRIPTEN
}

void Collection::addSongFile(const std::u8string& filepath, const u8* songData, const u64 songDataSize)
{
#ifdef SHR3D_SHRED_AND_PSARC
  if (File::type(filepath.c_str()) == File::Type::shred)
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_SHRED
  {
    Shred::Info shredInfo = Shred::parse(songData, songDataSize);
    Song::Info songInfo = Song::ShredParser::loadSongInfo(File::filename(filepath.c_str()), shredInfo);

    {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
      const std::unique_lock lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
      const SongIndex songIndex = SongIndex(Global::songFilePath.size());
      Global::songFilePath[songIndex] = filepath;
      Global::shredInfos[songIndex] = std::move(shredInfo);
      Global::songInfos[songIndex] = std::move(songInfo);
    }
  }
#endif // SHR3D_SHRED
#ifdef SHR3D_SHRED_AND_PSARC
  else
#endif // SHR3D_SHRED_AND_PSARC
#ifdef SHR3D_PSARC
  {
    ASSERT(File::type(filepath.c_str()) == File::Type::psarc);

    Psarc::Info psarcInfo = Psarc::parse(songData, songDataSize);
    Song::Info songInfo = Song::PsarcParser::loadSongInfo(psarcInfo, File::filename(filepath.c_str()));

    ASSERT(songInfo.arrangementInfos.size() >= 1);
    if (songInfo.arrangementInfos.size() == 0)
      return;

    {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
      const std::unique_lock lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
      const SongIndex songIndex = SongIndex(Global::songFilePath.size());
      Global::songFilePath[songIndex] = filepath;
      Global::psarcInfos[songIndex] = std::move(psarcInfo);
      Global::songInfos[songIndex] = std::move(songInfo);
    }
  }
#endif // SHR3D_PSARC
}
