#include <frostbite2D/resource/pvf_archive.h>

#include <SDL.h>

#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <sstream>

#include <frostbite2D/utils/startup_trace.h>

namespace frostbite2D {

namespace {

bool canReadSpan(const std::vector<uint8>& bytes, size_t offset, size_t length) {
  return offset <= bytes.size() && length <= bytes.size() - offset;
}

template <typename T>
bool readPodAt(const std::vector<uint8>& bytes, size_t offset, T& value) {
  if (!canReadSpan(bytes, offset, sizeof(T))) {
    value = T{};
    return false;
  }

  std::memcpy(&value, bytes.data() + offset, sizeof(T));
  return true;
}

std::string readStringAt(const std::vector<uint8>& bytes, size_t offset,
                         size_t length) {
  if (!canReadSpan(bytes, offset, length)) {
    return {};
  }

  return std::string(reinterpret_cast<const char*>(bytes.data() + offset),
                     length);
}

} // namespace

PvfArchive& PvfArchive::get() {
  static PvfArchive instance;
  return instance;
}

bool PvfArchive::open(const std::string& filePath) {
  close();

  ScopedStartupTrace stageTrace("PvfArchive::open stream");
  if (!reader_.open(filePath)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: unable to open file: %s", filePath.c_str());
    return false;
  }

  return true;
}

void PvfArchive::close() {
  reader_.close();
  clearInitData();
}

bool PvfArchive::isOpen() const {
  return reader_.isOpen();
}

void PvfArchive::init() {
  if (!isOpen()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: open the archive before calling init");
    return;
  }

  clearInitData();

  {
    ScopedStartupTrace stageTrace("PvfArchive::initHeader");
    initHeader();
  }
  if (fileInfo_.empty()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "PvfArchive: header parse produced no file entries");
    return;
  }

  {
    ScopedStartupTrace stageTrace("PvfArchive::initBinStringTable");
    initBinStringTable();
  }

  {
    ScopedStartupTrace stageTrace("PvfArchive::initLoadStrings");
    initLoadStrings();
  }
}

void PvfArchive::initHeader() {
  fileInfo_.clear();
  dataStartPos_ = 0;
  decodedFileCache_.clear();

  if (!isOpen()) {
    return;
  }

  reader_.seek(0);

  int32 uuidLength = reader_.readInt32();
  if (uuidLength < 0 || static_cast<size_t>(uuidLength) > reader_.remaining()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: invalid uuid length in archive header");
    return;
  }

  std::string uuid = reader_.readString(static_cast<size_t>(uuidLength));
  (void)uuid;

  int32 version = reader_.readInt32();
  (void)version;

  int32 alignedIndexHeaderSize = reader_.readInt32();
  int32 indexHeaderCrc = reader_.readInt32();
  int32 indexSize = reader_.readInt32();

  if (alignedIndexHeaderSize <= 0 || indexSize < 0) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: invalid index header metadata (size=%d, count=%d)",
                 alignedIndexHeaderSize, indexSize);
    return;
  }

  size_t indexHeaderPos = reader_.tell();
  auto headerBytesOpt =
      readArchiveBytes(indexHeaderPos, static_cast<size_t>(alignedIndexHeaderSize));
  if (!headerBytesOpt.has_value()) {
    return;
  }

  std::vector<uint8> headerBytes = std::move(headerBytesOpt.value());
  crcDecodeBuffer(headerBytes, static_cast<uint32>(indexHeaderCrc));
  dataStartPos_ = indexHeaderPos + static_cast<size_t>(alignedIndexHeaderSize);

  size_t cursor = 0;
  for (int32 i = 0; i < indexSize; ++i) {
    int32 fileNumber = 0;
    int32 filePathLength = 0;
    int32 fileLength = 0;
    int32 crc32 = 0;
    int32 relativeOffset = 0;

    if (!readPodAt(headerBytes, cursor, fileNumber) ||
        !readPodAt(headerBytes, cursor + 4, filePathLength)) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "PvfArchive: truncated index header at entry %d", i);
      break;
    }
    (void)fileNumber;
    cursor += 8;

    if (filePathLength < 0) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "PvfArchive: negative path length in index entry %d", i);
      break;
    }

    size_t pathLength = static_cast<size_t>(filePathLength);
    if (!canReadSpan(headerBytes, cursor, pathLength + 12)) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "PvfArchive: truncated file entry payload at index %d", i);
      break;
    }

    std::string fileName =
        normalizePath(readStringAt(headerBytes, cursor, pathLength));
    cursor += pathLength;

    readPodAt(headerBytes, cursor, fileLength);
    readPodAt(headerBytes, cursor + 4, crc32);
    readPodAt(headerBytes, cursor + 8, relativeOffset);
    cursor += 12;

    if (fileLength > 0 && relativeOffset >= 0) {
      uint32 alignedLength = (static_cast<uint32>(fileLength) + 3u) & 0xFFFFFFFCu;
      PvfFileInfo info;
      info.offset = static_cast<size_t>(relativeOffset);
      info.crc32 = static_cast<uint32>(crc32);
      info.length = static_cast<size_t>(alignedLength);
      info.decoded = false;
      fileInfo_[fileName] = info;
    }
  }
}

