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

#include "DiscIO/DirectoryBlob.h"

#include <algorithm>
#include <array>
#include <cstring>
#include <locale>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <variant>
#include <vector>

#include "Common/Align.h"
#include "Common/Assert.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
#include "Core/Boot/DolReader.h"
#include "Core/IOS/ES/Formats.h"
#include "DiscIO/Blob.h"
#include "DiscIO/DiscUtils.h"
#include "DiscIO/VolumeDisc.h"
#include "DiscIO/VolumeWii.h"
#include "DiscIO/WiiEncryptionCache.h"

namespace DiscIO
{
// Reads as many bytes as the vector fits (or less, if the file is smaller).
// Returns the number of bytes read.
static size_t ReadFileToVector(const std::string& path, std::vector<u8>* vector);

static void PadToAddress(u64 start_address, u64* address, u64* length, u8** buffer);
static void Write32(u32 data, u32 offset, std::vector<u8>* buffer);

enum class PartitionType : u32
{
  Game = 0,
  Update = 1,
  Channel = 2,
  // There are more types used by Super Smash Bros. Brawl, but they don't have special names
};

// 0xFF is an arbitrarily picked value. Note that we can't use 0x00, because that means NTSC-J
constexpr u32 INVALID_REGION = 0xFF;

constexpr u32 PARTITION_DATA_OFFSET = 0x20000;

constexpr u8 ENTRY_SIZE = 0x0c;
constexpr u8 FILE_ENTRY = 0;
constexpr u8 DIRECTORY_ENTRY = 1;

DiscContent::DiscContent(u64 offset, u64 size, ContentSource source)
    : m_offset(offset), m_size(size), m_content_source(std::move(source))
{
}

DiscContent::DiscContent(u64 offset) : m_offset(offset)
{
}

u64 DiscContent::GetOffset() const
{
  return m_offset;
}

u64 DiscContent::GetEndOffset() const
{
  return m_offset + m_size;
}

u64 DiscContent::GetSize() const
{
  return m_size;
}

bool DiscContent::Read(u64* offset, u64* length, u8** buffer) const
{
  if (m_size == 0)
    return true;

  DEBUG_ASSERT(*offset >= m_offset);
  const u64 offset_in_content = *offset - m_offset;

  if (offset_in_content < m_size)
  {
    const u64 bytes_to_read = std::min(m_size - offset_in_content, *length);

    if (std::holds_alternative<ContentFile>(m_content_source))
    {
      const auto& content = std::get<ContentFile>(m_content_source);
      File::IOFile file(content.m_filename, "rb");
      if (!file.Seek(content.m_offset + offset_in_content, File::SeekOrigin::Begin) ||
          !file.ReadBytes(*buffer, bytes_to_read))
      {
        return false;
      }
    }
    else if (std::holds_alternative<const u8*>(m_content_source))
    {
      const u8* const content_pointer = std::get<const u8*>(m_content_source) + offset_in_content;
      std::copy(content_pointer, content_pointer + bytes_to_read, *buffer);
    }
    else if (std::holds_alternative<ContentPartition>(m_content_source))
    {
      const auto& content = std::get<ContentPartition>(m_content_source);
      DirectoryBlobReader* blob = content.m_reader;
      const u64 decrypted_size = m_size * VolumeWii::BLOCK_DATA_SIZE / VolumeWii::BLOCK_TOTAL_SIZE;
      if (!blob->EncryptPartitionData(content.m_offset + offset_in_content, bytes_to_read, *buffer,
                                      content.m_partition_data_offset, decrypted_size))
      {
        return false;
      }
    }
    else if (std::holds_alternative<ContentVolume>(m_content_source))
    {
      const auto& source = std::get<ContentVolume>(m_content_source);
      if (!source.m_volume->Read(source.m_offset + offset_in_content, bytes_to_read, *buffer,
                                 source.m_partition))
      {
        return false;
      }
    }
    else if (std::holds_alternative<ContentFixedByte>(m_content_source))
    {
      const ContentFixedByte& source = std::get<ContentFixedByte>(m_content_source);
      std::fill_n(*buffer, bytes_to_read, source.m_byte);
    }
    else
    {
      PanicAlertFmt("DirectoryBlob: Invalid content source in DiscContent.");
      return false;
    }

    *length -= bytes_to_read;
    *buffer += bytes_to_read;
    *offset += bytes_to_read;
  }

  return true;
}

void DiscContentContainer::Add(u64 offset, u64 size, ContentSource source)
{
  if (size != 0)
    m_contents.emplace(offset, size, std::move(source));
}

u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, const std::string& path)
{
  const u64 size = File::GetSize(path);
  Add(offset, size, ContentFile{path, 0});
  return size;
}

u64 DiscContentContainer::CheckSizeAndAdd(u64 offset, u64 max_size, const std::string& path)
{
  const u64 size = std::min(File::GetSize(path), max_size);
  Add(offset, size, ContentFile{path, 0});
  return size;
}

bool DiscContentContainer::Read(u64 offset, u64 length, u8* buffer) const
{
  // Determine which DiscContent the offset refers to
  std::set<DiscContent>::const_iterator it = m_contents.upper_bound(DiscContent(offset));

  while (it != m_contents.end() && length > 0)
  {
    // Zero fill to start of DiscContent data
    PadToAddress(it->GetOffset(), &offset, &length, &buffer);

    if (length == 0)
      return true;

    if (!it->Read(&offset, &length, &buffer))
      return false;

    ++it;
    DEBUG_ASSERT(it == m_contents.end() || it->GetOffset() >= offset);
  }

  // Zero fill if we went beyond the last DiscContent
  std::fill_n(buffer, static_cast<size_t>(length), 0);

  return true;
}

