#pragma once

#include <algorithm>
#include <filesystem>
#include <functional>
#include <span>
#include <string>
#include <string_view>
#include <vector>

#include <glm/glm.hpp>
#include <imgui.h>
#include <imgui_internal.h>

#include "../resource/audio.hpp"
#include "../resource/character.hpp"
#include "../resource/font.hpp"
#include "../resource/schema.hpp"
#include "../entity.hpp"
#include "../resources.hpp"
#include "../util/clipboard.hpp"
#include "../util/dialog.hpp"
#include "../util/imgui/entity_canvas.hpp"

namespace game::developer
{
  extern bool gIsDocumentUndoMenuRequested;
  extern bool gIsDocumentRedoMenuRequested;

  namespace editor_schema
  {
    using Schema = resource::xml::Schema;
    using Element = Schema::Element;

    struct ElementTypeInfo
    {
      std::string_view xmlName{};
      Element::Type type{};
    };

    constexpr ElementTypeInfo ELEMENT_TYPES[]{
#define GAME_SCHEMA_DEVELOPER_ELEMENT_TYPE(NAME, ENUM) {#NAME, Element::ENUM},
        GAME_SCHEMA_ELEMENT_TYPES(GAME_SCHEMA_DEVELOPER_ELEMENT_TYPE)
#undef GAME_SCHEMA_DEVELOPER_ELEMENT_TYPE
    };

    struct ElementActionOptions
    {
      int parent{-1};
      int target{-1};
      int after{-1};
      int pasteParent{-1};
      int pasteAfter{-2};
      Element::Type type{Element::UNKNOWN};
      bool isInsertVisible{};
      bool isInsertEnabled{};
      bool isDuplicateEnabled{true};
      bool isDeleteEnabled{true};
      bool isClearVisible{};
      bool isClearEnabled{};
      bool isPasteEnabled{true};
    };

    struct ElementActionResult
    {
      bool isDirty{};
      bool isStructureChanged{};
    };

    inline const ElementTypeInfo& element_type_info_get(Element::Type type)
    {
      for (auto& info : ELEMENT_TYPES)
        if (info.type == type) return info;
      return ELEMENT_TYPES[0];
    }

    inline bool shortcut_is_pressed(ImGuiKeyChord chord)
    {
      auto& io = ImGui::GetIO();
      return !io.WantTextInput && !ImGui::IsAnyItemActive() &&
             ImGui::Shortcut(chord, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused);
    }

    inline void document_history_context_menu_draw()
    {
      ImGui::Separator();
      if (ImGui::MenuItem("Undo", "Ctrl+Z")) gIsDocumentUndoMenuRequested = true;
      if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z")) gIsDocumentRedoMenuRequested = true;
    }

    inline bool element_index_is_valid(const Schema& schema, int index)
    {
      return index >= 0 && index < (int)schema.elements.size();
    }

    inline void child_insert_after(Schema& schema, int parent, int child, int after)
    {
      if (!element_index_is_valid(schema, parent)) return;
      auto& children = schema.elements[parent].children;
      auto childIt = std::ranges::find(children, child);
      if (childIt == children.end()) return;
      auto value = *childIt;
      children.erase(childIt);
      auto afterIt = std::ranges::find(children, after);
      if (afterIt == children.end())
        children.emplace_back(value);
      else
        children.insert(afterIt + 1, value);
    }

    inline int element_paste(util::Clipboard& clipboard, Schema& schema, int parent, int after,
                             Element::Type expected = Element::UNKNOWN)
    {
      auto index = schema.element_insert_xml(clipboard.get(), parent);
      if (index < 0) return -1;
      if (expected != Element::UNKNOWN && schema.elements[index].type != expected)
      {
        schema.element_erase(index);
        return -1;
      }
      child_insert_after(schema, parent, index, after);
      schema.indices_refresh();
      return index;
    }

    inline int element_duplicate(Schema& schema, int parent, int source, int after,
                                 Element::Type expected = Element::UNKNOWN)
    {
      if (!element_index_is_valid(schema, source)) return -1;
      if (parent < 0) parent = schema.elements[source].parent;
      if (!element_index_is_valid(schema, parent)) return -1;
      auto index = schema.element_insert_xml(schema.element_serialize(source), parent);
      if (index < 0) return -1;
      if (expected != Element::UNKNOWN && schema.elements[index].type != expected)
      {
        schema.element_erase(index);
        return -1;
      }
      child_insert_after(schema, parent, index, after);
      schema.indices_refresh();
      return index;
    }

