// 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>
#else // PLATFORM_EMSCRIPTEN
#ifdef PLATFORM_WINDOWS
#include <io.h>
#else // PLATFORM_WINDOWS
#include <unistd.h>
#endif // PLATFORM_WINDOWS
#endif // PLATFORM_EMSCRIPTEN

#include <thread>

#ifndef PLATFORM_EMSCRIPTEN
static void repairCacheTsvFileCorruptLastLine()
{
  FILE* cacheTsv = File::fopen_u8("cache.tsv", "r+b");
  if (cacheTsv == nullptr)
    return;

  fseek(cacheTsv, 0, SEEK_END);
  const size_t size = ftell(cacheTsv);
  if (size <= 0)
  {
    fclose(cacheTsv);
    return;
  }

  constexpr size_t BUF_SIZE = 4096;
  char buffer[BUF_SIZE];

  long pos = size;

  while (pos > 0)
  {
    const size_t chunk = min_(static_cast<long>(BUF_SIZE), pos);
    pos -= chunk;

    fseek(cacheTsv, pos, SEEK_SET);
    fread(buffer, 1, chunk, cacheTsv);

    for (long i = chunk - 1; i >= 0; --i)
    {
      if (buffer[i] == '\n')
      {
        const long new_size = pos + i + 1;

#ifdef PLATFORM_WINDOWS
        _chsize(_fileno(cacheTsv), new_size);
#else // PLATFORM_WINDOWS
        ftruncate(fileno(cacheTsv), new_size);
#endif // PLATFORM_WINDOWS
        fclose(cacheTsv);
        return;
      }
    }
  }
#ifdef PLATFORM_WINDOWS
  _chsize(_fileno(cacheTsv), 0);
#else // PLATFORM_WINDOWS
  ftruncate(fileno(cacheTsv), 0);
#endif // PLATFORM_WINDOWS

  fclose(cacheTsv);
}

static void appendCacheTsvLine(const Song::Info& songInfo, FILE* cacheTsvWrite, std::string& row)
{
  row = songInfo.filepath;
  row += '\t';
  row += to_string(File::modTime(songInfo.filepath.c_str()));
  row += '\t';
  row += songInfo.songName;
  row += '\t';
  row += songInfo.artistName;
  row += '\t';
  row += songInfo.albumName;
  row += '\t';
  row += to_string(songInfo.songYear);
  row += '\t';
  row += TimeNS_To_String(songInfo.songLength);
  row += '\t';
  row += to_string(songInfo.track);
  row += '\t';
  for (i32 i = 0; i < songInfo.arrangementInfos.size(); ++i)
  {
    const Arrangement::Info& arrangementInfo = songInfo.arrangementInfos[i];
    if (i != 0)
      row += '|';

    row += arrangementInfo.arrangementName;
    row += ':';
    row += to_string(arrangementInfo.centOffset);
    row += ':';
    row += to_string(arrangementInfo.tuning.string[0]);
    for (i32 i = 1; i < 8; ++i)
    {
      if (arrangementInfo.tuning.string[i] == -128)
        break;
      row += ',';
      row += to_string(arrangementInfo.tuning.string[i]);
    }
  }
  row += '\n';
  fwrite(row.c_str(), row.size(), 1, cacheTsvWrite);
}

static void writeCleanCacheTsvFile()
{
  FILE* cacheTsvWrite = File::fopen_u8("cache.tsv.tmp", "wb");
  ASSERT(cacheTsvWrite != nullptr);
  std::string row;
  row.reserve(512);
  for (const auto& [songIndex, songInfo] : Global::songInfos)
  {
    appendCacheTsvLine(songInfo, cacheTsvWrite, row);
  }
  fclose(cacheTsvWrite);
  i32 res = remove("cache.tsv");
  if (res == 0 || res == -1)
  {
    res = rename("cache.tsv.tmp", "cache.tsv");
    if (res == -1)
    {
      res = remove("cache.tsv.tmp");
      ASSERT(res == 0);
    }
  }
}
#endif // PLATFORM_EMSCRIPTEN