static std::optional<PartitionType> ParsePartitionDirectoryName(const std::string& name)
{
  if (name.size() < 2)
    return {};

  if (!strcasecmp(name.c_str(), "DATA"))
    return PartitionType::Game;
  if (!strcasecmp(name.c_str(), "UPDATE"))
    return PartitionType::Update;
  if (!strcasecmp(name.c_str(), "CHANNEL"))
    return PartitionType::Channel;

  if (name[0] == 'P' || name[0] == 'p')
  {
    // e.g. "P-HA8E" (normally only used for Super Smash Bros. Brawl's VC partitions)
    if (name[1] == '-' && name.size() == 6)
    {
      const u32 result = Common::swap32(reinterpret_cast<const u8*>(name.data() + 2));
      return static_cast<PartitionType>(result);
    }

    // e.g. "P0"
    if (std::all_of(name.cbegin() + 1, name.cend(), [](char c) { return c >= '0' && c <= '9'; }))
    {
      u32 result;
      if (TryParse(name.substr(1), &result))
        return static_cast<PartitionType>(result);
    }
  }

  return {};
}

static bool IsDirectorySeparator(char c)
{
  return c == '/'
#ifdef _WIN32
         || c == '\\'
#endif
      ;
}

static bool PathCharactersEqual(char a, char b)
{
  return a == b || (IsDirectorySeparator(a) && IsDirectorySeparator(b));
}

static bool PathEndsWith(const std::string& path, const std::string& suffix)
{
  if (suffix.size() > path.size())
    return false;

  std::string::const_iterator path_iterator = path.cend() - suffix.size();
  std::string::const_iterator suffix_iterator = suffix.cbegin();
  while (path_iterator != path.cend())
  {
    if (!PathCharactersEqual(*path_iterator, *suffix_iterator))
      return false;
    path_iterator++;
    suffix_iterator++;
  }

  return true;
}

static bool IsValidDirectoryBlob(const std::string& dol_path, std::string* partition_root,
                                 std::string* true_root = nullptr)
{
  if (!PathEndsWith(dol_path, "/sys/main.dol"))
    return false;

  const size_t chars_to_remove = std::string("sys/main.dol").size();
  *partition_root = dol_path.substr(0, dol_path.size() - chars_to_remove);

  if (File::GetSize(*partition_root + "sys/boot.bin") < 0x20)
    return false;

#ifdef _WIN32
  constexpr const char* dir_separator = "/\\";
#else
  constexpr char dir_separator = '/';
#endif
  if (true_root)
  {
    *true_root =
        dol_path.substr(0, dol_path.find_last_of(dir_separator, partition_root->size() - 2) + 1);
  }

  return true;
}

static bool ExistsAndIsValidDirectoryBlob(const std::string& dol_path)
{
  std::string partition_root;
  return File::Exists(dol_path) && IsValidDirectoryBlob(dol_path, &partition_root);
}

static bool IsInFilesDirectory(const std::string& path)
{
  size_t files_pos = std::string::npos;
  while (true)
  {
    files_pos = path.rfind("files", files_pos);
    if (files_pos == std::string::npos)
      return false;

    const size_t slash_before_pos = files_pos - 1;
    const size_t slash_after_pos = files_pos + 5;
    if ((files_pos == 0 || IsDirectorySeparator(path[slash_before_pos])) &&
        (slash_after_pos == path.size() || (IsDirectorySeparator(path[slash_after_pos]))) &&
        ExistsAndIsValidDirectoryBlob(path.substr(0, files_pos) + "sys/main.dol"))
    {
      return true;
    }

    --files_pos;
  }
}

static bool IsMainDolForNonGamePartition(const std::string& path)
{
  std::string partition_root, true_root;
  if (!IsValidDirectoryBlob(path, &partition_root, &true_root))
    return false;  // This is not a /sys/main.dol

  std::string partition_directory_name = partition_root.substr(true_root.size());
  partition_directory_name.pop_back();  // Remove trailing slash
  const std::optional<PartitionType> partition_type =
      ParsePartitionDirectoryName(partition_directory_name);
  if (!partition_type || *partition_type == PartitionType::Game)
    return false;  // volume_path is the game partition's /sys/main.dol

  const File::FSTEntry true_root_entry = File::ScanDirectoryTree(true_root, false);
  for (const File::FSTEntry& entry : true_root_entry.children)
  {
    if (entry.isDirectory &&
        ParsePartitionDirectoryName(entry.virtualName) == PartitionType::Game &&
        ExistsAndIsValidDirectoryBlob(entry.physicalName + "/sys/main.dol"))
    {
      return true;  // volume_path is the /sys/main.dol for a non-game partition
    }
  }

  return false;  // volume_path is the game partition's /sys/main.dol
}

bool ShouldHideFromGameList(const std::string& volume_path)
{
  return IsInFilesDirectory(volume_path) || IsMainDolForNonGamePartition(volume_path);
}

std::unique_ptr<DirectoryBlobReader> DirectoryBlobReader::Create(const std::string& dol_path)
{
  std::string partition_root, true_root;
  if (!IsValidDirectoryBlob(dol_path, &partition_root, &true_root))
    return nullptr;

  return std::unique_ptr<DirectoryBlobReader>(new DirectoryBlobReader(partition_root, true_root));
}

std::unique_ptr<DirectoryBlobReader> DirectoryBlobReader::Create(
    std::unique_ptr<DiscIO::VolumeDisc> volume,
    const std::function<void(std::vector<FSTBuilderNode>* fst_nodes, FSTBuilderNode* dol_node)>&
        fst_callback)
{
  if (!volume)
    return nullptr;

  return std::unique_ptr<DirectoryBlobReader>(
      new DirectoryBlobReader(std::move(volume), fst_callback));
}

