// Copyright 2016 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "Core/WiiRoot.h"

#include <optional>
#include <string>
#include <vector>

#include <fmt/format.h>

#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/Logging/Log.h"
#include "Common/NandPaths.h"
#include "Common/StringUtil.h"
#include "Core/Boot/Boot.h"
#include "Core/CommonTitles.h"
#include "Core/Config/SessionSettings.h"
#include "Core/ConfigManager.h"
#include "Core/HW/WiiSave.h"
#include "Core/IOS/ES/ES.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/Uids.h"
#include "Core/Movie.h"
#include "Core/NetPlayClient.h"
#include "Core/SysConf.h"

namespace Core
{
namespace FS = IOS::HLE::FS;

static std::string s_temp_wii_root;
static std::string s_temp_redirect_root;
static bool s_wii_root_initialized = false;
static std::vector<IOS::HLE::FS::NandRedirect> s_nand_redirects;

// When Temp NAND + Redirects are both active, we need to keep track of where each redirect path
// should be copied back to after a successful session finish.
struct TempRedirectPath
{
  std::string real_path;
  std::string temp_path;
};
static std::vector<TempRedirectPath> s_temp_nand_redirects;

const std::vector<IOS::HLE::FS::NandRedirect>& GetActiveNandRedirects()
{
  return s_nand_redirects;
}

static bool CopyBackupFile(const std::string& path_from, const std::string& path_to)
{
  if (!File::Exists(path_from))
    return false;

  File::CreateFullPath(path_to);

  return File::Copy(path_from, path_to);
}

static void DeleteBackupFile(const std::string& file_name)
{
  File::Delete(File::GetUserPath(D_BACKUP_IDX) + file_name);
}

static void BackupFile(const std::string& path_in_nand)
{
  const std::string file_name = PathToFileName(path_in_nand);
  const std::string original_path = File::GetUserPath(D_WIIROOT_IDX) + path_in_nand;
  const std::string backup_path = File::GetUserPath(D_BACKUP_IDX) + file_name;

  CopyBackupFile(original_path, backup_path);
}

static void RestoreFile(const std::string& path_in_nand)
{
  const std::string file_name = PathToFileName(path_in_nand);
  const std::string original_path = File::GetUserPath(D_WIIROOT_IDX) + path_in_nand;
  const std::string backup_path = File::GetUserPath(D_BACKUP_IDX) + file_name;

  if (CopyBackupFile(backup_path, original_path))
    DeleteBackupFile(file_name);
}

static void CopySave(FS::FileSystem* source, FS::FileSystem* dest, const u64 title_id)
{
  dest->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, Common::GetTitleDataPath(title_id) + '/',
                       0, {FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite});
  const auto source_save = WiiSave::MakeNandStorage(source, title_id);
  const auto dest_save = WiiSave::MakeNandStorage(dest, title_id);
  WiiSave::Copy(source_save.get(), dest_save.get());
}

static bool CopyNandFile(FS::FileSystem* source_fs, const std::string& source_file,
                         FS::FileSystem* dest_fs, const std::string& dest_file)
{
  auto source_handle =
      source_fs->OpenFile(IOS::PID_KERNEL, IOS::PID_KERNEL, source_file, IOS::HLE::FS::Mode::Read);
  // If the source file doesn't exist, there is nothing more to do.
  // This function must not create an empty file on the destination filesystem.
  if (!source_handle)
    return true;

  dest_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, dest_file, 0,
                          {FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite});

  auto dest_handle =
      dest_fs->CreateAndOpenFile(IOS::PID_KERNEL, IOS::PID_KERNEL, source_file,
                                 {IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite,
                                  IOS::HLE::FS::Mode::ReadWrite});
  if (!dest_handle)
    return false;

  std::vector<u8> buffer(source_handle->GetStatus()->size);
  if (!source_handle->Read(buffer.data(), buffer.size()))
    return false;
  if (!dest_handle->Write(buffer.data(), buffer.size()))
    return false;

  return true;
}

