#include "physfs.hpp"

#include <physfs.h>

#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string_view>
#include <vector>

#include "../log.hpp"

#include <format>

namespace game::util::physfs
{
  namespace physfs_detail
  {
    constexpr auto BYTE_BITS = 8;
    constexpr auto UINT16_BYTE_COUNT = 2;
    constexpr auto UINT32_BYTE_COUNT = 4;
    constexpr auto ZIP_VERSION = 20;
    constexpr auto ZIP_COMPRESSION_STORE = 0;
    constexpr auto ZIP_FLAG_NONE = 0;
    constexpr auto ZIP_TIME_NONE = 0;
    constexpr auto ZIP_DATE_NONE = 0;
    constexpr auto ZIP_DISK_NONE = 0;
    constexpr auto ZIP_ATTRIBUTE_NONE = 0;
    constexpr auto ZIP_COMMENT_LENGTH_NONE = 0;
    constexpr auto ZIP_EXTRA_LENGTH_NONE = 0;
    constexpr auto ZIP_LOCAL_HEADER_SIGNATURE = 0x04034b50u;
    constexpr auto ZIP_CENTRAL_HEADER_SIGNATURE = 0x02014b50u;
    constexpr auto ZIP_END_SIGNATURE = 0x06054b50u;
    constexpr auto BYTE_MASK = 0xff;
    constexpr auto CRC_INITIAL = 0xffffffffu;
    constexpr auto CRC_POLYNOMIAL = 0xedb88320u;
    constexpr auto CRC_XOR = 0xffffffffu;
    constexpr auto MOUNT_EXPORT = "__developer_export__";

    struct ZipEntry
    {
      std::string path{};
      std::vector<uint8_t> data{};
      uint32_t crc{};
      uint32_t offset{};
    };

    uint32_t crc32_get(const std::vector<uint8_t>& data)
    {
      auto crc = CRC_INITIAL;
      for (auto value : data)
      {
        crc ^= value;
        for (auto bit = 0; bit < BYTE_BITS; ++bit)
        {
          auto mask = (uint32_t)(-(int)(crc & 1));
          crc = (crc >> 1) ^ (CRC_POLYNOMIAL & mask);
        }
      }
      return crc ^ CRC_XOR;
    }

    void uint16_write(std::ostream& stream, uint16_t value)
    {
      for (auto i = 0; i < UINT16_BYTE_COUNT; ++i)
        stream.put((char)((value >> (i * BYTE_BITS)) & BYTE_MASK));
    }

    void uint32_write(std::ostream& stream, uint32_t value)
    {
      for (auto i = 0; i < UINT32_BYTE_COUNT; ++i)
        stream.put((char)((value >> (i * BYTE_BITS)) & BYTE_MASK));
    }

    void local_header_write(std::ostream& stream, const ZipEntry& entry)
    {
      uint32_write(stream, ZIP_LOCAL_HEADER_SIGNATURE);
      uint16_write(stream, ZIP_VERSION);
      uint16_write(stream, ZIP_FLAG_NONE);
      uint16_write(stream, ZIP_COMPRESSION_STORE);
      uint16_write(stream, ZIP_TIME_NONE);
      uint16_write(stream, ZIP_DATE_NONE);
      uint32_write(stream, entry.crc);
      uint32_write(stream, (uint32_t)entry.data.size());
      uint32_write(stream, (uint32_t)entry.data.size());
      uint16_write(stream, (uint16_t)entry.path.size());
      uint16_write(stream, ZIP_EXTRA_LENGTH_NONE);
      stream.write(entry.path.data(), (std::streamsize)entry.path.size());
    }

    void central_header_write(std::ostream& stream, const ZipEntry& entry)
    {
      uint32_write(stream, ZIP_CENTRAL_HEADER_SIGNATURE);
      uint16_write(stream, ZIP_VERSION);
      uint16_write(stream, ZIP_VERSION);
      uint16_write(stream, ZIP_FLAG_NONE);
      uint16_write(stream, ZIP_COMPRESSION_STORE);
      uint16_write(stream, ZIP_TIME_NONE);
      uint16_write(stream, ZIP_DATE_NONE);
      uint32_write(stream, entry.crc);
      uint32_write(stream, (uint32_t)entry.data.size());
      uint32_write(stream, (uint32_t)entry.data.size());
      uint16_write(stream, (uint16_t)entry.path.size());
      uint16_write(stream, ZIP_EXTRA_LENGTH_NONE);
      uint16_write(stream, ZIP_COMMENT_LENGTH_NONE);
      uint16_write(stream, ZIP_DISK_NONE);
      uint16_write(stream, ZIP_ATTRIBUTE_NONE);
      uint32_write(stream, ZIP_ATTRIBUTE_NONE);
      uint32_write(stream, entry.offset);
      stream.write(entry.path.data(), (std::streamsize)entry.path.size());
    }

    void end_header_write(std::ostream& stream, uint16_t entries, uint32_t directorySize, uint32_t directoryOffset)
    {
      uint32_write(stream, ZIP_END_SIGNATURE);
      uint16_write(stream, ZIP_DISK_NONE);
      uint16_write(stream, ZIP_DISK_NONE);
      uint16_write(stream, entries);
      uint16_write(stream, entries);
      uint32_write(stream, directorySize);
      uint32_write(stream, directoryOffset);
      uint16_write(stream, ZIP_COMMENT_LENGTH_NONE);
    }
  }