DirectoryBlobReader::DirectoryBlobReader(const std::string& game_partition_root,
                                         const std::string& true_root)
    : m_encryption_cache(this)
{
  DirectoryBlobPartition game_partition(game_partition_root, {});
  m_is_wii = game_partition.IsWii();

  if (!m_is_wii)
  {
    m_gamecube_pseudopartition = std::move(game_partition);
    m_data_size = m_gamecube_pseudopartition.GetDataSize();
    m_encrypted = false;
  }
  else
  {
    SetNonpartitionDiscHeaderFromFile(game_partition.GetHeader(), game_partition_root);
    SetWiiRegionDataFromFile(game_partition_root);

    std::vector<PartitionWithType> partitions;
    partitions.emplace_back(std::move(game_partition), PartitionType::Game);

    std::string game_partition_directory_name = game_partition_root.substr(true_root.size());
    game_partition_directory_name.pop_back();  // Remove trailing slash
    if (ParsePartitionDirectoryName(game_partition_directory_name) == PartitionType::Game)
    {
      const File::FSTEntry true_root_entry = File::ScanDirectoryTree(true_root, false);
      for (const File::FSTEntry& entry : true_root_entry.children)
      {
        if (entry.isDirectory)
        {
          const std::optional<PartitionType> type = ParsePartitionDirectoryName(entry.virtualName);
          if (type && *type != PartitionType::Game)
          {
            partitions.emplace_back(DirectoryBlobPartition(entry.physicalName + "/", m_is_wii),
                                    *type);
          }
        }
      }
    }

    SetPartitions(std::move(partitions));
  }
}

DirectoryBlobReader::DirectoryBlobReader(
    std::unique_ptr<DiscIO::VolumeDisc> volume,
    const std::function<void(std::vector<FSTBuilderNode>* fst_nodes, FSTBuilderNode* dol_node)>&
        fst_callback)
    : m_encryption_cache(this), m_wrapped_volume(std::move(volume))
{
  DirectoryBlobPartition game_partition(
      m_wrapped_volume.get(), m_wrapped_volume->GetGamePartition(), std::nullopt, fst_callback);
  m_is_wii = game_partition.IsWii();

  if (!m_is_wii)
  {
    m_gamecube_pseudopartition = std::move(game_partition);
    m_data_size = m_gamecube_pseudopartition.GetDataSize();
    m_encrypted = false;
  }
  else
  {
    std::vector<u8> header_bin(WII_NONPARTITION_DISCHEADER_SIZE);
    if (!m_wrapped_volume->Read(WII_NONPARTITION_DISCHEADER_ADDRESS,
                                WII_NONPARTITION_DISCHEADER_SIZE, header_bin.data(),
                                PARTITION_NONE))
    {
      header_bin.clear();
    }
    SetNonpartitionDiscHeader(game_partition.GetHeader(), std::move(header_bin));

    std::vector<u8> wii_region_data(WII_REGION_DATA_SIZE);
    if (!m_wrapped_volume->Read(WII_REGION_DATA_ADDRESS, WII_REGION_DATA_SIZE,
                                wii_region_data.data(), PARTITION_NONE))
    {
      wii_region_data.clear();
    }
    SetWiiRegionData(wii_region_data, "volume");

    std::vector<PartitionWithType> partitions;
    partitions.emplace_back(std::move(game_partition), PartitionType::Game);

    for (Partition partition : m_wrapped_volume->GetPartitions())
    {
      if (partition == m_wrapped_volume->GetGamePartition())
        continue;

      auto type = m_wrapped_volume->GetPartitionType(partition);
      if (type)
      {
        partitions.emplace_back(
            DirectoryBlobPartition(m_wrapped_volume.get(), partition, m_is_wii, nullptr),
            static_cast<PartitionType>(*type));
      }
    }

    SetPartitions(std::move(partitions));
  }
}

bool DirectoryBlobReader::Read(u64 offset, u64 length, u8* buffer)
{
  if (offset + length > m_data_size)
    return false;

  return (m_is_wii ? m_nonpartition_contents : m_gamecube_pseudopartition.GetContents())
      .Read(offset, length, buffer);
}

const DirectoryBlobPartition* DirectoryBlobReader::GetPartition(u64 offset, u64 size,
                                                                u64 partition_data_offset) const
{
  const auto it = m_partitions.find(partition_data_offset);
  if (it == m_partitions.end())
    return nullptr;

  if (offset + size > it->second.GetDataSize())
    return nullptr;

  return &it->second;
}

bool DirectoryBlobReader::SupportsReadWiiDecrypted(u64 offset, u64 size,
                                                   u64 partition_data_offset) const
{
  return static_cast<bool>(GetPartition(offset, size, partition_data_offset));
}

bool DirectoryBlobReader::ReadWiiDecrypted(u64 offset, u64 size, u8* buffer,
                                           u64 partition_data_offset)
{
  const DirectoryBlobPartition* partition = GetPartition(offset, size, partition_data_offset);
  if (!partition)
    return false;

  return partition->GetContents().Read(offset, size, buffer);
}

bool DirectoryBlobReader::EncryptPartitionData(u64 offset, u64 size, u8* buffer,
                                               u64 partition_data_offset,
                                               u64 partition_data_decrypted_size)
{
  auto it = m_partitions.find(partition_data_offset);
  if (it == m_partitions.end())
    return false;

  if (!m_encrypted)
    return it->second.GetContents().Read(offset, size, buffer);

  return m_encryption_cache.EncryptGroups(offset, size, buffer, partition_data_offset,
                                          partition_data_decrypted_size, it->second.GetKey());
}

BlobType DirectoryBlobReader::GetBlobType() const
{
  return BlobType::DIRECTORY;
}

u64 DirectoryBlobReader::GetRawSize() const
{
  // Not implemented
  return 0;
}

u64 DirectoryBlobReader::GetDataSize() const
{
  return m_data_size;
}