#ifndef PLATFORM_EMSCRIPTEN
static std::unordered_map<std::string, Song::Info> readCacheTsvFile()
{
  std::unordered_map<std::string, Song::Info> readCacheTsv;

  FILE* cacheTsvRead = File::fopen_u8("cache.tsv", "rb");
  if (cacheTsvRead == nullptr)
    return readCacheTsv;

  fseek(cacheTsvRead, 0, SEEK_END);
  const size_t size = ftell(cacheTsvRead);
  rewind(cacheTsvRead);
  std::string content;
  content.resize(size);
  fread(content.data(), 1, size, cacheTsvRead);
  fclose(cacheTsvRead);

  if (content.empty())
    return readCacheTsv;

  const char* ptr = content.data();
  const char* end = ptr + content.size();

  while (ptr < end)
  {
    const char* line_start = ptr;
    const char* col_start = line_start;
    std::string_view columns[9];
    int col_index = 0;

    // Scan columns (look for 8 tabs)
    while (ptr < end && col_index < 8)
    {
      if (*ptr == '\t')
      {
        columns[col_index++] = std::string_view(col_start, ptr - col_start);
        col_start = ptr + 1;
      }
      ptr++;
    }

    ASSERT(col_index == 8);

    while (ptr < end)
    {
      if (*ptr == '\n')
      {
        columns[col_index++] = std::string_view(col_start, ptr - col_start);
        col_start = ptr + 1;
        ptr++;
        break;
      }
      ptr++;
    }

    ASSERT(col_index == 9);

    std::vector<Arrangement::Info> arrangementInfos;
    std::string_view line = columns[8];
    const std::string filepath(columns[0]);
    for (;;)
    {
      Arrangement::Info arrangementInfo;
      size_t pos = line.find(':');
      arrangementInfo.arrangementName = line.substr(0, pos);
      arrangementInfo.persistentId = std::string(File::filename(filepath.c_str())) + '|' + arrangementInfo.arrangementName;
      line = line.substr(pos + 1);
      pos = line.find(':');
      const std::string s(line.substr(0, pos));
      arrangementInfo.centOffset = atof2(s.c_str());
      line = line.substr(pos + 1);
      const u64 tuningEndPos = line.find('|');
      std::string_view tuningStr = line.substr(0, tuningEndPos);
      i8 i = 0;
      do
      {
        pos = tuningStr.find(',');
        const std::string s(tuningStr.substr(0, pos));
        tuningStr = tuningStr.substr(pos + 1);
        const i32 string = atoi2(s.c_str());
        if (string == -128)
          break;
        arrangementInfo.tuning.string[i] = string;
        ++i;
      } while (pos != std::string::npos);
      arrangementInfos.emplace_back(std::move(arrangementInfo));
      if (tuningEndPos == std::string::npos)
        break;

      pos = tuningEndPos;
      line = line.substr(tuningEndPos + 1);
    }

    Song::Info songInfo{
      .loadState = Song::LoadState::cache,
      .filepath = filepath,
      .arrangementInfos = std::move(arrangementInfos),
      .albumName = std::string(columns[4]),
      .artistName = std::string(columns[3]),
      .songName = std::string(columns[2]),
      .songYear = atoi2(std::string(columns[5]).c_str()),
      .track = atoi2(std::string(columns[7]).c_str()),
      .songLength = timeNS_From_String(std::string(columns[6])),
    };

    readCacheTsv.emplace(filepath + std::string(columns[1]), std::move(songInfo));
  }

  return readCacheTsv;
}
#else // PLATFORM_EMSCRIPTEN
static void readCacheTsvFile(const std::string& content)
{
  if (content.empty())
    return;

  const char* ptr = content.data();
  const char* end = ptr + content.size();

  while (ptr < end)
  {
    const char* line_start = ptr;
    const char* col_start = line_start;
    std::string_view columns[9];
    int col_index = 0;

    // Scan columns (look for 9 tabs)
    while (ptr < end && col_index < 8)
    {
      if (*ptr == '\t')
      {
        columns[col_index++] = std::string_view(col_start, ptr - col_start);
        col_start = ptr + 1;
      }
      ptr++;
    }

    ASSERT(col_index == 8);

    while (ptr < end)
    {
      if (*ptr == '\n')
      {
        columns[col_index++] = std::string_view(col_start, ptr - col_start);
        col_start = ptr + 1;
        ptr++;
        break;
      }
      ptr++;
    }

    ASSERT(col_index == 9);

    std::vector<Arrangement::Info> arrangementInfos;
    std::string_view line = columns[8];
    for (;;)
    {
      Arrangement::Info arrangementInfo;
      size_t pos = line.find(':');
      arrangementInfo.arrangementName = line.substr(0, pos);
      line = line.substr(pos + 1);
      pos = line.find(':');
      const std::string s(line.substr(0, pos));
      arrangementInfo.centOffset = atof2(s.c_str());
      line = line.substr(pos + 1);
      const u64 tuningEndPos = line.find('|');
      std::string_view tuningStr = line.substr(0, tuningEndPos);
      i8 i = 0;
      do
      {
        pos = tuningStr.find(',');
        const std::string s(tuningStr.substr(0, pos));
        tuningStr = tuningStr.substr(pos + 1);
        const i32 string = atoi2(s.c_str());
        if (string == -128)
          break;
        arrangementInfo.tuning.string[i] = string;
        ++i;
      } while (pos != std::string::npos);
      arrangementInfos.emplace_back(std::move(arrangementInfo));
      if (tuningEndPos == std::string::npos)
        break;

      pos = tuningEndPos;
      line = line.substr(tuningEndPos + 1);
    }

    Song::Info songInfo
    {
      .loadState = Song::LoadState::cache,
      .filepath = std::string(columns[0]),
      .arrangementInfos = std::move(arrangementInfos),
      .albumName = std::string(columns[4]),
      .artistName = std::string(columns[3]),
      .songName = std::string(columns[2]),
      .songYear = atoi2(std::string(columns[5]).c_str()),
      .track = atoi2(std::string(columns[7]).c_str()),
      .songLength = timeNS_From_String(std::string(columns[6])),
      .albumCoverUrlReadyToDownload = std::string(columns[1]).c_str()
    };

    {
      const std::lock_guard lock(Global::songInfosMutex);
      const SongIndex songIndex = SongIndex(Global::songInfos.size());
      Global::songInfos[songIndex] = std::move(songInfo);
    }
  }
}
#endif // PLATFORM_EMSCRIPTEN