static void InitializeDeterministicWiiSaves(FS::FileSystem* session_fs,
                                            const BootSessionData& boot_session_data)
{
  const u64 title_id = SConfig::GetInstance().GetTitleID();
  const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured);
  if (Movie::IsRecordingInput())
  {
    if (NetPlay::IsNetPlayRunning() && !SConfig::GetInstance().bCopyWiiSaveNetplay)
    {
      Movie::SetClearSave(true);
    }
    else
    {
      // TODO: Check for the actual save data
      const std::string path = Common::GetTitleDataPath(title_id) + "/banner.bin";
      Movie::SetClearSave(!configured_fs->GetMetadata(IOS::PID_KERNEL, IOS::PID_KERNEL, path));
    }
  }

  if ((NetPlay::IsNetPlayRunning() && SConfig::GetInstance().bCopyWiiSaveNetplay) ||
      (Movie::IsMovieActive() && !Movie::IsStartingFromClearSave()))
  {
    // Copy the current user's save to the Blank NAND
    auto* sync_fs = boot_session_data.GetWiiSyncFS();
    auto& sync_titles = boot_session_data.GetWiiSyncTitles();
    if (sync_fs)
    {
      INFO_LOG_FMT(CORE, "Wii Save Init: Copying from sync_fs to session_fs.");

      for (const u64 title : sync_titles)
      {
        INFO_LOG_FMT(CORE, "Wii Save Init: Copying {0:016x}.", title);
        CopySave(sync_fs, session_fs, title);
      }

      // Copy Mii data
      if (!CopyNandFile(sync_fs, Common::GetMiiDatabasePath(), session_fs,
                        Common::GetMiiDatabasePath()))
      {
        WARN_LOG_FMT(CORE, "Failed to copy Mii database to the NAND");
      }
    }
    else
    {
      INFO_LOG_FMT(CORE, "Wii Save Init: Copying from configured_fs to session_fs.");

      for (const u64 title : sync_titles)
      {
        INFO_LOG_FMT(CORE, "Wii Save Init: Copying {0:016x}.", title);
        CopySave(configured_fs.get(), session_fs, title);
      }

      // Copy Mii data
      if (!CopyNandFile(configured_fs.get(), Common::GetMiiDatabasePath(), session_fs,
                        Common::GetMiiDatabasePath()))
      {
        WARN_LOG_FMT(CORE, "Failed to copy Mii database to the NAND");
      }
    }

    const auto& netplay_redirect_folder = boot_session_data.GetWiiSyncRedirectFolder();
    if (!netplay_redirect_folder.empty())
      File::CopyDir(netplay_redirect_folder, s_temp_redirect_root + "/");
  }
}

static void MoveToBackupIfExists(const std::string& path)
{
  if (File::Exists(path))
  {
    const std::string backup_path = path.substr(0, path.size() - 1) + ".backup" DIR_SEP;
    WARN_LOG_FMT(IOS_FS, "Temporary directory at {} exists, moving to backup...", path);

    // If backup exists, delete it as we don't want a mess
    if (File::Exists(backup_path))
    {
      WARN_LOG_FMT(IOS_FS, "Temporary backup directory at {} exists, deleting...", backup_path);
      File::DeleteDirRecursively(backup_path);
    }

    File::CopyDir(path, backup_path, true);
  }
}

void InitializeWiiRoot(bool use_temporary)
{
  ASSERT(!s_wii_root_initialized);

  if (use_temporary)
  {
    s_temp_wii_root = File::GetUserPath(D_USER_IDX) + "WiiSession" DIR_SEP;
    s_temp_redirect_root = File::GetUserPath(D_USER_IDX) + "RedirectSession" DIR_SEP;
    WARN_LOG_FMT(IOS_FS, "Using temporary directory {} for minimal Wii FS", s_temp_wii_root);
    WARN_LOG_FMT(IOS_FS, "Using temporary directory {} for redirected saves", s_temp_redirect_root);

    // If directory exists, make a backup
    MoveToBackupIfExists(s_temp_wii_root);
    MoveToBackupIfExists(s_temp_redirect_root);

    File::SetUserPath(D_SESSION_WIIROOT_IDX, s_temp_wii_root);
  }
  else
  {
    File::SetUserPath(D_SESSION_WIIROOT_IDX, File::GetUserPath(D_WIIROOT_IDX));
  }

  s_nand_redirects.clear();
  s_wii_root_initialized = true;
}