void PvfArchive::initBinStringTable() {
  binStringTable_.clear();

  auto infoOpt = getFileInfo("stringtable.bin");
  if (!infoOpt.has_value()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: stringtable.bin was not found");
    return;
  }

  auto bytesOpt = readDecodedFileBytes(infoOpt.value());
  if (!bytesOpt.has_value()) {
    return;
  }

  const std::vector<uint8>& bytes = bytesOpt.value();
  int32 count = 0;
  if (!readPodAt(bytes, 0, count) || count < 0) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: invalid stringtable.bin header");
    return;
  }

  for (int32 i = 0; i < count; ++i) {
    size_t offsetsPos = static_cast<size_t>(i) * 4 + 4;
    int32 startPos = 0;
    int32 endPos = 0;
    if (!readPodAt(bytes, offsetsPos, startPos) ||
        !readPodAt(bytes, offsetsPos + 4, endPos)) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "PvfArchive: truncated string table at entry %d", i);
      break;
    }

    if (startPos < 0 || endPos < startPos) {
      continue;
    }

    size_t stringPos = static_cast<size_t>(startPos) + 4;
    size_t length = static_cast<size_t>(endPos - startPos);
    if (!canReadSpan(bytes, stringPos, length)) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "PvfArchive: invalid string span at entry %d", i);
      continue;
    }

    binStringTable_[i] = readStringAt(bytes, stringPos, length);
  }
}

void PvfArchive::initLoadStrings() {
  loadStrings_.clear();

  auto infoOpt = getFileInfo("n_string.lst");
  if (!infoOpt.has_value()) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: n_string.lst was not found");
    return;
  }

  auto listBytesOpt = readDecodedFileBytes(infoOpt.value());
  if (!listBytesOpt.has_value()) {
    return;
  }

  const std::vector<uint8>& listBytes = listBytesOpt.value();
  if (listBytes.size() < sizeof(int16)) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "PvfArchive: n_string.lst is too small");
    return;
  }

  std::map<std::string, std::string> stringFileCache;
  size_t cursor = sizeof(int16);
  while (cursor + 10 <= listBytes.size()) {
    int32 findKey = 0;
    if (!readPodAt(listBytes, cursor + 6, findKey)) {
      break;
    }

    auto binStrOpt = getBinString(findKey);
    if (!binStrOpt.has_value()) {
      cursor += 10;
      continue;
    }

    std::string key = normalizePath(binStrOpt.value());
    if (key.empty()) {
      cursor += 10;
      continue;
    }

    size_t slashPos = key.find('/');
    std::string type =
        (slashPos != std::string::npos) ? normalizePath(key.substr(0, slashPos))
                                        : std::string();

    std::string content;
    auto cacheIt = stringFileCache.find(key);
    if (cacheIt != stringFileCache.end()) {
      content = cacheIt->second;
    } else {
      auto fileIt = fileInfo_.find(key);
      if (fileIt != fileInfo_.end()) {
        auto contentBytesOpt = readDecodedFileBytes(fileIt->second);
        if (contentBytesOpt.has_value()) {
          const std::vector<uint8>& contentBytes = contentBytesOpt.value();
          content.assign(reinterpret_cast<const char*>(contentBytes.data()),
                         contentBytes.size());
          stringFileCache[key] = content;
        }
      }
    }

    if (!content.empty()) {
      auto& table = loadStrings_[type];
      std::vector<std::string> lines = splitString(content, "\n");
      for (const auto& line : lines) {
        size_t gtPos = line.find('>');
        if (gtPos != std::string::npos && gtPos + 1 < line.length()) {
          table[line.substr(0, gtPos)] = line.substr(gtPos + 1);
        }
      }
    }

    cursor += 10;
  }
}

bool PvfArchive::hasFile(const std::string& path) const {
  return fileInfo_.find(normalizePath(path)) != fileInfo_.end();
}