#ifndef PLATFORM_EMSCRIPTEN
static void fillCollection()
{
  // 1. the cache.tsv might be corrupted. The file is corrupted when it does not end with LF. In this case all characters before the last LF must be deleted.
  // 2. read whole cache.tsv into a hashmap.
  // 3. iterate over all song files, if they are in the hashmap use the song metadata from there. In case all files has been found in the cache, we are done.
  // 4. if a file is not found, parse the song file and append the metadata to the existing cache.tsv file.
  // 5. after all song files got iterated over, write a clean cache.tsv.tmp and use atomic rename and replace the cache.tsv file

  u64 estimatedSongFiles = 0;

#ifdef SHR3D_SHRED
  const std::vector<std::string> shredFiles = File::filesInDirectory(Settings::pathShred.c_str());
  estimatedSongFiles += shredFiles.size();
#endif // SHR3D_SHRED

#ifdef SHR3D_PSARC
  const std::vector<std::string> psarcFiles = File::filesInDirectory(Settings::pathPsarc.c_str());
  estimatedSongFiles += psarcFiles.size();
#endif // SHR3D_PSARC

  Global::songInfos.reserve(estimatedSongFiles);

#ifndef PLATFORM_EMSCRIPTEN
  repairCacheTsvFileCorruptLastLine();
#endif // PLATFORM_EMSCRIPTEN

  std::unordered_map<std::string, Song::Info> cacheTsv = readCacheTsvFile();

#ifndef PLATFORM_EMSCRIPTEN
  FILE* cacheTsvWriteAppend = File::fopen_u8("cache.tsv", "ab");
  bool writeCleanCacheTsv = false;
  std::string reuseStringBuffer;
  reuseStringBuffer.reserve(512);
#endif // PLATFORM_EMSCRIPTEN

#ifdef SHR3D_SHRED
  for (const std::string& filepath : shredFiles)
  {
    const File::Type fileType = File::type(filepath.c_str());
    if (fileType != File::Type::shred)
      continue;

    const std::string cacheKey = filepath + to_string(File::modTime(filepath.c_str()));

    const auto it = cacheTsv.find(cacheKey);
    if (it != cacheTsv.end())
    {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
      const std::lock_guard lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
      const SongIndex songIndex = SongIndex(Global::songInfos.size());
      Global::songInfos[songIndex] = std::move(it->second);
      continue;
    }

#ifndef PLATFORM_EMSCRIPTEN
    writeCleanCacheTsv = true;

    {
      const std::vector<u8> fileData = File::read(filepath.c_str());
      const Shred::Info shredInfo = Shred::parse(fileData.data(), fileData.size());
      Song::Info songInfo = Song::ShredParser::loadSongInfo(filepath.c_str(), shredInfo);

      if (Global::installMode == InstallMode::installed)
      {
        if (!shredInfo.albumPng.empty())
        { // cache albumCover
          const char* filename_ = File::filename(filepath.c_str());
          const std::string albumCoverFilename = File::replaceExtension(filename_, ".png");
          const std::string cachedAlbumCoverFilename = Settings::pathCache + albumCoverFilename;

          File::write(cachedAlbumCoverFilename.c_str(), shredInfo.albumPng.data(), shredInfo.albumPng.size());
        }
        appendCacheTsvLine(songInfo, cacheTsvWriteAppend, reuseStringBuffer);
      }

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

#ifdef SHR3D_PSARC
  for (const std::string& filepath : psarcFiles)
  {
    const File::Type fileType = File::type(filepath.c_str());
    if (fileType != File::Type::psarc)
      continue;

    // #ifdef SHR3D_SHRED
    //     const std::string shredFilepath = Settings::pathShred + File::replaceExtension(File::filename(filepath.c_str()), ".shred");
    //     if (File::exists(shredFilepath.c_str()))
    //       continue;
    // #endif // SHR3D_SHRED

    const std::string cacheKey = filepath + to_string(File::modTime(filepath.c_str()));

    const auto it = cacheTsv.find(cacheKey);
    if (it != cacheTsv.end())
    {
#ifdef SHR3D_COLLECTION_WORKER_THREAD
      const std::lock_guard lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
      const SongIndex songIndex = SongIndex(Global::songInfos.size());
      Global::songInfos[songIndex] = std::move(it->second);
      continue;
    }

#ifndef PLATFORM_EMSCRIPTEN
    writeCleanCacheTsv = true;

    {
      const std::vector<u8> fileData = File::read(filepath.c_str());
      const Psarc::Info psarcInfo = Psarc::parse(fileData.data(), fileData.size());
      Song::Info songInfo = Song::PsarcParser::loadSongInfo(psarcInfo, filepath);

      if (Global::installMode == InstallMode::installed)
      {
        if (!psarcInfo.tocEntries[songInfo.psarcAlbumCover256_tocIndex].content.empty())
        { // cache albumCover
          const char* filename_ = File::filename(filepath.c_str());
          const std::string albumCoverFilename = File::replaceExtension(filename_, ".dds");
          const std::string cachedAlbumCoverFilename = Settings::pathCache + albumCoverFilename;

          File::write(cachedAlbumCoverFilename.c_str(), psarcInfo.tocEntries[songInfo.psarcAlbumCover256_tocIndex].content.data(), psarcInfo.tocEntries[songInfo.psarcAlbumCover256_tocIndex].content.size());
        }
        appendCacheTsvLine(songInfo, cacheTsvWriteAppend, reuseStringBuffer);
      }

#ifdef SHR3D_COLLECTION_WORKER_THREAD
      const std::lock_guard lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
      const SongIndex songIndex = SongIndex(Global::songInfos.size());
      Global::songInfos[songIndex] = std::move(songInfo);
    }
#endif // PLATFORM_EMSCRIPTEN
  }
#endif // SHR3D_PSARC

#ifndef PLATFORM_EMSCRIPTEN
  fclose(cacheTsvWriteAppend);

  if (writeCleanCacheTsv)
    writeCleanCacheTsvFile();
#endif // PLATFORM_EMSCRIPTEN
}
#endif // PLATFORM_EMSCRIPTEN

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

#ifdef SHR3D_COLLECTION_WORKER_THREAD
    static std::thread wokerThread(fillCollection);
#else // SHR3D_COLLECTION_WORKER_THREAD
    fillCollection();
#endif // SHR3D_COLLECTION_WORKER_THREAD
  }
}
#endif // PLATFORM_EMSCRIPTEN