void DirectoryBlobReader::SetNonpartitionDiscHeaderFromFile(const std::vector<u8>& partition_header,
                                                            const std::string& game_partition_root)
{
  std::vector<u8> header_bin(WII_NONPARTITION_DISCHEADER_SIZE);
  const size_t header_bin_bytes_read =
      ReadFileToVector(game_partition_root + "disc/header.bin", &header_bin);
  header_bin.resize(header_bin_bytes_read);
  SetNonpartitionDiscHeader(partition_header, std::move(header_bin));
}

void DirectoryBlobReader::SetNonpartitionDiscHeader(const std::vector<u8>& partition_header,
                                                    std::vector<u8> header_bin)
{
  const size_t header_bin_size = header_bin.size();
  m_disc_header_nonpartition = std::move(header_bin);
  m_disc_header_nonpartition.resize(WII_NONPARTITION_DISCHEADER_SIZE);

  // If header.bin is missing or smaller than expected, use the content of sys/boot.bin instead
  if (header_bin_size < m_disc_header_nonpartition.size())
  {
    std::copy(partition_header.data() + header_bin_size,
              partition_header.data() + m_disc_header_nonpartition.size(),
              m_disc_header_nonpartition.data() + header_bin_size);
  }

  // 0x60 and 0x61 are the only differences between the partition and non-partition headers
  if (header_bin_size < 0x60)
    m_disc_header_nonpartition[0x60] = 0;
  if (header_bin_size < 0x61)
    m_disc_header_nonpartition[0x61] = 0;

  m_encrypted = std::all_of(m_disc_header_nonpartition.data() + 0x60,
                            m_disc_header_nonpartition.data() + 0x64, [](u8 x) { return x == 0; });

  m_nonpartition_contents.AddReference(WII_NONPARTITION_DISCHEADER_ADDRESS,
                                       m_disc_header_nonpartition);
}

void DirectoryBlobReader::SetWiiRegionDataFromFile(const std::string& game_partition_root)
{
  std::vector<u8> wii_region_data(WII_REGION_DATA_SIZE);
  const std::string region_bin_path = game_partition_root + "disc/region.bin";
  const size_t bytes_read = ReadFileToVector(region_bin_path, &wii_region_data);
  wii_region_data.resize(bytes_read);
  SetWiiRegionData(wii_region_data, region_bin_path);
}

void DirectoryBlobReader::SetWiiRegionData(const std::vector<u8>& wii_region_data,
                                           const std::string& log_path)
{
  m_wii_region_data.resize(0x10, 0x00);
  m_wii_region_data.resize(WII_REGION_DATA_SIZE, 0x80);
  Write32(INVALID_REGION, 0, &m_wii_region_data);

  std::copy_n(wii_region_data.begin(),
              std::min<size_t>(wii_region_data.size(), WII_REGION_DATA_SIZE),
              m_wii_region_data.begin());

  if (wii_region_data.size() < 0x4)
    ERROR_LOG_FMT(DISCIO, "Couldn't read region from {}", log_path);
  else if (wii_region_data.size() < 0x20)
    ERROR_LOG_FMT(DISCIO, "Couldn't read age ratings from {}", log_path);

  m_nonpartition_contents.AddReference(WII_REGION_DATA_ADDRESS, m_wii_region_data);
}

void DirectoryBlobReader::SetPartitions(std::vector<PartitionWithType>&& partitions)
{
  std::sort(partitions.begin(), partitions.end(),
            [](const PartitionWithType& lhs, const PartitionWithType& rhs) {
              if (lhs.type == rhs.type)
                return lhs.partition.GetRootDirectory() < rhs.partition.GetRootDirectory();

              // Ascending sort by partition type, except Update (1) comes before before Game (0)
              return (lhs.type > PartitionType::Update || rhs.type > PartitionType::Update) ?
                         lhs.type < rhs.type :
                         lhs.type > rhs.type;
            });

  u32 subtable_1_size = 0;
  while (subtable_1_size < partitions.size() && subtable_1_size < 3 &&
         partitions[subtable_1_size].type <= PartitionType::Channel)
  {
    ++subtable_1_size;
  }
  const u32 subtable_2_size = static_cast<u32>(partitions.size() - subtable_1_size);

  constexpr u32 PARTITION_TABLE_ADDRESS = 0x40000;
  constexpr u32 PARTITION_SUBTABLE1_OFFSET = 0x20;
  constexpr u32 PARTITION_SUBTABLE2_OFFSET = 0x40;
  m_partition_table.resize(PARTITION_SUBTABLE2_OFFSET + subtable_2_size * 8);

  Write32(subtable_1_size, 0x0, &m_partition_table);
  Write32((PARTITION_TABLE_ADDRESS + PARTITION_SUBTABLE1_OFFSET) >> 2, 0x4, &m_partition_table);
  if (subtable_2_size != 0)
  {
    Write32(subtable_2_size, 0x8, &m_partition_table);
    Write32((PARTITION_TABLE_ADDRESS + PARTITION_SUBTABLE2_OFFSET) >> 2, 0xC, &m_partition_table);
  }

  constexpr u64 STANDARD_UPDATE_PARTITION_ADDRESS = 0x50000;
  constexpr u64 STANDARD_GAME_PARTITION_ADDRESS = 0xF800000;
  u64 partition_address = STANDARD_UPDATE_PARTITION_ADDRESS;
  u64 offset_in_table = PARTITION_SUBTABLE1_OFFSET;
  for (size_t i = 0; i < partitions.size(); ++i)
  {
    if (i == subtable_1_size)
      offset_in_table = PARTITION_SUBTABLE2_OFFSET;

    if (partitions[i].type == PartitionType::Game)
      partition_address = std::max(partition_address, STANDARD_GAME_PARTITION_ADDRESS);

    Write32(static_cast<u32>(partition_address >> 2), offset_in_table, &m_partition_table);
    offset_in_table += 4;
    Write32(static_cast<u32>(partitions[i].type), offset_in_table, &m_partition_table);
    offset_in_table += 4;

    SetPartitionHeader(&partitions[i].partition, partition_address);

    const u64 data_size = partitions[i].partition.GetDataSize();
    const u64 partition_data_offset = partition_address + PARTITION_DATA_OFFSET;
    m_partitions.emplace(partition_data_offset, std::move(partitions[i].partition));
    m_nonpartition_contents.Add(partition_data_offset, data_size,
                                ContentPartition{this, 0, partition_data_offset});
    const u64 unaligned_next_partition_address = VolumeWii::EncryptedPartitionOffsetToRawOffset(
        data_size, Partition(partition_address), PARTITION_DATA_OFFSET);
    partition_address = Common::AlignUp(unaligned_next_partition_address, 0x10000ull);
  }
  m_data_size = partition_address;

  m_nonpartition_contents.AddReference(PARTITION_TABLE_ADDRESS, m_partition_table);
}

