// SPDX-License-Identifier: Unlicense

#include "file.h"

#include "global.h"
#include "skybox.h"
#include "stage.h"
#include "string_.h"

#include <stdio.h>
#include <string.h>
#include <string>
#include <fstream>
#include <sstream>
#include <filesystem>

#ifdef PLATFORM_WINDOWS
#include <Windows.h>
#ifndef SHR3D_MINIMAL_WIN32
#include <shlobj.h>
#endif // SHR3D_MINIMAL_WIN32
#endif // PLATFORM_WINDOWS

#ifdef __ANDROID__
#ifdef PLATFORM_OPENXR_ANDROID
#include "dirent.h"
#endif // PLATFORM_OPENXR_ANDROID
#include "collection.h"
#include <jni.h>
#endif // __ANDROID__

#include <sys/stat.h>


#ifdef PLATFORM_WINDOWS
static std::wstring windowsExtendedPath(std::wstring relPath)
{
  if (relPath.empty())
    return relPath;

  std::replace(relPath.begin(), relPath.end(), u8'/', u8'\\');

  if (relPath[0] == L'\\' && relPath[1] == L'\\')
    return relPath;

  std::wstring extendedPath;
  if (relPath.length() >= 2 && relPath[1] == L':')
  {
    extendedPath = relPath;
  }
  else
  {
    extendedPath.resize(4096);
    DWORD length2 = GetCurrentDirectoryW(u32(extendedPath.size()), extendedPath.data());
    ASSERT(extendedPath[length2] == L'\0');
    extendedPath.resize(length2);
    if (extendedPath.back() != L'\\')
      extendedPath += L'\\';
    extendedPath += relPath;
  }

  { // normalize path
    std::vector<std::wstring> components;
    size_t length = extendedPath.length();
    size_t start = extendedPath[0] == L'\\' ? 4 : 0;

    while (start < length)
    {
      size_t end = extendedPath.find(L'\\', start);
      if (end == std::wstring::npos)
        end = length;

      std::wstring token = extendedPath.substr(start, end - start);
      start = end + 1;

      if (token == L"." || token.empty())
        continue;

      if (token != L"..")
        components.push_back(token);
      else if (!components.empty())
        components.pop_back();
    }

    extendedPath = LR"(\\?)";
    for (const std::wstring& comp : components)
      extendedPath += L"\\" + comp;
  }

  if (extendedPath.back() == L':')
    extendedPath += L"\\";

  if (extendedPath.back() != L'\\' && relPath.back() == L'\\')
    extendedPath += L"\\";

  return extendedPath;
}
#endif // PLATFORM_WINDOWS

bool File::exists(const char8_t* filePath)
{
#if defined(PLATFORM_WINDOWS)
  const std::wstring wFilepath = String::s2ws(filePath, i32(strlen(reinterpret_cast<const char*>(filePath))));
  const std::wstring wFilepathExpanded = String::ws_ExpandEnvironmentStrings(wFilepath);
  struct _stat buffer;
  return _wstat(wFilepathExpanded.c_str(), &buffer) == 0;
#elif defined(__ANDROID__)
  const std::u8string rootFilepath = filePath[0] == u8'/' ? filePath : (Global::rootPath + filePath);
  struct stat buffer;
  return stat(reinterpret_cast<const char*>(rootFilepath.c_str()), &buffer) == 0;
#else
  struct stat buffer;
  return stat(reinterpret_cast<const char*>(filePath), &buffer) == 0;
#endif
}