#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, Song::Info* songInfo, 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;
  attr.userData = songInfo;
  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);

  Song::Info* songInfo = static_cast<Song::Info*>(fetch->userData);
  songInfo->albumCoverTexture = Texture::openGlLoadTexturePng(reinterpret_cast<const u8*>(fetch->data), fetch->numBytes);

  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);

  Song::Info* songInfo = static_cast<Song::Info*>(fetch->userData);
  songInfo->albumCoverTexture = Texture::openGlLoadTextureDds(reinterpret_cast<const u8*>(fetch->data), fetch->numBytes);

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

static void downloadSuccessFunc(emscripten_fetch_t* fetch)
{
  switch (File::type_cacheIpfsCid(fetch->url, reinterpret_cast<const u8*>(fetch->data)))
  {
    case File::Type::shred:
      {
        if (Global::downloadSongIndexInProgress == -1)
          Global::downloadSongIndexInProgress = Global::songInfos.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);

        Shred::Info shredInfo = Shred::parse(reinterpret_cast<const u8*>(fetch->data), fetch->numBytes);
        Song::Info songInfo = Song::ShredParser::loadSongInfo(fetch->url, shredInfo);
        songInfo.albumCoverTexture = Texture::openGlLoadTexturePng(shredInfo.albumPng.data(), i32(shredInfo.albumPng.size()));
        Song::ShredParser::loadSongTones(Global::downloadSongIndexInProgress, songInfo, shredInfo);

        {
          const std::lock_guard lock(Global::songInfosMutex);
          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::songInfos.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);

        Psarc::Info psarcInfo = Psarc::parse(reinterpret_cast<const u8*>(fetch->data), fetch->numBytes);
        Song::Info songInfo = Song::PsarcParser::loadSongInfo(psarcInfo, fetch->url);
        songInfo.albumCoverTexture = Texture::openGlLoadTextureDds(psarcInfo.tocEntries[songInfo.psarcAlbumCover256_tocIndex].content.data(), i32(psarcInfo.tocEntries[songInfo.psarcAlbumCover256_tocIndex].content.size()));

        {
          const std::lock_guard lock(Global::songInfosMutex);
          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::tsv:
      {
        DEBUG_PRINT("downloadSuccessFunc(): Finished downloading tsv %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);

        const std::string cacheTsv(fetch->data, fetch->data + fetch->numBytes - 1);

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

        readCacheTsvFile(cacheTsv);
      }
      break;
    //case File::Type::unknown:
    //  {
    //    DEBUG_PRINT("downloadSuccessFunc(): Finished downloading unknown %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);

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

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

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

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

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

    //      if (urls.size() >= 1)
    //      {
    //        const std::lock_guard lock(Global::songInfosMutex);
    //        const SongIndex songIndex = SongIndex(Global::songInfos.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::string(".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::string& filepath = Global::songInfos[Global::downloadSongIndexInProgress].filepath;

  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, filepath.c_str());
}

void Collection::downloadAlbumCoverLoadAsTexture(Song::Info* songInfo)
{
  DEBUG_PRINT("downloadAlbumCoverLoadAsTexture()\n");

  std::string url = std::move(songInfo->albumCoverUrlReadyToDownload);
  ASSERT(songInfo->albumCoverUrlReadyToDownload.emtpy());
  ASSERT(!url.emtpy());

  const std::string extension = File::extension(url.c_str());

  if (extension == ".png")
    fetchFileFromWebserverNoProgress(url.c_str(), songInfo, successFetchCachePngFunc);
  else if (extension == ".dds")
    fetchFileFromWebserverNoProgress(url.c_str(), songInfo, successFetchCacheDdsFunc);
}
#endif // PLATFORM_EMSCRIPTEN

void Collection::init()
{
#ifdef PLATFORM_EMSCRIPTEN
  fetchFileFromWebserverNoProgress(Global::autoFetchUrl.c_str(), nullptr, &downloadSuccessFunc);
#else // PLATFORM_EMSCRIPTEN
  if (!Global::skipFetchingCollection)
    initFromFile();
#endif // PLATFORM_EMSCRIPTEN
}

void Collection::addSongFile(const std::string& 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::lock_guard lock(Global::songInfosMutex);
#endif // SHR3D_COLLECTION_WORKER_THREAD
      const SongIndex songIndex = SongIndex(Global::songInfos.size());
      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, filepath);

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

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