#include "character/CharacterDataLoader.h"
#include <SDL2/SDL.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/script_parser.h>
#include <algorithm>
#include <cctype>
#include <map>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

namespace frostbite2D::character {
namespace {

constexpr char kCharacterListPath[] = "character/character.lst";
constexpr char kEquipmentListPath[] = "equipment/equipment.lst";

constexpr auto kTagJobName = u8"[职业名称]";
constexpr auto kTagDefaultAvatar = u8"[默认时装]";
constexpr auto kTagDefaultAvatarEnd = u8"[/默认时装]";
constexpr auto kTagJobLabel = u8"[职业标签]";
constexpr auto kTagBaseJob = u8"[基础职业属性]";
constexpr auto kTagActionAnimation = u8"[动作动画]";
constexpr auto kTagActionAnimationEnd = u8"[/动作动画]";
constexpr char kTagName[] = "[name]";
constexpr char kTagUsableJob[] = "[usable job]";
constexpr char kTagUsableJobEnd[] = "[/usable job]";
constexpr char kTagAnimationJob[] = "[animation job]";
constexpr char kTagVariation[] = "[variation]";
constexpr char kTagLayerVariation[] = "[layer variation]";
constexpr char kTagEquipmentAniScript[] = "[equipment ani script]";

std::string toLowerCase(const std::string& str) {
  std::string result = str;
  std::transform(result.begin(), result.end(), result.begin(),
                 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
  return result;
}

int toInt(const std::string& value, int fallback = 0) {
  try {
    return std::stoi(value);
  } catch (...) {
    return fallback;
  }
}

std::string trim(const std::string& value) {
  size_t start = 0;
  size_t end = value.size();
  while (start < end && std::isspace(static_cast<unsigned char>(value[start]))) {
    ++start;
  }
  while (end > start &&
         std::isspace(static_cast<unsigned char>(value[end - 1]))) {
    --end;
  }
  return value.substr(start, end - start);
}

bool isToken(const std::string& token, const char* expected) {
  return trim(token) == expected;
}

class ScriptTokenStream {
public:
  explicit ScriptTokenStream(const std::string& path)
      : path_(toLowerCase(path)) {
    auto& pvf = PvfArchive::get();
    auto rawData = pvf.getFileRawData(path_);
    if (!rawData) {
      return;
    }

    ScriptParser parser(*rawData, path_);
    if (!parser.isValid()) {
      return;
    }

    for (const auto& value : parser.parseAll()) {
      tokens_.push_back(value.toString());
    }
    valid_ = true;
  }

  bool isValid() const { return valid_; }
  bool isEnd() const { return index_ >= tokens_.size(); }

  std::string get() {
    if (isEnd()) {
      return {};
    }
    return tokens_[index_++];
  }

  void back() {
    if (index_ > 0) {
      --index_;
    }
  }

private:
  std::string path_;
  std::vector<std::string> tokens_;
  size_t index_ = 0;
  bool valid_ = false;
};

int jobNameToIndex(const std::string& jobName) {
  static const std::unordered_map<std::string, int> kJobMap = {
      {"[swordman]", 0},         {"[fighter]", 1},      {"[gunner]", 2},
      {"[mage]", 3},             {"[priest]", 4},       {"[atgunner]", 5},
      {"[thief]", 6},            {"[atfighter]", 7},    {"[atmage]", 8},
      {"[demonic swordman]", 9}, {"[creatormage]", 10},
  };

  auto it = kJobMap.find(toLowerCase(jobName));
  return it == kJobMap.end() ? -1 : it->second;
}

std::string normalizeCharacterPath(const std::string& path) {
  auto& pvf = PvfArchive::get();
  std::string normalized = pvf.normalizePath(path);
  if (normalized.rfind("character/", 0) == 0) {
    return normalized;
  }
  return pvf.normalizePath("character/" + normalized);
}

void logTokenPreview(const char* label, const std::vector<std::string>& tokens) {
  std::string preview;
  size_t previewCount = std::min<size_t>(tokens.size(), 8);
  for (size_t i = 0; i < previewCount; ++i) {
    if (!preview.empty()) {
      preview += " | ";
    }
    preview += tokens[i];
  }
  SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "%s: %s", label, preview.c_str());
}

std::optional<JobConfig> parseJobConfig(const std::string& path) {
  ScriptTokenStream stream(path);
  if (!stream.isValid()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterDataLoader: missing job config %s", path.c_str());
    return std::nullopt;
  }

  JobConfig config;
  while (!stream.isEnd()) {
    std::string segment = stream.get();
    if (isToken(segment, kTagJobName)) {
      config.name = stream.get();
    } else if (isToken(segment, kTagDefaultAvatar)) {
      while (!stream.isEnd()) {
        std::string slotName = stream.get();
        if (isToken(slotName, kTagDefaultAvatarEnd)) {
          break;
        }
        config.defaultAvatarList[slotName] = toInt(stream.get(), -1);
      }
    }
  }

  return config;
}

std::map<int, std::string>& characterIndexCache() {
  static std::map<int, std::string> cache;
  static bool loaded = false;
  if (!loaded) {
    ScriptTokenStream stream(kCharacterListPath);
    if (stream.isValid()) {
      while (!stream.isEnd()) {
        std::string indexToken = stream.get();
        if (indexToken.empty()) {
          break;
        }
        std::string pathToken = stream.get();
        if (pathToken.empty()) {
          break;
        }
        cache[toInt(indexToken, -1)] = normalizeCharacterPath(pathToken);
      }
    }
    loaded = true;
  }
  return cache;
}

std::map<int, std::string>& equipmentIndexCache() {
  static std::map<int, std::string> cache;
  static bool loaded = false;
  if (!loaded) {
    ScriptTokenStream stream(kEquipmentListPath);
    if (stream.isValid()) {
      while (!stream.isEnd()) {
        std::string indexToken = stream.get();
        if (indexToken.empty()) {
          break;
        }
        std::string pathToken = stream.get();
        if (pathToken.empty()) {
          break;
        }
        cache[toInt(indexToken, -1)] =
            PvfArchive::get().normalizePath("equipment/" + pathToken);
      }
    } else {
      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                  "CharacterDataLoader: equipment index %s not found, fallback to skin-only character rendering",
                  kEquipmentListPath);
    }
    loaded = true;
  }
  return cache;
}

std::map<int, CharacterConfig>& characterConfigCache() {
  static std::map<int, CharacterConfig> cache;
  return cache;
}

std::map<int, EquipmentConfig>& equipmentConfigCache() {
  static std::map<int, EquipmentConfig> cache;
  return cache;
}

void appendVariationForJobs(EquipmentConfig& config,
                            const std::vector<int>& jobIndices,
                            int param1,
                            int param2,
                            ScriptTokenStream& stream) {
  std::vector<EquipmentVariation> variations;

  while (!stream.isEnd()) {
    std::string token = stream.get();
    if (token == kTagLayerVariation) {
      EquipmentVariation variation;
      variation.layer = toInt(stream.get(), 0);
      variation.animationGroup = PvfArchive::get().normalizePath(stream.get());
      variation.imgFormat = {param1, param2};
      variations.push_back(variation);

      if (!stream.isEnd()) {
        std::string maybeTag = stream.get();
        if (maybeTag == kTagEquipmentAniScript) {
          if (!stream.isEnd()) {
            std::string ignoredScriptPath = stream.get();
            (void)ignoredScriptPath;
          }
        } else {
          stream.back();
        }
      }
      continue;
    }

    if (config.path.find("skin") != std::string::npos) {
      EquipmentVariation variation;
      variation.layer = 0;
      variation.animationGroup.clear();
      variation.imgFormat = {param1, param2};
      variations.push_back(variation);
    }

    stream.back();
    break;
  }

  for (int jobIndex : jobIndices) {
    if (jobIndex < 0) {
      continue;
    }
    auto& target = config.jobAnimations[jobIndex];
    target.insert(target.end(), variations.begin(), variations.end());
  }
}

} // namespace

