#include "document.hpp"

#include <array>
#include <format>

namespace game::developer
{
  namespace
  {
    using Schema = resource::xml::Schema;
    using Element = Schema::Element;

    static constexpr std::array elementTypeNames{
#define GAME_DEVELOPER_ELEMENT_TYPE_NAME(NAME, ENUM) std::pair{Element::ENUM, std::string_view{#NAME}},
        GAME_SCHEMA_ELEMENT_TYPES(GAME_DEVELOPER_ELEMENT_TYPE_NAME)
#undef GAME_DEVELOPER_ELEMENT_TYPE_NAME
    };

    std::string type_name_get(Element::Type type)
    {
      for (auto [mappedType, name] : elementTypeNames)
        if (mappedType == type) return std::string{name};
      return "Element";
    }

    std::string element_name_get(const Schema& schema, int index)
    {
      if (index < 0 || index >= (int)schema.elements.size()) return "element";
      auto& element = schema.elements[index];
      auto typeName = type_name_get(element.type);
      if (!element.idString.empty()) return std::format("{} '{}'", typeName, element.idString);
      if (!element.name.empty()) return std::format("{} '{}'", typeName, element.name);
      return typeName;
    }

    void snapshot_push(std::vector<Document::Snapshot>& snapshots, Document::Snapshot snapshot)
    {
      static constexpr auto MAX_SNAPSHOTS = 100;
      snapshots.emplace_back(std::move(snapshot));
      if ((int)snapshots.size() > MAX_SNAPSHOTS) snapshots.erase(snapshots.begin());
    }
  }

  Document::SchemaFile* Document::schema_file_get(std::string_view filename)
  {
    for (auto& schema : schemas)
      if (schema.filename == filename) return &schema;
    return nullptr;
  }

  const Document::SchemaFile* Document::schema_file_get(std::string_view filename) const
  {
    for (auto& schema : schemas)
      if (schema.filename == filename) return &schema;
    return nullptr;
  }

  Document::SchemaFile* Document::dialogue_schema_get() { return schema_file_get("dialogue.xml"); }

  const Document::SchemaFile* Document::dialogue_schema_get() const { return schema_file_get("dialogue.xml"); }

  bool Document::can_undo() const { return !undo.empty(); }
  bool Document::can_redo() const { return !redo.empty(); }

  void Document::edit_snapshot_begin()
  {
    auto* dialogueSchema = dialogue_schema_get();
    if (!pendingEditSnapshot && dialogueSchema) pendingEditSnapshot = dialogueSchema->schema;
  }

  resource::xml::Schema Document::edit_snapshot_take_or(resource::xml::Schema fallback)
  {
    if (!pendingEditSnapshot) return fallback;
    auto snapshot = std::move(*pendingEditSnapshot);
    pendingEditSnapshot.reset();
    return snapshot;
  }

  void Document::edit_snapshot_clear() { pendingEditSnapshot.reset(); }

  void Document::snapshot_push(resource::xml::Schema snapshot, std::string message)
  {
    developer::snapshot_push(undo, {.schema = std::move(snapshot), .message = std::move(message)});
    redo.clear();
    isDirty = true;
  }

  std::string Document::undo_apply()
  {
    if (undo.empty()) return {};
    auto* dialogueSchema = dialogue_schema_get();
    if (!dialogueSchema) return {};

    auto snapshot = std::move(undo.back());
    undo.pop_back();
    developer::snapshot_push(redo, {.schema = dialogueSchema->schema, .message = snapshot.message});
    dialogueSchema->schema = std::move(snapshot.schema);
    isDirty = true;
    return snapshot.message;
  }

  std::string Document::redo_apply()
  {
    if (redo.empty()) return {};
    auto* dialogueSchema = dialogue_schema_get();
    if (!dialogueSchema) return {};

    auto snapshot = std::move(redo.back());
    redo.pop_back();
    developer::snapshot_push(undo, {.schema = dialogueSchema->schema, .message = snapshot.message});
    dialogueSchema->schema = std::move(snapshot.schema);
    isDirty = true;
    return snapshot.message;
  }

  void Document::history_clear()
  {
    undo.clear();
    redo.clear();
  }

  std::string Document::change_message_get(const Schema& before, const Schema& after)
  {
    if (after.elements.size() > before.elements.size())
    {
      for (int i = 0; i < (int)after.elements.size(); ++i)
      {
        auto serialized = after.element_serialize(i);
        auto existed = false;
        for (int beforeIndex = 0; beforeIndex < (int)before.elements.size(); ++beforeIndex)
        {
          if (before.element_serialize(beforeIndex) != serialized) continue;
          existed = true;
          break;
        }
        if (!existed) return std::format("Add {}", element_name_get(after, i));
      }
      return "Add element";
    }

    if (after.elements.size() < before.elements.size())
    {
      for (int i = 0; i < (int)before.elements.size(); ++i)
      {
        auto serialized = before.element_serialize(i);
        auto exists = false;
        for (int afterIndex = 0; afterIndex < (int)after.elements.size(); ++afterIndex)
        {
          if (after.element_serialize(afterIndex) != serialized) continue;
          exists = true;
          break;
        }
        if (!exists) return std::format("Delete {}", element_name_get(before, i));
      }
      return "Delete element";
    }

    for (int i = 0; i < (int)before.elements.size(); ++i)
    {
      if (before.element_serialize(i) == after.element_serialize(i)) continue;
      if (before.elements[i].type != after.elements[i].type)
        return std::format("Change {} to {}", element_name_get(before, i), element_name_get(after, i));
      return std::format("Change {}", element_name_get(after, i));
    }

    return "Change schema";
  }
}