// This function sets the header that's shortly before the start of the encrypted
// area, not the header that's right at the beginning of the encrypted area
void DirectoryBlobReader::SetPartitionHeader(DirectoryBlobPartition* partition,
                                             u64 partition_address)
{
  constexpr u32 TMD_OFFSET = 0x2c0;
  constexpr u32 H3_OFFSET = 0x4000;

  const std::optional<DiscIO::Partition>& wrapped_partition = partition->GetWrappedPartition();
  const std::string& partition_root = partition->GetRootDirectory();

  u64 ticket_size;
  if (wrapped_partition)
  {
    const auto& ticket = m_wrapped_volume->GetTicket(*wrapped_partition).GetBytes();
    auto& new_ticket = m_extra_data.emplace_back(ticket);
    if (new_ticket.size() > WII_PARTITION_TICKET_SIZE)
      new_ticket.resize(WII_PARTITION_TICKET_SIZE);
    ticket_size = new_ticket.size();
    m_nonpartition_contents.AddReference(partition_address + WII_PARTITION_TICKET_ADDRESS,
                                         new_ticket);
  }
  else
  {
    ticket_size = m_nonpartition_contents.CheckSizeAndAdd(
        partition_address + WII_PARTITION_TICKET_ADDRESS, WII_PARTITION_TICKET_SIZE,
        partition_root + "ticket.bin");
  }

  u64 tmd_size;
  if (wrapped_partition)
  {
    const auto& tmd = m_wrapped_volume->GetTMD(*wrapped_partition).GetBytes();
    auto& new_tmd = m_extra_data.emplace_back(tmd);
    if (new_tmd.size() > IOS::ES::MAX_TMD_SIZE)
      new_tmd.resize(IOS::ES::MAX_TMD_SIZE);
    tmd_size = new_tmd.size();
    m_nonpartition_contents.AddReference(partition_address + TMD_OFFSET, new_tmd);
  }
  else
  {
    tmd_size = m_nonpartition_contents.CheckSizeAndAdd(
        partition_address + TMD_OFFSET, IOS::ES::MAX_TMD_SIZE, partition_root + "tmd.bin");
  }

  const u64 cert_offset = Common::AlignUp(TMD_OFFSET + tmd_size, 0x20ull);
  const u64 max_cert_size = H3_OFFSET - cert_offset;

  u64 cert_size;
  if (wrapped_partition)
  {
    const auto& cert = m_wrapped_volume->GetCertificateChain(*wrapped_partition);
    auto& new_cert = m_extra_data.emplace_back(cert);
    if (new_cert.size() > max_cert_size)
      new_cert.resize(max_cert_size);
    cert_size = new_cert.size();
    m_nonpartition_contents.AddReference(partition_address + cert_offset, new_cert);
  }
  else
  {
    cert_size = m_nonpartition_contents.CheckSizeAndAdd(partition_address + cert_offset,
                                                        max_cert_size, partition_root + "cert.bin");
  }

  if (wrapped_partition)
  {
    if (m_wrapped_volume->IsEncryptedAndHashed())
    {
      const std::optional<u64> offset = m_wrapped_volume->ReadSwappedAndShifted(
          wrapped_partition->offset + WII_PARTITION_H3_OFFSET_ADDRESS, PARTITION_NONE);
      if (offset)
      {
        auto& new_h3 = m_extra_data.emplace_back(WII_PARTITION_H3_SIZE);
        if (m_wrapped_volume->Read(wrapped_partition->offset + *offset, new_h3.size(),
                                   new_h3.data(), PARTITION_NONE))
        {
          m_nonpartition_contents.AddReference(partition_address + H3_OFFSET, new_h3);
        }
      }
    }
  }
  else
  {
    m_nonpartition_contents.CheckSizeAndAdd(partition_address + H3_OFFSET, WII_PARTITION_H3_SIZE,
                                            partition_root + "h3.bin");
  }

  constexpr u32 PARTITION_HEADER_SIZE = 0x1c;
  const u64 data_size = Common::AlignUp(partition->GetDataSize(), 0x7c00) / 0x7c00 * 0x8000;
  std::vector<u8>& partition_header = m_extra_data.emplace_back(PARTITION_HEADER_SIZE);
  Write32(static_cast<u32>(tmd_size), 0x0, &partition_header);
  Write32(TMD_OFFSET >> 2, 0x4, &partition_header);
  Write32(static_cast<u32>(cert_size), 0x8, &partition_header);
  Write32(static_cast<u32>(cert_offset >> 2), 0x0C, &partition_header);
  Write32(H3_OFFSET >> 2, 0x10, &partition_header);
  Write32(PARTITION_DATA_OFFSET >> 2, 0x14, &partition_header);
  Write32(static_cast<u32>(data_size >> 2), 0x18, &partition_header);

  m_nonpartition_contents.AddReference(partition_address + WII_PARTITION_TICKET_SIZE,
                                       partition_header);

  std::vector<u8> ticket_buffer(ticket_size);
  m_nonpartition_contents.Read(partition_address + WII_PARTITION_TICKET_ADDRESS, ticket_size,
                               ticket_buffer.data());
  IOS::ES::TicketReader ticket(std::move(ticket_buffer));
  if (ticket.IsValid())
    partition->SetKey(ticket.GetTitleKey());
}