bool loadCharacterIndex(std::map<int, std::string>& outIndex) {
  outIndex = characterIndexCache();
  return !outIndex.empty();
}

std::optional<CharacterConfig> loadCharacterConfig(int jobId) {
  auto cacheIt = characterConfigCache().find(jobId);
  if (cacheIt != characterConfigCache().end()) {
    return cacheIt->second;
  }

  auto indexIt = characterIndexCache().find(jobId);
  if (indexIt == characterIndexCache().end()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterDataLoader: character job %d not found in index", jobId);
    return std::nullopt;
  }

  ScriptTokenStream stream(indexIt->second);
  if (!stream.isValid()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterDataLoader: unable to load character config %s",
                indexIt->second.c_str());
    return std::nullopt;
  }

  CharacterConfig config;
  config.jobId = jobId;
  std::vector<std::string> tokenPreview;
  while (!stream.isEnd()) {
    std::string segment = stream.get();
    if (tokenPreview.size() < 8) {
      tokenPreview.push_back(segment);
    }

    if (isToken(segment, kTagJobLabel)) {
      std::string jobTag = stream.get();
      if (jobTag.size() >= 2 && jobTag.front() == '[' && jobTag.back() == ']') {
        jobTag = jobTag.substr(1, jobTag.size() - 2);
      }
      config.jobTag = toLowerCase(jobTag);
    } else if (isToken(segment, kTagBaseJob)) {
      config.baseJobPath = PvfArchive::get().normalizePath(stream.get());
      auto baseJobConfig = parseJobConfig(config.baseJobPath);
      if (baseJobConfig) {
        config.baseJobConfig = *baseJobConfig;
      } else {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                    "CharacterDataLoader: base job config missing for job %d from %s",
                    jobId, config.baseJobPath.c_str());
      }
    } else if (isToken(segment, kTagActionAnimation)) {
      while (!stream.isEnd()) {
        std::string actionName = stream.get();
        if (isToken(actionName, kTagActionAnimationEnd)) {
          break;
        }
        std::string actionPath = stream.get();
        config.animationPath[actionName] = PvfArchive::get().normalizePath(
            "character/" + config.jobTag + "/" + actionPath);
      }
    }
  }

  if (config.jobTag.empty() || config.animationPath.empty()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterDataLoader: character job %d config incomplete (jobTag=%s, baseJobPath=%s, actionCount=%d, script=%s)",
                jobId, config.jobTag.c_str(), config.baseJobPath.c_str(),
                static_cast<int>(config.animationPath.size()), indexIt->second.c_str());
    logTokenPreview("CharacterDataLoader token preview", tokenPreview);
    return std::nullopt;
  }


  characterConfigCache()[jobId] = config;
  return config;
}

