#include <frostbite2D/resource/npk_archive.h>
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <system_error>
#include <SDL.h>

#include <frostbite2D/utils/startup_trace.h>

namespace frostbite2D {

namespace {

namespace fs = std::filesystem;

constexpr char kIndexCacheFileName[] = ".frostbite_npk_index_cache.bin";
constexpr char kIndexCacheMagic[8] = {'F', 'B', 'N', 'P', 'K', 'I', 'D', 'X'};
constexpr uint32 kIndexCacheVersion = 2;
constexpr uint32 kMaxCacheStringLength = 4096;
constexpr uint32 kMaxCacheSourceFileCount = 20000;
constexpr uint32 kMaxCacheImageCount = 20000000;

fs::path toFsPath(const std::string& path) {
#ifdef _WIN32
  return fs::u8path(path);
#else
  return fs::path(path);
#endif
}

int64 toCacheTimestamp(const fs::file_time_type& value) {
  auto duration =
      std::chrono::duration_cast<std::chrono::nanoseconds>(
          value.time_since_epoch())
          .count();
  return static_cast<int64>(duration);
}

bool appendBytes(std::vector<uint8>& buffer, const void* data, size_t size) {
  if (size == 0) {
    return true;
  }

  if (buffer.size() > buffer.max_size() - size) {
    return false;
  }

  size_t oldSize = buffer.size();
  buffer.resize(oldSize + size);
  std::memcpy(buffer.data() + oldSize, data, size);
  return true;
}

template <typename T>
bool appendPod(std::vector<uint8>& buffer, const T& value) {
  return appendBytes(buffer, &value, sizeof(T));
}

template <typename T>
bool readPod(std::ifstream& stream, T& value) {
  stream.read(reinterpret_cast<char*>(&value), sizeof(T));
  return stream.good();
}

bool appendString(std::vector<uint8>& buffer, const std::string& value) {
  if (value.size() > kMaxCacheStringLength) {
    return false;
  }

  uint32 length = static_cast<uint32>(value.size());
  if (!appendPod(buffer, length)) {
    return false;
  }

  if (length == 0) {
    return true;
  }

  return appendBytes(buffer, value.data(), length);
}

bool readString(std::ifstream& stream, std::string& value) {
  uint32 length = 0;
  if (!readPod(stream, length)) {
    return false;
  }

  if (length > kMaxCacheStringLength) {
    return false;
  }

  value.clear();
  if (length == 0) {
    return true;
  }

  value.resize(length);
  stream.read(value.data(), length);
  return stream.good();
}

const char* getZlibErrorName(int code) {
  switch (code) {
    case Z_OK:
      return "Z_OK";
    case Z_STREAM_END:
      return "Z_STREAM_END";
    case Z_NEED_DICT:
      return "Z_NEED_DICT";
    case Z_ERRNO:
      return "Z_ERRNO";
    case Z_STREAM_ERROR:
      return "Z_STREAM_ERROR";
    case Z_DATA_ERROR:
      return "Z_DATA_ERROR";
    case Z_MEM_ERROR:
      return "Z_MEM_ERROR";
    case Z_BUF_ERROR:
      return "Z_BUF_ERROR";
    case Z_VERSION_ERROR:
      return "Z_VERSION_ERROR";
    default:
      return "UNKNOWN_ZLIB_ERROR";
  }
}

} // namespace

const uint8 NpkArchive::NPK_KEY[256] = {
    112, 117, 99, 104, 105, 107, 111, 110, 64, 110, 101, 111, 112, 108, 101, 32,
    100, 117, 110, 103, 101, 111, 110, 32, 97, 110, 100, 32, 102, 105, 103, 104,
    116, 101, 114, 32, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
    70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
    68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68,
    78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
    70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
    68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68,
    78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78,
    70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70,
    68, 78, 70, 68, 78, 70, 68, 78, 70, 68, 78, 70, 0
};

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

void NpkArchive::setImagePackDirectory(const std::string& dir) {
  imagePackDirectory_ = dir;
}

const std::string& NpkArchive::getImagePackDirectory() const {
  return imagePackDirectory_;
}

void NpkArchive::init() {
  if (initialized_) {
    close();
  }

  scanNpkFiles();
  initialized_ = true;
}

void NpkArchive::close() {
  imgIndex_.clear();
  imageCache_.clear();
  lruList_.clear();
  lruLookup_.clear();
  currentCacheSize_ = 0;
  initialized_ = false;
}

bool NpkArchive::isOpen() const {
  return initialized_;
}

std::string NpkArchive::normalizePath(const std::string& path) const {
  std::string result = path;
  std::transform(result.begin(), result.end(), result.begin(),
                 [](unsigned char c) { return std::tolower(c); });
  return result;
}

std::string NpkArchive::getCacheFilePath() const {
  Asset& asset = Asset::get();
  std::string relativePath =
      asset.combinePath(imagePackDirectory_, kIndexCacheFileName);
  return asset.resolvePath(relativePath);
}

NpkArchive::IndexCacheValidationMode NpkArchive::getIndexCacheValidationMode()
    const {
#ifdef __SWITCH__
  return IndexCacheValidationMode::TrustCache;
#else
  return IndexCacheValidationMode::StrictSourceState;
#endif
}

std::vector<NpkArchive::SourceFileState> NpkArchive::collectSourceFileStates()
    const {
  Asset& asset = Asset::get();
  std::string npkDir = asset.resolvePath(imagePackDirectory_);
  std::vector<NpkArchive::SourceFileState> result;

  if (!asset.isDirectory(npkDir)) {
    return result;
  }

  std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
  result.reserve(files.size());

  for (const auto& file : files) {
    std::error_code error;
    fs::path path = toFsPath(file);
    if (!fs::is_regular_file(path, error)) {
      continue;
    }

    SourceFileState state;
    state.fileName = asset.getFileName(file);
    state.size = static_cast<uint64>(fs::file_size(path, error));
    if (error) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "NpkArchive: failed to read file size for %s",
                  file.c_str());
      state.size = asset.getFileSize(file);
    }