static void GenerateBuilderNodesFromFileSystem(const DiscIO::VolumeDisc& volume,
                                               const DiscIO::Partition& partition,
                                               std::vector<FSTBuilderNode>* nodes,
                                               const FileInfo& parent_info)
{
  for (const FileInfo& file_info : parent_info)
  {
    if (file_info.IsDirectory())
    {
      std::vector<FSTBuilderNode> child_nodes;
      GenerateBuilderNodesFromFileSystem(volume, partition, &child_nodes, file_info);
      nodes->emplace_back(FSTBuilderNode{file_info.GetName(), file_info.GetTotalChildren(),
                                         std::move(child_nodes)});
    }
    else
    {
      std::vector<BuilderContentSource> source;
      source.emplace_back(BuilderContentSource{
          0, file_info.GetSize(), ContentVolume{file_info.GetOffset(), &volume, partition}});
      nodes->emplace_back(
          FSTBuilderNode{file_info.GetName(), file_info.GetSize(), std::move(source)});
    }
  }
}

DirectoryBlobPartition::DirectoryBlobPartition(const std::string& root_directory,
                                               std::optional<bool> is_wii)
    : m_root_directory(root_directory)
{
  SetDiscHeaderFromFile(m_root_directory + "sys/boot.bin");
  SetDiscType(is_wii);
  SetBI2FromFile(m_root_directory + "sys/bi2.bin");
  const u64 dol_address = SetApploaderFromFile(m_root_directory + "sys/apploader.img");
  const u64 fst_address = SetDOLFromFile(m_root_directory + "sys/main.dol", dol_address);
  BuildFSTFromFolder(m_root_directory + "files/", fst_address);
}

DirectoryBlobPartition::DirectoryBlobPartition(
    DiscIO::VolumeDisc* volume, const DiscIO::Partition& partition, std::optional<bool> is_wii,
    const std::function<void(std::vector<FSTBuilderNode>* fst_nodes, FSTBuilderNode* dol_node)>&
        fst_callback)
    : m_wrapped_partition(partition)
{
  std::vector<u8> disc_header(DISCHEADER_SIZE);
  if (!volume->Read(DISCHEADER_ADDRESS, DISCHEADER_SIZE, disc_header.data(), partition))
    disc_header.clear();
  SetDiscHeader(std::move(disc_header));
  SetDiscType(is_wii);

  std::vector<u8> bi2(BI2_SIZE);
  if (!volume->Read(BI2_ADDRESS, BI2_SIZE, bi2.data(), partition))
    bi2.clear();
  SetBI2(std::move(bi2));

  std::vector<u8> apploader;
  const auto apploader_size = GetApploaderSize(*volume, partition);
  if (apploader_size)
  {
    apploader.resize(*apploader_size);
    if (!volume->Read(APPLOADER_ADDRESS, *apploader_size, apploader.data(), partition))
      apploader.clear();
  }
  const u64 new_dol_address = SetApploader(apploader, "apploader");

  FSTBuilderNode dol_node{"main.dol", 0, {}};
  const auto dol_offset = GetBootDOLOffset(*volume, partition);
  if (dol_offset)
  {
    const auto dol_size = GetBootDOLSize(*volume, partition, *dol_offset);
    if (dol_size)
    {
      std::vector<BuilderContentSource> dol_contents;
      dol_contents.emplace_back(
          BuilderContentSource{0, *dol_size, ContentVolume{*dol_offset, volume, partition}});
      dol_node.m_size = *dol_size;
      dol_node.m_content = std::move(dol_contents);
    }
  }

  std::vector<FSTBuilderNode> nodes;

  const FileSystem* fs = volume->GetFileSystem(partition);
  if (fs && fs->IsValid())
    GenerateBuilderNodesFromFileSystem(*volume, partition, &nodes, fs->GetRoot());

  if (fst_callback)
    fst_callback(&nodes, &dol_node);

  const u64 new_fst_address = SetDOL(std::move(dol_node), new_dol_address);
  BuildFST(std::move(nodes), new_fst_address);
}

void DirectoryBlobPartition::SetDiscHeaderFromFile(const std::string& boot_bin_path)
{
  m_disc_header.resize(DISCHEADER_SIZE);
  if (ReadFileToVector(boot_bin_path, &m_disc_header) < 0x20)
    ERROR_LOG_FMT(DISCIO, "{} doesn't exist or is too small", boot_bin_path);

  m_contents.AddReference(DISCHEADER_ADDRESS, m_disc_header);
}

void DirectoryBlobPartition::SetDiscHeader(std::vector<u8> boot_bin)
{
  m_disc_header = std::move(boot_bin);
  m_disc_header.resize(DISCHEADER_SIZE);
  m_contents.AddReference(DISCHEADER_ADDRESS, m_disc_header);
}

void DirectoryBlobPartition::SetDiscType(std::optional<bool> is_wii)
{
  if (is_wii.has_value())
  {
    m_is_wii = *is_wii;
  }
  else
  {
    m_is_wii = Common::swap32(&m_disc_header[0x18]) == WII_DISC_MAGIC;
    const bool is_gc = Common::swap32(&m_disc_header[0x1c]) == GAMECUBE_DISC_MAGIC;
    if (m_is_wii == is_gc)
    {
      ERROR_LOG_FMT(DISCIO, "Couldn't detect disc type based on disc header; assuming {}",
                    m_is_wii ? "Wii" : "GameCube");
    }
  }

  m_address_shift = m_is_wii ? 2 : 0;
}