bool loadEquipmentIndex(std::map<int, std::string>& outIndex) {
  outIndex = equipmentIndexCache();
  return !outIndex.empty();
}

std::optional<EquipmentConfig> loadEquipmentConfig(int equipmentId) {
  auto cacheIt = equipmentConfigCache().find(equipmentId);
  if (cacheIt != equipmentConfigCache().end()) {
    return cacheIt->second;
  }

  auto indexIt = equipmentIndexCache().find(equipmentId);
  if (indexIt == equipmentIndexCache().end()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterDataLoader: equipment %d missing from equipment.lst",
                equipmentId);
    return std::nullopt;
  }

  ScriptTokenStream stream(indexIt->second);
  if (!stream.isValid()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterDataLoader: unable to load equipment %d from %s",
                equipmentId, indexIt->second.c_str());
    return std::nullopt;
  }

  EquipmentConfig config;
  config.id = equipmentId;
  config.path = indexIt->second;

  while (!stream.isEnd()) {
    std::string segment = stream.get();
    if (segment == kTagName) {
      config.name = stream.get();
      continue;
    }

    if (segment == kTagUsableJob) {
      while (!stream.isEnd()) {
        std::string jobName = stream.get();
        if (jobName == kTagUsableJobEnd) {
          break;
        }
        int jobIndex = jobNameToIndex(jobName);
        if (jobIndex >= 0) {
          config.usableJobs.push_back(jobIndex);
        }
      }
      continue;
    }

    if (segment != kTagAnimationJob) {
      if (segment == kTagVariation) {
        int param1 = toInt(stream.get(), 0);
        int param2 = toInt(stream.get(), 0);
        appendVariationForJobs(config, config.usableJobs, param1, param2, stream);
      }
      continue;
    }

    std::string jobName = stream.get();
    int jobIndex = jobNameToIndex(jobName);
    if (jobIndex < 0) {
      continue;
    }

    if (stream.isEnd()) {
      break;
    }

    std::string variationTag = stream.get();
    if (variationTag != kTagVariation) {
      continue;
    }

    int param1 = toInt(stream.get(), 0);
    int param2 = toInt(stream.get(), 0);
    appendVariationForJobs(config, {jobIndex}, param1, param2, stream);
  }

  equipmentConfigCache()[equipmentId] = config;
  return config;
}

} // namespace frostbite2D::character
