#include "map/GameDataLoader.h"
#include <SDL2/SDL.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/script_parser.h>
#include <algorithm>
#include <cctype>
#include <optional>

namespace frostbite2D::game {

namespace {

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;
}

std::string trimLayer(const std::string& str) {
  if (str.size() >= 2 && str.front() == '[' && str.back() == ']') {
    return toLowerCase(str.substr(1, str.size() - 2));
  }
  return toLowerCase(str);
}

std::string normalizeWithPrefix(const std::string& prefix, const std::string& path) {
  auto& pvf = PvfArchive::get();
  std::string normalizedPath = pvf.normalizePath(path);
  if (normalizedPath.empty()) {
    return normalizedPath;
  }
  if (normalizedPath.rfind(prefix + "/", 0) == 0) {
    return normalizedPath;
  }
  return pvf.normalizePath(prefix + "/" + normalizedPath);
}

std::string resolveMapPath(const std::string& baseDir, const std::string& leaf) {
  return PvfArchive::get().resolvePath(baseDir, leaf);
}

std::string resolveSpritePath(const std::string& baseDir, const std::string& leaf) {
  return normalizeWithPrefix("sprite", PvfArchive::get().resolvePath(baseDir, leaf));
}

class ScriptTokenStream {
public:
  explicit ScriptTokenStream(const std::string& path)
      : path_(toLowerCase(path)) {
    auto& pvf = PvfArchive::get();
    auto rawData = pvf.getFileRawData(path_);
    if (!rawData) {
      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameDataLoader: missing script %s",
                   path_.c_str());
      return;
    }

    ScriptParser parser(*rawData, path_);
    if (!parser.isValid()) {
      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                   "GameDataLoader: invalid script payload %s", path_.c_str());
      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(); }
  const std::string& path() const { return path_; }

  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 toInt(const std::string& value, int fallback = 0) {
  try {
    return std::stoi(value);
  } catch (...) {
    return fallback;
  }
}

MoveAreaBounds normalizeMoveAreaBounds(int x1, int y1, int x2, int y2) {
  MoveAreaBounds bounds;
  bounds.leftTop.x = static_cast<float>(std::min(x1, x2));
  bounds.leftTop.y = static_cast<float>(std::min(y1, y2));
  bounds.rightBottom.x = static_cast<float>(std::max(x1, x2));
  bounds.rightBottom.y = static_cast<float>(std::max(y1, y2));
  return bounds;
}

Rect makeMoveAreaRect(const MoveAreaBounds& bounds) {
  return Rect(bounds.leftTop.x, bounds.leftTop.y,
              bounds.rightBottom.x - bounds.leftTop.x,
              bounds.rightBottom.y - bounds.leftTop.y);
}

bool readScript(const std::string& path, ScriptTokenStream& outStream) {
  (void)path;
  return outStream.isValid();
}

} // namespace

std::map<int, std::string> loadTownList() {
  std::map<int, std::string> townList;
  ScriptTokenStream stream("town/town.lst");
  if (!stream.isValid()) {
    return townList;
  }

  while (!stream.isEnd()) {
    std::string index = stream.get();
    if (index.empty()) {
      break;
    }
    std::string townPath = stream.get();
    if (townPath.empty()) {
      break;
    }
    townList[toInt(index)] = normalizeWithPrefix("town", townPath);
  }

  return townList;
}

bool loadTownConfig(const std::string& townPath, TownConfig& outConfig) {
  ScriptTokenStream stream(townPath);
  if (!stream.isValid()) {
    return false;
  }

  outConfig = TownConfig();
  while (!stream.isEnd()) {
    std::string segment = stream.get();
    if (segment == "[entering title]") {
      outConfig.enteringTitle = "sprite/" + toLowerCase(stream.get());
    } else if (segment == "[cutscene image]") {
      outConfig.enteringCutscene = "sprite/" + toLowerCase(stream.get());
    } else if (segment == "[area]") {
      int index = toInt(stream.get(), -1);
      std::string mapPath = normalizeWithPrefix("map", stream.get());
      std::string type = trimLayer(stream.get());
      AreaConfig areaConfig;
      areaConfig.mapPath = mapPath;
      areaConfig.mapType = type;
      if (type == "gate") {
        areaConfig.generatePos.x = static_cast<float>(toInt(stream.get(), -1));
        areaConfig.generatePos.y = static_cast<float>(toInt(stream.get(), -1));
      }
      outConfig.areas[index] = areaConfig;
    } else if (segment == "[name]") {
      outConfig.townName = stream.get();
    } else if (segment == "[need level]") {
      outConfig.needLevel = toInt(stream.get(), -1);
    } else if (segment == "[need quest id]") {
      outConfig.needQuestId = toInt(stream.get(), -1);
    }
  }

  return !outConfig.areas.empty();
}

bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) {
  ScriptTokenStream stream(mapPath);
  if (!stream.isValid()) {
    return false;
  }

  outConfig = MapConfig();
  outConfig.mapPath = toLowerCase(mapPath);
  size_t slashPos = outConfig.mapPath.find_last_of('/');
  outConfig.mapDir = slashPos == std::string::npos ? "" : outConfig.mapPath.substr(0, slashPos + 1);

  while (!stream.isEnd()) {
    std::string segment = stream.get();
    if (segment == "[background pos]") {
      outConfig.backgroundPos = toInt(stream.get());
    } else if (segment == "[tile pos]") {
      outConfig.tilePos.x = static_cast<float>(toInt(stream.get(), 0));
      outConfig.tilePos.y = static_cast<float>(toInt(stream.get(), 0));
    } else if (segment == "[map name]") {
      outConfig.name = stream.get();
    } else if (segment == "[limit map camera move]") {
      outConfig.hasCameraLimit = true;
      outConfig.cameraLimitMinX = toInt(stream.get(), -1);
      outConfig.cameraLimitMaxX = toInt(stream.get(), -1);
      outConfig.cameraLimitMinY = toInt(stream.get(), -1);
      outConfig.cameraLimitMaxY = toInt(stream.get(), -1);
    } else if (segment == "[far sight scroll]") {
      outConfig.farSightScroll = toInt(stream.get(), 100);
    } else if (segment == "[tile]") {
      while (!stream.isEnd()) {
        std::string token = stream.get();
        if (token == "[/tile]") {
          break;
        }
        outConfig.tilePaths.push_back(resolveMapPath(outConfig.mapDir, token));
      }
    } else if (segment == "[extended tile]") {
      while (!stream.isEnd()) {
        std::string token = stream.get();
        if (token == "[/extended tile]") {
          break;
        }
        outConfig.extendedTilePaths.push_back(resolveMapPath(outConfig.mapDir, token));
      }
    } else if (segment == "[background animation]") {
      while (!stream.isEnd()) {
        std::string token = stream.get();
        if (token == "[/background animation]") {
          break;
        }
        if (token != "[ani info]") {
          continue;
        }
        BackgroundAnimationConfig ani;
        stream.get();
        ani.filename = resolveMapPath(outConfig.mapDir, stream.get());
        stream.get();
        ani.layer = trimLayer(stream.get());
        stream.get();
        ani.order = stream.get();
        outConfig.backgroundAnimations.push_back(ani);
      }
    } else if (segment == "[sound]") {
      while (!stream.isEnd()) {
        std::string token = stream.get();
        if (token == "[/sound]") {
          break;
        }
        outConfig.soundIds.push_back(token);
      }
    } else if (segment == "[animation]") {
      while (!stream.isEnd()) {
        std::string token = stream.get();
        if (token == "[/animation]") {
          break;
        }
        MapAnimationConfig ani;
        ani.filename = resolveMapPath(outConfig.mapDir, token);
        ani.layer = trimLayer(stream.get());
        ani.xPos = toInt(stream.get());
        ani.yPos = toInt(stream.get());
        ani.zPos = toInt(stream.get());
        outConfig.mapAnimations.push_back(ani);
      }
    } else if (segment == "[town movable area]") {
      while (!stream.isEnd()) {
        std::string token = stream.get();
        if (token == "[/town movable area]") {
          break;
        }
        int leftTopX = toInt(token);
        int leftTopY = toInt(stream.get());
        int rightBottomX = toInt(stream.get());
        int rightBottomY = toInt(stream.get());
        MoveAreaBounds bounds = normalizeMoveAreaBounds(
            leftTopX, leftTopY, rightBottomX, rightBottomY);
        Rect rect = makeMoveAreaRect(bounds);
        MoveAreaTarget target;
        target.town = toInt(stream.get(), -2);
        target.area = toInt(stream.get(), -2);
        outConfig.townMovableAreas.push_back(rect);
        outConfig.townMovableAreaBounds.push_back(bounds);
        outConfig.townMovableAreaTargets.push_back(target);
      }
    } else if (segment == "[virtual movable area]") {
      std::vector<int> polygonCoords;
      while (!stream.isEnd()) {
        std::string token = stream.get();
        if (token == "[/virtual movable area]") {
          break;
        }
        polygonCoords.push_back(toInt(token));
      }

      if (polygonCoords.empty()) {
        continue;
      }

      if (polygonCoords.size() % 2 != 0) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                    "GameDataLoader: map %s has odd [virtual movable area] coordinate count (%zu)",
                    outConfig.mapPath.c_str(), polygonCoords.size());
        continue;
      }

      if (polygonCoords.size() < 6) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                    "GameDataLoader: map %s [virtual movable area] needs at least 3 vertices, got %zu",
                    outConfig.mapPath.c_str(), polygonCoords.size() / 2);
        continue;
      }

      outConfig.virtualMovablePolygon.clear();
      outConfig.virtualMovablePolygon.reserve(polygonCoords.size() / 2);
      for (size_t i = 0; i + 1 < polygonCoords.size(); i += 2) {
        outConfig.virtualMovablePolygon.emplace_back(
            static_cast<float>(polygonCoords[i]),
            static_cast<float>(polygonCoords[i + 1]));
      }
    }
  }

  if (!outConfig.hasCameraLimit) {
    outConfig.hasCameraLimit = true;
    outConfig.cameraLimitMinX = 0;
    outConfig.cameraLimitMaxX = 0;
    outConfig.cameraLimitMinY = 0;
    outConfig.cameraLimitMaxY = 0;
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "GameDataLoader: map %s missing [limit map camera move], fallback to 0 0 0 0",
                outConfig.mapPath.c_str());
  }

  return true;
}

bool loadTileInfo(const std::string& path, TileInfo& outInfo) {
  ScriptTokenStream stream(path);
  if (!stream.isValid()) {
    return false;
  }

  outInfo = TileInfo();
  while (!stream.isEnd()) {
    std::string segment = toLowerCase(stream.get());
    if (segment == "[image]") {
      std::string rawImagePath = stream.get();
      outInfo.spritePath = normalizeWithPrefix("sprite", rawImagePath);
      outInfo.spriteIndex = toInt(stream.get());
      if (outInfo.spritePath.empty()) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                    "GameDataLoader: tile image path resolved empty for %s, raw=%s",
                    path.c_str(), rawImagePath.c_str());
      }
    }
  }

  return true;
}

} // namespace frostbite2D::game