void DirectoryBlobPartition::SetBI2FromFile(const std::string& bi2_path)
{
  m_bi2.resize(BI2_SIZE);

  if (!m_is_wii)
    Write32(INVALID_REGION, 0x18, &m_bi2);

  const size_t bytes_read = ReadFileToVector(bi2_path, &m_bi2);
  if (!m_is_wii && bytes_read < 0x1C)
    ERROR_LOG_FMT(DISCIO, "Couldn't read region from {}", bi2_path);

  m_contents.AddReference(BI2_ADDRESS, m_bi2);
}

void DirectoryBlobPartition::SetBI2(std::vector<u8> bi2)
{
  const size_t bi2_size = bi2.size();
  m_bi2 = std::move(bi2);
  m_bi2.resize(BI2_SIZE);

  if (!m_is_wii && bi2_size < 0x1C)
    Write32(INVALID_REGION, 0x18, &m_bi2);

  m_contents.AddReference(BI2_ADDRESS, m_bi2);
}

u64 DirectoryBlobPartition::SetApploaderFromFile(const std::string& path)
{
  File::IOFile file(path, "rb");
  std::vector<u8> apploader(file.GetSize());
  file.ReadBytes(apploader.data(), apploader.size());
  return SetApploader(std::move(apploader), path);
}

u64 DirectoryBlobPartition::SetApploader(std::vector<u8> apploader, const std::string& log_path)
{
  bool success = false;

  m_apploader = std::move(apploader);
  if (m_apploader.size() < 0x20)
  {
    ERROR_LOG_FMT(DISCIO, "{} couldn't be accessed or is too small", log_path);
  }
  else
  {
    const size_t apploader_size = 0x20 + Common::swap32(*(u32*)&m_apploader[0x14]) +
                                  Common::swap32(*(u32*)&m_apploader[0x18]);
    if (apploader_size != m_apploader.size())
      ERROR_LOG_FMT(DISCIO, "{} is the wrong size... Is it really an apploader?", log_path);
    else
      success = true;
  }

  if (!success)
  {
    m_apploader.resize(0x20);
    // Make sure BS2 HLE doesn't try to run the apploader
    Write32(static_cast<u32>(-1), 0x10, &m_apploader);
  }

  m_contents.AddReference(APPLOADER_ADDRESS, m_apploader);

  // Return DOL address, 32 byte aligned (plus 32 byte padding)
  return Common::AlignUp(APPLOADER_ADDRESS + m_apploader.size() + 0x20, 0x20ull);
}

u64 DirectoryBlobPartition::SetDOLFromFile(const std::string& path, u64 dol_address)
{
  const u64 dol_size = m_contents.CheckSizeAndAdd(dol_address, path);

  Write32(static_cast<u32>(dol_address >> m_address_shift), 0x0420, &m_disc_header);

  // Return FST address, 32 byte aligned (plus 32 byte padding)
  return Common::AlignUp(dol_address + dol_size + 0x20, 0x20ull);
}

u64 DirectoryBlobPartition::SetDOL(FSTBuilderNode dol_node, u64 dol_address)
{
  for (auto& content : dol_node.GetFileContent())
    m_contents.Add(dol_address + content.m_offset, content.m_size, std::move(content.m_source));

  Write32(static_cast<u32>(dol_address >> m_address_shift), 0x0420, &m_disc_header);

  // Return FST address, 32 byte aligned (plus 32 byte padding)
  return Common::AlignUp(dol_address + dol_node.m_size + 0x20, 0x20ull);
}

static std::vector<FSTBuilderNode> ConvertFSTEntriesToBuilderNodes(const File::FSTEntry& parent)
{
  std::vector<FSTBuilderNode> nodes;
  nodes.reserve(parent.children.size());
  for (const File::FSTEntry& entry : parent.children)
  {
    std::variant<std::vector<BuilderContentSource>, std::vector<FSTBuilderNode>> content;
    if (entry.isDirectory)
    {
      content = ConvertFSTEntriesToBuilderNodes(entry);
    }
    else
    {
      content =
          std::vector<BuilderContentSource>{{0, entry.size, ContentFile{entry.physicalName, 0}}};
    }

    nodes.emplace_back(FSTBuilderNode{entry.virtualName, entry.size, std::move(content)});
  }
  return nodes;
}

void DirectoryBlobPartition::BuildFSTFromFolder(const std::string& fst_root_path, u64 fst_address)
{
  auto nodes = ConvertFSTEntriesToBuilderNodes(File::ScanDirectoryTree(fst_root_path, true));
  BuildFST(std::move(nodes), fst_address);
}

static void ConvertUTF8NamesToSHIFTJIS(std::vector<FSTBuilderNode>* fst)
{
  for (FSTBuilderNode& entry : *fst)
  {
    if (entry.IsFolder())
      ConvertUTF8NamesToSHIFTJIS(&entry.GetFolderContent());
    entry.m_filename = UTF8ToSHIFTJIS(entry.m_filename);
  }
}

static u32 ComputeNameSize(const std::vector<FSTBuilderNode>& files)
{
  u32 name_size = 0;
  for (const FSTBuilderNode& entry : files)
  {
    if (entry.IsFolder())
      name_size += ComputeNameSize(entry.GetFolderContent());
    name_size += static_cast<u32>(entry.m_filename.length() + 1);
  }
  return name_size;
}

static size_t RecalculateFolderSizes(std::vector<FSTBuilderNode>* fst)
{
  size_t size = 0;
  for (FSTBuilderNode& entry : *fst)
  {
    ++size;
    if (entry.IsFile())
      continue;

    entry.m_size = RecalculateFolderSizes(&entry.GetFolderContent());
    size += entry.m_size;
  }
  return size;
}