std::optional<PvfFileInfo> PvfArchive::getFileInfo(const std::string& path) const {
  std::string normalizedPath = normalizePath(path);
  auto it = fileInfo_.find(normalizedPath);
  if (it != fileInfo_.end()) {
    return it->second;
  }
  return std::nullopt;
}

std::vector<std::string> PvfArchive::listFiles() const {
  std::vector<std::string> files;
  files.reserve(fileInfo_.size());
  for (const auto& pair : fileInfo_) {
    files.push_back(pair.first);
  }
  return files;
}

std::optional<std::string> PvfArchive::getFileContent(const std::string& path) {
  std::string normalizedPath = normalizePath(path);
  auto it = fileInfo_.find(normalizedPath);
  if (it == fileInfo_.end()) {
    return std::nullopt;
  }

  PvfFileInfo& info = it->second;
  if (!decodeFile(normalizedPath, info)) {
    return std::nullopt;
  }

  auto cacheIt = decodedFileCache_.find(normalizedPath);
  if (cacheIt == decodedFileCache_.end()) {
    return std::nullopt;
  }

  const std::vector<uint8>& bytes = cacheIt->second;
  if (bytes.empty()) {
    return std::string();
  }
  return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
}

std::optional<std::vector<uint8>> PvfArchive::getFileBytes(const std::string& path) {
  std::string normalizedPath = normalizePath(path);
  auto it = fileInfo_.find(normalizedPath);
  if (it == fileInfo_.end()) {
    return std::nullopt;
  }

  PvfFileInfo& info = it->second;
  if (!decodeFile(normalizedPath, info)) {
    return std::nullopt;
  }

  auto cacheIt = decodedFileCache_.find(normalizedPath);
  if (cacheIt == decodedFileCache_.end()) {
    return std::nullopt;
  }

  return cacheIt->second;
}

std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
  std::string normalizedPath = normalizePath(path);
  auto it = fileInfo_.find(normalizedPath);
  if (it == fileInfo_.end()) {
    return std::nullopt;
  }

  PvfFileInfo& info = it->second;
  if (!decodeFile(normalizedPath, info)) {
    return std::nullopt;
  }

  auto cacheIt = decodedFileCache_.find(normalizedPath);
  if (cacheIt == decodedFileCache_.end()) {
    return std::nullopt;
  }

  const std::vector<uint8>& bytes = cacheIt->second;
  RawData result;
  result.size = bytes.size();
  result.data = std::make_unique<char[]>(bytes.size());
  if (!bytes.empty()) {
    std::memcpy(result.data.get(), bytes.data(), bytes.size());
  }
  return result;
}

std::optional<std::string> PvfArchive::getBinString(int key) const {
  auto it = binStringTable_.find(key);
  if (it != binStringTable_.end()) {
    return it->second;
  }
  return std::nullopt;
}

std::optional<std::string> PvfArchive::getLoadString(const std::string& type,
                                                     const std::string& key) const {
  std::string normalizedType = normalizePath(type);
  auto typeIt = loadStrings_.find(normalizedType);
  if (typeIt != loadStrings_.end()) {
    auto keyIt = typeIt->second.find(key);
    if (keyIt != typeIt->second.end()) {
      return keyIt->second;
    }
  }
  return std::nullopt;
}

bool PvfArchive::hasBinString(int key) const {
  return binStringTable_.find(key) != binStringTable_.end();
}

bool PvfArchive::hasLoadString(const std::string& type,
                               const std::string& key) const {
  std::string normalizedType = normalizePath(type);
  auto typeIt = loadStrings_.find(normalizedType);
  if (typeIt != loadStrings_.end()) {
    return typeIt->second.find(key) != typeIt->second.end();
  }
  return false;
}

std::string PvfArchive::normalizePath(const std::string& path) const {
  if (path.empty()) {
    return {};
  }

  std::string normalized = path;
  std::replace(normalized.begin(), normalized.end(), '\\', '/');
  std::transform(normalized.begin(), normalized.end(), normalized.begin(),
                 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });

  std::vector<std::string> parts;
  std::stringstream stream(normalized);
  std::string part;
  while (std::getline(stream, part, '/')) {
    if (part.empty() || part == ".") {
      continue;
    }
    if (part == "..") {
      if (!parts.empty() && parts.back() != "..") {
        parts.pop_back();
      } else {
        parts.push_back(part);
      }
      continue;
    }
    parts.push_back(part);
  }

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