  using namespace physfs_detail;

  std::string path_normalize(std::string_view path)
  {
    const bool absolute = !path.empty() && path.front() == '/';
    std::vector<std::string> parts{};
    std::string segment{};

    auto flush_segment = [&]()
    {
      if (segment.empty() || segment == ".")
      {
        segment.clear();
        return;
      }

      if (segment == "..")
      {
        if (!parts.empty() && parts.back() != "..")
          parts.pop_back();
        else if (!absolute)
          parts.emplace_back("..");
      }
      else
      {
        parts.emplace_back(std::move(segment));
      }

      segment.clear();
    };

    for (char ch : path)
    {
      if (ch == '/')
      {
        flush_segment();
        continue;
      }

      segment.push_back(ch);
    }

    flush_segment();

    std::string pathNormalized{};
    if (absolute) pathNormalized.push_back('/');
    for (size_t i = 0; i < parts.size(); ++i)
    {
      if (i > 0) pathNormalized.push_back('/');
      pathNormalized.append(parts[i]);
    }

    if (pathNormalized.empty() && absolute) return "/";
    return pathNormalized;
  }

  std::string error_get() { return PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()); }

  Archive::Archive(const std::filesystem::path& path, const std::string& mount)
  {
    auto pathString = path.string();
    if (!PHYSFS_mount(pathString.c_str(), mount.c_str(), 0))
    {
      logger.error(std::format("Failed to mount archive: {} ({})", pathString, error_get()));
      return;
    }

    *this = std::string(mount);
  }

  Archive::~Archive() { PHYSFS_unmount(c_str()); }

  bool Archive::is_valid() { return !empty(); }

  std::vector<uint8_t> Path::read() const
  {
    auto pathNormalized = path_normalize(*this);
    auto file = PHYSFS_openRead(pathNormalized.c_str());

    if (!file)
    {
      logger.error(
          std::format("Failed to read PhysicsFS file (PHYFSFS_openRead): {} ({})", pathNormalized, error_get()));
      return {};
    }

    auto size = PHYSFS_fileLength(file);
    if (size < 0)
    {
      logger.error(
          std::format("Failed to get PhysicsFS file length (PHYSFS_fileLength): {} ({})", pathNormalized, error_get()));
      return {};
    }

    std::vector<uint8_t> buffer((size_t)size);

    auto read = PHYSFS_readBytes(file, buffer.data(), size);

    PHYSFS_close(file);

    if (read != size)
    {
      logger.error(std::format("Invalid PhysicsFS file read size: {} ({})", pathNormalized, error_get()));
      return {};
    }

    return buffer;
  }

  physfs::Path Path::directory_get() const
  {
    auto pos = find_last_of('/');
    if (pos == std::string_view::npos) return physfs::Path("");
    if (pos == 0) return physfs::Path("/");
    return physfs::Path(substr(0, pos));
  }

  bool Path::is_valid() const
  {
    auto pathNormalized = path_normalize(*this);
    return PHYSFS_exists(pathNormalized.c_str());
  }

  bool archive_write(const std::filesystem::path& source, const std::filesystem::path& destination)
  {
    std::error_code error{};
    if (!std::filesystem::is_directory(source, error)) return false;

    auto output = destination;
    if (output.extension().empty()) output += ".zip";
    if (!output.parent_path().empty()) std::filesystem::create_directories(output.parent_path(), error);

    Archive archive(source, MOUNT_EXPORT);
    if (!archive.is_valid()) return false;

    std::vector<std::filesystem::path> paths{};
    for (auto& entry : std::filesystem::recursive_directory_iterator(source, error))
      if (!error && entry.is_regular_file(error)) paths.emplace_back(entry.path());
    std::ranges::sort(paths);

    std::vector<ZipEntry> entries{};
    entries.reserve(paths.size());
    for (auto& path : paths)
    {
      auto relative = std::filesystem::relative(path, source, error).generic_string();
      if (relative.empty() || error) continue;
      auto physfsPath = Path(std::string(archive) + "/" + relative);
      auto data = physfsPath.read();
      auto size = std::filesystem::file_size(path, error);
      if (!error && size > 0 && data.empty()) return false;
      entries.push_back({.path = relative, .data = std::move(data)});
      entries.back().crc = crc32_get(entries.back().data);
    }

    std::ofstream stream(output, std::ios::binary | std::ios::trunc);
    if (!stream.is_open()) return false;

    for (auto& entry : entries)
    {
      entry.offset = (uint32_t)stream.tellp();
      local_header_write(stream, entry);
      if (!entry.data.empty()) stream.write((const char*)entry.data.data(), (std::streamsize)entry.data.size());
    }

    auto directoryOffset = (uint32_t)stream.tellp();
    for (auto& entry : entries)
      central_header_write(stream, entry);
    auto directorySize = (uint32_t)stream.tellp() - directoryOffset;
    end_header_write(stream, (uint16_t)entries.size(), directorySize, directoryOffset);

    return stream.good();
  }
}
