#pragma once

#include <cstdint>
#include <filesystem>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>

#include <glm/glm.hpp>
#include <tinyxml2/tinyxml2.h>

#include "icon.hpp"
#include "strings.hpp"
#include "types.hpp"

#define ANM2_ELEMENT_TYPES                                                                                             \
  X(UNKNOWN, "")                                                                                                       \
  X(ANIMATED_ACTOR, "AnimatedActor")                                                                                   \
  X(INFO, "Info")                                                                                                      \
  X(CONTENT, "Content")                                                                                                \
  X(SPRITESHEETS, "Spritesheets")                                                                                      \
  X(SPRITESHEET, "Spritesheet")                                                                                        \
  X(REGION, "Region")                                                                                                  \
  X(LAYERS, "Layers")                                                                                                  \
  X(LAYER_ELEMENT, "Layer")                                                                                            \
  X(NULLS, "Nulls")                                                                                                    \
  X(NULL_ELEMENT, "Null")                                                                                              \
  X(EVENTS, "Events")                                                                                                  \
  X(EVENT_ELEMENT, "Event")                                                                                            \
  X(SOUNDS, "Sounds")                                                                                                  \
  X(SOUND_ELEMENT, "Sound")                                                                                            \
  X(ANIMATIONS, "Animations")                                                                                          \
  X(ANIMATION, "Animation")                                                                                            \
  X(ROOT_ANIMATION, "RootAnimation")                                                                                   \
  X(LAYER_ANIMATIONS, "LayerAnimations")                                                                               \
  X(LAYER_ANIMATION, "LayerAnimation")                                                                                 \
  X(NULL_ANIMATIONS, "NullAnimations")                                                                                 \
  X(NULL_ANIMATION, "NullAnimation")                                                                                   \
  X(GROUP, "Group")                                                                                                    \
  X(TRIGGERS, "Triggers")                                                                                              \
  X(FRAME, "Frame")                                                                                                    \
  X(TRIGGER, "Trigger")

namespace anm2ed
{
  enum class ElementType
  {
#define X(symbol, tag) symbol,
    ANM2_ELEMENT_TYPES
#undef X
        COUNT
  };

  enum class Interpolation
  {
    NONE,
    LINEAR,
    EASE_IN,
    EASE_OUT,
    EASE_IN_OUT,
    COUNT
  };

  enum class Origin
  {
    CUSTOM,
    TOP_LEFT,
    CENTER,
    COUNT
  };

  enum class ItemType
  {
    NONE,
    ROOT,
    LAYER,
    NULL_,
    TRIGGER,
    COUNT
  };

  inline constexpr int NONE = (int)ItemType::NONE;
  inline constexpr int ROOT = (int)ItemType::ROOT;
  inline constexpr int LAYER = (int)ItemType::LAYER;
  inline constexpr int NULL_ = (int)ItemType::NULL_;
  inline constexpr int TRIGGER = (int)ItemType::TRIGGER;

  inline constexpr int FRAME_DURATION_MIN = 1;
  inline constexpr int FRAME_DURATION_MAX = 1000000;
  inline constexpr int FRAME_NUM_MIN = 1;
  inline constexpr int FRAME_NUM_MAX = FRAME_DURATION_MAX;
  inline constexpr int FPS_MIN = 1;
  inline constexpr int FPS_MAX = 120;

  enum class SpritesheetMergeOrigin
  {
    APPEND_RIGHT,
    APPEND_BOTTOM,
    COUNT
  };

  inline constexpr int APPEND_RIGHT = (int)SpritesheetMergeOrigin::APPEND_RIGHT;
  inline constexpr int APPEND_BOTTOM = (int)SpritesheetMergeOrigin::APPEND_BOTTOM;

  inline const glm::vec4 ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f);
  inline const glm::vec4 ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f);
  inline const glm::vec4 ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f);

  inline const glm::vec4 LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f);
  inline const glm::vec4 LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f);
  inline const glm::vec4 LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f);

  inline const glm::vec4 NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f);
  inline const glm::vec4 NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f);
  inline const glm::vec4 NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f);

  inline const glm::vec4 TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f);
  inline const glm::vec4 TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f);
  inline const glm::vec4 TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f);