bool File::directoryExists(const char8_t* filePath)
{
#if defined(PLATFORM_WINDOWS)
  const std::wstring wFilepath = String::s2ws(filePath, i32(strlen(reinterpret_cast<const char*>(filePath))));
  const std::wstring wFilepathExpanded = String::ws_ExpandEnvironmentStrings(wFilepath);
  const std::wstring wExtendedPath = windowsExtendedPath(wFilepathExpanded);
  const DWORD fileAttributes = GetFileAttributesW(wExtendedPath.c_str());
  if (fileAttributes == INVALID_FILE_ATTRIBUTES)
    return false;
  return (fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
#elif defined(PLATFORM_OPENXR_ANDROID)
  const std::u8string rootFilepath = filePath[0] == u8'/' ? filePath : (Global::rootPath + filePath);
  struct stat buffer;
  if (stat(reinterpret_cast<const char*>(rootFilepath.c_str()), &buffer) != 0)
    return false;
  return buffer.st_mode & S_IFDIR;
#elif defined(PLATFORM_ANDROID_SDL)
  const std::filesystem::path path(filePath[0] == u8'/' ? reinterpret_cast<const char*>(filePath) : reinterpret_cast<const char*>((Global::rootPath + filePath).c_str()));
  return std::filesystem::is_directory(path);
#else
  return std::filesystem::is_directory(filePath);
#endif
}

const char8_t* File::filename(const char8_t* filePath)
{
  const char8_t* sep = reinterpret_cast<const char8_t*>(strrchr(reinterpret_cast<const char*>(filePath), '/'));
  if (!sep || sep == filePath)
    return filePath;
  return sep + 1;
}

const char8_t* File::extension(const char8_t* filePath)
{
  const char8_t* sep = reinterpret_cast<const char8_t*>(strrchr(reinterpret_cast<const char*>(filePath), '.'));
  if (!sep || sep == filePath)
    return u8"";
  return sep;
}

std::u8string File::replaceExtension(const char8_t* filePath, const char8_t* newExtension)
{
  const char* dot = strrchr(reinterpret_cast<const char*>(filePath), '.');
  if (dot == nullptr)
    return std::u8string(filePath) + newExtension;

  return std::u8string(filePath, reinterpret_cast<const char8_t*>(dot)) + newExtension;
}

void File::createDirectories(const char8_t* filePath)
{
#ifndef PLATFORM_PICO_4
  if (Global::installMode != InstallMode::installed)
    return;

#ifdef PLATFORM_WINDOWS
  {
    const std::wstring extendedPath = windowsExtendedPath(String::s2ws(filePath, i32(strlen(reinterpret_cast<const char*>(filePath)))));
    std::wstring partialExtendedPath;
    partialExtendedPath.reserve(extendedPath.size());

    for (size_t i = 0; i < extendedPath.size(); ++i)
    {
      partialExtendedPath += extendedPath[i];

      if (extendedPath[i] != L'\\')
        continue;

      if (partialExtendedPath.size() <= 7 || partialExtendedPath.back() == L':')
        continue;

      const bool success = CreateDirectoryW(partialExtendedPath.c_str(), nullptr);
      ASSERT(success || GetLastError() == ERROR_ALREADY_EXISTS);
    }
    return;
  }
#else // PLATFORM_WINDOWS

  if (!directoryExists(filePath))
  {
#if defined PLATFORM_ANDROID_SDL || defined PLATFORM_OPENXR_ANDROID
    const std::filesystem::path path(filePath[0] == u8'/' ? reinterpret_cast<const char*>(filePath) : reinterpret_cast<const char*>((Global::rootPath + filePath).c_str()));
#else // PLATFORM_ANDROID_SDL
    std::filesystem::path path(filePath);
#endif // PLATFORM_ANDROID_SDL

    ASSERT(path.u8string().back() == u8'/');

    {
      const bool success = std::filesystem::create_directories(path.parent_path()); // remove trailing '/'. It works but create_directories will return false.
      ASSERT(success);
    }
  }

#endif // PLATFORM_WINDOWS
#else // PLATFORM_PICO_4
  const std::u8string rootFilepath = filePath[0] == u8'/' ? filePath : (Global::rootPath + filePath);
  struct stat buffer;

  if (directoryExists(rootFilepath.c_str()))
    return;

  // Find the parent directory
  const size_t pos = rootFilepath.find_last_of('/');
  if (pos != std::string::npos) {
    std::u8string parent = rootFilepath.substr(0, pos);
    if (!directoryExists(parent.c_str()))
      createDirectories(parent.c_str());
  }

  if (mkdir(reinterpret_cast<const char*>(rootFilepath.c_str()), 0755) == 0 || errno == EEXIST) {
    return;
  }
#endif // PLATFORM_PICO_4
}

void File::copy(const char8_t* srcFilepath, const char8_t* destFilepath)
{
#ifndef PLATFORM_OPENXR_ANDROID
  if (Global::installMode != InstallMode::installed)
    return;

#if defined PLATFORM_ANDROID_SDL || defined PLATFORM_OPENXR_ANDROID
  std::filesystem::copy(srcFilepath[0] == u8'/' ? std::filesystem::path(reinterpret_cast<const char*>(srcFilepath)) : std::filesystem::path(reinterpret_cast<const char*>(Global::rootPath.c_str())) / std::filesystem::path(reinterpret_cast<const char*>(srcFilepath)),
    destFilepath[0] == u8'/' ? std::filesystem::path(reinterpret_cast<const char*>(destFilepath)) : std::filesystem::path(reinterpret_cast<const char*>(Global::rootPath.c_str())) / std::filesystem::path(reinterpret_cast<const char*>(destFilepath)),
    std::filesystem::copy_options::overwrite_existing);
#else // PLATFORM_ANDROID_SDL
  std::filesystem::copy(std::filesystem::path(srcFilepath), std::filesystem::path(destFilepath), std::filesystem::copy_options::overwrite_existing);
#endif // PLATFORM_ANDROID_SDL
#endif // PLATFORM_OPENXR_ANDROID
}

std::vector<std::u8string> File::filesInDirectory(const char8_t* filePath)
{
  std::vector<std::u8string> files;

#ifndef PLATFORM_OPENXR_ANDROID

  if (directoryExists(filePath))
  {
#if defined PLATFORM_ANDROID_SDL || defined PLATFORM_OPENXR_ANDROID
    const std::filesystem::path path(filePath[0] == u8'/' ? reinterpret_cast<const char*>(filePath) : reinterpret_cast<const char*>((Global::rootPath + filePath).c_str()));
#else // PLATFORM_ANDROID_SDL

#ifdef PLATFORM_WINDOWS
    const std::wstring wFilepath = String::s2ws(filePath, i32(strlen(reinterpret_cast<const char*>(filePath))));
    const std::wstring wFilepathExpanded = String::ws_ExpandEnvironmentStrings(wFilepath);
    const std::wstring extendedPath = windowsExtendedPath(wFilepathExpanded);
    const std::filesystem::path path(extendedPath);
#else // PLATFORM_WINDOWS
    const std::filesystem::path path(filePath);
#endif // PLATFORM_WINDOWS

#endif // PLATFORM_ANDROID_SDL

    for (const std::filesystem::path& file : std::filesystem::recursive_directory_iterator(path))
    {
#if defined PLATFORM_ANDROID_SDL || defined PLATFORM_OPENXR_ANDROID
      files.push_back(reinterpret_cast<const char8_t*>(file.generic_string().c_str()));
#else // PLATFORM_ANDROID_SDL && !PLATFORM_OPENXR_ANDROID
      files.push_back(file.generic_u8string());
#endif // PLATFORM_ANDROID_SDL && !PLATFORM_OPENXR_ANDROID
    }
  }
#else // PLATFORM_OPENXR_ANDROID
  const std::u8string rootFilepath =
#ifdef __ANDROID__
    filePath[0] == u8'/' ? filePath : (Global::rootPath + filePath);
#else // __ANDROID__
    filePath;
#endif // __ANDROID__

  DIR* dir = opendir(reinterpret_cast<const char*>(rootFilepath.c_str()));
  if (dir == nullptr) {
    //errorf("opendir %s error %d", path.c_str(), errno);
    return files;
  }
  struct dirent* file;
  while ((file = readdir(dir)) != nullptr) {
    if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0) {
      continue;
    }
    if (file->d_type == DT_DIR)
    {
      std::u8string path_next = filePath;
      //path_next += u8"/";
      path_next += reinterpret_cast<const char8_t*>(file->d_name);

      const std::vector<std::u8string> moreFiles = filesInDirectory(path_next.c_str());
      files.insert(files.end(), moreFiles.begin(), moreFiles.end());
    }
    else
    {
      std::u8string fileFullName = filePath;
      fileFullName += reinterpret_cast<const char8_t*>(file->d_name);
      files.push_back(fileFullName);
    }
  }
#endif // PLATFORM_OPENXR_ANDROID

  return files;
}

