#include "schema.hpp"

#include "../log.hpp"
#include "../util/string.hpp"
#include "../util/vector.hpp"

#include <algorithm>
#include <filesystem>
#include <format>
#include <string_view>
#include <tinyxml2/tinyxml2.h>

using namespace tinyxml2;

namespace game::resource::xml
{
  std::string* Schema::AnimationEntryCollection::get()
  {
    if (empty()) return nullptr;
    return &at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).animation;
  }

  const Audio* Schema::SoundEntryCollection::get() const
  {
    if (empty()) return nullptr;
    auto index =
        util::vector::random_index_weighted(*this, [](const auto* entry) { return entry ? entry->weight : 0.0f; });
    return index < 0 || !at(index) ? nullptr : &at(index)->sound;
  }

  void Schema::SoundEntryCollection::play() const
  {
    if (empty()) return;
    auto audio = get();
    if (!audio) return;

    for (auto* entry : *this)
      if (entry && &entry->sound != audio) entry->sound.stop();

    audio->play();
  }

  namespace
  {
    XMLError query_result_merge(XMLError result, XMLError next) { return result == XML_SUCCESS ? next : result; }

    XMLError document_load(const util::physfs::Path& path, XMLDocument& document)
    {
      if (!path.is_valid())
      {
        logger.error(std::format("Failed to open XML document: {} ({})", path.c_str(), util::physfs::error_get()));
        return XML_ERROR_FILE_NOT_FOUND;
      }

      auto buffer = path.read();

      if (buffer.empty())
      {
        logger.error(std::format("Failed to read XML document: {} ({})", path.c_str(), util::physfs::error_get()));
        return XML_ERROR_FILE_COULD_NOT_BE_OPENED;
      }

      auto result = document.Parse((const char*)buffer.data(), buffer.size());
      if (result != XML_SUCCESS)
        logger.error(std::format("Failed to parse XML document: {} ({})", path.c_str(), document.ErrorStr()));

      return result;
    }

    XMLError query_color_attribute(XMLElement* element, const char* attribute, float* value)
    {
      int temp{};
      auto result = element->QueryIntAttribute(attribute, &temp);
      if (result == XML_SUCCESS && value) *value = (temp / 255.0f);
      return result;
    }

    XMLError query_ivec2(XMLElement* element, const char* attributeX, const char* attributeY, glm::ivec2& value)
    {
      auto result = element->QueryIntAttribute(attributeX, &value.x);
      result = query_result_merge(result, element->QueryIntAttribute(attributeY, &value.y));
      return result;
    }

    XMLError query_vec2(XMLElement* element, const char* attributeX, const char* attributeY, glm::vec2& value)
    {
      auto result = element->QueryFloatAttribute(attributeX, &value.x);
      result = query_result_merge(result, element->QueryFloatAttribute(attributeY, &value.y));
      return result;
    }

    XMLError query_vec3(XMLElement* element, const char* attributeX, const char* attributeY, const char* attributeZ,
                        glm::vec3& value)
    {
      auto result = element->QueryFloatAttribute(attributeX, &value.x);
      result = query_result_merge(result, element->QueryFloatAttribute(attributeY, &value.y));
      result = query_result_merge(result, element->QueryFloatAttribute(attributeZ, &value.z));
      return result;
    }

    XMLError set_ivec2_attribute(XMLElement* element, const char* attributeX, const char* attributeY,
                                 const glm::ivec2& value)
    {
      element->SetAttribute(attributeX, value.x);
      element->SetAttribute(attributeY, value.y);
      return XML_SUCCESS;
    }

    XMLError set_vec2_attribute(XMLElement* element, const char* attributeX, const char* attributeY,
                                const glm::vec2& value)
    {
      element->SetAttribute(attributeX, value.x);
      element->SetAttribute(attributeY, value.y);
      return XML_SUCCESS;
    }

    XMLError set_vec3_attribute(XMLElement* element, const char* attributeX, const char* attributeY,
                                const char* attributeZ, const glm::vec3& value)
    {
      element->SetAttribute(attributeX, value.x);
      element->SetAttribute(attributeY, value.y);
      element->SetAttribute(attributeZ, value.z);
      return XML_SUCCESS;
    }

    struct TypeName
    {
      std::string_view name{};
      Schema::Element::Type type{};
    };

    constexpr auto UPDATES_PER_SECOND = 60.0f;

#define GAME_SCHEMA_DIALOGUE_EFFECT_TYPES(X)                                                                          \
  X(Animation)                                                                                                         \
  X(Big)                                                                                                               \
  X(Color)                                                                                                             \
  X(Delay)                                                                                                             \
  X(Oscillation)                                                                                                       \
  X(Screenshake)                                                                                                       \
  X(Shake)                                                                                                             \
  X(Small)                                                                                                             \
  X(Sound)

    constexpr TypeName TYPE_MAP[]{
#define GAME_SCHEMA_ELEMENT_TYPE_NAME(NAME, ENUM) {#NAME, Schema::Element::ENUM},
        GAME_SCHEMA_ELEMENT_TYPES(GAME_SCHEMA_ELEMENT_TYPE_NAME)
#undef GAME_SCHEMA_ELEMENT_TYPE_NAME
    };

    Schema::Element::Type type_get(const char* name)
    {
      if (!name) return Schema::Element::UNKNOWN;
      auto view = std::string_view{name};
      for (auto [mappedName, mappedType] : TYPE_MAP)
        if (mappedName == view) return mappedType;
      return Schema::Element::UNKNOWN;
    }

    bool dialogue_effect_name_is(std::string_view name)
    {
#define GAME_SCHEMA_DIALOGUE_EFFECT_NAME_MATCH(NAME)                                                                   \
  if (name == #NAME) return true;
      GAME_SCHEMA_DIALOGUE_EFFECT_TYPES(GAME_SCHEMA_DIALOGUE_EFFECT_NAME_MATCH)
#undef GAME_SCHEMA_DIALOGUE_EFFECT_NAME_MATCH
      return false;
    }
#undef GAME_SCHEMA_DIALOGUE_EFFECT_TYPES

    const char* attribute_symbol_get(std::string_view name)
    {
#define GAME_SCHEMA_ATTRIBUTE_SYMBOL_MATCH(KIND, KEY, KEY_2, KEY_3, MEMBER)                                            \
  if (name == KEY || (std::string_view{KEY_2}.size() > 0 && name == KEY_2) ||                                          \
      (std::string_view{KEY_3}.size() > 0 && name == KEY_3))                                                           \
    return #MEMBER;
      GAME_SCHEMA_ELEMENT_ATTRIBUTES(GAME_SCHEMA_ATTRIBUTE_SYMBOL_MATCH)
#undef GAME_SCHEMA_ATTRIBUTE_SYMBOL_MATCH
      return nullptr;
    }

    void attribute_order_read(XMLElement* xml, Schema::Element& element)
    {
      element.attributeOrder.clear();
      for (auto* attribute = xml->FirstAttribute(); attribute; attribute = attribute->Next())
      {
        auto* symbol = attribute_symbol_get(attribute->Name());
        if (!symbol) continue;
        if (std::ranges::find(element.attributeOrder, symbol) == element.attributeOrder.end())
          element.attributeOrder.emplace_back(symbol);
      }
    }

    void query_string(XMLElement* xml, const char* attribute, std::string& value)
    {
      const char* result = nullptr;
      if (xml->QueryStringAttribute(attribute, &result) == XML_SUCCESS && result) value = result;
    }

    void query_bool(XMLElement* xml, const char* attribute, bool& value)
    {
      if (!xml->FindAttribute(attribute)) return;
      std::string text{};
      query_string(xml, attribute, text);
      text = util::string::to_lower(text);
      value = text == "true" || text == "1";
    }

    std::string path_join(std::string base, const std::string& child)
    {
      if (base.empty()) return child;
      if (child.empty()) return base;
      if (base.back() != '/') base.push_back('/');
      base += child;
      return base;
    }

    std::string path_join(std::string archive, const std::string& rootPath, const std::string& child)
    {
      if (!rootPath.empty()) archive = path_join(std::move(archive), rootPath);
      return path_join(std::move(archive), child);
    }

    struct Context
    {
      std::string archive{};
      std::filesystem::path filesystemBase{};
      std::string anm2RootPath{};
      std::string textureRootPath{};
      std::string soundRootPath{};
      std::string fontRootPath{};
      Schema::Anm2Entry baseAnm2{};
      int itemSpritesheetID{};
      bool isDialogue{};
    };

    std::string resource_path_get(const Context& context, const std::string& rootPath, const std::string& path)
    {
      auto value = std::filesystem::path(path);
      if (value.is_absolute()) return value.string();

      auto root = std::filesystem::path(rootPath);
      auto pathIt = value.begin();
      auto rootIt = root.begin();
      while (rootIt != root.end() && pathIt != value.end() && *rootIt == *pathIt)
      {
        ++rootIt;
        ++pathIt;
      }
      auto alreadyIncludesRoot = rootIt == root.end();

      if (!context.archive.empty())
        return alreadyIncludesRoot ? path_join(context.archive, path) : path_join(context.archive, rootPath, path);
      return (context.filesystemBase / (alreadyIncludesRoot ? value : root / value)).string();
    }

    bool relative_path_is_local(const std::filesystem::path& path)
    {
      if (path.empty() || path.is_absolute()) return false;
      for (auto& part : path)
        if (part == "..") return false;
      return true;
    }

    std::filesystem::path root_prefix_strip(const std::filesystem::path& rootPath, const std::filesystem::path& path)
    {
      if (rootPath.empty()) return path;

      auto root = rootPath.lexically_normal();
      auto value = path.lexically_normal();
      auto rootIt = root.begin();
      auto valueIt = value.begin();
      while (rootIt != root.end() && valueIt != value.end() && *rootIt == *valueIt)
      {
        ++rootIt;
        ++valueIt;
      }
      if (rootIt != root.end()) return path;

      std::filesystem::path result{};
      for (; valueIt != value.end(); ++valueIt)
        result /= *valueIt;
      return result.empty() ? path : result;
    }

    std::string resource_source_path_get(const Context& context, const std::string& rootPath, const std::string& path)
    {
      if (path.empty()) return {};

      auto value = std::filesystem::path(path).lexically_normal();
      auto root = std::filesystem::path(rootPath).lexically_normal();
      if (!value.is_absolute()) return root_prefix_strip(root, value).generic_string();

      if (!context.filesystemBase.empty())
      {
        std::error_code error{};
        auto base = root.empty() ? context.filesystemBase : context.filesystemBase / root;
        auto relative = std::filesystem::relative(value, base, error);
        if (!error && relative_path_is_local(relative)) return relative.generic_string();

        error.clear();
        relative = std::filesystem::relative(value, context.filesystemBase, error);
        if (!error && relative_path_is_local(relative)) return root_prefix_strip(root, relative).generic_string();
      }

      return value.generic_string();
    }

    Schema::TextureEntry query_texture_entry(const std::string& path, const Context& context)
    {
      Schema::TextureEntry texture{};
      texture.path = resource_source_path_get(context, context.textureRootPath, path);
      auto resolvedPath = resource_path_get(context, context.textureRootPath, texture.path);
      texture.texture = context.archive.empty() ? Texture(resolvedPath) : Texture(util::physfs::Path(resolvedPath));
      return texture;
    }

    Schema::SoundEntry query_sound_entry(const std::string& path, const Context& context)
    {
      Schema::SoundEntry sound{};
      sound.path = resource_source_path_get(context, context.soundRootPath, path);
      auto resolvedPath = resource_path_get(context, context.soundRootPath, sound.path);
      sound.sound = context.archive.empty() ? Audio(resolvedPath) : Audio(util::physfs::Path(resolvedPath));
      return sound;
    }

    Schema::FontEntry query_font_entry(const std::string& path, const Context& context)
    {
      Schema::FontEntry font{};
      font.path = resource_source_path_get(context, context.fontRootPath, path);
      auto resolvedPath = resource_path_get(context, context.fontRootPath, font.path);
      font.font = context.archive.empty() ? Font(resolvedPath) : Font(util::physfs::Path(resolvedPath));
      return font;
    }

    Schema::Anm2Entry query_anm2_entry(const std::string& path, const Context& context)
    {
      Schema::Anm2Entry anm2{};
      auto rootPath = context.anm2RootPath.empty() ? context.textureRootPath : context.anm2RootPath;
      anm2.path = resource_source_path_get(context, rootPath, path);
      auto resolvedPath = resource_path_get(context, rootPath, anm2.path);
      anm2.anm2 = context.archive.empty() ? Anm2(resolvedPath) : Anm2(util::physfs::Path(resolvedPath));
      return anm2;
    }

    void parse_attributes(XMLElement* xml, Schema::Element& element, const Context& context)
    {
      Context resourceContext = context;

#define GAME_SCHEMA_QUERY_ATTRIBUTE_STRING(KEY, KEY_2, KEY_3, MEMBER) query_string(xml, KEY, element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_STRING_TARGET(KEY, KEY_2, KEY_3, MEMBER) query_string(xml, KEY, element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_INT_RW(KEY, KEY_2, KEY_3, MEMBER) xml->QueryIntAttribute(KEY, &element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_INT_RO(KEY, KEY_2, KEY_3, MEMBER) xml->QueryIntAttribute(KEY, &element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_FLOAT(KEY, KEY_2, KEY_3, MEMBER) xml->QueryFloatAttribute(KEY, &element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_FLOAT_PER_SECOND(KEY, KEY_2, KEY_3, MEMBER)                                       \
  {                                                                                                                    \
    float value{};                                                                                                     \
    if (xml->QueryFloatAttribute(KEY, &value) == XML_SUCCESS) element.MEMBER = value / UPDATES_PER_SECOND;             \
  }
#define GAME_SCHEMA_QUERY_ATTRIBUTE_VEC2(KEY, KEY_2, KEY_3, MEMBER) query_vec2(xml, KEY, KEY_2, element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_IVEC2(KEY, KEY_2, KEY_3, MEMBER) query_ivec2(xml, KEY, KEY_2, element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_VEC3(KEY, KEY_2, KEY_3, MEMBER) query_vec3(xml, KEY, KEY_2, KEY_3, element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_COLOR_RO(KEY, KEY_2, KEY_3, MEMBER)                                                \
  query_color_attribute(xml, KEY, &element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_BOOL(KEY, KEY_2, KEY_3, MEMBER) query_bool(xml, KEY, element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE_BOOL_FALSE(KEY, KEY_2, KEY_3, MEMBER) query_bool(xml, KEY, element.MEMBER);
#define GAME_SCHEMA_QUERY_ATTRIBUTE(KIND, KEY, KEY_2, KEY_3, MEMBER)                                                   \
  GAME_SCHEMA_QUERY_ATTRIBUTE_##KIND(KEY, KEY_2, KEY_3, MEMBER)
      GAME_SCHEMA_ELEMENT_ATTRIBUTES(GAME_SCHEMA_QUERY_ATTRIBUTE)
#undef GAME_SCHEMA_QUERY_ATTRIBUTE
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_BOOL_FALSE
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_BOOL
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_COLOR_RO
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_VEC3
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_IVEC2
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_VEC2
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_FLOAT_PER_SECOND
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_FLOAT
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_INT_RO
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_INT_RW
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_STRING_TARGET
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_STRING

      if (element.type == Schema::Element::POOL_ENTRY) query_string(xml, "Entry", element.idString);

      if (!element.textureRootPath.empty()) resourceContext.textureRootPath = element.textureRootPath;
      if (!element.anm2RootPath.empty())
        resourceContext.anm2RootPath = element.anm2RootPath;
      else if (resourceContext.anm2RootPath.empty())
        resourceContext.anm2RootPath = resourceContext.textureRootPath;
      if (!element.soundRootPath.empty()) resourceContext.soundRootPath = element.soundRootPath;
      if (!element.fontRootPath.empty()) resourceContext.fontRootPath = element.fontRootPath;

      if (!element.texture.empty())
        element.texture = resource_source_path_get(resourceContext, resourceContext.textureRootPath, element.texture);
      if (!element.portrait.path.empty())
        element.portrait.path =
            resource_source_path_get(resourceContext, resourceContext.textureRootPath, element.portrait.path);
      if (!element.render.path.empty())
        element.render.path =
            resource_source_path_get(resourceContext, resourceContext.textureRootPath, element.render.path);
      if (!element.sound.empty())
        element.sound = resource_source_path_get(resourceContext, resourceContext.soundRootPath, element.sound);
      if (element.type == Schema::Element::SOUND && !element.path.empty())
        element.path = resource_source_path_get(resourceContext, resourceContext.soundRootPath, element.path);
      if (!element.font.empty())
        element.font = resource_source_path_get(resourceContext, resourceContext.fontRootPath, element.font);
      if (!element.iconsFont.empty())
        element.iconsFont = resource_source_path_get(resourceContext, resourceContext.fontRootPath, element.iconsFont);
      auto anm2RootPath =
          resourceContext.anm2RootPath.empty() ? resourceContext.textureRootPath : resourceContext.anm2RootPath;
      if (!element.baseAnm2.empty())
        element.baseAnm2 = resource_source_path_get(resourceContext, anm2RootPath, element.baseAnm2);
      if (!element.anm2.empty()) element.anm2 = resource_source_path_get(resourceContext, anm2RootPath, element.anm2);

      if (!element.texture.empty()) element.textureEntry = query_texture_entry(element.texture, resourceContext);
      if (!element.portrait.path.empty())
        element.portrait = query_texture_entry(element.portrait.path, resourceContext);
      if (!element.render.path.empty()) element.render = query_texture_entry(element.render.path, resourceContext);
      if (!element.sound.empty()) element.soundEntry = query_sound_entry(element.sound, resourceContext);
      if (element.type == Schema::Element::SOUND && !element.path.empty())
        element.soundEntry = query_sound_entry(element.path, resourceContext);
      if (!element.font.empty()) element.fontEntry = query_font_entry(element.font, resourceContext);
      if (!element.iconsFont.empty()) element.iconsFontEntry = query_font_entry(element.iconsFont, resourceContext);
      if (!element.baseAnm2.empty()) element.anm2Entry = query_anm2_entry(element.baseAnm2, resourceContext);
      if (!element.anm2.empty()) element.anm2Entry = query_anm2_entry(element.anm2, resourceContext);
      if (element.type == Schema::Element::ITEM && !element.anm2Entry.is_valid() && resourceContext.baseAnm2.is_valid())
        element.anm2Entry = resourceContext.baseAnm2;
      if (element.type == Schema::Element::ITEM && element.textureEntry.is_valid() &&
          element.anm2Entry.anm2.spritesheets.contains(resourceContext.itemSpritesheetID))
        element.anm2Entry.anm2.spritesheets.at(resourceContext.itemSpritesheetID).texture =
            element.textureEntry.texture;
    }

    void parse_tree(XMLElement* xml, Schema& schema, int parent, Context context)
    {
      if (!xml) return;

      Schema::Element element{};
      auto name = std::string_view{xml->Name()};
      auto parentType = parent >= 0 ? schema.elements[parent].type : Schema::Element::UNKNOWN;
      auto isDialogueEffectParent = parentType == Schema::Element::ENTRY || parentType == Schema::Element::ENTRIES;
      element.type =
          context.isDialogue && parentType == Schema::Element::POOL && name == "Entry"
              ? Schema::Element::POOL_ENTRY
          : context.isDialogue && isDialogueEffectParent && dialogue_effect_name_is(name) ? Schema::Element::EFFECT
                                                                                         : type_get(xml->Name());
      element.parent = parent;
      attribute_order_read(xml, element);
      parse_attributes(xml, element, context);
      if (context.isDialogue && element.type == Schema::Element::EFFECT && element.typeString.empty())
        element.typeString = name;

      if (!element.textureRootPath.empty())
      {
        context.textureRootPath = element.textureRootPath;
        if (context.anm2RootPath.empty()) context.anm2RootPath = element.textureRootPath;
      }
      if (!element.anm2RootPath.empty()) context.anm2RootPath = element.anm2RootPath;
      if (!element.soundRootPath.empty()) context.soundRootPath = element.soundRootPath;
      if (!element.fontRootPath.empty()) context.fontRootPath = element.fontRootPath;
      if (!element.baseAnm2.empty() && element.anm2Entry.is_valid()) context.baseAnm2 = element.anm2Entry;
      if (element.type == Schema::Element::ITEMS) context.itemSpritesheetID = element.spritesheetID;
      if (element.type == Schema::Element::DIALOGUE) context.isDialogue = true;

      auto index = (int)schema.elements.size();
      schema.elements.emplace_back(std::move(element));
      if (parent >= 0) schema.elements[parent].children.emplace_back(index);

      auto childCount = 0;
      for (auto child = xml->FirstChildElement(); child; child = child->NextSiblingElement())
        ++childCount;
      schema.elements[index].children.reserve(childCount);

      for (auto child = xml->FirstChildElement(); child; child = child->NextSiblingElement())
        parse_tree(child, schema, index, context);
    }

    void element_attributes_write(XMLDocument& document, XMLElement* xml, const Schema::Element& element,
                                  const Context& context)
    {
      auto set_string = [&](const char* name, const std::string& value)
      {
        if (!value.empty()) xml->SetAttribute(name, value.c_str());
      };

      auto set_nonzero = [&](const char* name, auto value)
      {
        if (value != decltype(value){}) xml->SetAttribute(name, value);
      };

      auto resource_string_get = [&](std::string_view symbol, const std::string& value)
      {
        if (symbol == "texture" || symbol == "portrait.path" || symbol == "render.path")
          return resource_source_path_get(context, context.textureRootPath, value);
        if (symbol == "sound" || (symbol == "path" && element.type == Schema::Element::SOUND))
          return resource_source_path_get(context, context.soundRootPath, value);
        if (symbol == "font" || symbol == "iconsFont")
          return resource_source_path_get(context, context.fontRootPath, value);
        if (symbol == "anm2" || symbol == "baseAnm2")
        {
          auto rootPath = context.anm2RootPath.empty() ? context.textureRootPath : context.anm2RootPath;
          return resource_source_path_get(context, rootPath, value);
        }
        return value;
      };

      auto write_attribute = [&](std::string_view symbol)
      {
        auto isEffect = element.type == Schema::Element::EFFECT;
        auto isEffectOscillation = isEffect && element.typeString == "Oscillation";
        auto isEffectShake = isEffect && element.typeString == "Shake";
        auto isEffectScreenshake = isEffect && element.typeString == "Screenshake";
        if ((isEffectOscillation || isEffectShake) &&
            (symbol == "amplitude" || symbol == "frequency" || symbol == "period"))
          return true;
        if (isEffectScreenshake && (symbol == "magnitude" || symbol == "timeTicks")) return true;
        if (symbol == "end" && element.type == Schema::Element::EFFECT &&
            (element.typeString == "Animation" || element.typeString == "Delay"))
          return true;
        if (symbol == "typeString" && element.type == Schema::Element::EFFECT)
          return true;
        if (symbol == "idString" && element.type == Schema::Element::POOL_ENTRY)
        {
          set_string("Entry", element.idString);
          return true;
        }
#define GAME_SCHEMA_WRITE_ATTRIBUTE_STRING(KEY, KEY_2, KEY_3, MEMBER)                                                  \
  set_string(KEY, resource_string_get(#MEMBER, element.MEMBER));
#define GAME_SCHEMA_WRITE_ATTRIBUTE_STRING_TARGET(KEY, KEY_2, KEY_3, MEMBER)                                           \
  set_string(KEY, resource_string_get(#MEMBER, element.MEMBER));
#define GAME_SCHEMA_WRITE_ATTRIBUTE_INT_RW(KEY, KEY_2, KEY_3, MEMBER) set_nonzero(KEY, element.MEMBER);
#define GAME_SCHEMA_WRITE_ATTRIBUTE_INT_RO(KEY, KEY_2, KEY_3, MEMBER)
#define GAME_SCHEMA_WRITE_ATTRIBUTE_FLOAT(KEY, KEY_2, KEY_3, MEMBER) set_nonzero(KEY, element.MEMBER);
#define GAME_SCHEMA_WRITE_ATTRIBUTE_FLOAT_PER_SECOND(KEY, KEY_2, KEY_3, MEMBER)                                        \
  set_nonzero(KEY, element.MEMBER * UPDATES_PER_SECOND);
#define GAME_SCHEMA_WRITE_ATTRIBUTE_VEC2(KEY, KEY_2, KEY_3, MEMBER)                                                    \
  if (element.MEMBER != glm::vec2{}) set_vec2_attribute(xml, KEY, KEY_2, element.MEMBER);
#define GAME_SCHEMA_WRITE_ATTRIBUTE_IVEC2(KEY, KEY_2, KEY_3, MEMBER)                                                   \
  if (element.MEMBER != glm::ivec2{}) set_ivec2_attribute(xml, KEY, KEY_2, element.MEMBER);
#define GAME_SCHEMA_WRITE_ATTRIBUTE_VEC3(KEY, KEY_2, KEY_3, MEMBER)                                                    \
  if (element.MEMBER != glm::vec3{}) set_vec3_attribute(xml, KEY, KEY_2, KEY_3, element.MEMBER);
#define GAME_SCHEMA_WRITE_ATTRIBUTE_COLOR_RO(KEY, KEY_2, KEY_3, MEMBER)
#define GAME_SCHEMA_WRITE_ATTRIBUTE_BOOL(KEY, KEY_2, KEY_3, MEMBER)                                                    \
  if (element.MEMBER) xml->SetAttribute(KEY, "true");
#define GAME_SCHEMA_WRITE_ATTRIBUTE_BOOL_FALSE(KEY, KEY_2, KEY_3, MEMBER)                                              \
  if (!element.MEMBER) xml->SetAttribute(KEY, "false");
#define GAME_SCHEMA_WRITE_ATTRIBUTE(KIND, KEY, KEY_2, KEY_3, MEMBER)                                                   \
  if (symbol == #MEMBER)                                                                                               \
  {                                                                                                                    \
    GAME_SCHEMA_WRITE_ATTRIBUTE_##KIND(KEY, KEY_2, KEY_3, MEMBER) return true;                                         \
  }
        GAME_SCHEMA_ELEMENT_ATTRIBUTES(GAME_SCHEMA_WRITE_ATTRIBUTE)
#undef GAME_SCHEMA_WRITE_ATTRIBUTE
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_BOOL_FALSE
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_BOOL
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_COLOR_RO
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_VEC3
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_IVEC2
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_VEC2
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_FLOAT_PER_SECOND
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_FLOAT
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_INT_RO
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_INT_RW
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_STRING_TARGET
#undef GAME_SCHEMA_WRITE_ATTRIBUTE_STRING
        return false;
      };

      std::vector<std::string_view> written{};
      written.reserve(element.attributeOrder.size());
      for (auto& symbol : element.attributeOrder)
      {
        if (write_attribute(symbol)) written.emplace_back(symbol);
      }

#define GAME_SCHEMA_WRITE_ORDERED_ATTRIBUTE(KIND, KEY, KEY_2, KEY_3, MEMBER)                                           \
  if (std::ranges::find(written, std::string_view{#MEMBER}) == written.end()) write_attribute(#MEMBER);
      GAME_SCHEMA_ELEMENT_ATTRIBUTES(GAME_SCHEMA_WRITE_ORDERED_ATTRIBUTE)
#undef GAME_SCHEMA_WRITE_ORDERED_ATTRIBUTE
      (void)document;
    }

    const char* element_name_get(Schema::Element::Type type)
    {
      if (type == Schema::Element::POOL_ENTRY) return "Entry";
      for (auto [name, mappedType] : TYPE_MAP)
        if (mappedType == type) return name.data();
      return "Unknown";
    }

    XMLElement* element_write(XMLDocument& document, const Schema& schema, int index, Context context = {})
    {
      auto& source = schema.elements.at(index);
      auto xml = document.NewElement(source.type == Schema::Element::EFFECT && !source.typeString.empty()
                                         ? source.typeString.c_str()
                                         : element_name_get(source.type));

      if (!source.textureRootPath.empty())
      {
        context.textureRootPath = source.textureRootPath;
        if (context.anm2RootPath.empty()) context.anm2RootPath = source.textureRootPath;
      }
      if (!source.anm2RootPath.empty()) context.anm2RootPath = source.anm2RootPath;
      if (!source.soundRootPath.empty()) context.soundRootPath = source.soundRootPath;
      if (!source.fontRootPath.empty()) context.fontRootPath = source.fontRootPath;

      element_attributes_write(document, xml, source, context);
      for (auto child : source.children)
        xml->InsertEndChild(element_write(document, schema, child, context));
      return xml;
    }

    void animation_entry_add(Schema::Element& element, Schema::AnimationEntryCollection& collection)
    {
      if (element.animation.empty()) return;
      collection.push_back({.animation = element.animation, .weight = element.weight == 0.0f ? 1.0f : element.weight});
    }

    void sound_entry_add(Schema::Element& element, Schema::SoundEntryCollection& collection)
    {
      if (!element.soundEntry.is_valid()) return;
      element.soundEntry.weight = element.weight == 0.0f ? 1.0f : element.weight;
      collection.push_back(&element.soundEntry);
    }

    void sound_entries_add(const Schema::SoundEntryCollection& source, Schema::SoundEntryCollection& collection)
    {
      for (auto* entry : source)
        if (entry) collection.push_back(entry);
    }

    bool element_attribute_has(const Schema::Element& element, std::string_view attribute)
    {
      return std::ranges::find(element.attributeOrder, attribute) != element.attributeOrder.end();
    }

    bool element_child_has(const Schema& schema, const Schema::Element& element, Schema::Element::Type type)
    {
      for (auto child : element.children)
        if (child >= 0 && child < (int)schema.elements.size() && schema.elements[child].type == type) return true;
      return false;
    }

    void interact_area_defaults_apply(Schema& schema)
    {
      auto* root = schema.root();
      auto* areas = root ? schema.child_get(*root, Schema::Element::INTERACT_AREAS) : nullptr;
      auto* types = root ? schema.child_get(*root, Schema::Element::INTERACT_AREA_TYPES) : nullptr;
      if (!areas) return;
      if (!types) return;

      std::unordered_map<std::string, Schema::Element*> defaults{};
      for (auto* area : schema.children_get(*types, Schema::Element::INTERACT_AREA))
      {
        if (!area || area->typeString.empty()) continue;
        defaults[area->typeString] = area;
      }

      for (auto* area : schema.children_get(*areas, Schema::Element::INTERACT_AREA))
      {
        if (!area || area->typeString.empty() || area->null.empty()) continue;
        auto it = defaults.find(area->typeString);
        if (it == defaults.end()) continue;
        auto* base = it->second;

#define GAME_SCHEMA_INTERACT_AREA_DEFAULTS(X)                                                                          \
  X(animation)                                                                                                         \
  X(animationCursorActive)                                                                                             \
  X(animationCursorHover)                                                                                              \
  X(digestionBonusOnClick)                                                                                             \
  X(digestionBonusOnHover)                                                                                             \
  X(iconGlyph)                                                                                                         \
  X(isHold)                                                                                                            \
  X(layer)                                                                                                             \
  X(scaleEffectAmplitude)                                                                                              \
  X(scaleEffectCycles)                                                                                                 \
  X(timeTicks)                                                                                                         \
  X(timeToActivateNewDialogueTicks)
#define GAME_SCHEMA_INTERACT_AREA_DEFAULT_APPLY(MEMBER)                                                                \
  if (!element_attribute_has(*area, #MEMBER) && area->MEMBER == decltype(area->MEMBER){}) area->MEMBER = base->MEMBER;
        GAME_SCHEMA_INTERACT_AREA_DEFAULTS(GAME_SCHEMA_INTERACT_AREA_DEFAULT_APPLY)
#undef GAME_SCHEMA_INTERACT_AREA_DEFAULT_APPLY
#undef GAME_SCHEMA_INTERACT_AREA_DEFAULTS

        if (!element_child_has(schema, *area, Schema::Element::SOUND))
          area->soundEntries = base->soundEntries;
      }
    }

#define GAME_SCHEMA_ACTION_SOUNDS(X)                                                                                   \
  X(BOUNCE, soundBounce)                                                                                               \
  X(CHEATS_ACTIVATED, soundCheatsActivated)                                                                            \
  X(CLOSE, soundClose)                                                                                                 \
  X(COLLECT, soundCollect)                                                                                             \
  X(DIGEST, soundDigest)                                                                                               \
  X(DISPOSE, soundDispose)                                                                                             \
  X(GRAB, soundGrab)                                                                                                   \
  X(GURGLE, soundGurgle)                                                                                               \
  X(HOVER, soundHover)                                                                                                 \
  X(KILL, soundKill)                                                                                                   \
  X(LEVEL_UP, soundLevelUp)                                                                                             \
  X(LEVEL_UP_READY, soundLevelUpReady)                                                                                  \
  X(OPEN, soundOpen)                                                                                                   \
  X(RELEASE, soundRelease)                                                                                             \
  X(SELECT, soundSelect)                                                                                               \
  X(SUMMON, soundSummon)                                                                                               \
  X(THROW, soundThrow)                                                                                                 \
  X(RETURN_, soundReturn)                                                                                              \
  X(UPGRADE, soundUpgrade)                                                                                             \
  X(UPGRADE_FAIL, soundUpgradeFail)

    void cache_refresh(Schema& schema)
    {
      schema.areas.clear();
      schema.categories.clear();
      schema.rarities.clear();
      schema.flavors.clear();
      schema.items.clear();
      schema.anm2s.clear();
      schema.rarityIDs.clear();
      schema.pools.clear();
      schema.stringToIDMap.clear();
      schema.idToStringMap.clear();
      schema.elementsByType.clear();
      schema.baseAnm2 = {};
      schema.font = {};
      schema.iconsFont = {};
      schema.durability = 2;
      schema.quantityMax = 99;
      schema.rounding = 0.0f;

      auto* root = schema.root();
      if (!root) return;

      schema.elementsByType.reserve(schema.elements.size());
      for (int i = 0; i < (int)schema.elements.size(); i++)
        schema.elementsByType[schema.elements[i].type].push_back(i);

      auto type_count = [&](Schema::Element::Type type)
      {
        auto it = schema.elementsByType.find(type);
        return it == schema.elementsByType.end() ? 0 : (int)it->second.size();
      };

      schema.areas.reserve(type_count(Schema::Element::AREA));
      schema.categories.reserve(type_count(Schema::Element::CATEGORY));
      schema.rarities.reserve(type_count(Schema::Element::RARITY));
      schema.flavors.reserve(type_count(Schema::Element::FLAVOR));
      schema.items.reserve(type_count(Schema::Element::ITEM));
      schema.anm2s.reserve(type_count(Schema::Element::ITEM));
      schema.rarityIDs.reserve(type_count(Schema::Element::RARITY));

      root->animationGrab = {};
      root->animationHover = {};
      root->animationIdle = {};
      root->animationClick = {};
      root->animationPan = {};
      root->animationReturn = {};
      root->animationZoom = {};
      root->soundBlip = {};
      root->soundBounce = {};
      root->soundCheatsActivated = {};
      root->soundClose = {};
      root->soundCollect = {};
      root->soundDigest = {};
      root->soundDispose = {};
      root->soundGrab = {};
      root->soundGurgle = {};
      root->soundHover = {};
      root->soundKill = {};
      root->soundLevelUp = {};
      root->soundLevelUpReady = {};
      root->soundOpen = {};
      root->soundRelease = {};
      root->soundReturn = {};
      root->soundSelect = {};
      root->soundSummon = {};
      root->soundThrow = {};
      root->soundUpgrade = {};
      root->soundUpgradeFail = {};
      root->animationChew = {};

      for (auto& element : schema.elements)
      {
        element.soundEntries = {};
        for (auto childID : element.children)
        {
          auto& child = schema.elements.at(childID);
          if (child.type == Schema::Element::SOUND) sound_entry_add(child, element.soundEntries);
        }
      }
      interact_area_defaults_apply(schema);

      schema.baseAnm2 = root->anm2Entry.anm2;
      schema.font = root->fontEntry.font;
      schema.iconsFont = root->iconsFontEntry.font;
      schema.rounding = root->rounding;

      if (auto* animations = schema.child_get(*root, Schema::Element::ANIMATIONS))
      {
        if (auto* element = schema.child_get(*animations, Schema::Element::IDLE))
          animation_entry_add(*element, root->animationIdle);
        if (auto* element = schema.child_get(*animations, Schema::Element::CLICK))
          animation_entry_add(*element, root->animationClick);
        if (auto* element = schema.child_get(*animations, Schema::Element::HOVER))
          animation_entry_add(*element, root->animationHover);
        if (auto* element = schema.child_get(*animations, Schema::Element::GRAB))
          animation_entry_add(*element, root->animationGrab);
        if (auto* element = schema.child_get(*animations, Schema::Element::PAN))
          animation_entry_add(*element, root->animationPan);
        if (auto* element = schema.child_get(*animations, Schema::Element::ZOOM))
          animation_entry_add(*element, root->animationZoom);
        if (auto* element = schema.child_get(*animations, Schema::Element::RETURN_))
          animation_entry_add(*element, root->animationReturn);
        if (auto* element = schema.child_get(*animations, Schema::Element::CHEW))
          root->animationChew = element->animation;
      }

      if (auto* sounds = schema.child_get(*root, Schema::Element::SOUNDS))
      {
        for (auto* element : schema.children_get(*sounds, Schema::Element::BLIP))
          sound_entry_add(*element, root->soundBlip);
        for (auto* element : schema.children_get(*sounds, Schema::Element::BOUNCE))
          sound_entry_add(*element, root->soundBounce);
        for (auto* element : schema.children_get(*sounds, Schema::Element::CHEATS_ACTIVATED))
          sound_entry_add(*element, root->soundCheatsActivated);
        for (auto* element : schema.children_get(*sounds, Schema::Element::CLOSE))
          sound_entry_add(*element, root->soundClose);
        for (auto* element : schema.children_get(*sounds, Schema::Element::COLLECT))
          sound_entry_add(*element, root->soundCollect);
        for (auto* element : schema.children_get(*sounds, Schema::Element::DIGEST))
          sound_entry_add(*element, root->soundDigest);
        for (auto* element : schema.children_get(*sounds, Schema::Element::DISPOSE))
          sound_entry_add(*element, root->soundDispose);
        for (auto* element : schema.children_get(*sounds, Schema::Element::GRAB))
          sound_entry_add(*element, root->soundGrab);
        for (auto* element : schema.children_get(*sounds, Schema::Element::GURGLE))
          sound_entry_add(*element, root->soundGurgle);
        for (auto* element : schema.children_get(*sounds, Schema::Element::HOVER))
          sound_entry_add(*element, root->soundHover);
        for (auto* element : schema.children_get(*sounds, Schema::Element::KILL))
          sound_entry_add(*element, root->soundKill);
        for (auto* element : schema.children_get(*sounds, Schema::Element::LEVEL_UP))
          sound_entry_add(*element, root->soundLevelUp);
        for (auto* element : schema.children_get(*sounds, Schema::Element::LEVEL_UP_READY))
          sound_entry_add(*element, root->soundLevelUpReady);
        for (auto* element : schema.children_get(*sounds, Schema::Element::OPEN))
          sound_entry_add(*element, root->soundOpen);
        for (auto* element : schema.children_get(*sounds, Schema::Element::RELEASE))
          sound_entry_add(*element, root->soundRelease);
        for (auto* element : schema.children_get(*sounds, Schema::Element::RETURN_))
          sound_entry_add(*element, root->soundReturn);
        for (auto* element : schema.children_get(*sounds, Schema::Element::SELECT))
          sound_entry_add(*element, root->soundSelect);
        for (auto* element : schema.children_get(*sounds, Schema::Element::SUMMON))
          sound_entry_add(*element, root->soundSummon);
        for (auto* element : schema.children_get(*sounds, Schema::Element::THROW))
          sound_entry_add(*element, root->soundThrow);
        for (auto* element : schema.children_get(*sounds, Schema::Element::UPGRADE))
          sound_entry_add(*element, root->soundUpgrade);
        for (auto* element : schema.children_get(*sounds, Schema::Element::UPGRADE_FAIL))
          sound_entry_add(*element, root->soundUpgradeFail);
      }
      if (auto* actions = schema.child_get(*root, Schema::Element::ACTIONS))
      {
#define GAME_SCHEMA_ACTION_SOUND_CACHE(TYPE, MEMBER)                                                                   \
  for (auto* action : schema.children_get(*actions, Schema::Element::TYPE))                                            \
    for (auto* element : schema.children_get(*action, Schema::Element::SOUND))                                         \
      sound_entry_add(*element, root->MEMBER);
        GAME_SCHEMA_ACTION_SOUNDS(GAME_SCHEMA_ACTION_SOUND_CACHE)
#undef GAME_SCHEMA_ACTION_SOUND_CACHE
      }
#undef GAME_SCHEMA_ACTION_SOUNDS
      if (auto* textPlayback = schema.child_get(*root, Schema::Element::TEXT_PLAYBACK))
        if (auto* blip = schema.child_get(*textPlayback, Schema::Element::BLIP))
          sound_entries_add(blip->soundEntries, root->soundBlip);
      for (auto* element : schema.children_get(*root, Schema::Element::BLIP))
        sound_entry_add(*element, root->soundBlip);

      for (auto* element : schema.get_all(Schema::Element::AREA))
        schema.areas.push_back({.texture = element->textureEntry.texture,
                                .anm2 = element->anm2Entry.anm2,
                                .gravity = element->gravity == 0.0f ? 0.95f : element->gravity,
                                .friction = element->friction == 0.0f ? 0.80f : element->friction,
                                .airResistance = element->airResistance == 0.0f ? 0.975f : element->airResistance});
      if (root->type == Schema::Element::AREAS && schema.areas.empty()) schema.areas.emplace_back();

      for (auto* element : schema.get_all(Schema::Element::CATEGORY))
        schema.categories.push_back({.name = element->name, .isEdible = element->isEdible});

      for (auto* element : schema.get_all(Schema::Element::RARITY))
      {
        schema.rarityIDs.push_back((int)schema.rarities.size());
        schema.rarities.push_back({.name = element->name.empty() ? "???" : element->name,
                                   .weight = element->weight,
                                   .isHidden = element->isHidden,
                                   .sound = element->soundEntry.sound});
      }

      for (auto* element : schema.get_all(Schema::Element::FLAVOR))
        schema.flavors.push_back({.name = element->name.empty() ? "???" : element->name});

      if (auto* itemsRoot = schema.get(Schema::Element::ITEMS))
      {
        if (itemsRoot->durability != 0) schema.durability = itemsRoot->durability;
        if (itemsRoot->quantityMax != 0) schema.quantityMax = itemsRoot->quantityMax;
      }

      auto itemElements = schema.get_all(Schema::Element::ITEM);
      for (auto* element : itemElements)
      {
        if (element->parent < 0 || schema.elements[element->parent].type != Schema::Element::ITEMS) continue;
        auto itemID = (int)schema.stringToIDMap.size();
        schema.stringToIDMap[element->name] = itemID;
        schema.idToStringMap[itemID] = element->name;
      }

      for (auto* element : itemElements)
      {
        if (element->parent < 0 || schema.elements[element->parent].type != Schema::Element::ITEMS) continue;
        Schema::ItemEntry item{};
        item.name = element->name.empty() ? "???" : element->name;
        item.description = element->description.empty() ? "???" : element->description;
        item.categoryID = schema.categoryMap.contains(element->category) ? schema.categoryMap[element->category] : -1;
        item.rarityID = schema.rarityMap.contains(element->rarity) ? schema.rarityMap[element->rarity] : -1;
        item.isFlavor = schema.flavorMap.contains(element->flavor);
        if (item.isFlavor) item.flavorID = schema.flavorMap[element->flavor];
        item.isCalories = element->calories != 0.0f;
        item.calories = element->calories;
        item.isCapacityBonus = element->capacityBonus != 0.0f;
        item.capacityBonus = element->capacityBonus;
        item.isDigestionBonus = element->digestionBonus != 0.0f;
        item.digestionBonus = element->digestionBonus;
        item.isEatSpeedBonus = element->eatSpeedBonus != 0.0f;
        item.eatSpeedBonus = element->eatSpeedBonus;
        item.isGravity = element->gravity != 0.0f;
        item.gravity = element->gravity;
        item.isDurability = element->durability != 0;
        item.durability = element->durability;
        item.isColor = element->color != glm::vec3{};
        item.color = element->color;
        if (!element->upgradeID.empty() && schema.stringToIDMap.contains(element->upgradeID))
        {
          item.isUpgradeID = true;
          item.upgradeID = schema.stringToIDMap[element->upgradeID];
        }
        item.isUpgradeCount = element->upgradeCount != 0;
        item.upgradeCount = element->upgradeCount;
        item.isToggleSpritesheet = element->isToggleSpritesheet;
        schema.pools[item.rarityID].push_back((int)schema.items.size());
        schema.anm2s.push_back(element->anm2Entry.anm2);
        schema.items.push_back(std::move(item));
      }
    }

  }

  Schema::Schema(const Schema& other) { *this = other; }
  Schema::Schema(Schema&& other) noexcept { *this = std::move(other); }
  Schema::Schema(const std::filesystem::path& path) { load(path); }
  Schema::Schema(const util::physfs::Path& path) { load(path); }

  Schema& Schema::operator=(const Schema& other)
  {
    if (this == &other) return *this;

    elements = other.elements;
    isValid = other.isValid;
    indices_refresh();
    cache_refresh(*this);
    return *this;
  }

  Schema& Schema::operator=(Schema&& other) noexcept
  {
    if (this == &other) return *this;

    elements = std::move(other.elements);
    isValid = other.isValid;
    indices_refresh();
    cache_refresh(*this);

    other.elements.clear();
    other.isValid = false;
    other.indices_refresh();
    cache_refresh(other);
    return *this;
  }

  bool Schema::Element::is_valid() const { return type != UNKNOWN; }
  bool Schema::Element::is_last() const { return next.empty() && children.empty(); }

  bool Schema::load(const std::filesystem::path& path)
  {
    elements.clear();
    isValid = false;

    XMLDocument document;
    auto pathString = path.string();
    auto result = document.LoadFile(pathString.c_str());
    if (result == XML_ERROR_FILE_NOT_FOUND || result == XML_ERROR_FILE_COULD_NOT_BE_OPENED) return false;
    if (result != XML_SUCCESS)
    {
      logger.error(std::format("Failed to load Schema XML: {} ({})", pathString, document.ErrorStr()));
      return false;
    }

    Context context{};
    context.filesystemBase = path.parent_path();
    parse_tree(document.RootElement(), *this, -1, context);
    indices_refresh();
    cache_refresh(*this);
    isValid = !elements.empty();
    return isValid;
  }

  bool Schema::load(const util::physfs::Path& path)
  {
    elements.clear();
    isValid = false;

    XMLDocument document;
    if (document_load(path, document) != XML_SUCCESS)
    {
      logger.error(std::format("Failed to load Schema XML: {}", path.c_str()));
      return false;
    }

    Context context{};
    context.archive = path.directory_get();
    parse_tree(document.RootElement(), *this, -1, context);
    indices_refresh();
    cache_refresh(*this);
    isValid = !elements.empty();
    return isValid;
  }

  bool Schema::load_archive(const std::filesystem::path& path, const std::string& innerPath)
  {
    elements.clear();
    isValid = false;

    util::physfs::Archive archive(path, path.filename().string());
    if (!archive.is_valid())
    {
      logger.error(std::format("Failed to load Schema archive: {}", path.string()));
      return false;
    }

    auto xmlPath = util::physfs::Path(std::string(archive) + "/" + innerPath);
    XMLDocument document;
    if (document_load(xmlPath, document) != XML_SUCCESS)
    {
      logger.error(std::format("Failed to load Schema archive XML: {}", xmlPath.c_str()));
      return false;
    }

    Context context{};
    context.archive = archive;
    parse_tree(document.RootElement(), *this, -1, context);
    indices_refresh();
    cache_refresh(*this);
    isValid = !elements.empty();
    return isValid;
  }

  void Schema::settings_default()
  {
    elements.clear();
    Element root{};
    root.type = Element::SETTINGS;
    root.measurementSystem = "Metric";
    root.volume = 50;
    root.isAudioMuted = false;
    root.color = {0.120f, 0.515f, 0.115f};
    root.windowSize = {1600, 900};
    root.isUseCharacterColor = true;
    elements.emplace_back(std::move(root));
    indices_refresh();
    cache_refresh(*this);
    isValid = true;
  }

  void Schema::serialize(const std::filesystem::path& path) const
  {
    auto rootElement = root();
    if (!rootElement) return;

    XMLDocument document;
    Context context{};
    context.filesystemBase = path.parent_path();
    document.InsertEndChild(element_write(document, *this, 0, context));
    auto pathString = path.string();
    if (document.SaveFile(pathString.c_str()) != XML_SUCCESS)
      logger.error(std::format("Failed to save schema: {} ({})", pathString, document.ErrorStr()));
  }

  bool Schema::is_valid() const { return isValid; }

  Schema::Element* Schema::root() { return elements.empty() ? nullptr : &elements.front(); }
  const Schema::Element* Schema::root() const { return elements.empty() ? nullptr : &elements.front(); }

  Schema::Element& Schema::element_add(Element::Type type, int parent)
  {
    Element element{};
    element.type = type;
    element.parent = parent;
    auto index = (int)elements.size();
    elements.emplace_back(std::move(element));
    elementsByType[type].push_back(index);
    if (parent >= 0) elements.at(parent).children.emplace_back(index);
    isValid = true;
    return elements.back();
  }

  bool Schema::element_erase(int index)
  {
    if (index <= 0 || index >= (int)elements.size()) return false;

    std::vector<bool> erase(elements.size());
    auto mark = [&](auto&& self, int current) -> void
    {
      if (current < 0 || current >= (int)elements.size() || erase[current]) return;
      erase[current] = true;
      for (auto child : elements[current].children)
        self(self, child);
    };
    mark(mark, index);

    std::vector<int> remap(elements.size(), -1);
    std::vector<Element> kept{};
    kept.reserve(elements.size());
    for (int i = 0; i < (int)elements.size(); ++i)
    {
      if (erase[i]) continue;
      remap[i] = (int)kept.size();
      kept.emplace_back(std::move(elements[i]));
    }

    for (auto& element : kept)
    {
      if (element.parent >= 0) element.parent = remap[element.parent];

      std::vector<int> children{};
      children.reserve(element.children.size());
      for (auto child : element.children)
        if (child >= 0 && child < (int)remap.size() && remap[child] >= 0) children.emplace_back(remap[child]);
      element.children = std::move(children);
    }

    elements = std::move(kept);
    indices_refresh();
    cache_refresh(*this);
    isValid = !elements.empty();
    return true;
  }

  int Schema::element_insert_xml(const std::string& text, int parent, int beforeSibling)
  {
    XMLDocument document;
    if (document.Parse(text.c_str()) != XML_SUCCESS || !document.RootElement()) return -1;
    if (parent < -1 || parent >= (int)elements.size()) return -1;
    if (parent < 0 && !elements.empty()) return -1;

    auto index = (int)elements.size();
    Context context{};
    parse_tree(document.RootElement(), *this, parent, context);

    if (parent >= 0 && beforeSibling >= 0)
    {
      auto& children = elements[parent].children;
      auto inserted = std::ranges::find(children, index);
      auto before = std::ranges::find(children, beforeSibling);
      if (inserted != children.end() && before != children.end() && inserted != before)
      {
        auto value = *inserted;
        children.erase(inserted);
        before = std::ranges::find(children, beforeSibling);
        children.insert(before, value);
      }
    }

    indices_refresh();
    cache_refresh(*this);
    isValid = !elements.empty();
    return index;
  }

  bool Schema::element_move(int index, int parent, int beforeSibling)
  {
    if (index <= 0 || index >= (int)elements.size()) return false;
    if (parent < 0 || parent >= (int)elements.size()) return false;
    if (index == parent || index == beforeSibling) return false;

    for (auto ancestor = parent; ancestor >= 0; ancestor = elements[ancestor].parent)
      if (ancestor == index) return false;

    auto oldParent = elements[index].parent;
    if (oldParent >= 0)
    {
      auto& oldSiblings = elements[oldParent].children;
      oldSiblings.erase(std::remove(oldSiblings.begin(), oldSiblings.end(), index), oldSiblings.end());
    }

    auto& siblings = elements[parent].children;
    auto before = beforeSibling >= 0 ? std::ranges::find(siblings, beforeSibling) : siblings.end();
    siblings.insert(before, index);
    elements[index].parent = parent;

    indices_refresh();
    cache_refresh(*this);
    return true;
  }

  std::string Schema::element_serialize(int index) const
  {
    if (index < 0 || index >= (int)elements.size()) return {};

    XMLDocument document;
    document.InsertEndChild(element_write(document, *this, index));

    XMLPrinter printer{};
    document.Print(&printer);
    return printer.CStr();
  }

  Schema::Element* Schema::get(Element::Type type, int index)
  {
    auto it = elementsByType.find(type);
    if (it == elementsByType.end() || index < 0 || index >= (int)it->second.size()) return nullptr;
    return &elements.at(it->second.at(index));
  }

  const Schema::Element* Schema::get(Element::Type type, int index) const
  {
    auto it = elementsByType.find(type);
    if (it == elementsByType.end() || index < 0 || index >= (int)it->second.size()) return nullptr;
    return &elements.at(it->second.at(index));
  }

  std::vector<Schema::Element*> Schema::get_all(Element::Type type)
  {
    std::vector<Element*> result{};
    auto it = elementsByType.find(type);
    if (it == elementsByType.end()) return result;
    result.reserve(it->second.size());
    for (auto index : it->second)
      result.emplace_back(&elements.at(index));
    return result;
  }

  std::vector<const Schema::Element*> Schema::get_all(Element::Type type) const
  {
    std::vector<const Element*> result{};
    auto it = elementsByType.find(type);
    if (it == elementsByType.end()) return result;
    result.reserve(it->second.size());
    for (auto index : it->second)
      result.emplace_back(&elements.at(index));
    return result;
  }

  Schema::Element* Schema::child_get(Element& element, Element::Type type, int index)
  {
    for (auto child : element.children)
    {
      auto& candidate = elements.at(child);
      if (candidate.type != type) continue;
      if (index-- == 0) return &candidate;
    }
    return nullptr;
  }

  const Schema::Element* Schema::child_get(const Element& element, Element::Type type, int index) const
  {
    for (auto child : element.children)
    {
      auto& candidate = elements.at(child);
      if (candidate.type != type) continue;
      if (index-- == 0) return &candidate;
    }
    return nullptr;
  }

  std::vector<Schema::Element*> Schema::children_get(Element& element, Element::Type type)
  {
    std::vector<Element*> result{};
    for (auto child : element.children)
      if (auto& candidate = elements.at(child); candidate.type == type) result.emplace_back(&candidate);
    return result;
  }

  std::vector<const Schema::Element*> Schema::children_get(const Element& element, Element::Type type) const
  {
    std::vector<const Element*> result{};
    for (auto child : element.children)
      if (auto& candidate = elements.at(child); candidate.type == type) result.emplace_back(&candidate);
    return result;
  }

  Schema::Element* Schema::dialogue_entry_get(int id)
  {
    if (id < 0 || id >= (int)elements.size()) return nullptr;
    auto idIt = entryIDMapReverse.find(id);
    return idIt == entryIDMapReverse.end() ? nullptr : dialogue_entry_get(idIt->second);
  }

  Schema::Element* Schema::dialogue_entry_get(const std::string& id)
  {
    auto it = entryIDMap.find(id);
    return it == entryIDMap.end() ? nullptr : &elements.at(it->second);
  }

  Schema::Element* Schema::dialogue_entry_get(Element& reference)
  {
    if (!reference.next.empty()) return dialogue_entry_get(reference.next);
    if (!reference.idString.empty()) return dialogue_entry_get(reference.idString);
    return nullptr;
  }

  Schema::Element* Schema::dialogue_pool_entry_get(int poolID)
  {
    if (poolID < 0) return nullptr;
    auto poolIt = std::ranges::find_if(poolMap, [&](auto& entry) { return entry.second == poolID; });
    if (poolIt == poolMap.end()) return nullptr;
    return dialogue_pool_entry_get(poolIt->first);
  }

  Schema::Element* Schema::dialogue_pool_entry_get(const std::string& poolID)
  {
    auto poolIt = poolMap.find(poolID);
    if (poolIt == poolMap.end()) return nullptr;

    auto& pool = elements.at(poolIt->second);
    auto entries = children_get(pool, Element::POOL_ENTRY);
    if (entries.empty()) return nullptr;

    auto picked = entries.at(selectionCounter++ % entries.size());
    return dialogue_entry_get(picked->idString);
  }

  Schema::Element* Schema::dialogue_pool_entry_get(Element& reference)
  {
    if (!reference.dialoguePoolID.empty()) return dialogue_pool_entry_get(reference.dialoguePoolID);
    if (reference.poolID.empty()) return nullptr;
    return dialogue_pool_entry_get(reference.poolID);
  }

  int Schema::dialogue_entry_id_get(const std::string& id) const
  {
    auto it = entryIDMap.find(id);
    return it == entryIDMap.end() ? -1 : it->second;
  }

  int Schema::dialogue_pool_id_get(const std::string& id) const
  {
    auto it = poolMap.find(id);
    return it == poolMap.end() ? -1 : it->second;
  }

  Schema::Element* Schema::item_get(int id)
  {
    auto it = itemIDMapReverse.find(id);
    return it == itemIDMapReverse.end() ? nullptr : item_get(it->second);
  }

  Schema::Element* Schema::item_get(const std::string& id)
  {
    auto it = itemIDMap.find(id);
    return it == itemIDMap.end() ? nullptr : &elements.at(it->second);
  }

  int Schema::item_id_get(const std::string& id) const
  {
    auto it = itemIDMap.find(id);
    return it == itemIDMap.end() ? -1 : it->second;
  }

  std::vector<int>* Schema::item_pool_get(int rarityID)
  {
    auto it = itemPoolsByRarity.find(rarityID);
    return it == itemPoolsByRarity.end() ? nullptr : &it->second;
  }

  void Schema::indices_refresh()
  {
    entryIDMap.clear();
    entryIDMapReverse.clear();
    poolMap.clear();
    categoryMap.clear();
    rarityMap.clear();
    flavorMap.clear();
    itemIDMap.clear();
    itemIDMapReverse.clear();
    itemPoolsByRarity.clear();
    entrySelectionOrder.clear();
    selectionCounter = 0;

    int entryID{};
    int poolID{};
    int categoryID{};
    int rarityID{};
    int flavorID{};
    int itemID{};

    for (int i = 0; i < (int)elements.size(); i++)
    {
      auto& element = elements[i];
      if (element.type == Element::ENTRY && !element.idString.empty())
      {
        entryIDMap[element.idString] = i;
        entryIDMapReverse[entryID++] = element.idString;
      }
      else if (element.type == Element::POOL && !element.idString.empty())
      {
        poolMap[element.idString] = i;
        poolID++;
      }
      else if (element.type == Element::CATEGORY)
        categoryMap[element.name] = categoryID++;
      else if (element.type == Element::RARITY)
        rarityMap[element.name] = rarityID++;
      else if (element.type == Element::FLAVOR)
        flavorMap[element.name] = flavorID++;
      else if (element.type == Element::ITEM && !element.name.empty())
      {
        itemIDMap[element.name] = i;
        itemIDMapReverse[itemID] = element.name;
        auto rarity = rarityMap.contains(element.rarity) ? rarityMap[element.rarity] : -1;
        itemPoolsByRarity[rarity].push_back(i);
        itemID++;
      }
    }

    entrySelectionOrder.assign(entryIDMap.size(), -1);
    (void)poolID;
  }
}