    error.clear();
    fs::file_time_type lastWriteTime = fs::last_write_time(path, error);
    if (error) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "NpkArchive: failed to read write time for %s",
                  file.c_str());
      state.writeTime = 0;
    } else {
      state.writeTime = toCacheTimestamp(lastWriteTime);
    }
    result.push_back(std::move(state));
  }

  std::sort(result.begin(), result.end(),
            [](const SourceFileState& left, const SourceFileState& right) {
              return left.fileName < right.fileName;
            });
  return result;
}

bool NpkArchive::loadIndexCache(
    const std::vector<SourceFileState>& sourceFiles) {
  Asset& asset = Asset::get();
  std::string cachePath = getCacheFilePath();
  if (!asset.isRegularFile(cachePath)) {
    return false;
  }

  std::ifstream stream(toFsPath(cachePath), std::ios::binary);
  if (!stream.is_open()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "NpkArchive: failed to open cache file %s",
                cachePath.c_str());
    return false;
  }

  IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
  uint32 version = 0;
  uint32 cachedValidationMode = 0;
  uint32 cachedSourceCount = 0;
  uint32 cachedImageCount = 0;
  {
    ScopedStartupTrace stageTrace("NpkArchive cache header");
    char magic[sizeof(kIndexCacheMagic)] = {};
    stream.read(magic, sizeof(magic));
    if (!stream.good() ||
        std::memcmp(magic, kIndexCacheMagic, sizeof(kIndexCacheMagic)) != 0) {
      SDL_Log("NpkArchive: cache magic mismatch, will rescan");
      return false;
    }

    if (!readPod(stream, version) || !readPod(stream, cachedValidationMode) ||
        !readPod(stream, cachedSourceCount) || !readPod(stream, cachedImageCount)) {
      SDL_Log("NpkArchive: cache header incomplete, will rescan");
      return false;
    }

    if (version != kIndexCacheVersion) {
      SDL_Log("NpkArchive: cache version mismatch (%u != %u), will rescan",
              version, kIndexCacheVersion);
      return false;
    }

    if (cachedSourceCount > kMaxCacheSourceFileCount ||
        cachedImageCount > kMaxCacheImageCount) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "NpkArchive: cache counts look invalid (sources=%u, images=%u)",
                  cachedSourceCount, cachedImageCount);
      return false;
    }

    if (cachedValidationMode != static_cast<uint32>(validationMode)) {
      SDL_Log("NpkArchive: cache validation mode mismatch (%u != %u), will rescan",
              cachedValidationMode, static_cast<uint32>(validationMode));
      return false;
    }
  }

  if (validationMode == IndexCacheValidationMode::StrictSourceState) {
    if (sourceFiles.empty()) {
      SDL_Log("NpkArchive: strict cache validation requires source metadata, will rescan");
      return false;
    }

    std::vector<SourceFileState> cachedSourceFiles;
    {
      ScopedStartupTrace stageTrace("NpkArchive cache sources");
      cachedSourceFiles.reserve(cachedSourceCount);
      for (uint32 i = 0; i < cachedSourceCount; ++i) {
        SourceFileState state;
        if (!readString(stream, state.fileName) || !readPod(stream, state.size) ||
            !readPod(stream, state.writeTime)) {
          SDL_Log("NpkArchive: cache source file section incomplete, will rescan");
          return false;
        }
        cachedSourceFiles.push_back(std::move(state));
      }
    }

    if (cachedSourceFiles != sourceFiles) {
      SDL_Log("NpkArchive: cache invalidated by directory changes, will rescan");
      return false;
    }
  } else if (cachedSourceCount != 0) {
    SDL_Log("NpkArchive: trusted cache should not contain source metadata, will rescan");
    return false;
  }

  std::unordered_map<std::string, ImgRef> cachedIndex;
  {
    ScopedStartupTrace stageTrace("NpkArchive cache images");
    cachedIndex.reserve(cachedImageCount);
    for (uint32 i = 0; i < cachedImageCount; ++i) {
      std::string path;
      std::string npkFile;
      ImgRef img;
      if (!readString(stream, path) || !readString(stream, npkFile) ||
          !readPod(stream, img.frameCount) || !readPod(stream, img.offset) ||
          !readPod(stream, img.size)) {
        SDL_Log("NpkArchive: cache image section incomplete, will rescan");
        return false;
      }

      img.path = path;
      img.npkFile = std::move(npkFile);
      img.loaded = false;
      cachedIndex.emplace(std::move(path), std::move(img));
    }
  }

  if (!stream.good() && !stream.eof()) {
    SDL_Log("NpkArchive: cache read failed, will rescan");
    return false;
  }

  {
    ScopedStartupTrace stageTrace("NpkArchive cache commit");
    imgIndex_.swap(cachedIndex);
  }
  SDL_Log("NpkArchive: loaded %zu image refs from cache %s", imgIndex_.size(),
          cachePath.c_str());
  return true;
}