    inline void element_copy(util::Clipboard& clipboard, const Schema& schema, int index)
    {
      if (element_index_is_valid(schema, index)) clipboard.copy(schema.element_serialize(index));
    }

    inline bool element_cut(util::Clipboard& clipboard, Schema& schema, int index)
    {
      if (!element_index_is_valid(schema, index)) return false;
      clipboard.cut(schema.element_serialize(index));
      return schema.element_erase(index);
    }

    inline bool children_clear(Schema& schema, int parent, Element::Type type)
    {
      if (!element_index_is_valid(schema, parent)) return false;
      std::vector<int> indices{};
      for (auto child : schema.elements[parent].children)
      {
        if (!element_index_is_valid(schema, child)) continue;
        if (schema.elements[child].type == type) indices.emplace_back(child);
      }
      if (indices.empty()) return false;

      std::ranges::sort(indices, std::greater{});
      auto isDirty = false;
      for (auto index : indices)
        if (schema.element_erase(index)) isDirty = true;
      return isDirty;
    }

    template <typename Insert, typename Select, typename Clear>
    ElementActionResult element_context_menu_draw(util::Clipboard& clipboard, Schema& schema,
                                                  const ElementActionOptions& options, int& selected, Insert insert,
                                                  Select select, Clear clear)
    {
      ElementActionResult result{};
      auto isTargetValid = element_index_is_valid(schema, options.target);
      auto pasteParent = options.pasteParent >= 0 ? options.pasteParent : options.parent;
      auto pasteAfter = options.pasteAfter >= -1 ? options.pasteAfter : options.after;

      if (options.isInsertVisible && ImGui::MenuItem("Insert", nullptr, false, options.isInsertEnabled))
      {
        selected = insert(options.after);
        if (selected >= 0)
        {
          select(selected);
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      if (ImGui::MenuItem("Duplicate", nullptr, false, options.isDuplicateEnabled && isTargetValid))
      {
        selected = element_duplicate(schema, options.parent, options.target, options.after, options.type);
        if (selected >= 0)
        {
          select(selected);
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      if (ImGui::MenuItem("Delete", "Del", false, options.isDeleteEnabled && isTargetValid))
      {
        if (schema.element_erase(options.target))
        {
          clear();
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      if (options.isClearVisible && ImGui::MenuItem("Clear", nullptr, false, options.isClearEnabled))
      {
        if (children_clear(schema, options.parent, options.type))
        {
          clear();
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      ImGui::Separator();
      if (ImGui::MenuItem("Cut", "Ctrl+X", false, options.isDeleteEnabled && isTargetValid))
      {
        if (element_cut(clipboard, schema, options.target))
        {
          clear();
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      if (ImGui::MenuItem("Copy", "Ctrl+C", false, isTargetValid)) element_copy(clipboard, schema, options.target);
      if (ImGui::MenuItem("Paste", "Ctrl+V", false, options.isPasteEnabled && element_index_is_valid(schema, pasteParent)))
      {
        selected = element_paste(clipboard, schema, pasteParent, pasteAfter, options.type);
        if (selected >= 0)
        {
          select(selected);
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      return result;
    }

    template <typename Select, typename Clear>
    ElementActionResult element_shortcuts_apply(util::Clipboard& clipboard, Schema& schema,
                                                const ElementActionOptions& options, int& selected, Select select,
                                                Clear clear)
    {
      ElementActionResult result{};
      auto isTargetValid = element_index_is_valid(schema, options.target);
      auto pasteParent = options.pasteParent >= 0 ? options.pasteParent : options.parent;
      auto pasteAfter = options.pasteAfter >= -1 ? options.pasteAfter : options.after;

      if (options.isDeleteEnabled && isTargetValid && shortcut_is_pressed(ImGuiKey_Delete))
      {
        if (schema.element_erase(options.target))
        {
          clear();
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      else if (options.isDeleteEnabled && isTargetValid && shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_X))
      {
        if (element_cut(clipboard, schema, options.target))
        {
          clear();
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      else if (isTargetValid && shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_C))
      {
        element_copy(clipboard, schema, options.target);
      }
      else if (options.isPasteEnabled && element_index_is_valid(schema, pasteParent) &&
               shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_V))
      {
        selected = element_paste(clipboard, schema, pasteParent, pasteAfter, options.type);
        if (selected >= 0)
        {
          select(selected);
          result.isDirty = true;
          result.isStructureChanged = true;
        }
      }
      return result;
    }

    inline void attribute_order_ensure(Element& element, std::string_view symbol)
    {
      if (std::ranges::find(element.attributeOrder, symbol) == element.attributeOrder.end())
        element.attributeOrder.emplace_back(symbol);
    }

    inline void attribute_order_remove(Element& element, std::string_view symbol)
    {
      element.attributeOrder.erase(std::remove(element.attributeOrder.begin(), element.attributeOrder.end(), symbol),
                                   element.attributeOrder.end());
    }

    template <typename Insert, typename Clear, typename Paste>
    bool attribute_context_menu_draw(util::Clipboard& clipboard, const std::string& assignments, Insert insert,
                                     Clear clear, Paste paste)
    {
      auto isDirty = insert();
      ImGui::BeginDisabled();
      ImGui::MenuItem("Duplicate");
      if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
        ImGui::SetTooltip("XML attributes are unique by name.");
      ImGui::EndDisabled();
      if (ImGui::MenuItem("Delete", "Del"))
      {
        clear();
        isDirty = true;
      }
      ImGui::Separator();
      if (ImGui::MenuItem("Cut", "Ctrl+X"))
      {
        clipboard.cut(assignments);
        clear();
        isDirty = true;
      }
      if (ImGui::MenuItem("Copy", "Ctrl+C")) clipboard.copy(assignments);
      if (ImGui::MenuItem("Paste", "Ctrl+V") && paste(clipboard.get())) isDirty = true;
      return isDirty;
    }
  }

  class Editor
  {
  public:
    struct LoadedSchema
    {
      const std::filesystem::path* path{};
      const resource::xml::Schema* schema{};
    };

    enum Mode
    {
      ENTRY,
      POOL
    };

    enum PendingSetting
    {
      PENDING_NONE,
      PENDING_SOUND
    };

    Mode mode{ENTRY};
    int selectedEntry{-1};
    int selectedPool{-1};
    std::vector<int> selectedEntries{};
    std::vector<int> selectedPools{};
    int selectedPoolEntry{-1};
    int selectedChoice{-1};
    int selectedEffect{-1};
    int scrollToEntry{-1};
    int scrollToPool{-1};
    int scrollToPoolEntry{-1};
    int scrollToChoice{-1};
    int scrollToEffect{-1};
    int textSelectionStart{};
    int textSelectionEnd{};
    int textSelectionStartByte{};
    int textSelectionEndByte{};
    int previewIndex{};
    int pendingSoundEffect{-1};
    bool isPlaying{};
    bool previewNeedsAnimationPlay{};
    bool isPreviewBlipQueued{};
    float previewTimer{};
    int previewBaseDelay{4};
    int previewDelay{4};
    int delay{4};
    int blipDelay{4};
    glm::vec3 nameColor{0.498039f, 0.498039f, 0.498039f};
    glm::vec3 textColor{1.0f, 1.0f, 1.0f};
    PendingSetting pendingSetting{PENDING_NONE};
    resource::xml::Character previewCharacter{};
    util::Clipboard clipboard{};
    resource::Font previewFont{};
    const resource::Audio* previewBlip{};
    Entity previewEntity{};
    util::imgui::widget::EntityCanvas previewCanvas{};
    std::string animationAppendID{};
    std::string fontPath{};
    std::string fontRootPath{};
    std::filesystem::path fontSchemaPath{};
    std::string previewAnm2Path{};
    std::string previewAnimation{};
    std::string previewAnimationRequest{};

    bool update(Resources&, resource::xml::Schema&, const std::filesystem::path&, util::Dialog&,
                std::span<const LoadedSchema>);
  };
}