#define ANM2_ITEM_TYPES                                                                                                \
  X(NONE, STRING_UNDEFINED, "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4())                           \
  X(ROOT, BASIC_ROOT, "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED)        \
  X(LAYER, BASIC_LAYER_ANIMATION, "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE,            \
    LAYER_COLOR_HOVERED)                                                                                               \
  X(NULL_, BASIC_NULL_ANIMATION, "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE,                \
    NULL_COLOR_HOVERED)                                                                                                \
  X(TRIGGER, BASIC_TRIGGERS, "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE,                \
    TRIGGER_COLOR_HOVERED)

  constexpr StringType TYPE_STRINGS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string,
      ANM2_ITEM_TYPES
#undef X
  };

  constexpr const char* TYPE_ITEM_STRINGS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString,
      ANM2_ITEM_TYPES
#undef X
  };

  constexpr resource::icon::Type TYPE_ICONS[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon,
      ANM2_ITEM_TYPES
#undef X
  };

  inline const glm::vec4 TYPE_COLOR[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color,
      ANM2_ITEM_TYPES
#undef X
  };

  inline const glm::vec4 TYPE_COLOR_ACTIVE[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive,
      ANM2_ITEM_TYPES
#undef X
  };

  inline const glm::vec4 TYPE_COLOR_HOVERED[] = {
#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered,
      ANM2_ITEM_TYPES
#undef X
  };

  enum class ChangeType
  {
    ADJUST,
    ADD,
    SUBTRACT,
    MULTIPLY,
    DIVIDE
  };

  enum Flag
  {
    SERIALIZE_GROUPS = 1 << 0,
    SERIALIZE_REGIONS = 1 << 1,
    SERIALIZE_SOUNDS = 1 << 2,
    SERIALIZE_REDUNDANT_FRAME_REGION_VALUES = 1 << 3,
    SERIALIZE_BAKE_SPECIAL_INTERPOLATED_FRAMES = 1 << 4
  };

  using Flags = int;

  constexpr bool has_flag(Flags flags, Flag flag) { return (flags & flag) != 0; }
  constexpr Flags SERIALIZE_EDITOR_DEFAULT =
      SERIALIZE_GROUPS | SERIALIZE_REGIONS | SERIALIZE_SOUNDS | SERIALIZE_REDUNDANT_FRAME_REGION_VALUES;
  constexpr Flags SERIALIZE_DEFAULT = SERIALIZE_EDITOR_DEFAULT | SERIALIZE_BAKE_SPECIAL_INTERPOLATED_FRAMES;

  struct Options
  {
    Flags flags{SERIALIZE_DEFAULT};
  };

  struct FrameChange
  {
    std::optional<bool> isVisible{};
    std::optional<Interpolation> interpolation{};
    std::optional<float> rotation{};
    std::optional<int> duration{};
    std::optional<int> regionId{};
    std::optional<float> pivotX{};
    std::optional<float> pivotY{};
    std::optional<float> cropX{};
    std::optional<float> cropY{};
    std::optional<float> positionX{};
    std::optional<float> positionY{};
    std::optional<float> sizeX{};
    std::optional<float> sizeY{};
    std::optional<float> scaleX{};
    std::optional<float> scaleY{};
    std::optional<float> colorOffsetR{};
    std::optional<float> colorOffsetG{};
    std::optional<float> colorOffsetB{};
    std::optional<float> tintR{};
    std::optional<float> tintG{};
    std::optional<float> tintB{};
    std::optional<float> tintA{};
    std::optional<bool> isFlipX{};
    std::optional<bool> isFlipY{};
  };

  struct Element
  {
    ElementType type{ElementType::UNKNOWN};
    std::string tag{};
    std::vector<Element> children{};
    std::string name{};
    std::string createdBy{"robot"};
    std::string createdOn{};
    std::filesystem::path path{};
    std::string defaultAnimation{};
    int id{-1};
    int layerId{-1};
    int nullId{-1};
    int spritesheetId{};
    int fps{30};
    int version{};
    int frameNum{1};
    int duration{1};
    int atFrame{-1};
    int eventId{-1};
    int regionId{-1};
    int soundId{-1};
    int groupId{-1};
    bool isLoop{true};
    bool isVisible{true};
    bool isShowRect{};
    bool isExpanded{true};
    Interpolation interpolation{Interpolation::NONE};
    Origin origin{Origin::CUSTOM};
    float rotation{};
    std::vector<int> soundIds{};
    glm::vec2 pivot{};
    glm::vec2 crop{};
    glm::vec2 position{};
    glm::vec2 size{};
    glm::vec2 scale{100.0f, 100.0f};
    glm::vec3 colorOffset{};
    glm::vec4 tint{types::color::WHITE};
  };

  struct Reference
  {
    int animationIndex{-1};
    int itemType{(int)ItemType::NONE};
    int itemID{-1};
    int frameIndex{-1};

    auto operator<=>(const Reference&) const = default;
  };

  Element element_make(ElementType);
  Element element_read(const tinyxml2::XMLElement*);
  std::string element_to_string(const Element&, Flags = SERIALIZE_EDITOR_DEFAULT);
  std::string element_to_string(const Element&, ElementType, Flags = SERIALIZE_EDITOR_DEFAULT);
  tinyxml2::XMLElement* element_to_xml(tinyxml2::XMLDocument&, const Element&, Flags = SERIALIZE_EDITOR_DEFAULT);
  tinyxml2::XMLElement* element_to_xml(tinyxml2::XMLDocument&, const Element&, ElementType,
                                       Flags = SERIALIZE_EDITOR_DEFAULT);
  Element* element_child_first_get(Element&, ElementType);
  const Element* element_child_first_get(const Element&, ElementType);
  Element* element_child_id_get(Element&, ElementType, int);
  const Element* element_child_id_get(const Element&, ElementType, int);
  int element_child_next_id_get(const Element&, ElementType);
  int element_child_max_id_get(const Element&, ElementType);
  bool element_child_id_erase(Element&, ElementType, int);
  Element* element_first_get(Element&, ElementType);
  const Element* element_first_get(const Element&, ElementType);
  Element* animation_item_get(Element&, ItemType, int = -1);
  const Element* animation_item_get(const Element&, ItemType, int = -1);
  ElementType item_type_to_track_type_get(ItemType);
  Element* track_frame_get(Element&, int);
  const Element* track_frame_get(const Element&, int);
  Element frame_generate(const Element&, float);
  int frame_index_from_at_frame_get(const Element&, int);
  int frame_index_from_time_get(const Element&, float);
  float frame_time_from_index_get(const Element&, int);
  void frame_bake(Element&, int, int, bool, bool);
  void frames_generate_from_grid(Element&, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int);
  bool frames_deserialize(Element&, const std::string&, int, std::set<int>&, std::string* = nullptr);
  void frames_sort_by_at_frame(Element&);
  void frames_change(Element&, FrameChange, ItemType, ChangeType, const std::set<int>&);
  int track_length_get(const Element&);
  int animation_length_get(const Element&);

  class Anm2
  {
  public:
    bool isValid{true};
    Element root{};

    Anm2();
    Anm2(const std::filesystem::path&, std::string* = nullptr);

    bool load(const std::filesystem::path&, std::string* = nullptr);
    bool load_string(std::string_view, std::string* = nullptr);
    bool save(const std::filesystem::path&, std::string* = nullptr, Options = {}) const;
    std::string to_string(Options = {}) const;
    tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Options = {}) const;
    std::uint64_t hash(Options = {}) const;
    bool is_special_interpolated_frames() const;
    void special_interpolated_frames_bake(int, bool, bool);
    void region_frames_sync(bool);
    Anm2 normalized_for_serialize() const;

    Element* element_get(ElementType);
    const Element* element_get(ElementType) const;
    Element* element_get(ElementType, int);
    const Element* element_get(ElementType, int) const;
    Element* element_get(int, ItemType, int = -1);
    const Element* element_get(int, ItemType, int = -1) const;
    Element* element_get(int, ItemType, int, int);
    const Element* element_get(int, ItemType, int, int) const;
    std::set<int> element_unused(ElementType) const;
    std::set<int> element_unused(ElementType, const Element&) const;
    std::set<int> element_unused(ElementType, int) const;
    bool deserialize(ElementType, const std::string&, bool, std::string* = nullptr, const std::filesystem::path& = {});
    bool regions_deserialize(int, const std::string&, bool, std::string* = nullptr);
    Element frame_effective(int, const Element&) const;
    glm::vec4 animation_rect(const Element&, bool) const;
    int layer_animation_add(int, int = -1, int = -1, std::string = {}, int = 0,
                            types::destination::Type = types::destination::ALL);
    int null_animation_add(int, int = -1, std::string = {}, bool = false,
                           types::destination::Type = types::destination::ALL);
    bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
    int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
  };
}