bool NpkArchive::saveIndexCache(
    const std::vector<SourceFileState>& sourceFiles) const {
  std::string cachePath = getCacheFilePath();
  IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
  uint32 sourceCount =
      validationMode == IndexCacheValidationMode::StrictSourceState
          ? static_cast<uint32>(sourceFiles.size())
          : 0;
  uint32 imageCount = static_cast<uint32>(imgIndex_.size());
  std::vector<uint8> buffer;
  {
    ScopedStartupTrace stageTrace("NpkArchive cache serialize");
    size_t estimatedSize = sizeof(kIndexCacheMagic) + sizeof(uint32) * 4;
    if (validationMode == IndexCacheValidationMode::StrictSourceState) {
      for (const auto& sourceFile : sourceFiles) {
        estimatedSize += sizeof(uint32) + sourceFile.fileName.size();
        estimatedSize += sizeof(sourceFile.size);
        estimatedSize += sizeof(sourceFile.writeTime);
      }
    }

    for (const auto& [path, img] : imgIndex_) {
      estimatedSize += sizeof(uint32) + path.size();
      estimatedSize += sizeof(uint32) + img.npkFile.size();
      estimatedSize += sizeof(img.frameCount);
      estimatedSize += sizeof(img.offset);
      estimatedSize += sizeof(img.size);
    }

    buffer.reserve(estimatedSize);

    if (!appendBytes(buffer, kIndexCacheMagic, sizeof(kIndexCacheMagic)) ||
        !appendPod(buffer, kIndexCacheVersion) ||
        !appendPod(buffer, static_cast<uint32>(validationMode)) ||
        !appendPod(buffer, sourceCount) ||
        !appendPod(buffer, imageCount)) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "NpkArchive: failed to serialize cache header %s",
                  cachePath.c_str());
      return false;
    }

    if (validationMode == IndexCacheValidationMode::StrictSourceState) {
      for (const auto& sourceFile : sourceFiles) {
        if (!appendString(buffer, sourceFile.fileName) ||
            !appendPod(buffer, sourceFile.size) ||
            !appendPod(buffer, sourceFile.writeTime)) {
          SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                      "NpkArchive: failed to serialize cache source data %s",
                      cachePath.c_str());
          return false;
        }
      }
    }

    for (const auto& [path, img] : imgIndex_) {
      if (!appendString(buffer, path) || !appendString(buffer, img.npkFile) ||
          !appendPod(buffer, img.frameCount) || !appendPod(buffer, img.offset) ||
          !appendPod(buffer, img.size)) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                    "NpkArchive: failed to serialize cache image data %s",
                    cachePath.c_str());
        return false;
      }
    }
  }

  {
    ScopedStartupTrace stageTrace("NpkArchive cache flush");
    std::ofstream stream(toFsPath(cachePath),
                         std::ios::binary | std::ios::trunc);
    if (!stream.is_open()) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "NpkArchive: failed to create cache file %s",
                  cachePath.c_str());
      return false;
    }

    if (!buffer.empty()) {
      stream.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
    }

    if (!stream.good()) {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "NpkArchive: cache flush failed %s", cachePath.c_str());
      return false;
    }
  }

  SDL_Log("NpkArchive: wrote index cache %s (mode=%u, %u source files, %u images)",
          cachePath.c_str(), static_cast<uint32>(validationMode), sourceCount,
          imageCount);
  return true;
}