std::u8string File::fixInvalidCharsInFilepath(const std::u8string& filePath)
{
  const u64 size = filePath.size();

  if (size == 0)
    return {};

  std::u8string s;
  s.reserve(size);

  u64 i = 0;

  for (; i < size; ++i) // trim left
    if (filePath[i] != u8' ')
      break;
  for (; i < size; ++i)
  {
    switch (filePath[i])
    {
    default:
      s += filePath[i];
      break;
    case u8'\\':
      s += u8"{BACKSLASH}";
      break;
    case u8'/':
      s += u8"{SLASH}";
      break;
    case u8':':
      s += u8"{COLON}";
      break;
    case u8'*':
      s += u8"{ASTERISK}";
      break;
    case u8'?':
      s += u8"{QUESTIONMARK}";
      break;
    case u8'\"':
      s += u8"{QUOTE}";
      break;
    case u8'<':
      s += u8"{LESSTHAN}";
      break;
    case u8'>':
      s += u8"{GREATERTHAN}";
      break;
    case u8'|':
      s += u8"{PIPE}";
      break;
    }
  }
  while (s.back() == u8' ') // trim right
    s.pop_back();
  if (s.back() == u8'.') // last character '.' is not allowed on windows
  {
    s.pop_back();
    s += u8"{DOT}";
  }

  return s;
}