std::string PvfArchive::resolvePath(const std::string& baseDir,
                                    const std::string& path) const {
  if (path.empty()) {
    return {};
  }

  std::string rawPath = path;
  std::replace(rawPath.begin(), rawPath.end(), '\\', '/');
  std::transform(rawPath.begin(), rawPath.end(), rawPath.begin(),
                 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });

  size_t slashPos = rawPath.find('/');
  std::string root =
      slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos);
  static const char* kLogicalRoots[] = {"map",       "sprite", "town",
                                        "sound",     "audio",  "monster",
                                        "character", "ui"};
  for (const char* logicalRoot : kLogicalRoots) {
    if (root == logicalRoot) {
      return normalizePath(rawPath);
    }
  }

  std::string normalizedBaseDir = normalizePath(baseDir);
  if (!normalizedBaseDir.empty() && normalizedBaseDir.back() != '/') {
    normalizedBaseDir.push_back('/');
  }
  return normalizePath(normalizedBaseDir + rawPath);
}

std::vector<std::string> PvfArchive::splitString(
    const std::string& str, const std::string& delimiter) const {
  std::vector<std::string> tokens;
  size_t pos = 0;
  size_t found;

  while ((found = str.find(delimiter, pos)) != std::string::npos) {
    tokens.push_back(str.substr(pos, found - pos));
    pos = found + delimiter.length();
  }

  if (pos < str.length()) {
    tokens.push_back(str.substr(pos));
  }

  return tokens;
}

bool PvfArchive::decodeFile(const std::string& normalizedPath,
                            PvfFileInfo& info) {
  auto cacheIt = decodedFileCache_.find(normalizedPath);
  if (cacheIt != decodedFileCache_.end()) {
    info.decoded = true;
    return true;
  }

  auto bytesOpt = readDecodedFileBytes(info);
  if (!bytesOpt.has_value()) {
    return false;
  }

  decodedFileCache_[normalizedPath] = std::move(bytesOpt.value());
  info.decoded = true;
  return true;
}

void PvfArchive::clearInitData() {
  dataStartPos_ = 0;
  fileInfo_.clear();
  binStringTable_.clear();
  loadStrings_.clear();
  decodedFileCache_.clear();
}

std::optional<std::vector<uint8>> PvfArchive::readArchiveBytes(
    size_t absoluteOffset, size_t length) {
  if (!isOpen()) {
    return std::nullopt;
  }

  if (length == 0) {
    return std::vector<uint8>();
  }

  if (absoluteOffset > reader_.size() ||
      length > reader_.size() - absoluteOffset) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: attempted to read beyond archive bounds (offset=%zu, size=%zu, archive=%zu)",
                 absoluteOffset, length, reader_.size());
    return std::nullopt;
  }

  reader_.seek(absoluteOffset);
  std::vector<uint8> bytes = reader_.readBytes(length);
  if (bytes.size() != length) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "PvfArchive: short read at offset %zu (expected %zu bytes, got %zu)",
                 absoluteOffset, length, bytes.size());
    return std::nullopt;
  }

  return bytes;
}

std::optional<std::vector<uint8>> PvfArchive::readDecodedFileBytes(
    const PvfFileInfo& info) {
  auto bytesOpt = readArchiveBytes(dataStartPos_ + info.offset, info.length);
  if (!bytesOpt.has_value()) {
    return std::nullopt;
  }

  std::vector<uint8> bytes = std::move(bytesOpt.value());
  crcDecodeBuffer(bytes, info.crc32);
  return bytes;
}

void PvfArchive::crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32) {
  if (data.empty()) {
    return;
  }

  const uint32 key = 0x81A79011u;
  for (size_t i = 0; i + 3 < data.size(); i += 4) {
    uint32 value = static_cast<uint32>(data[i]) |
                   (static_cast<uint32>(data[i + 1]) << 8) |
                   (static_cast<uint32>(data[i + 2]) << 16) |
                   (static_cast<uint32>(data[i + 3]) << 24);

    uint32 decoded = (value ^ key ^ crc32);
    decoded = (decoded >> 6) | ((decoded << (32 - 6)) & 0xFFFFFFFFu);

    data[i] = static_cast<uint8>((decoded >> 0) & 0xFFu);
    data[i + 1] = static_cast<uint8>((decoded >> 8) & 0xFFu);
    data[i + 2] = static_cast<uint8>((decoded >> 16) & 0xFFu);
    data[i + 3] = static_cast<uint8>((decoded >> 24) & 0xFFu);
  }
}

} // namespace frostbite2D