void NpkArchive::scanNpkFiles() {
  Asset& asset = Asset::get();
  std::string npkDir = asset.resolvePath(imagePackDirectory_);
  if (!asset.isDirectory(npkDir)) {
    SDL_Log("NPK directory not found: %s", npkDir.c_str());
    return;
  }

  imgIndex_.clear();
  IndexCacheValidationMode validationMode = getIndexCacheValidationMode();

  std::vector<SourceFileState> sourceFiles;
  if (validationMode == IndexCacheValidationMode::StrictSourceState) {
    {
      ScopedStartupTrace stageTrace("NpkArchive collect source states");
      sourceFiles = collectSourceFileStates();
    }
    if (sourceFiles.empty()) {
      SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
      return;
    }

    {
      ScopedStartupTrace stageTrace("NpkArchive cache strict-load");
      if (loadIndexCache(sourceFiles)) {
        return;
      }
    }
  } else {
    {
      ScopedStartupTrace stageTrace("NpkArchive cache fast-load");
      if (loadIndexCache(sourceFiles)) {
        return;
      }
    }
  }

  std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
  std::sort(files.begin(), files.end());
  if (files.empty()) {
    SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
    return;
  }
  SDL_Log("Scanning %d NPK files...", static_cast<int>(files.size()));
  {
    ScopedStartupTrace stageTrace("NpkArchive parse files");
    for (const auto& file : files) {
      parseNpkFile(file);
    }
  }

  {
    ScopedStartupTrace stageTrace("NpkArchive cache save");
    saveIndexCache(sourceFiles);
  }
}