void ShutdownWiiRoot()
{
  if (WiiRootIsTemporary())
  {
    File::DeleteDirRecursively(s_temp_wii_root);
    s_temp_wii_root.clear();
    File::DeleteDirRecursively(s_temp_redirect_root);
    s_temp_redirect_root.clear();
    s_temp_nand_redirects.clear();
  }

  s_nand_redirects.clear();
  s_wii_root_initialized = false;
}

bool WiiRootIsInitialized()
{
  return s_wii_root_initialized;
}

bool WiiRootIsTemporary()
{
  return !s_temp_wii_root.empty();
}

void BackupWiiSettings()
{
  // Back up files which Dolphin can modify at boot, so that we can preserve the original contents.
  // For SYSCONF, the backup is only needed in case Dolphin crashes or otherwise exists unexpectedly
  // during emulation, since the config system will restore the SYSCONF settings at emulation end.
  // For setting.txt, there is no other code that restores the original values for us.

  BackupFile(Common::GetTitleDataPath(Titles::SYSTEM_MENU) + "/" WII_SETTING);
  BackupFile("/shared2/sys/SYSCONF");
}

void RestoreWiiSettings(RestoreReason reason)
{
  RestoreFile(Common::GetTitleDataPath(Titles::SYSTEM_MENU) + "/" WII_SETTING);

  // We must not restore the SYSCONF backup when ending emulation cleanly, since the user may have
  // edited the SYSCONF file in the NAND using the emulated software (e.g. the Wii Menu settings).
  if (reason == RestoreReason::CrashRecovery)
    RestoreFile("/shared2/sys/SYSCONF");
  else
    DeleteBackupFile("SYSCONF");
}

/// Copy a directory from host_source_path (on the host FS) to nand_target_path on the NAND.
///
/// Both paths should not have trailing slashes. To specify the NAND root, use "".
static bool CopySysmenuFilesToFS(FS::FileSystem* fs, const std::string& host_source_path,
                                 const std::string& nand_target_path)
{
  const auto entries = File::ScanDirectoryTree(host_source_path, false);
  for (const File::FSTEntry& entry : entries.children)
  {
    const std::string host_path = host_source_path + '/' + entry.virtualName;
    const std::string nand_path = nand_target_path + '/' + entry.virtualName;
    constexpr FS::Modes public_modes{FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite};

    if (entry.isDirectory)
    {
      fs->CreateDirectory(IOS::SYSMENU_UID, IOS::SYSMENU_GID, nand_path, 0, public_modes);
      if (!CopySysmenuFilesToFS(fs, host_path, nand_path))
        return false;
    }
    else
    {
      // Do not overwrite any existing files.
      if (fs->GetMetadata(IOS::SYSMENU_UID, IOS::SYSMENU_UID, nand_path).Succeeded())
        continue;

      File::IOFile host_file{host_path, "rb"};
      std::vector<u8> file_data(host_file.GetSize());
      if (!host_file.ReadBytes(file_data.data(), file_data.size()))
        return false;

      const auto nand_file =
          fs->CreateAndOpenFile(IOS::SYSMENU_UID, IOS::SYSMENU_GID, nand_path, public_modes);
      if (!nand_file || !nand_file->Write(file_data.data(), file_data.size()))
        return false;
    }
  }
  return true;
}

void InitializeWiiFileSystemContents(
    std::optional<DiscIO::Riivolution::SavegameRedirect> save_redirect,
    const BootSessionData& boot_session_data)
{
  const auto fs = IOS::HLE::GetIOS()->GetFS();

  // Some games (such as Mario Kart Wii) assume that NWC24 files will always be present
  // even upon the first launch as they are normally created by the system menu.
  // Because we do not require the system menu to be run, WiiConnect24 files must be copied
  // to the NAND manually.
  if (!CopySysmenuFilesToFS(fs.get(), File::GetSysDirectory() + WII_USER_DIR, ""))
    WARN_LOG_FMT(CORE, "Failed to copy initial System Menu files to the NAND");

  const bool is_temp_nand = WiiRootIsTemporary();
  if (is_temp_nand)
  {
    // Generate a SYSCONF with default settings for the temporary Wii NAND.
    SysConf sysconf{fs};
    sysconf.Save();

    InitializeDeterministicWiiSaves(fs.get(), boot_session_data);
  }

  if (save_redirect)
  {
    const u64 title_id = SConfig::GetInstance().GetTitleID();
    std::string source_path = Common::GetTitleDataPath(title_id);

    if (is_temp_nand)
    {
      // remember the actual path for copying back on shutdown and redirect to a temp folder instead
      s_temp_nand_redirects.emplace_back(
          TempRedirectPath{save_redirect->m_target_path, s_temp_redirect_root});
      save_redirect->m_target_path = s_temp_redirect_root;
    }

    if (!File::IsDirectory(save_redirect->m_target_path))
    {
      File::CreateFullPath(save_redirect->m_target_path + "/");
      if (save_redirect->m_clone)
      {
        File::CopyDir(Common::GetTitleDataPath(title_id, Common::FROM_SESSION_ROOT),
                      save_redirect->m_target_path);
      }
    }
    s_nand_redirects.emplace_back(IOS::HLE::FS::NandRedirect{
        std::move(source_path), std::move(save_redirect->m_target_path)});
    fs->SetNandRedirects(s_nand_redirects);
  }
}