std::u8string File::uniqueFilepathInDirectory(const std::u8string& directory, const std::u8string& extension)
{
  std::u8string filePath = directory + u8"YYYY-MM-DD_HH-MM-SS" + extension;

  const time_t t = time(nullptr);
  const tm* tm = localtime(&t);

  sprintf(reinterpret_cast<char*>(&filePath[directory.size()]), "%4d-%02d-%02d_%02d-%02d-%02d%s", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, reinterpret_cast<const char*>(extension.c_str()));

  if (!File::exists(filePath.c_str()))
    return filePath;

  filePath.resize(filePath.size() + 3);
  for (i32 i = 0; i < 100; ++i)
  {
    sprintf(reinterpret_cast<char*>(&filePath[directory.size() + 16]), "_%02d%s", i, reinterpret_cast<const char*>(extension.c_str()));
    if (!File::exists(filePath.c_str()))
      return filePath;
  }

  ASSERT(false);
  return {};
}

//#ifndef PLATFORM_EMSCRIPTEN
FILE* File::fopen_u8(const char8_t* filePath, const bool isWrite)
{
#if defined(PLATFORM_WINDOWS)
  return _wfopen(String::s2ws(filePath, i32(strlen(reinterpret_cast<const char*>(filePath)))).c_str(), isWrite ? L"wb" : L"rb");
#elif defined(__ANDROID__)
  return fopen(filePath[0] == u8'/' ? reinterpret_cast<const char*>(filePath) : reinterpret_cast<const char*>((Global::rootPath + filePath).c_str()), isWrite ? "wb" : "rb");
#else
  return fopen(reinterpret_cast<const char*>(filePath), isWrite ? "wb" : "rb");
#endif
}

std::vector<u8> File::read(const char8_t* filePath)
{
  DEBUG_PRINT("%s\n", reinterpret_cast<const char*>(filePath));

  FILE* file = fopen_u8(filePath, false);
  ASSERT(file != nullptr); // if this fails and strerror(errno); returns Permission denied on android then the user needs to be asked for runtime permissions

  fseek(file, 0, SEEK_END);
  const i32 lSize = ftell(file);
  rewind(file);

  std::vector<u8> fileData(lSize);

  fread(fileData.data(), lSize, 1, file);

  fclose(file);

  return fileData;
}