bool NpkArchive::parseNpkFile(const std::string& npkPath) {
  Asset& asset = Asset::get();
  BinaryFileStreamReader reader(npkPath);

  if (!reader.isOpen()) {
    SDL_Log("Failed to open NPK file: %s", npkPath.c_str());
    return false;
  }

  std::string npkFileName = asset.getFileName(npkPath);
  std::string header = reader.readNullTerminatedString();

  if (header.find("NeoplePack_Bill") == std::string::npos) {
    return false;
  }

  int32 imageCount = reader.readInt32();

  for (int32 i = 0; i < imageCount; ++i) {
    int32 offset = reader.readInt32();
    int32 length = reader.readInt32();
    std::string imgPath = readNpkInfoString(reader);

    ImgRef img;
    img.path = normalizePath(imgPath);
    img.npkFile = npkFileName;
    img.offset = static_cast<uint32>(offset);
    img.size = static_cast<uint32>(length);
    img.loaded = false;

    imgIndex_[img.path] = img;
  }

  return true;
}

bool NpkArchive::hasImg(const std::string& path) const {
  return imgIndex_.find(normalizePath(path)) != imgIndex_.end();
}

std::optional<ImgRef> NpkArchive::getImg(const std::string& path) {
  auto it = imgIndex_.find(normalizePath(path));
  if (it == imgIndex_.end()) {
    return std::nullopt;
  }
  return it->second;
}

std::vector<std::string> NpkArchive::listImgs() const {
  std::vector<std::string> result;
  result.reserve(imgIndex_.size());
  for (const auto& pair : imgIndex_) {
    result.push_back(pair.first);
  }
  std::sort(result.begin(), result.end());
  return result;
}

std::optional<ImageFrame> NpkArchive::getImageFrame(const ImgRef& img, size_t index) {
  std::string imgPath = normalizePath(img.path);

  auto cacheIt = imageCache_.find(imgPath);
  if (cacheIt == imageCache_.end()) {
    auto it = imgIndex_.find(imgPath);
    if (it == imgIndex_.end()) {
      return std::nullopt;
    }

    if (!loadImgData(it->second)) {
      return std::nullopt;
    }
    cacheIt = imageCache_.find(imgPath);
    if (cacheIt == imageCache_.end()) {
      return std::nullopt;
    }
  }

  updateCacheUsage(imgPath);

  if (index >= cacheIt->second.frames.size()) {
    return std::nullopt;
  }

  return cacheIt->second.frames[index];
}

size_t NpkArchive::getFrameCount(const ImgRef& img) const {
  std::string imgPath = normalizePath(img.path);
  auto it = imgIndex_.find(imgPath);
  if (it == imgIndex_.end()) {
    return 0;
  }
  return it->second.frameCount;
}