void CleanUpWiiFileSystemContents(const BootSessionData& boot_session_data)
{
  // In TAS mode, copy back always.
  // In Netplay, only copy back when we're the host and writing back is enabled.
  const bool wii_root_is_temporary = WiiRootIsTemporary();
  const auto* netplay_settings = boot_session_data.GetNetplaySettings();
  const bool is_netplay_write = netplay_settings && netplay_settings->savedata_write;
  const bool is_netplay_host = netplay_settings && netplay_settings->is_hosting;
  const bool cleanup_required =
      wii_root_is_temporary && (!netplay_settings || (is_netplay_write && is_netplay_host));

  INFO_LOG_FMT(CORE,
               "Wii FS Cleanup: cleanup_required = {} (wii_root_is_temporary = {}, "
               "is netplay = {}, is_netplay_write = {}, is_netplay_host = {})",
               cleanup_required, wii_root_is_temporary, !!netplay_settings, is_netplay_write,
               is_netplay_host);

  if (!cleanup_required)
    return;

  INFO_LOG_FMT(CORE, "Wii FS Cleanup: Copying from temporary FS to configured_fs.");

  // copy back the temp nand redirected files to where they should normally be redirected to
  for (const auto& redirect : s_temp_nand_redirects)
    File::CopyDir(redirect.temp_path, redirect.real_path + "/", true);

  IOS::HLE::EmulationKernel* ios = IOS::HLE::GetIOS();

  // clear the redirects in the session FS, otherwise the back-copy might grab redirected files
  s_nand_redirects.clear();
  ios->GetFS()->SetNandRedirects({});

  const auto configured_fs = FS::MakeFileSystem(FS::Location::Configured);

  // Copy back Mii data
  if (!CopyNandFile(ios->GetFS().get(), Common::GetMiiDatabasePath(), configured_fs.get(),
                    Common::GetMiiDatabasePath()))
  {
    WARN_LOG_FMT(CORE, "Failed to copy Mii database to the NAND");
  }

  // If we started by copying only certain saves, we also only want to copy back those exact saves.
  // This prevents a situation where you change game and create a new save that was not loaded from
  // the real NAND during netplay, and that then overwrites your existing local save during this
  // cleanup process.
  const bool copy_all = !netplay_settings || netplay_settings->savedata_sync_all_wii;
  for (const u64 title_id :
       (copy_all ? ios->GetES()->GetInstalledTitles() : boot_session_data.GetWiiSyncTitles()))
  {
    INFO_LOG_FMT(CORE, "Wii FS Cleanup: Copying {0:016x}.", title_id);

    const auto session_save = WiiSave::MakeNandStorage(ios->GetFS().get(), title_id);

    // FS won't write the save if the directory doesn't exist
    const std::string title_path = Common::GetTitleDataPath(title_id);
    configured_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path + '/', 0,
                                  {FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite});

    const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id);

    const std::string backup_path =
        fmt::format("{}/{:016x}.bin", File::GetUserPath(D_BACKUP_IDX), title_id);
    const auto backup_save = WiiSave::MakeDataBinStorage(&ios->GetIOSC(), backup_path, "w+b");

    // Backup the existing save just in case it's still needed.
    WiiSave::Copy(user_save.get(), backup_save.get());
    WiiSave::Copy(session_save.get(), user_save.get());
  }
}
}  // namespace Core