void File::write(const char8_t* filePath, const void* content, size_t len)
{
  FILE* file = fopen_u8(filePath, true);
  ASSERT(file != nullptr);

  fwrite(content, len, 1, file);

  fclose(file);
}
//#endif // PLATFORM_EMSCRIPTEN

std::vector<std::u8string> File::openFileDialog(const wchar_t* filter, const bool allowMultiSelect)
{
#ifndef PLATFORM_PICO_4 // sadly the openFileDialog will crash on Pico4

#ifndef __ANDROID__
#ifdef PLATFORM_WINDOWS
#ifndef SHR3D_MINIMAL_WIN32
  wchar_t szFile[4096];
  OPENFILENAME ofn{};
  ofn.lStructSize = sizeof(ofn);
  ofn.hwndOwner = nullptr;
  ofn.lpstrFile = szFile;
  ofn.lpstrFile[0] = '\0';
  ofn.nMaxFile = sizeof(szFile);
  ofn.lpstrFilter = filter;
  ofn.nFilterIndex = 1;
  ofn.lpstrFileTitle = nullptr;
  ofn.nMaxFileTitle = 0;
  ofn.lpstrInitialDir = nullptr;
  ofn.Flags = OFN_HIDEREADONLY | OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
  if (allowMultiSelect)
    ofn.Flags |= OFN_ALLOWMULTISELECT;

  std::vector<std::u8string> filePaths;

  if (GetOpenFileNameW(&ofn))
  {
    const wchar_t* p = ofn.lpstrFile;

    std::u8string dirPathOrSingleFilepath = String::ws2s(p, i32(wcslen(p)));
    std::replace(dirPathOrSingleFilepath.begin(), dirPathOrSingleFilepath.end(), '\\', '/');

    p += dirPathOrSingleFilepath.size() + 1;

    if (*p == L'\0')
    {
      // single file
      ASSERT(File::exists(dirPathOrSingleFilepath.c_str()));
      filePaths.push_back(dirPathOrSingleFilepath);
    }
    else
    {
      // multiple files
      dirPathOrSingleFilepath += u8'/';
      while (*p != L'\0')
      {
        const i32 length = i32(wcslen(p));
        const std::u8string filepath = dirPathOrSingleFilepath + String::ws2s(p, length);
        ASSERT(File::exists(filepath.c_str()));
        filePaths.push_back(filepath);
        p += length + 1;
      }
    }
  }

  return filePaths;

#endif // SHR3D_MINIMAL_WIN32
#endif // PLATFORM_WINDOWS
#else // __ANDROID__
  JNIEnv* env = nullptr;
  Global::g_JVM->GetEnv((void**)&env, JNI_VERSION_1_4);

  jclass fileClass = env->FindClass("android/content/Intent");
  jmethodID intendClassConstructorId = env->GetMethodID(fileClass, "<init>", "(Ljava/lang/String;)V");
  jstring intentParam = env->NewStringUTF("android.intent.action.OPEN_DOCUMENT");
  jobject intent = env->NewObject(fileClass, intendClassConstructorId, intentParam);
  env->DeleteLocalRef(intentParam);

  jmethodID intendAddCategoryId = env->GetMethodID(fileClass, "addCategory", "(Ljava/lang/String;)Landroid/content/Intent;");
  jstring intentAddCategoryParam = env->NewStringUTF("android.intent.category.OPENABLE");
  env->CallObjectMethod(intent, intendAddCategoryId, intentAddCategoryParam);
  env->DeleteLocalRef(intentAddCategoryParam);

  jmethodID intendSetTypeId = env->GetMethodID(fileClass, "setType", "(Ljava/lang/String;)Landroid/content/Intent;");
  jstring intentSetTypeParam = env->NewStringUTF("*/*");
  env->CallObjectMethod(intent, intendSetTypeId, intentSetTypeParam);
  env->DeleteLocalRef(intentSetTypeParam);

  jmethodID intentPutExtraBoolId = env->GetMethodID(fileClass, "putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;");
  jstring intentExtraAllowMultiple = env->NewStringUTF("android.intent.extra.ALLOW_MULTIPLE");
  env->CallObjectMethod(intent, intentPutExtraBoolId, intentExtraAllowMultiple, JNI_TRUE);
  env->DeleteLocalRef(intentExtraAllowMultiple);

  //  jmethodID intendPutExtraId = env->GetMethodID(fileClass, "putExtra", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;");
  //  jstring intentPutExtraParam0 = env->NewStringUTF("android.intent.extra.TITLE");
  //  jstring intentPutExtraParam1 = env->NewStringUTF("_p.psarc");
  //  env->CallObjectMethod(intent, intendPutExtraId, intentPutExtraParam0, intentPutExtraParam1);
  //  env->DeleteLocalRef(intentPutExtraParam0);
  //  env->DeleteLocalRef(intentPutExtraParam1);

  jobject activity = (jobject)Global::androidActivity;
  jclass activityClass(env->GetObjectClass(activity));
  jmethodID startActivityForResultMethodId = env->GetMethodID(activityClass, "startActivityForResult", "(Landroid/content/Intent;I)V");
  env->CallVoidMethod(activity, startActivityForResultMethodId, intent, 1);

  env->DeleteLocalRef(intent);
  env->DeleteLocalRef(activityClass);
  env->DeleteLocalRef(fileClass);

  return {};

#endif // __ANDROID__
#endif // PLATFORM_PICO_4

  unreachable();
}