bool NpkArchive::loadImgData(ImgRef& img) {
  Asset& asset = Asset::get();
  std::string npkPath = asset.combinePath(imagePackDirectory_, img.npkFile);
  npkPath = asset.resolvePath(npkPath);

  BinaryFileStreamReader reader(npkPath);
  if (!reader.isOpen()) {
    SDL_Log("Failed to open NPK for IMG: %s", npkPath.c_str());
    return false;
  }

  reader.seek(img.offset);
  std::string flag = reader.readNullTerminatedString();

  if (flag.find("Neople Img File") == std::string::npos) {
    return false;
  }

  int64 tableLength = reader.readInt64();
  reader.readInt32();
  int32 indexCount = reader.readInt32();

  img.frameCount = static_cast<size_t>(indexCount);

  CachedImageData cachedData;
  cachedData.frames.resize(indexCount);

  size_t dataStartPos = img.offset + static_cast<uint32>(tableLength) + 32;

  for (int32 i = 0; i < indexCount; ++i) {
    ImageFrame& frame = cachedData.frames[i];
    frame.type = reader.readInt32();

    if (frame.type == 17) {
      int32 refIndex = reader.readInt32();
      frame.compressionType = refIndex;
      frame.width = cachedData.frames[refIndex].width;
      frame.height = cachedData.frames[refIndex].height;
      frame.xPos = cachedData.frames[refIndex].xPos;
      frame.yPos = cachedData.frames[refIndex].yPos;
      frame.frameXPos = cachedData.frames[refIndex].frameXPos;
      frame.frameYPos = cachedData.frames[refIndex].frameYPos;
      frame.offset = cachedData.frames[refIndex].offset;
      frame.size = cachedData.frames[refIndex].size;
      continue;
    }

    frame.compressionType = reader.readInt32();
    frame.width = reader.readInt32();
    frame.height = reader.readInt32();
    int32 size = reader.readInt32();
    frame.xPos = reader.readInt32();
    frame.yPos = reader.readInt32();
    frame.frameXPos = reader.readInt32();
    frame.frameYPos = reader.readInt32();

    frame.size = size;
    if (i == 0) {
      frame.offset = static_cast<uint32>(dataStartPos);
    } else {
      frame.offset = cachedData.frames[i - 1].offset + cachedData.frames[i - 1].size;
    }
  }

  for (int32 i = 0; i < indexCount; ++i) {
    ImageFrame& frame = cachedData.frames[i];

    if (frame.type == 17) {
      int32 refIndex = frame.compressionType;
      frame.data = cachedData.frames[refIndex].data;
      continue;
    }

    reader.seek(frame.offset);
    std::vector<uint8> compressedData = reader.readBytes(static_cast<size_t>(frame.size));

    int32 deSize = frame.width * frame.height * 4;
    std::vector<uint8> decompressedData(deSize);
    unsigned long realSize = static_cast<unsigned long>(deSize);
    
    int uncompressResult = uncompress(
      decompressedData.data(), 
      &realSize, 
      compressedData.data(), 
      static_cast<unsigned long>(frame.size)
    );

    if (uncompressResult != Z_OK) {
      if (frame.type == 16 && frame.compressionType == 5 &&
          frame.size == static_cast<uint32>(deSize)) {
        frame.data = std::move(compressedData);
        cachedData.memoryUsage += frame.data.size();
        if (verboseFallbackLog_) {
          SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                      "NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, "
                      "type=%d, compression=%d, size=%dx%d",
                      img.path.c_str(), i, frame.type, frame.compressionType,
                      frame.width, frame.height);
        }
        continue;
      }
      if (verboseFallbackLog_) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "Failed to uncompress image data: %d (%s), img=%s, "
                     "npk=%s, frame=%d, "
                     "type=%d, compression=%d, size=%dx%d, offset=%u, "
                     "packed=%u, expected=%d",
                     uncompressResult, getZlibErrorName(uncompressResult),
                     img.path.c_str(), img.npkFile.c_str(), i, frame.type,
                     frame.compressionType, frame.width, frame.height,
                     frame.offset, frame.size, deSize);
      }
      continue;
    }

    if (frame.type != 16) {
      int32 pngByteSize = deSize * 2;
      frame.data.resize(pngByteSize);

      for (int32 e = 0; e < pngByteSize; e += 4) {
        uint8 needData[2] = {0, 0};
        if ((e / 4) * 2 + 1 < static_cast<int32>(decompressedData.size())) {
          needData[0] = decompressedData[(e / 4) * 2];
          needData[1] = decompressedData[(e / 4) * 2 + 1];
        }
        parseColor(needData, frame.type, frame.data.data(), e);
      }
    } else {
      frame.data = std::move(decompressedData);
    }

    cachedData.memoryUsage += frame.data.size();
  }

  auto existingCacheIt = imageCache_.find(img.path);
  if (existingCacheIt != imageCache_.end()) {
    currentCacheSize_ -= existingCacheIt->second.memoryUsage;
    imageCache_.erase(existingCacheIt);
  }

  evictCacheIfNeeded(cachedData.memoryUsage);
  auto cacheIt = imageCache_.emplace(img.path, std::move(cachedData)).first;
  currentCacheSize_ += cacheIt->second.memoryUsage;
  updateCacheUsage(img.path);

  img.loaded = true;
  imgIndex_[img.path] = img;

  return true;
}