void DirectoryBlobPartition::BuildFST(std::vector<FSTBuilderNode> root_nodes, u64 fst_address)
{
  m_fst_data.clear();

  ConvertUTF8NamesToSHIFTJIS(&root_nodes);

  u32 name_table_size = Common::AlignUp(ComputeNameSize(root_nodes), 1ull << m_address_shift);

  // 1 extra for the root entry
  u64 total_entries = RecalculateFolderSizes(&root_nodes) + 1;

  const u64 name_table_offset = total_entries * ENTRY_SIZE;
  m_fst_data.resize(name_table_offset + name_table_size);

  // 32 KiB aligned start of data on disc
  u64 current_data_address = Common::AlignUp(fst_address + m_fst_data.size(), 0x8000ull);

  u32 fst_offset = 0;   // Offset within FST data
  u32 name_offset = 0;  // Offset within name table
  u32 root_offset = 0;  // Offset of root of FST

  // write root entry
  WriteEntryData(&fst_offset, DIRECTORY_ENTRY, 0, 0, total_entries, m_address_shift);

  WriteDirectory(&root_nodes, &fst_offset, &name_offset, &current_data_address, root_offset,
                 name_table_offset);

  // overflow check, compare the aligned name offset with the aligned name table size
  ASSERT(Common::AlignUp(name_offset, 1ull << m_address_shift) == name_table_size);

  // write FST size and location
  Write32((u32)(fst_address >> m_address_shift), 0x0424, &m_disc_header);
  Write32((u32)(m_fst_data.size() >> m_address_shift), 0x0428, &m_disc_header);
  Write32((u32)(m_fst_data.size() >> m_address_shift), 0x042c, &m_disc_header);

  m_contents.AddReference(fst_address, m_fst_data);

  m_data_size = current_data_address;
}

void DirectoryBlobPartition::WriteEntryData(u32* entry_offset, u8 type, u32 name_offset,
                                            u64 data_offset, u64 length, u32 address_shift)
{
  m_fst_data[(*entry_offset)++] = type;

  m_fst_data[(*entry_offset)++] = (name_offset >> 16) & 0xff;
  m_fst_data[(*entry_offset)++] = (name_offset >> 8) & 0xff;
  m_fst_data[(*entry_offset)++] = (name_offset)&0xff;

  Write32((u32)(data_offset >> address_shift), *entry_offset, &m_fst_data);
  *entry_offset += 4;

  Write32((u32)length, *entry_offset, &m_fst_data);
  *entry_offset += 4;
}

void DirectoryBlobPartition::WriteEntryName(u32* name_offset, const std::string& name,
                                            u64 name_table_offset)
{
  strncpy((char*)&m_fst_data[*name_offset + name_table_offset], name.c_str(), name.length() + 1);

  *name_offset += (u32)(name.length() + 1);
}

void DirectoryBlobPartition::WriteDirectory(std::vector<FSTBuilderNode>* parent_entries,
                                            u32* fst_offset, u32* name_offset, u64* data_offset,
                                            u32 parent_entry_index, u64 name_table_offset)
{
  std::vector<FSTBuilderNode>& sorted_entries = *parent_entries;

  // Sort for determinism
  std::sort(sorted_entries.begin(), sorted_entries.end(),
            [](const FSTBuilderNode& one, const FSTBuilderNode& two) {
              std::string one_upper = one.m_filename;
              std::string two_upper = two.m_filename;
              Common::ToUpper(&one_upper);
              Common::ToUpper(&two_upper);
              return one_upper == two_upper ? one.m_filename < two.m_filename :
                                              one_upper < two_upper;
            });

  for (FSTBuilderNode& entry : sorted_entries)
  {
    if (entry.IsFolder())
    {
      u32 entry_index = *fst_offset / ENTRY_SIZE;
      WriteEntryData(fst_offset, DIRECTORY_ENTRY, *name_offset, parent_entry_index,
                     entry_index + entry.m_size + 1, 0);
      WriteEntryName(name_offset, entry.m_filename, name_table_offset);

      auto& child_nodes = entry.GetFolderContent();
      WriteDirectory(&child_nodes, fst_offset, name_offset, data_offset, entry_index,
                     name_table_offset);
    }
    else
    {
      // put entry in FST
      WriteEntryData(fst_offset, FILE_ENTRY, *name_offset, *data_offset, entry.m_size,
                     m_address_shift);
      WriteEntryName(name_offset, entry.m_filename, name_table_offset);

      // write entry to virtual disc
      auto& contents = entry.GetFileContent();
      for (BuilderContentSource& content : contents)
      {
        m_contents.Add(*data_offset + content.m_offset, content.m_size,
                       std::move(content.m_source));
      }

      // 32 KiB aligned - many games are fine with less alignment, but not all
      *data_offset = Common::AlignUp(*data_offset + entry.m_size, 0x8000ull);
    }
  }
}

static size_t ReadFileToVector(const std::string& path, std::vector<u8>* vector)
{
  File::IOFile file(path, "rb");
  size_t bytes_read;
  file.ReadArray<u8>(vector->data(), std::min<u64>(file.GetSize(), vector->size()), &bytes_read);
  return bytes_read;
}

static void PadToAddress(u64 start_address, u64* address, u64* length, u8** buffer)
{
  if (start_address > *address && *length > 0)
  {
    u64 padBytes = std::min(start_address - *address, *length);
    memset(*buffer, 0, (size_t)padBytes);
    *length -= padBytes;
    *buffer += padBytes;
    *address += padBytes;
  }
}

static void Write32(u32 data, u32 offset, std::vector<u8>* buffer)
{
  (*buffer)[offset++] = (data >> 24);
  (*buffer)[offset++] = (data >> 16) & 0xff;
  (*buffer)[offset++] = (data >> 8) & 0xff;
  (*buffer)[offset] = data & 0xff;
}
}  // namespace DiscIO