std::u8string File::openDirectoryDialog()
{
#if defined(PLATFORM_WINDOWS) && !defined(SHR3D_MINIMAL_WIN32)
  BROWSEINFO bi{};
  LPITEMIDLIST itemIdList = SHBrowseForFolder(&bi);
  if (itemIdList != 0)
  {
    wchar_t folderPath[512];
    const bool res = SHGetPathFromIDList(itemIdList, folderPath);
    ASSERT(res);

    CoTaskMemFree(itemIdList);
    std::u8string u8String = String::ws2s(folderPath, i32(wcslen(folderPath)));
    std::replace(u8String.begin(), u8String.end(), '\\', '/');
    return u8String;
  }
  return {};
#else // PLATFORM_WINDOWS && !SHR3D_MINIMAL_WIN32
  unreachable(); // TODO: not implemented
#endif // PLATFORM_WINDOWS && !SHR3D_MINIMAL_WIN32
}

void File::shellExecute(const char8_t* filePathOrUrl)
{
#if defined(PLATFORM_WINDOWS) && !defined(SHR3D_MINIMAL_WIN32)
  ShellExecuteW(NULL, L"open", String::s2ws(filePathOrUrl, i32(strlen(reinterpret_cast<const char*>(filePathOrUrl)))).c_str(), NULL, NULL, SW_SHOWDEFAULT);
#elif defined(__ANDROID__)
  JNIEnv* env = nullptr;
  Global::g_JVM->GetEnv((void**)&env, JNI_VERSION_1_4);

  jclass intentClass = env->FindClass("android/content/Intent");
  jmethodID intentConstructor = env->GetMethodID(intentClass, "<init>", "(Ljava/lang/String;)V");
  jstring actionView = env->NewStringUTF("android.intent.action.VIEW");
  jobject intent = env->NewObject(intentClass, intentConstructor, actionView);
  env->DeleteLocalRef(actionView);

  jmethodID setDataMethod = env->GetMethodID(intentClass, "setData", "(Landroid/net/Uri;)Landroid/content/Intent;");
  jclass uriClass = env->FindClass("android/net/Uri");
  jmethodID uriParseMethod = env->GetStaticMethodID(uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
  jstring urlString = env->NewStringUTF(reinterpret_cast<const char*>(filePathOrUrl));
  jobject uri = env->CallStaticObjectMethod(uriClass, uriParseMethod, urlString);
  env->DeleteLocalRef(urlString);
  env->DeleteLocalRef(uriClass);
  env->DeleteLocalRef(intentClass);

  env->CallObjectMethod(intent, setDataMethod, uri);
  env->DeleteLocalRef(uri);

  jclass contextClass = env->FindClass("android/content/Context");
  jmethodID startActivityMethod = env->GetMethodID(contextClass, "startActivity", "(Landroid/content/Intent;)V");
  jobject activity = (jobject)Global::androidActivity;
  env->CallVoidMethod(activity, startActivityMethod, intent);

  env->DeleteLocalRef(contextClass);
  env->DeleteLocalRef(intent);
#else // PLATFORM_WINDOWS && !SHR3D_MINIMAL_WIN32
  unreachable(); // TODO: not implemented
#endif // PLATFORM_WINDOWS && !SHR3D_MINIMAL_WIN32
}

void File::createPathDirectories()
{
#ifdef SHR3D_SHRED_OR_PSARC
  File::createDirectories(Settings::pathCache.c_str());
#endif // SHR3D_SHRED_OR_PSARC

#ifdef SHR3D_PSARC
  File::createDirectories(Settings::pathPsarc.c_str());
#endif // SHR3D_PSARC

#ifdef SHR3D_SHRED
  File::createDirectories(Settings::pathShred.c_str());
#endif // SHR3D_SHRED

#ifdef SHR3D_ENVIRONMENT_MILK
  File::createDirectories(Settings::pathMilk.c_str());
#endif // SHR3D_ENVIRONMENT_MILK

#ifdef SHR3D_SFX_CORE_NEURALAMPMODELER
  File::createDirectories(Settings::pathNam.c_str());
#endif // SHR3D_SFX_CORE_NEURALAMPMODELER

#ifdef SHR3D_SFX_PLUGIN_CLAP
  File::createDirectories(Settings::pathClap.c_str());
#endif // SHR3D_SFX_PLUGIN_CLAP

#ifdef SHR3D_SFX_PLUGIN_VST
  File::createDirectories(Settings::pathVst.c_str());
#endif // SHR3D_SFX_PLUGIN_VST

#ifdef SHR3D_SFX_PLUGIN_VST3
  File::createDirectories(Settings::pathVst3.c_str());
#endif // SHR3D_SFX_PLUGIN_VST3

#ifdef SHR3D_ENVIRONMENT_SKYBOX
  File::createDirectories(Settings::pathSkybox.c_str());
#endif // SHR3D_ENVIRONMENT_SKYBOX

#ifdef SHR3D_ENVIRONMENT_STAGE
  File::createDirectories(Settings::pathStage.c_str());
#endif // SHR3D_ENVIRONMENT_STAGE

#ifdef SHR3D_RECORDER
  File::createDirectories(Settings::pathRecorder.c_str());
#endif // SHR3D_RECORDER
}

#ifdef PLATFORM_EMSCRIPTEN
File::Type File::type_cacheIpfsCid(const char8_t* filePath, const u8* fileData4CC)
{ // on ipfs we might not have a filename

  { // detect based on filename
    const char8_t* extension = File::extension(filePath);
    if (strcmp(reinterpret_cast<const char*>(extension), ".shred") == 0)
      return File::Type::shred;
    if (strcmp(reinterpret_cast<const char*>(extension), ".psarc") == 0)
      return File::Type::psarc;
  }

  {
    File::Type fileType;

    if (memcmp(fileData4CC, "PK", 2) == 0)
    {
      DEBUG_PRINT("File::type4CC: .shred file detected.\n");
      fileType = File::Type::shred;
    }
    else if (memcmp(fileData4CC, "PSAR", 4) == 0)
    {
      DEBUG_PRINT("File::type4CC: .psarc file detected.\n");
      fileType = File::Type::psarc;
    }
    else if (memcmp(&fileData4CC[1], "PNG", 3) == 0)
    {
      DEBUG_PRINT("File::type4CC: .png file detected.\n");
      fileType = File::Type::png;
    }
    else if (memcmp(fileData4CC, "DDS", 3) == 0)
    {
      DEBUG_PRINT("File::type4CC: .dds file detected.\n");
      fileType = File::Type::dds;
    }
    else
    {
      DEBUG_PRINT("File::type4CC: unknown/urls.txt file detected.\n");
      fileType = File::Type::unknown;
    }

    Global::ipfsFileTypeCache[filePath] = fileType;

    return fileType;
  }
}
#endif // PLATFORM_EMSCRIPTEN

File::Type File::type(const char8_t* filePath)
{
  DEBUG_PRINT("File::type() of %s\n", reinterpret_cast<const char*>(filePath));

  { // detect based on filename
    const char8_t* extension = File::extension(filePath);
    if (strcmp(reinterpret_cast<const char*>(extension), ".shred") == 0)
      return File::Type::shred;
    if (strcmp(reinterpret_cast<const char*>(extension), ".psarc") == 0)
      return File::Type::psarc;
    if (strcmp(reinterpret_cast<const char*>(extension), ".png") == 0)
      return File::Type::png;
    if (strcmp(reinterpret_cast<const char*>(extension), ".dds") == 0)
      return File::Type::dds;
    if (strcmp(reinterpret_cast<const char*>(extension), ".astc") == 0)
      return File::Type::astc;
#ifdef SHR3D_ENVIRONMENT_SKYBOX
    if (strcmp(reinterpret_cast<const char*>(extension), ".astc6") == 0)
      return File::Type::astc6;
#endif // SHR3D_ENVIRONMENT_SKYBOX
#ifdef SHR3D_ENVIRONMENT_STAGE
    if (strcmp(reinterpret_cast<const char*>(extension), ".stage") == 0)
      return File::Type::stage;
    if (strcmp(reinterpret_cast<const char*>(extension), ".stage_a") == 0)
      return File::Type::stage_a;
#endif // SHR3D_ENVIRONMENT_STAGE
  }

#ifdef PLATFORM_EMSCRIPTEN
  ASSERT(Global::ipfsFileTypeCache.contains(filePath));
  return Global::ipfsFileTypeCache.at(filePath);
#else // PLATFORM_EMSCRIPTEN
  DEBUG_PRINT("Unknown file detected %s.\n", reinterpret_cast<const char*>(filePath));
  return File::Type::unknown;
#endif // PLATFORM_EMSCRIPTEN
}

#ifdef __ANDROID__
extern "C" JNIEXPORT void JNICALL
#ifdef SHR3D_WINDOW_SDL
Java_org_libsdl_app_SDLActivity_openFileDialogResult
#else // SHR3D_WINDOW_SDL
Java_app_shr3d_MainActivity_openFileDialogResult
#endif // SHR3D_WINDOW_SDL
(JNIEnv* env, jclass clazz, jstring path) {
  const char8_t* pathRaw = reinterpret_cast<const char8_t*>(env->GetStringUTFChars(path, nullptr));
  const std::u8string filePath = pathRaw;
  env->ReleaseStringUTFChars(path, reinterpret_cast<const char*>(pathRaw));

  const File::Type fileType = File::type(filePath.c_str());
  switch (fileType)
  {
  case File::Type::psarc:
  case File::Type::shred:
  {
    const std::vector<u8> fileContent = File::read(filePath.c_str());
    Collection::addSongFile(filePath, fileContent.data(), fileContent.size());
  }
  break;
#ifdef SHR3D_ENVIRONMENT_SKYBOX
  case File::Type::astc:
  case File::Type::astc6:
    Global::environmentSkyboxNames.clear();
    Skybox::init();
    break;
#endif // SHR3D_ENVIRONMENT_SKYBOX
#ifdef SHR3D_ENVIRONMENT_STAGE
  case File::Type::stage_a:
    Global::environmentStageNames.clear();
    Stage::init();
    break;
#endif // SHR3D_ENVIRONMENT_STAGE
  default:
    break;
  }
}
#endif // __ANDROID__