void NpkArchive::parseColor(const uint8* tab, int type, uint8* saveByte, int offset) {
  uint8 a = 0, r = 0, g = 0, b = 0;

  switch (type) {
    case 0x0e:
      a = static_cast<uint8>(tab[1] >> 7);
      r = static_cast<uint8>((tab[1] >> 2) & 0x1f);
      g = static_cast<uint8>((tab[0] >> 5) | ((tab[1] & 3) << 3));
      b = static_cast<uint8>(tab[0] & 0x1f);
      a = static_cast<uint8>(a * 0xff);
      r = static_cast<uint8>((r << 3) | (r >> 2));
      g = static_cast<uint8>((g << 3) | (g >> 2));
      b = static_cast<uint8>((b << 3) | (b >> 2));
      break;

    case 0x0f:
      a = static_cast<uint8>(tab[1] & 0xf0);
      r = static_cast<uint8>((tab[1] & 0xf) << 4);
      g = static_cast<uint8>(tab[0] & 0xf0);
      b = static_cast<uint8>((tab[0] & 0xf) << 4);
      break;
  }

  saveByte[offset + 0] = b;
  saveByte[offset + 1] = g;
  saveByte[offset + 2] = r;
  saveByte[offset + 3] = a;
}

void NpkArchive::setCacheSize(size_t maxBytes) {
  maxCacheSize_ = maxBytes;
  evictCacheIfNeeded(0);
}

void NpkArchive::clearCache() {
  imageCache_.clear();
  lruList_.clear();
  lruLookup_.clear();
  currentCacheSize_ = 0;
}

size_t NpkArchive::getCacheUsage() const {
  return currentCacheSize_;
}

void NpkArchive::evictCacheIfNeeded(size_t requiredSize) {
  while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) {
    std::string oldest = lruList_.back();
    lruList_.pop_back();
    lruLookup_.erase(oldest);

    auto it = imageCache_.find(oldest);
    if (it != imageCache_.end()) {
      currentCacheSize_ -= it->second.memoryUsage;
      imageCache_.erase(it);
    }
  }
}

void NpkArchive::updateCacheUsage(const std::string& imgPath) {
  auto lruIt = lruLookup_.find(imgPath);
  if (lruIt != lruLookup_.end()) {
    lruList_.erase(lruIt->second);
  }
  lruList_.push_front(imgPath);
  lruLookup_[imgPath] = lruList_.begin();

  auto cacheIt = imageCache_.find(imgPath);
  if (cacheIt != imageCache_.end()) {
    cacheIt->second.lastUseTime = SDL_GetTicks();
  }
}

std::string NpkArchive::readNpkInfoString(BinaryFileStreamReader& reader) {
  if (reader.eof()) {
    return "";
  }

  std::vector<uint8> encrypted = reader.readBytes(256);
  if (encrypted.size() < 256) {
    return "";
  }

  std::vector<char> decrypted(256);
  for (int i = 0; i < 256; ++i) {
    decrypted[i] = static_cast<char>(encrypted[i] ^ NPK_KEY[i]);
  }

  return std::string(decrypted.data());
}

void NpkArchive::setDefaultImg(const std::string& imgPath, size_t frameIndex) {
  defaultImgPath_ = normalizePath(imgPath);
  defaultImgFrame_ = frameIndex;
}

const std::string& NpkArchive::getDefaultImgPath() const {
  return defaultImgPath_;
}

size_t NpkArchive::getDefaultImgFrame() const {
  return defaultImgFrame_;
}

}
