#include "anm2.hpp"

#include "../util/physfs.hpp"
#include "../util/working_directory.hpp"

#include "../log.hpp"

#include <array>
#include <cstring>
#include <format>
#include <ranges>

using namespace tinyxml2;
using namespace game::util;

namespace game::resource::xml
{
  namespace
  {
    XMLError document_load(const physfs::Path& path, XMLDocument& document)
    {
      if (!path.is_valid())
      {
        logger.error(std::format("Failed to open XML document: {} ({})", path.c_str(), 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(), 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_string_attribute(XMLElement* element, const char* attribute, std::string* value)
    {
      const char* temp = nullptr;
      auto result = element->QueryStringAttribute(attribute, &temp);
      if (result == XML_SUCCESS && temp && value) *value = temp;
      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;
    }

    Anm2::Frame::Interpolation interpolation_get(XMLElement* element)
    {
      if (!element) return Anm2::Frame::INTERPOLATION_NONE;

      auto* value = element->Attribute("Interpolated");
      if (!value) return Anm2::Frame::INTERPOLATION_NONE;

      if (std::strcmp(value, "true") == 0 || std::strcmp(value, "1") == 0) return Anm2::Frame::LINEAR;
      if (std::strcmp(value, "false") == 0 || std::strcmp(value, "0") == 0) return Anm2::Frame::INTERPOLATION_NONE;
      if (std::strcmp(value, "EaseIn") == 0) return Anm2::Frame::EASE_IN;
      if (std::strcmp(value, "EaseOut") == 0) return Anm2::Frame::EASE_OUT;
      if (std::strcmp(value, "EaseInOut") == 0) return Anm2::Frame::EASE_IN_OUT;
      return Anm2::Frame::INTERPOLATION_NONE;
    }

    void attributes_parse(XMLElement* element, Anm2::Element& parsed)
    {
      if (!element) return;

      query_string_attribute(element, "CreatedBy", &parsed.createdBy);
      query_string_attribute(element, "CreatedOn", &parsed.createdOn);
      query_string_attribute(element, "DefaultAnimation", &parsed.defaultAnimation);
      query_string_attribute(element, "Name", &parsed.name);
      query_string_attribute(element, "Path", &parsed.path);

      element->QueryIntAttribute("Id", &parsed.id);
      element->QueryIntAttribute("LayerId", &parsed.layerID);
      element->QueryIntAttribute("NullId", &parsed.nullID);
      element->QueryIntAttribute("SpritesheetId", &parsed.spritesheetID);
      element->QueryIntAttribute("EventId", &parsed.eventID);
      element->QueryIntAttribute("RegionId", &parsed.regionID);
      element->QueryIntAttribute("GroupId", &parsed.groupID);
      element->QueryIntAttribute("AtFrame", &parsed.atFrame);
      element->QueryIntAttribute("FrameNum", &parsed.frameNum);
      element->QueryIntAttribute("Fps", &parsed.fps);
      element->QueryIntAttribute("Version", &parsed.version);
      element->QueryIntAttribute("Delay", &parsed.delay);

      element->QueryFloatAttribute("XCrop", &parsed.crop.x);
      element->QueryFloatAttribute("YCrop", &parsed.crop.y);
      element->QueryFloatAttribute("XPosition", &parsed.position.x);
      element->QueryFloatAttribute("YPosition", &parsed.position.y);
      element->QueryFloatAttribute("XPivot", &parsed.pivot.x);
      element->QueryFloatAttribute("YPivot", &parsed.pivot.y);
      element->QueryFloatAttribute("Width", &parsed.size.x);
      element->QueryFloatAttribute("Height", &parsed.size.y);
      element->QueryFloatAttribute("XScale", &parsed.scale.x);
      element->QueryFloatAttribute("YScale", &parsed.scale.y);
      element->QueryFloatAttribute("Rotation", &parsed.rotation);

      query_color_attribute(element, "RedTint", &parsed.tint.r);
      query_color_attribute(element, "GreenTint", &parsed.tint.g);
      query_color_attribute(element, "BlueTint", &parsed.tint.b);
      query_color_attribute(element, "AlphaTint", &parsed.tint.a);
      query_color_attribute(element, "RedOffset", &parsed.colorOffset.r);
      query_color_attribute(element, "GreenOffset", &parsed.colorOffset.g);
      query_color_attribute(element, "BlueOffset", &parsed.colorOffset.b);

      element->QueryBoolAttribute("Visible", &parsed.isVisible);
      element->QueryBoolAttribute("ShowRect", &parsed.isShowRect);
      element->QueryBoolAttribute("Loop", &parsed.isLoop);
      element->QueryBoolAttribute("Expanded", &parsed.isExpanded);
    }

    Anm2::Element element_parse(XMLElement* element)
    {
      Anm2::Element parsed{};
      if (!element) return parsed;

      parsed.type = Anm2::Element::type_get(element->Name());
      if (parsed.type == Anm2::Element::UNKNOWN) return parsed;

      attributes_parse(element, parsed);

      for (auto* child = element->FirstChildElement(); child; child = child->NextSiblingElement())
        if (auto parsedChild = element_parse(child); parsedChild.type != Anm2::Element::UNKNOWN)
          parsed.children.emplace_back(std::move(parsedChild));

      return parsed;
    }
  }

  Anm2::Element::Type Anm2::Element::type_get(std::string_view tag)
  {
    static constexpr std::array<std::pair<Type, std::string_view>, TYPE_COUNT - 1> TAGS{{
        {ACTOR, "AnimatedActor"},
        {INFO, "Info"},
        {CONTENT, "Content"},
        {SPRITESHEETS, "Spritesheets"},
        {SPRITESHEET, "Spritesheet"},
        {REGION, "Region"},
        {LAYERS, "Layers"},
        {LAYER_ELEMENT, "Layer"},
        {NULLS, "Nulls"},
        {NULL_ELEMENT, "Null"},
        {EVENTS, "Events"},
        {EVENT, "Event"},
        {SOUNDS, "Sounds"},
        {SOUND_ELEMENT, "Sound"},
        {ANIMATIONS, "Animations"},
        {ANIMATION, "Animation"},
        {ROOT_ANIMATION, "RootAnimation"},
        {LAYER_ANIMATIONS, "LayerAnimations"},
        {LAYER_ANIMATION, "LayerAnimation"},
        {NULL_ANIMATIONS, "NullAnimations"},
        {NULL_ANIMATION, "NullAnimation"},
        {GROUP, "Group"},
        {TRIGGERS, "Triggers"},
        {TRIGGER_ELEMENT, "Trigger"},
        {FRAME, "Frame"},
    }};

    for (auto [type, typeTag] : TAGS)
      if (typeTag == tag) return type;
    return UNKNOWN;
  }

  Anm2::Element* Anm2::Element::child_get(Type type, std::size_t index)
  {
    std::size_t current{};
    for (auto& child : children)
      if (child.type == type && current++ == index) return &child;
    return nullptr;
  }

  const Anm2::Element* Anm2::Element::child_get(Type type, std::size_t index) const
  {
    std::size_t current{};
    for (auto& child : children)
      if (child.type == type && current++ == index) return &child;
    return nullptr;
  }

  std::vector<Anm2::Element*> Anm2::Element::children_get(Type type)
  {
    std::vector<Element*> result{};
    for (auto& child : children)
      if (child.type == type) result.emplace_back(&child);
    return result;
  }

  std::vector<const Anm2::Element*> Anm2::Element::children_get(Type type) const
  {
    std::vector<const Element*> result{};
    for (auto& child : children)
      if (child.type == type) result.emplace_back(&child);
    return result;
  }

  Anm2::Anm2(const Anm2&) = default;
  Anm2::Anm2(Anm2&&) = default;
  Anm2& Anm2::operator=(const Anm2&) = default;
  Anm2& Anm2::operator=(Anm2&&) = default;

  void Anm2::init(XMLDocument& document, Flags anm2Flags, const physfs::Path& archive)
  {
    this->flags = anm2Flags;

    auto element = document.RootElement();
    if (!element) return;
    root = element_parse(element);

    auto parse_frame = [&](XMLElement* element, Type type, int spritesheetID = -1)
    {
      Frame frame{};
      if (!element) return frame;

      if (type != TRIGGER)
      {
        element->QueryFloatAttribute("XPosition", &frame.position.x);
        element->QueryFloatAttribute("YPosition", &frame.position.y);
        if (type == LAYER)
        {
          auto isRegionFrame = element->FindAttribute("RegionId") && spritesheets.contains(spritesheetID);
          if (isRegionFrame)
          {
            auto& spritesheet = spritesheets.at(spritesheetID);
            element->QueryIntAttribute("RegionId", &frame.regionID);
            if (auto region = spritesheet.regions.find(frame.regionID); region != spritesheet.regions.end())
            {
              frame.crop = region->second.crop;
              frame.size = region->second.size;
              frame.pivot = region->second.origin == Spritesheet::Region::Origin::CENTER
                                ? glm::vec2(glm::ivec2(frame.size * 0.5f))
                            : region->second.origin == Spritesheet::Region::Origin::TOP_LEFT ? glm::vec2{}
                                                                                             : region->second.pivot;
            }
            else
              isRegionFrame = false;
          }
          if (!isRegionFrame)
          {
            element->QueryFloatAttribute("XPivot", &frame.pivot.x);
            element->QueryFloatAttribute("YPivot", &frame.pivot.y);
            element->QueryFloatAttribute("XCrop", &frame.crop.x);
            element->QueryFloatAttribute("YCrop", &frame.crop.y);
            element->QueryFloatAttribute("Width", &frame.size.x);
            element->QueryFloatAttribute("Height", &frame.size.y);
          }
        }
        element->QueryFloatAttribute("XScale", &frame.scale.x);
        element->QueryFloatAttribute("YScale", &frame.scale.y);
        element->QueryIntAttribute("Delay", &frame.duration);
        element->QueryBoolAttribute("Visible", &frame.isVisible);
        query_color_attribute(element, "RedTint", &frame.tint.r);
        query_color_attribute(element, "GreenTint", &frame.tint.g);
        query_color_attribute(element, "BlueTint", &frame.tint.b);
        query_color_attribute(element, "AlphaTint", &frame.tint.a);
        query_color_attribute(element, "RedOffset", &frame.colorOffset.r);
        query_color_attribute(element, "GreenOffset", &frame.colorOffset.g);
        query_color_attribute(element, "BlueOffset", &frame.colorOffset.b);
        element->QueryFloatAttribute("Rotation", &frame.rotation);
        frame.interpolation = interpolation_get(element);
        frame.isInterpolated = frame.interpolation != Frame::INTERPOLATION_NONE;
      }
      else
      {
        int soundID{};
        if (element->QueryIntAttribute("SoundId", &soundID) == XML_SUCCESS) frame.soundIDs.emplace_back(soundID);

        for (auto child = element->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
        {
          child->QueryIntAttribute("Id", &soundID);
          frame.soundIDs.emplace_back(soundID);
        }

        element->QueryIntAttribute("EventId", &frame.eventID);
        element->QueryIntAttribute("AtFrame", &frame.atFrame);
      }

      return frame;
    };

    auto parse_item = [&](XMLElement* element, Type type, int& id)
    {
      Item item{};
      if (!element) return item;

      if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
      if (type == NULL_) element->QueryIntAttribute("NullId", &id);

      element->QueryBoolAttribute("Visible", &item.isVisible);

      auto spritesheetID = -1;
      if (type == LAYER)
        if (auto layer = layers.find(id); layer != layers.end()) spritesheetID = layer->second.spritesheetID;

      for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
           child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
        item.frames.emplace_back(parse_frame(child, type, spritesheetID));

      return item;
    };

    auto parse_animation = [&](XMLElement* element)
    {
      Animation animation{};
      if (!element) return animation;

      query_string_attribute(element, "Name", &animation.name);
      element->QueryIntAttribute("FrameNum", &animation.frameNum);
      element->QueryBoolAttribute("Loop", &animation.isLoop);

      int id{-1};

      if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
        animation.rootAnimation = parse_item(rootAnimationElement, ROOT, id);

      auto track_elements_parse = [](auto&& self, XMLElement* parent, const char* tag, auto&& callback) -> void
      {
        for (auto child = parent->FirstChildElement(); child; child = child->NextSiblingElement())
        {
          auto name = child->Name();
          if (name && std::strcmp(name, tag) == 0)
            callback(child);
          else if (name && std::strcmp(name, "Group") == 0)
            self(self, child, tag, callback);
        }
      };

      if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
      {
        track_elements_parse(track_elements_parse, layerAnimationsElement, "LayerAnimation", [&](XMLElement* child)
        {
          auto layerAnimation = parse_item(child, LAYER, id);
          animation.layerOrder.push_back(id);
          animation.layerAnimations.emplace(id, std::move(layerAnimation));
        });
      }

      if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
      {
        track_elements_parse(track_elements_parse, nullAnimationsElement, "NullAnimation", [&](XMLElement* child)
        {
          auto nullAnimation = parse_item(child, NULL_, id);
          animation.nullAnimations.emplace(id, std::move(nullAnimation));
        });
      }

      if (auto triggersElement = element->FirstChildElement("Triggers"))
        animation.triggers = parse_item(triggersElement, TRIGGER, id);

      return animation;
    };

    if (auto infoElement = element->FirstChildElement("Info")) infoElement->QueryIntAttribute("Fps", &fps);

    if (auto contentElement = element->FirstChildElement("Content"))
    {
      if (auto spritesheetsElement = contentElement->FirstChildElement("Spritesheets"))
      {
        for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
             child = child->NextSiblingElement("Spritesheet"))
        {
          int spritesheetId{};
          Spritesheet spritesheet{};
          child->QueryIntAttribute("Id", &spritesheetId);
          query_string_attribute(child, "Path", &spritesheet.path);

          if ((this->flags & NO_SPRITESHEETS) != 0)
            spritesheet.texture = Texture();
          else if (!archive.empty())
            spritesheet.texture = Texture(physfs::Path(archive + "/" + spritesheet.path));
          else
            spritesheet.texture = Texture(std::filesystem::path(spritesheet.path));

          for (auto regionChild = child->FirstChildElement("Region"); regionChild;
               regionChild = regionChild->NextSiblingElement("Region"))
          {
            Spritesheet::Region region{};
            int regionID{};

            regionChild->QueryIntAttribute("Id", &regionID);
            query_string_attribute(regionChild, "Name", &region.name);
            regionChild->QueryFloatAttribute("XCrop", &region.crop.x);
            regionChild->QueryFloatAttribute("YCrop", &region.crop.y);
            regionChild->QueryFloatAttribute("Width", &region.size.x);
            regionChild->QueryFloatAttribute("Height", &region.size.y);

            if (regionChild->FindAttribute("Origin"))
            {
              std::string origin{};
              query_string_attribute(regionChild, "Origin", &origin);
              region.origin = origin == "Center"    ? Spritesheet::Region::CENTER
                              : origin == "TopLeft" ? Spritesheet::Region::TOP_LEFT
                                                    : Spritesheet::Region::CUSTOM;
            }
            else
            {
              regionChild->QueryFloatAttribute("XPivot", &region.pivot.x);
              regionChild->QueryFloatAttribute("YPivot", &region.pivot.y);
            }

            spritesheet.regions.emplace(regionID, std::move(region));
            spritesheet.regionOrder.emplace_back(regionID);
          }

          spritesheets.emplace(spritesheetId, std::move(spritesheet));
        }
      }

      if (auto layersElement = contentElement->FirstChildElement("Layers"))
      {
        for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
        {
          int layerId{};
          Layer layer{};
          child->QueryIntAttribute("Id", &layerId);
          query_string_attribute(child, "Name", &layer.name);
          child->QueryIntAttribute("SpritesheetId", &layer.spritesheetID);
          layerMap[layer.name] = layerId;
          layers.emplace(layerId, std::move(layer));
        }
      }

      if (auto nullsElement = contentElement->FirstChildElement("Nulls"))
      {
        for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
        {
          int nullId{};
          Null null{};
          child->QueryIntAttribute("Id", &nullId);
          query_string_attribute(child, "Name", &null.name);
          child->QueryBoolAttribute("ShowRect", &null.isShowRect);
          nullMap[null.name] = nullId;
          nulls.emplace(nullId, std::move(null));
        }
      }

      if (auto eventsElement = contentElement->FirstChildElement("Events"))
      {
        for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
        {
          int eventId{};
          Event event{};
          child->QueryIntAttribute("Id", &eventId);
          query_string_attribute(child, "Name", &event.name);
          eventMap[event.name] = eventId;
          events.emplace(eventId, std::move(event));
        }
      }

      if (auto soundsElement = contentElement->FirstChildElement("Sounds"))
      {
        for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
        {
          int soundId{};
          Sound sound{};
          child->QueryIntAttribute("Id", &soundId);
          query_string_attribute(child, "Path", &sound.path);
          if ((this->flags & NO_SOUNDS) != 0)
            sound.audio = Audio();
          else if (!archive.empty())
            sound.audio = Audio(physfs::Path(archive + "/" + sound.path));
          else
            sound.audio = Audio(std::filesystem::path(sound.path));
          sounds.emplace(soundId, std::move(sound));
        }
      }
    }

    if (auto animationsElement = element->FirstChildElement("Animations"))
    {
      query_string_attribute(animationsElement, "DefaultAnimation", &defaultAnimation);

      for (auto child = animationsElement->FirstChildElement("Animation"); child;
           child = child->NextSiblingElement("Animation"))
      {
        if ((this->flags & DEFAULT_ANIMATION_ONLY) != 0)
        {
          std::string name{};
          query_string_attribute(child, "Name", &name);
          if (name == defaultAnimation)
          {
            animations.emplace_back(parse_animation(child));
            break;
          }
        }
        else
          animations.emplace_back(parse_animation(child));
      }

      for (int i = 0; i < (int)animations.size(); i++)
      {
        auto& animation = animations[i];
        animationMap[animation.name] = i;
        animationMapReverse[i] = animation.name;
      }

      if (animationMap.contains(defaultAnimation))
        defaultAnimationID = animationMap[defaultAnimation];
      else
        defaultAnimationID = -1;
    }

    isValid = true;
  }

  Anm2::Anm2(const std::filesystem::path& path, Flags anm2Flags)
  {
    XMLDocument document;
    auto pathString = path.string();

    if (document.LoadFile(pathString.c_str()) != XML_SUCCESS)
    {
      logger.error(std::format("Failed to initialize anm2: {} ({})", pathString, document.ErrorStr()));
      isValid = false;
      return;
    }

    WorkingDirectory workingDirectory(path, WorkingDirectory::FILE);

    this->path = path.string();

    init(document, anm2Flags);

    logger.info(std::format("Initialized anm2: {}", pathString));
  }

  Anm2::Anm2(const physfs::Path& path, Flags anm2Flags)
  {
    XMLDocument document;

    if (document_load(path, document) != XML_SUCCESS)
    {
      isValid = false;
      return;
    }

    this->path = path;
    init(document, anm2Flags, path.directory_get());

    logger.info(std::format("Initialized anm2: {}", path.c_str()));
  }

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

  int Anm2::item_length(const Item* item)
  {
    if (!item) return -1;

    int duration{};
    for (auto& frame : item->frames)
      duration += frame.duration;
    return duration;
  }

  float Anm2::interpolation_apply(Frame::Interpolation interpolation, float value)
  {
    value = glm::clamp(value, 0.0f, 1.0f);

    switch (interpolation)
    {
      case Frame::LINEAR:
        return value;
      case Frame::EASE_IN:
        return value * value;
      case Frame::EASE_OUT:
        return 1.0f - ((1.0f - value) * (1.0f - value));
      case Frame::EASE_IN_OUT:
        return value * value * (3.0f - 2.0f * value);
      case Frame::INTERPOLATION_NONE:
      default:
        return 0.0f;
    }
  }

  Anm2::Frame Anm2::frame_generate(Item& item, float frameTime, Type type, int id,
                                   const std::function<void(Frame&, Type, int)>& frameOverride)
  {
    Frame frame{};
    frame.isVisible = false;

    if (item.frames.empty()) return frame;

    frameTime = frameTime < 0.0f ? 0.0f : frameTime;

    Frame* frameNext = nullptr;
    Frame frameNextCopy{};
    int durationCurrent = 0;
    int durationNext = 0;

    for (int i = 0; i < (int)item.frames.size(); i++)
    {
      Frame& checkFrame = item.frames[i];

      frame = checkFrame;
      durationNext += frame.duration;

      if (frameTime >= durationCurrent && frameTime < durationNext)
      {
        if (i + 1 < (int)item.frames.size())
        {
          frameNext = &item.frames[i + 1];
          frameNextCopy = *frameNext;
        }
        else
          frameNext = nullptr;
        break;
      }

      durationCurrent += frame.duration;
    }

    if (frameOverride)
    {
      frameOverride(frame, type, id);
      if (frameNext) frameOverride(frameNextCopy, type, id);
    }

    if (frame.interpolation != Frame::INTERPOLATION_NONE && frameNext && frame.duration > 1)
    {
      auto interpolation =
          interpolation_apply(frame.interpolation, (frameTime - durationCurrent) / (durationNext - durationCurrent));

      frame.rotation = glm::mix(frame.rotation, frameNextCopy.rotation, interpolation);
      frame.position = glm::mix(frame.position, frameNextCopy.position, interpolation);
      frame.scale = glm::mix(frame.scale, frameNextCopy.scale, interpolation);
      frame.colorOffset = glm::mix(frame.colorOffset, frameNextCopy.colorOffset, interpolation);
      frame.tint = glm::mix(frame.tint, frameNextCopy.tint, interpolation);
    }

    return frame;
  }
}
