/*
Copyright (C) 2012 Sebastian Herbord. All rights reserved.

This file is part of Mod Organizer.

Mod Organizer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Mod Organizer is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Mod Organizer.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "directoryentry.h"
#include "../envfs.h"
#include "fileentry.h"
#include "filesorigin.h"
#include "originconnection.h"
#include "util.h"
#include "windows_error.h"
#include <filesystem>
#include <log.h>
#include <utility.h>

namespace MOShared
{

using namespace MOBase;
const int MAXPATH_UNICODE = 32767;

template <class F>
void elapsedImpl(std::chrono::nanoseconds& out, F&& f)
{
  if constexpr (DirectoryStats::EnableInstrumentation) {
    const auto start = std::chrono::high_resolution_clock::now();
    f();
    const auto end = std::chrono::high_resolution_clock::now();
    out += (end - start);
  } else {
    f();
  }
}

// elapsed() is not optimized out when EnableInstrumentation is false even
// though it's equivalent that this macro
#define elapsed(OUT, F) (F)();
// #define elapsed(OUT, F) elapsedImpl(OUT, F);

static bool SupportOptimizedFind()
{
  // large fetch and basic info for FindFirstFileEx is supported on win server 2008 r2,
  // win 7 and newer

  OSVERSIONINFOEX versionInfo;
  versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
  versionInfo.dwMajorVersion      = 6;
  versionInfo.dwMinorVersion      = 1;

  ULONGLONG mask = ::VerSetConditionMask(
      ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL), VER_MINORVERSION,
      VER_GREATER_EQUAL);

  return (::VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION,
                              mask) == TRUE);
}

bool DirCompareByName::operator()(const DirectoryEntry* lhs,
                                  const DirectoryEntry* rhs) const
{
  return _wcsicmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0;
}

DirectoryEntry::DirectoryEntry(std::wstring name, DirectoryEntry* parent, int originID)
    : m_OriginConnection(new OriginConnection), m_Name(std::move(name)),
      m_Parent(parent), m_Populated(false), m_TopLevel(true)
{
  m_FileRegister.reset(new FileRegister(m_OriginConnection));
  m_Origins.insert(originID);
}

DirectoryEntry::DirectoryEntry(std::wstring name, DirectoryEntry* parent, int originID,
                               boost::shared_ptr<FileRegister> fileRegister,
                               boost::shared_ptr<OriginConnection> originConnection)
    : m_FileRegister(fileRegister), m_OriginConnection(originConnection),
      m_Name(std::move(name)), m_Parent(parent), m_Populated(false), m_TopLevel(false)
{
  m_Origins.insert(originID);
}

DirectoryEntry::~DirectoryEntry()
{
  clear();
}

void DirectoryEntry::clear()
{
  for (auto itor = m_SubDirectories.rbegin(); itor != m_SubDirectories.rend(); ++itor) {
    delete *itor;
  }

  m_Files.clear();
  m_FilesLookup.clear();
  m_SubDirectories.clear();
  m_SubDirectoriesLookup.clear();
}

void DirectoryEntry::addFromOrigin(const std::wstring& originName,
                                   const std::wstring& directory, int priority,
                                   DirectoryStats& stats)
{
  env::DirectoryWalker walker;
  addFromOrigin(walker, originName, directory, priority, stats);
}

void DirectoryEntry::addFromOrigin(env::DirectoryWalker& walker,
                                   const std::wstring& originName,
                                   const std::wstring& directory, int priority,
                                   DirectoryStats& stats)
{
  FilesOrigin& origin = createOrigin(originName, directory, priority, stats);

  if (!directory.empty()) {
    addFiles(walker, origin, directory, stats);
  }

  m_Populated = true;
}

void DirectoryEntry::addFromList(const std::wstring& originName,
                                 const std::wstring& directory, env::Directory& root,
                                 int priority, DirectoryStats& stats)
{
  stats = {};

  FilesOrigin& origin = createOrigin(originName, directory, priority, stats);
  addDir(origin, root, stats);
}

void DirectoryEntry::addDir(FilesOrigin& origin, env::Directory& d,
                            DirectoryStats& stats)
{
  elapsed(stats.dirTimes, [&] {
    for (auto& sd : d.dirs) {
      auto* sdirEntry = getSubDirectory(sd, true, stats, origin.getID());
      sdirEntry->addDir(origin, sd, stats);
    }
  });

  elapsed(stats.fileTimes, [&] {
    for (auto& f : d.files) {
      insert(f, origin, L"", -1, stats);
    }
  });

  m_Populated = true;
}

void DirectoryEntry::addFromAllBSAs(const std::wstring& originName,
                                    const std::wstring& directory, int priority,
                                    const std::vector<std::wstring>& archives,
                                    const std::set<std::wstring>& enabledArchives,
                                    const std::vector<std::wstring>& loadOrder,
                                    DirectoryStats& stats)
{
  for (const auto& archive : archives) {
    const std::filesystem::path archivePath(archive);
    const auto filename = archivePath.filename().native();

    if (!enabledArchives.contains(filename)) {
      continue;
    }

    const auto filenameLc = ToLowerCopy(filename);

    int order = -1;

    for (auto plugin : loadOrder) {
      const auto pluginNameLc =
          ToLowerCopy(std::filesystem::path(plugin).stem().native());

      if (filenameLc.starts_with(pluginNameLc + L" - ") ||
          filenameLc.starts_with(pluginNameLc + L".")) {
        auto itor = std::find(loadOrder.begin(), loadOrder.end(), plugin);
        if (itor != loadOrder.end()) {
          order = std::distance(loadOrder.begin(), itor);
        }
      }
    }

    addFromBSA(originName, directory, archivePath.native(), priority, order, stats);
  }
}

void DirectoryEntry::addFromBSA(const std::wstring& originName,
                                const std::wstring& directory,
                                const std::wstring& archivePath, int priority,
                                int order, DirectoryStats& stats)
{
  FilesOrigin& origin    = createOrigin(originName, directory, priority, stats);
  const auto archiveName = std::filesystem::path(archivePath).filename().native();

  if (containsArchive(archiveName)) {
    return;
  }

  BSA::Archive archive;
  BSA::EErrorCode res = BSA::ERROR_NONE;

  try {
    // read() can return an error, but it can also throw if the file is not a
    // valid bsa
    res = archive.read(ToString(archivePath, false).c_str(), false);
  } catch (std::exception& e) {
    log::error("invalid bsa '{}', error {}", archivePath, e.what());
    return;
  }

  if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
    log::error("invalid bsa '{}', error {}", archivePath, res);
    return;
  }

  std::error_code ec;
  const auto lwt = std::filesystem::last_write_time(archivePath, ec);
  FILETIME ft    = {};

  if (ec) {
    log::warn("failed to get last modified date for '{}', {}", archivePath,
              ec.message());
  } else {
    ft = ToFILETIME(lwt);
  }

  addFiles(origin, archive.getRoot(), ft, archiveName, order, stats);

  m_Populated = true;
}

void DirectoryEntry::propagateOrigin(int origin)
{
  {
    std::scoped_lock lock(m_OriginsMutex);
    m_Origins.insert(origin);
  }

  if (m_Parent != nullptr) {
    m_Parent->propagateOrigin(origin);
  }
}

bool DirectoryEntry::originExists(const std::wstring& name) const
{
  return m_OriginConnection->exists(name);
}

FilesOrigin& DirectoryEntry::getOriginByID(int ID) const
{
  return m_OriginConnection->getByID(ID);
}

FilesOrigin& DirectoryEntry::getOriginByName(const std::wstring& name) const
{
  return m_OriginConnection->getByName(name);
}

const FilesOrigin* DirectoryEntry::findOriginByID(int ID) const
{
  return m_OriginConnection->findByID(ID);
}

int DirectoryEntry::anyOrigin() const
{
  bool ignore;

  for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
    FileEntryPtr entry = m_FileRegister->getFile(iter->second);
    if ((entry.get() != nullptr) && !entry->isFromArchive()) {
      return entry->getOrigin(ignore);
    }
  }

  // if we got here, no file directly within this directory is a valid indicator for a
  // mod, thus we continue looking in subdirectories
  for (DirectoryEntry* entry : m_SubDirectories) {
    int res = entry->anyOrigin();
    if (res != InvalidOriginID) {
      return res;
    }
  }

  return *(m_Origins.begin());
}

std::vector<FileEntryPtr> DirectoryEntry::getFiles() const
{
  std::vector<FileEntryPtr> result;
  result.reserve(m_Files.size());

  for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
    result.push_back(m_FileRegister->getFile(iter->second));
  }

  return result;
}

DirectoryEntry* DirectoryEntry::findSubDirectory(const std::wstring& name,
                                                 bool alreadyLowerCase) const
{
  SubDirectoriesLookup::const_iterator itor;

  if (alreadyLowerCase) {
    itor = m_SubDirectoriesLookup.find(name);
  } else {
    itor = m_SubDirectoriesLookup.find(ToLowerCopy(name));
  }

  if (itor == m_SubDirectoriesLookup.end()) {
    return nullptr;
  }

  return itor->second;
}

DirectoryEntry* DirectoryEntry::findSubDirectoryRecursive(const std::wstring& path)
{
  DirectoryStats dummy;
  return getSubDirectoryRecursive(path, false, dummy, InvalidOriginID);
}

const FileEntryPtr DirectoryEntry::findFile(const std::wstring& name,
                                            bool alreadyLowerCase) const
{
  FilesLookup::const_iterator iter;

  if (alreadyLowerCase) {
    iter = m_FilesLookup.find(DirectoryEntryFileKey(name));
  } else {
    iter = m_FilesLookup.find(DirectoryEntryFileKey(ToLowerCopy(name)));
  }

  if (iter != m_FilesLookup.end()) {
    return m_FileRegister->getFile(iter->second);
  } else {
    return FileEntryPtr();
  }
}

const FileEntryPtr DirectoryEntry::findFile(const DirectoryEntryFileKey& key) const
{
  auto iter = m_FilesLookup.find(key);

  if (iter != m_FilesLookup.end()) {
    return m_FileRegister->getFile(iter->second);
  } else {
    return FileEntryPtr();
  }
}

bool DirectoryEntry::hasFile(const std::wstring& name) const
{
  return m_Files.contains(ToLowerCopy(name));
}

bool DirectoryEntry::containsArchive(std::wstring archiveName)
{
  for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) {
    FileEntryPtr entry = m_FileRegister->getFile(iter->second);
    if (entry->isFromArchive(archiveName)) {
      return true;
    }
  }

  return false;
}

const FileEntryPtr DirectoryEntry::searchFile(const std::wstring& path,
                                              const DirectoryEntry** directory) const
{
  if (directory != nullptr) {
    *directory = nullptr;
  }

  if ((path.length() == 0) || (path == L"*")) {
    // no file name -> the path ended on a (back-)slash
    if (directory != nullptr) {
      *directory = this;
    }

    return FileEntryPtr();
  }

  const size_t len = path.find_first_of(L"\\/");

  if (len == std::string::npos) {
    // no more path components
    auto iter = m_Files.find(ToLowerCopy(path));

    if (iter != m_Files.end()) {
      return m_FileRegister->getFile(iter->second);
    } else if (directory != nullptr) {
      DirectoryEntry* temp = findSubDirectory(path);
      if (temp != nullptr) {
        *directory = temp;
      }
    }
  } else {
    // file is in a subdirectory, recurse into the matching subdirectory
    std::wstring pathComponent = path.substr(0, len);
    DirectoryEntry* temp       = findSubDirectory(pathComponent);

    if (temp != nullptr) {
      if (len >= path.size()) {
        log::error("{}", QObject::tr("unexpected end of path"));
        return FileEntryPtr();
      }

      return temp->searchFile(path.substr(len + 1), directory);
    }
  }

  return FileEntryPtr();
}

void DirectoryEntry::removeFile(FileIndex index)
{
  removeFileFromList(index);
}

bool DirectoryEntry::removeFile(const std::wstring& filePath, int* origin)
{
  size_t pos = filePath.find_first_of(L"\\/");

  if (pos == std::string::npos) {
    return this->remove(filePath, origin);
  }

  std::wstring dirName = filePath.substr(0, pos);
  std::wstring rest    = filePath.substr(pos + 1);

  DirectoryStats dummy;
  DirectoryEntry* entry = getSubDirectoryRecursive(dirName, false, dummy);

  if (entry != nullptr) {
    return entry->removeFile(rest, origin);
  } else {
    return false;
  }
}

void DirectoryEntry::removeDir(const std::wstring& path)
{
  size_t pos = path.find_first_of(L"\\/");

  if (pos == std::string::npos) {
    for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) {
      DirectoryEntry* entry = *iter;

      if (CaseInsensitiveEqual(entry->getName(), path)) {
        entry->removeDirRecursive();
        removeDirectoryFromList(iter);
        delete entry;
        break;
      }
    }
  } else {
    std::wstring dirName = path.substr(0, pos);
    std::wstring rest    = path.substr(pos + 1);

    DirectoryStats dummy;
    DirectoryEntry* entry = getSubDirectoryRecursive(dirName, false, dummy);

    if (entry != nullptr) {
      entry->removeDir(rest);
    }
  }
}

bool DirectoryEntry::remove(const std::wstring& fileName, int* origin)
{
  const auto lcFileName = ToLowerCopy(fileName);

  auto iter = m_Files.find(lcFileName);
  bool b    = false;

  if (iter != m_Files.end()) {
    if (origin != nullptr) {
      FileEntryPtr entry = m_FileRegister->getFile(iter->second);
      if (entry.get() != nullptr) {
        bool ignore;
        *origin = entry->getOrigin(ignore);
      }
    }

    b = m_FileRegister->removeFile(iter->second);
  }

  return b;
}

bool DirectoryEntry::hasContentsFromOrigin(int originID) const
{
  return m_Origins.find(originID) != m_Origins.end();
}

FilesOrigin& DirectoryEntry::createOrigin(const std::wstring& originName,
                                          const std::wstring& directory, int priority,
                                          DirectoryStats& stats)
{
  auto r = m_OriginConnection->getOrCreate(originName, directory, priority,
                                           m_FileRegister, m_OriginConnection, stats);

  if (r.second) {
    ++stats.originCreate;
  } else {
    ++stats.originExists;
  }

  return r.first;
}

void DirectoryEntry::removeFiles(const std::set<FileIndex>& indices)
{
  removeFilesFromList(indices);
}

FileEntryPtr DirectoryEntry::insert(std::wstring_view fileName, FilesOrigin& origin,
                                    FILETIME fileTime, std::wstring_view archive,
                                    int order, DirectoryStats& stats)
{
  std::wstring fileNameLower = ToLowerCopy(fileName);
  FileEntryPtr fe;

  DirectoryEntryFileKey key(std::move(fileNameLower));

  {
    std::unique_lock lock(m_FilesMutex);

    FilesLookup::iterator itor;

    elapsed(stats.filesLookupTimes, [&] {
      itor = m_FilesLookup.find(key);
    });

    if (itor != m_FilesLookup.end()) {
      lock.unlock();
      ++stats.fileExists;
      fe = m_FileRegister->getFile(itor->second);
    } else {
      ++stats.fileCreate;
      fe = m_FileRegister->createFile(std::wstring(fileName.begin(), fileName.end()),
                                      this, stats);

      elapsed(stats.addFileTimes, [&] {
        addFileToList(std::move(key.value), fe->getIndex());
      });

      // fileNameLower has moved from this point
    }
  }

  elapsed(stats.addOriginToFileTimes, [&] {
    fe->addOrigin(origin.getID(), fileTime, archive, order);
  });

  elapsed(stats.addFileToOriginTimes, [&] {
    origin.addFile(fe->getIndex());
  });

  return fe;
}

FileEntryPtr DirectoryEntry::insert(env::File& file, FilesOrigin& origin,
                                    std::wstring_view archive, int order,
                                    DirectoryStats& stats)
{
  FileEntryPtr fe;

  {
    std::unique_lock lock(m_FilesMutex);

    FilesMap::iterator itor;

    elapsed(stats.filesLookupTimes, [&] {
      itor = m_Files.find(file.lcname);
    });

    if (itor != m_Files.end()) {
      lock.unlock();
      ++stats.fileExists;
      fe = m_FileRegister->getFile(itor->second);
    } else {
      ++stats.fileCreate;
      fe = m_FileRegister->createFile(std::move(file.name), this, stats);
      // file.name has been moved from this point

      elapsed(stats.addFileTimes, [&] {
        addFileToList(std::move(file.lcname), fe->getIndex());
      });

      // file.lcname has been moved from this point
    }
  }

  elapsed(stats.addOriginToFileTimes, [&] {
    fe->addOrigin(origin.getID(), file.lastModified, archive, order);
  });

  elapsed(stats.addFileToOriginTimes, [&] {
    origin.addFile(fe->getIndex());
  });

  return fe;
}

struct DirectoryEntry::Context
{
  FilesOrigin& origin;
  DirectoryStats& stats;
  std::stack<DirectoryEntry*> current;
};

void DirectoryEntry::addFiles(env::DirectoryWalker& walker, FilesOrigin& origin,
                              const std::wstring& path, DirectoryStats& stats)
{
  Context cx = {origin, stats};
  cx.current.push(this);

  if (std::filesystem::exists(path)) {
    walker.forEachEntry(
        path, &cx,
        [](void* pcx, std::wstring_view path) {
          onDirectoryStart((Context*)pcx, path);
        },

        [](void* pcx, std::wstring_view path) {
          onDirectoryEnd((Context*)pcx, path);
        },

        [](void* pcx, std::wstring_view path, FILETIME ft, uint64_t) {
          onFile((Context*)pcx, path, ft);
        });
  }
}

void DirectoryEntry::onDirectoryStart(Context* cx, std::wstring_view path)
{
  elapsed(cx->stats.dirTimes, [&] {
    auto* sd =
        cx->current.top()->getSubDirectory(path, true, cx->stats, cx->origin.getID());

    cx->current.push(sd);
  });
}

void DirectoryEntry::onDirectoryEnd(Context* cx, std::wstring_view path)
{
  elapsed(cx->stats.dirTimes, [&] {
    cx->current.pop();
  });
}

void DirectoryEntry::onFile(Context* cx, std::wstring_view path, FILETIME ft)
{
  elapsed(cx->stats.fileTimes, [&] {
    cx->current.top()->insert(path, cx->origin, ft, L"", -1, cx->stats);
  });
}

void DirectoryEntry::addFiles(FilesOrigin& origin, const BSA::Folder::Ptr archiveFolder,
                              FILETIME fileTime, const std::wstring& archiveName,
                              int order, DirectoryStats& stats)
{
  // add files
  const auto fileCount = archiveFolder->getNumFiles();
  for (unsigned int i = 0; i < fileCount; ++i) {
    const BSA::File::Ptr file = archiveFolder->getFile(i);

    auto f = insert(ToWString(file->getName(), true), origin, fileTime, archiveName,
                    order, stats);

    if (f) {
      if (file->getUncompressedFileSize() > 0) {
        f->setFileSize(file->getFileSize(), file->getUncompressedFileSize());
      } else {
        f->setFileSize(file->getFileSize(), FileEntry::NoFileSize);
      }
    }
  }

  // recurse into subdirectories
  const auto dirCount = archiveFolder->getNumSubFolders();
  for (unsigned int i = 0; i < dirCount; ++i) {
    const BSA::Folder::Ptr folder = archiveFolder->getSubFolder(i);

    DirectoryEntry* folderEntry = getSubDirectoryRecursive(
        ToWString(folder->getName(), true), true, stats, origin.getID());

    folderEntry->addFiles(origin, folder, fileTime, archiveName, order, stats);
  }
}

DirectoryEntry* DirectoryEntry::getSubDirectory(std::wstring_view name, bool create,
                                                DirectoryStats& stats, int originID)
{
  std::wstring nameLc = ToLowerCopy(name);

  std::scoped_lock lock(m_SubDirMutex);

  SubDirectoriesLookup::iterator itor;
  elapsed(stats.subdirLookupTimes, [&] {
    itor = m_SubDirectoriesLookup.find(nameLc);
  });

  if (itor != m_SubDirectoriesLookup.end()) {
    ++stats.subdirExists;
    return itor->second;
  }

  if (create) {
    ++stats.subdirCreate;

    auto* entry = new DirectoryEntry(std::wstring(name.begin(), name.end()), this,
                                     originID, m_FileRegister, m_OriginConnection);

    elapsed(stats.addDirectoryTimes, [&] {
      addDirectoryToList(entry, std::move(nameLc));
      // nameLc is moved from this point
    });

    return entry;
  } else {
    return nullptr;
  }
}

DirectoryEntry* DirectoryEntry::getSubDirectory(env::Directory& dir, bool create,
                                                DirectoryStats& stats, int originID)
{
  SubDirectoriesLookup::iterator itor;

  std::scoped_lock lock(m_SubDirMutex);

  elapsed(stats.subdirLookupTimes, [&] {
    itor = m_SubDirectoriesLookup.find(dir.lcname);
  });

  if (itor != m_SubDirectoriesLookup.end()) {
    ++stats.subdirExists;
    return itor->second;
  }

  if (create) {
    ++stats.subdirCreate;

    auto* entry = new DirectoryEntry(std::move(dir.name), this, originID,
                                     m_FileRegister, m_OriginConnection);
    // dir.name is moved from this point

    elapsed(stats.addDirectoryTimes, [&] {
      addDirectoryToList(entry, std::move(dir.lcname));
    });

    // dir.lcname is moved from this point

    return entry;
  } else {
    return nullptr;
  }
}

DirectoryEntry* DirectoryEntry::getSubDirectoryRecursive(const std::wstring& path,
                                                         bool create,
                                                         DirectoryStats& stats,
                                                         int originID)
{
  if (path.length() == 0) {
    // path ended with a backslash?
    return this;
  }

  const size_t pos = path.find_first_of(L"\\/");

  if (pos == std::wstring::npos) {
    return getSubDirectory(path, create, stats);
  } else {
    DirectoryEntry* nextChild =
        getSubDirectory(path.substr(0, pos), create, stats, originID);

    if (nextChild == nullptr) {
      return nullptr;
    } else {
      return nextChild->getSubDirectoryRecursive(path.substr(pos + 1), create, stats,
                                                 originID);
    }
  }
}

void DirectoryEntry::removeDirRecursive()
{
  while (!m_Files.empty()) {
    m_FileRegister->removeFile(m_Files.begin()->second);
  }

  m_FilesLookup.clear();

  for (DirectoryEntry* entry : m_SubDirectories) {
    entry->removeDirRecursive();
    delete entry;
  }

  m_SubDirectories.clear();
  m_SubDirectoriesLookup.clear();
}

void DirectoryEntry::addDirectoryToList(DirectoryEntry* e, std::wstring nameLc)
{
  m_SubDirectories.insert(e);
  m_SubDirectoriesLookup.emplace(std::move(nameLc), e);
}

void DirectoryEntry::removeDirectoryFromList(SubDirectories::iterator itor)
{
  const auto* entry = *itor;

  {
    auto itor2 = std::find_if(m_SubDirectoriesLookup.begin(),
                              m_SubDirectoriesLookup.end(), [&](auto&& d) {
                                return (d.second == entry);
                              });

    if (itor2 == m_SubDirectoriesLookup.end()) {
      log::error("entry {} not in sub directories map", entry->getName());
    } else {
      m_SubDirectoriesLookup.erase(itor2);
    }
  }

  m_SubDirectories.erase(itor);
}

void DirectoryEntry::removeFileFromList(FileIndex index)
{
  auto removeFrom = [&](auto& list) {
    auto iter = std::find_if(list.begin(), list.end(), [&index](auto&& pair) {
      return (pair.second == index);
    });

    if (iter == list.end()) {
      auto f = m_FileRegister->getFile(index);

      if (f) {
        log::error("can't remove file '{}', not in directory entry '{}'", f->getName(),
                   getName());
      } else {
        log::error("can't remove file with index {}, not in directory entry '{}' and "
                   "not in register",
                   index, getName());
      }
    } else {
      list.erase(iter);
    }
  };

  removeFrom(m_FilesLookup);
  removeFrom(m_Files);
}

void DirectoryEntry::removeFilesFromList(const std::set<FileIndex>& indices)
{
  for (auto iter = m_Files.begin(); iter != m_Files.end();) {
    if (indices.find(iter->second) != indices.end()) {
      iter = m_Files.erase(iter);
    } else {
      ++iter;
    }
  }

  for (auto iter = m_FilesLookup.begin(); iter != m_FilesLookup.end();) {
    if (indices.find(iter->second) != indices.end()) {
      iter = m_FilesLookup.erase(iter);
    } else {
      ++iter;
    }
  }
}

void DirectoryEntry::addFileToList(std::wstring fileNameLower, FileIndex index)
{
  m_FilesLookup.emplace(fileNameLower, index);
  m_Files.emplace(std::move(fileNameLower), index);
  // fileNameLower has been moved from this point
}

struct DumpFailed : public std::runtime_error
{
  using runtime_error::runtime_error;
};

void DirectoryEntry::dump(const std::wstring& file) const
{
  try {
    std::FILE* f = nullptr;
    auto e       = _wfopen_s(&f, file.c_str(), L"wb");

    if (e != 0 || !f) {
      throw DumpFailed(std::format("failed to open, {} ({})", std::strerror(e), e));
    }

    Guard g([&] {
      std::fclose(f);
    });

    dump(f, L"Data");
  } catch (DumpFailed& e) {
    log::error("failed to write list to '{}': {}",
               QString::fromStdWString(file).toStdString(), e.what());
  }
}

void DirectoryEntry::dump(std::FILE* f, const std::wstring& parentPath) const
{
  {
    std::scoped_lock lock(m_FilesMutex);

    for (auto&& index : m_Files) {
      const auto file = m_FileRegister->getFile(index.second);
      if (!file) {
        continue;
      }

      if (file->isFromArchive()) {
        // TODO: don't list files from archives. maybe make this an option?
        continue;
      }

      const auto& o   = m_OriginConnection->getByID(file->getOrigin());
      const auto path = parentPath + L"\\" + file->getName();
      const auto line = path + L"\t(" + o.getName() + L")\r\n";

      const auto lineu8 = MOShared::ToString(line, true);

      if (std::fwrite(lineu8.data(), lineu8.size(), 1, f) != 1) {
        const auto e = errno;
        throw DumpFailed(std::format("failed to write, {} ({})", std::strerror(e), e));
      }
    }
  }

  {
    std::scoped_lock lock(m_SubDirMutex);
    for (auto&& d : m_SubDirectories) {
      const auto path = parentPath + L"\\" + d->m_Name;
      d->dump(f, path);
    }
  }
}

}  // namespace MOShared