#include "document.hpp"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <new>
#include <optional>
#include <set>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>

#include <format>

#include "log.hpp"
#include "manager.hpp"
#include "path.hpp"
#include "strings.hpp"
#include "toast.hpp"
#include "working_directory.hpp"

using namespace anm2ed::imgui;
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace glm;

namespace anm2ed::document
{
  ItemType item_type_get(int type) { return static_cast<ItemType>(type); }

  int animation_count_get(const Anm2& data)
  {
    int count{};
    if (auto animations = data.element_get(ElementType::ANIMATIONS))
      for (auto& animation : animations->children)
        if (animation.type == ElementType::ANIMATION) ++count;
    return count;
  }

  int frame_count_get(const Element& item)
  {
    auto frameType = item.type == ElementType::TRIGGERS ? ElementType::TRIGGER : ElementType::FRAME;
    int count{};
    for (auto& frame : item.children)
      if (frame.type == frameType) ++count;
    return count;
  }

  std::vector<std::string> animation_labels_get(const Anm2& data)
  {
    std::vector<std::string> labels{"None"};
    if (auto animations = data.element_get(ElementType::ANIMATIONS))
      for (auto& animation : animations->children)
        if (animation.type == ElementType::ANIMATION) labels.emplace_back(animation.name);
    return labels;
  }

  std::vector<std::string> element_name_labels_get(const Element* container, ElementType type, bool isNone)
  {
    std::vector<std::string> labels{};
    if (isNone) labels.emplace_back(localize.get(BASIC_NONE));
    if (!container) return labels;
    for (auto& element : container->children)
      if (element.type == type) labels.emplace_back(element.name);
    return labels;
  }

  std::vector<int> element_ids_get(const Element* container, ElementType type, bool isNone)
  {
    std::vector<int> ids{};
    if (isNone) ids.emplace_back(-1);
    if (!container) return ids;
    for (auto& element : container->children)
      if (element.type == type) ids.emplace_back(element.id);
    return ids;
  }

  std::vector<std::string> spritesheet_labels_get(const Anm2& data)
  {
    std::vector<std::string> labels{};
    if (auto spritesheets = data.element_get(ElementType::SPRITESHEETS))
      for (auto& spritesheet : spritesheets->children)
        if (spritesheet.type == ElementType::SPRITESHEET)
        {
          auto pathString = path::to_utf8(spritesheet.path);
          labels.emplace_back(
              std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(spritesheet.id, pathString)));
        }
    return labels;
  }

  std::vector<int> spritesheet_ids_get(const Anm2& data)
  {
    std::vector<int> ids{};
    if (auto spritesheets = data.element_get(ElementType::SPRITESHEETS))
      for (auto& spritesheet : spritesheets->children)
        if (spritesheet.type == ElementType::SPRITESHEET) ids.emplace_back(spritesheet.id);
    return ids;
  }

  std::vector<std::string> sound_labels_get(const Anm2& data)
  {
    std::vector<std::string> labels{localize.get(BASIC_NONE)};
    if (auto sounds = data.element_get(ElementType::SOUNDS))
      for (auto& sound : sounds->children)
        if (sound.type == ElementType::SOUND_ELEMENT)
        {
          auto pathString = path::to_utf8(sound.path);
          labels.emplace_back(std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(sound.id, pathString)));
        }
    return labels;
  }

  std::vector<int> sound_ids_get(const Anm2& data)
  {
    std::vector<int> ids{-1};
    if (auto sounds = data.element_get(ElementType::SOUNDS))
      for (auto& sound : sounds->children)
        if (sound.type == ElementType::SOUND_ELEMENT) ids.emplace_back(sound.id);
    return ids;
  }

  uint64_t spritesheet_hash_get(const Element& spritesheet, const resource::Texture* texture)
  {
    auto hash_combine = [](std::size_t& seed, std::size_t value)
    { seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); };

    std::size_t seed{};
    hash_combine(seed, std::hash<int>{}(texture ? texture->size.x : 0));
    hash_combine(seed, std::hash<int>{}(texture ? texture->size.y : 0));
    hash_combine(seed, std::hash<int>{}(texture ? texture->channels : 0));
    hash_combine(seed, std::hash<int>{}(texture ? texture->filter : 0));
    hash_combine(seed, std::hash<std::string>{}(path::to_utf8(spritesheet.path)));

    if (texture && !texture->pixels.empty())
    {
      std::string_view bytes(reinterpret_cast<const char*>(texture->pixels.data()), texture->pixels.size());
      hash_combine(seed, std::hash<std::string_view>{}(bytes));
    }
    else
      hash_combine(seed, 0);

    return static_cast<uint64_t>(seed);
  }

  void restored_snapshot_sanitize(Document& document)
  {
    auto& reference = document.reference;
    auto& selection = document.frames.selection;
    auto& frameReferences = document.frames.references;
    auto& itemReferences = document.items.references;
    auto& groupReferences = document.groupReferences;

    auto animationCount = animation_count_get(document.anm2);
    if (animationCount <= 0)
    {
      reference = {};
      selection.clear();
      frameReferences.clear();
      itemReferences.clear();
      groupReferences.clear();
      document.frameTime = 0.0f;
      return;
    }

    if (reference.animationIndex < 0 || reference.animationIndex >= animationCount)
      reference.animationIndex = std::clamp(reference.animationIndex, 0, animationCount - 1);

    auto item =
        document.anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID);
    if (!item)
    {
      reference.itemType = (int)ItemType::ROOT;
      reference.itemID = -1;
      item = document.anm2.element_get(reference.animationIndex, item_type_get(reference.itemType), reference.itemID);
    }

    if (!item)
    {
      reference.frameIndex = -1;
      selection.clear();
      frameReferences.clear();
      groupReferences.clear();
      document.frameTime = 0.0f;
      return;
    }

    for (auto it = itemReferences.begin(); it != itemReferences.end();)
    {
      auto item = document.anm2.element_get(it->animationIndex, item_type_get(it->itemType), it->itemID);
      if (!item)
        it = itemReferences.erase(it);
      else
        ++it;
    }

    for (auto it = groupReferences.begin(); it != groupReferences.end();)
    {
      auto animation = document.anm2.element_get(ElementType::ANIMATION, it->animationIndex);
      Element* container{};
      if (animation && it->itemType == LAYER)
        container = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS);
      else if (animation && it->itemType == NULL_)
        container = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS);
      auto group = container ? element_child_id_get(*container, ElementType::GROUP, it->itemID) : nullptr;
      if (!group)
        it = groupReferences.erase(it);
      else
        ++it;
    }

    for (auto it = frameReferences.begin(); it != frameReferences.end();)
    {
      auto item = document.anm2.element_get(it->animationIndex, item_type_get(it->itemType), it->itemID);
      auto frameCount = item ? frame_count_get(*item) : 0;
      if (!item || it->frameIndex < 0 || it->frameIndex >= frameCount)
        it = frameReferences.erase(it);
      else
        ++it;
    }

    auto frameCount = frame_count_get(*item);
    for (auto it = selection.begin(); it != selection.end();)
    {
      if (*it < 0 || *it >= frameCount)
        it = selection.erase(it);
      else
        ++it;
    }

    if (frameCount <= 0)
    {
      reference.frameIndex = -1;
      frameReferences.clear();
      document.frameTime = 0.0f;
      return;
    }

    if (reference.frameIndex < 0 || reference.frameIndex >= frameCount)
      reference.frameIndex =
          selection.empty() ? std::clamp(reference.frameIndex, 0, frameCount - 1) : *selection.begin();

    if (frameReferences.empty())
      for (auto frameIndex : selection)
        frameReferences.insert({reference.animationIndex, reference.itemType, reference.itemID, frameIndex});

    selection.clear();
    for (const auto& frameReference : frameReferences)
      if (frameReference.animationIndex == reference.animationIndex && frameReference.itemType == reference.itemType &&
          frameReference.itemID == reference.itemID)
        selection.insert(frameReference.frameIndex);

    document.frameTime = frame_time_from_index_get(*item, reference.frameIndex);
  }
}

namespace anm2ed
{
  Document::Document(const std::filesystem::path& path, bool isNew, std::string* errorString)
  {
    if (isNew)
    {
      anm2 = Anm2();
      if (!save(path, errorString))
      {
        isValid = false;
        this->path.clear();
        return;
      }
    }
    else
    {
      anm2 = Anm2(path, errorString);
      if (!anm2.isValid)
      {
        isValid = false;
        this->path.clear();
        return;
      }
    }

    this->path = path;
    isValid = anm2.isValid;
    clean();
    change(Document::ALL);
  }

  Document::Document(Document&& other) noexcept
      : path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current),
        playback(current.playback), animation(current.animation), event(current.event), frames(current.frames),
        items(current.items), layer(current.layer), merge(current.merge), null(current.null), region(current.region),
        sound(current.sound), spritesheet(current.spritesheet), textures(current.textures), sounds(current.sounds),
        anm2(current.anm2), reference(current.reference), groupReferences(current.groupReferences),
        frameTime(current.frameTime), message(current.message), regionBySpritesheet(std::move(other.regionBySpritesheet)),
        changeAllFramePropertiesRegionId(other.changeAllFramePropertiesRegionId), previewZoom(other.previewZoom),
        previewPan(other.previewPan), editorPan(other.editorPan), editorZoom(other.editorZoom),
        overlayIndex(other.overlayIndex), hash(other.hash), saveHash(other.saveHash), autosaveHash(other.autosaveHash),
        lastAutosaveTime(other.lastAutosaveTime), isValid(other.isValid), isOpen(other.isOpen),
        isForceDirty(other.isForceDirty), spritesheetHashes(std::move(other.spritesheetHashes)),
        spritesheetSaveHashes(std::move(other.spritesheetSaveHashes)), texturePaths(std::move(other.texturePaths)),
        soundPaths(std::move(other.soundPaths)), isAnimationPreviewSet(other.isAnimationPreviewSet),
        isSpritesheetEditorSet(other.isSpritesheetEditorSet)
  {
  }

  Document& Document::operator=(Document&& other) noexcept
  {
    if (this != &other) this->~Document();
    new (this) Document(std::move(other));
    return *this;
  }

  bool Document::save(const std::filesystem::path& path, std::string* errorString, Options options)
  {
    auto absolutePath = !path.empty() ? path : this->path;
    auto absolutePathUtf8 = path::to_utf8(absolutePath);
    if (anm2.save(absolutePath, errorString, options))
    {
      this->path = absolutePath;
      toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePathUtf8)));
      logger.info(
          std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), std::make_format_args(absolutePathUtf8)));
      clean();
      return true;
    }
    else if (errorString)
    {
      toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED),
                               std::make_format_args(absolutePathUtf8, *errorString)));
      logger.error(std::vformat(localize.get(TOAST_SAVE_DOCUMENT_FAILED, anm2ed::ENGLISH),
                                std::make_format_args(absolutePathUtf8, *errorString)));
    }

    return false;
  }

  std::filesystem::path Document::autosave_path_get()
  {
    auto fileNameUtf8 = path::to_utf8(filename_get());
    auto autosaveNameUtf8 = "." + fileNameUtf8 + ".autosave";
    return directory_get() / path::from_utf8(autosaveNameUtf8);
  }

  std::filesystem::path Document::path_from_autosave_get(const std::filesystem::path& path)
  {
    auto fileName = path::to_utf8(path.filename());
    if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin());
    constexpr std::string_view autosaveExtension = ".autosave";
    if (fileName.ends_with(autosaveExtension)) fileName.erase(fileName.size() - autosaveExtension.size());

    auto restorePath = path.parent_path() / std::filesystem::path(std::u8string(fileName.begin(), fileName.end()));

    return restorePath;
  }

  bool Document::autosave(std::string* errorString, Options options)
  {
    auto autosavePath = autosave_path_get();
    auto autosavePathUtf8 = path::to_utf8(autosavePath);
    if (anm2.save(autosavePath, errorString, options))
    {
      autosaveHash = hash;
      lastAutosaveTime = 0.0f;
      toasts.push(localize.get(TOAST_AUTOSAVING));
      logger.info(localize.get(TOAST_AUTOSAVING, anm2ed::ENGLISH));
      logger.info(std::format("Autosaved document to: {}", autosavePathUtf8));
      return true;
    }
    else if (errorString)
    {
      toasts.push(
          std::vformat(localize.get(TOAST_AUTOSAVE_FAILED), std::make_format_args(autosavePathUtf8, *errorString)));
      logger.error(std::vformat(localize.get(TOAST_AUTOSAVE_FAILED, anm2ed::ENGLISH),
                                std::make_format_args(autosavePathUtf8, *errorString)));
    }

    return false;
  }

  void Document::anm2_change(ChangeType type) { change(type); }

  void Document::texture_change(int id)
  {
    auto texture = texture_get(id);
    if (!texture || !anm2.element_get(ElementType::SPRITESHEET, id)) return;
    change(SPRITESHEETS);
  }

  bool Document::texture_reload(int id)
  {
    auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id);
    if (!spritesheet) return false;

    util::WorkingDirectory workingDirectory(directory_get());
    textures[id] = resource::Texture(path::case_insensitive_find(spritesheet->path));
    texturePaths[id] = spritesheet->path;
    return true;
  }

  bool Document::sound_reload(int id)
  {
    auto sound = anm2.element_get(ElementType::SOUND_ELEMENT, id);
    if (!sound) return false;

    util::WorkingDirectory workingDirectory(directory_get());
    sounds[id] = resource::Audio(path::case_insensitive_find(sound->path));
    soundPaths[id] = sound->path;
    return true;
  }

  void Document::assets_sync(ChangeType type)
  {
    if (type == ALL || type == SPRITESHEETS || type == TEXTURES)
    {
      std::set<int> validIds{};
      util::WorkingDirectory workingDirectory(directory_get());
      if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS))
        for (auto& spritesheet : spritesheets->children)
        {
          if (spritesheet.type != ElementType::SPRITESHEET) continue;
          validIds.insert(spritesheet.id);
          auto isReload = !textures.contains(spritesheet.id) || !texturePaths.contains(spritesheet.id) ||
                          texturePaths.at(spritesheet.id) != spritesheet.path;
          if (isReload)
          {
            textures[spritesheet.id] = resource::Texture(path::case_insensitive_find(spritesheet.path));
            texturePaths[spritesheet.id] = spritesheet.path;
          }
        }

      for (auto it = textures.begin(); it != textures.end();)
      {
        if (!validIds.contains(it->first))
        {
          texturePaths.erase(it->first);
          it = textures.erase(it);
        }
        else
          ++it;
      }
    }

    if (type == ALL || type == SOUNDS)
    {
      std::set<int> validIds{};
      util::WorkingDirectory workingDirectory(directory_get());
      if (auto soundItems = anm2.element_get(ElementType::SOUNDS))
        for (auto& sound : soundItems->children)
        {
          if (sound.type != ElementType::SOUND_ELEMENT) continue;
          validIds.insert(sound.id);
          auto isReload =
              !sounds.contains(sound.id) || !soundPaths.contains(sound.id) || soundPaths.at(sound.id) != sound.path;
          if (isReload)
          {
            sounds[sound.id] = resource::Audio(path::case_insensitive_find(sound.path));
            soundPaths[sound.id] = sound.path;
          }
        }

      for (auto it = sounds.begin(); it != sounds.end();)
      {
        if (!validIds.contains(it->first))
        {
          soundPaths.erase(it->first);
          it = sounds.erase(it);
        }
        else
          ++it;
      }
    }
  }

  resource::Texture* Document::texture_get(int id)
  {
    auto it = textures.find(id);
    return it == textures.end() ? nullptr : &it->second;
  }

  const resource::Texture* Document::texture_get(int id) const
  {
    auto it = textures.find(id);
    return it == textures.end() ? nullptr : &it->second;
  }

  resource::Audio* Document::sound_get(int id)
  {
    auto it = sounds.find(id);
    return it == sounds.end() ? nullptr : &it->second;
  }

  const resource::Audio* Document::sound_get(int id) const
  {
    auto it = sounds.find(id);
    return it == sounds.end() ? nullptr : &it->second;
  }

  bool Document::regions_trim(int spritesheetId, const std::set<int>& ids)
  {
    auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, spritesheetId);
    auto texture = texture_get(spritesheetId);
    if (!spritesheet || !texture || !texture->is_valid() || texture->pixels.empty() || ids.empty()) return false;

    bool isChanged{};
    for (auto id : ids)
    {
      auto region = element_child_id_get(*spritesheet, ElementType::REGION, id);
      if (!region) continue;

      auto minPoint = glm::ivec2(glm::min(region->crop, region->crop + region->size));
      auto maxPoint = glm::ivec2(glm::max(region->crop, region->crop + region->size));
      int minX = std::max(0, minPoint.x);
      int minY = std::max(0, minPoint.y);
      int maxX = std::min(texture->size.x, maxPoint.x);
      int maxY = std::min(texture->size.y, maxPoint.y);
      if (minX >= maxX || minY >= maxY) continue;

      int contentMinX = std::numeric_limits<int>::max();
      int contentMinY = std::numeric_limits<int>::max();
      int contentMaxX = std::numeric_limits<int>::min();
      int contentMaxY = std::numeric_limits<int>::min();

      for (int y = minY; y < maxY; ++y)
      {
        for (int x = minX; x < maxX; ++x)
        {
          auto index = ((std::size_t)y * texture->size.x + x) * resource::texture::CHANNELS;
          if (index + resource::texture::CHANNELS > texture->pixels.size()) continue;
          auto r = texture->pixels[index + 0];
          auto g = texture->pixels[index + 1];
          auto b = texture->pixels[index + 2];
          auto a = texture->pixels[index + 3];
          if (r == 0 && g == 0 && b == 0 && a == 0) continue;
          contentMinX = std::min(contentMinX, x);
          contentMinY = std::min(contentMinY, y);
          contentMaxX = std::max(contentMaxX, x);
          contentMaxY = std::max(contentMaxY, y);
        }
      }

      if (contentMinX == std::numeric_limits<int>::max()) continue;

      auto newCrop = glm::vec2(contentMinX, contentMinY);
      auto newSize = glm::vec2(contentMaxX - contentMinX + 1, contentMaxY - contentMinY + 1);
      if (region->crop == newCrop && region->size == newSize) continue;

      auto previousCrop = region->crop;
      region->crop = newCrop;
      region->size = newSize;
      if (region->origin == Origin::TOP_LEFT)
        region->pivot = {};
      else if (region->origin == Origin::CENTER)
        region->pivot = region->size * 0.5f;
      else
        region->pivot -= region->crop - previousCrop;
      isChanged = true;
    }

    return isChanged;
  }

  bool Document::spritesheet_pack(int id, int padding)
  {
    struct RectI
    {
      int x{};
      int y{};
      int w{};
      int h{};
    };

    struct PackItem
    {
      int regionId{-1};
      int srcX{};
      int srcY{};
      int width{};
      int height{};
      int packWidth{};
      int packHeight{};
    };

    class MaxRectsPacker
    {
      int width{};
      int height{};
      std::vector<RectI> freeRects{};

      static bool intersects(const RectI& a, const RectI& b)
      {
        return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y);
      }

      static bool contains(const RectI& a, const RectI& b)
      {
        return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
      }

      void split_free_rects(const RectI& used)
      {
        std::vector<RectI> next{};
        next.reserve(freeRects.size() * 2);
        for (auto& free : freeRects)
        {
          if (!intersects(free, used))
          {
            next.push_back(free);
            continue;
          }
          if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h});
          if (used.x + used.w < free.x + free.w)
            next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h});
          if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y});
          if (used.y + used.h < free.y + free.h)
            next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)});
        }
        freeRects = std::move(next);
      }

      void prune_free_rects()
      {
        for (int i = 0; i < (int)freeRects.size(); ++i)
        {
          if (freeRects[i].w <= 0 || freeRects[i].h <= 0)
          {
            freeRects.erase(freeRects.begin() + i--);
            continue;
          }
          for (int j = i + 1; j < (int)freeRects.size();)
          {
            if (contains(freeRects[i], freeRects[j]))
              freeRects.erase(freeRects.begin() + j);
            else if (contains(freeRects[j], freeRects[i]))
            {
              freeRects.erase(freeRects.begin() + i--);
              break;
            }
            else
              ++j;
          }
        }
      }

    public:
      MaxRectsPacker(int width, int height) : width(width), height(height), freeRects({{0, 0, width, height}}) {}

      bool insert(int width, int height, RectI& result)
      {
        int bestShort = std::numeric_limits<int>::max();
        int bestLong = std::numeric_limits<int>::max();
        RectI best{};
        bool isFound{};
        for (auto& free : freeRects)
        {
          if (width > free.w || height > free.h) continue;
          int leftOverW = free.w - width;
          int leftOverH = free.h - height;
          int shortSide = std::min(leftOverW, leftOverH);
          int longSide = std::max(leftOverW, leftOverH);
          if (shortSide < bestShort || (shortSide == bestShort && longSide < bestLong))
          {
            bestShort = shortSide;
            bestLong = longSide;
            best = {free.x, free.y, width, height};
            isFound = true;
          }
        }
        if (!isFound) return false;
        result = best;
        split_free_rects(best);
        prune_free_rects();
        return true;
      }
    };

    auto pack_regions = [&](const std::vector<PackItem>& items, int& packedWidth, int& packedHeight,
                            std::unordered_map<int, RectI>& packedRects)
    {
      if (items.empty()) return false;

      int maxWidth{};
      int maxHeight{};
      int sumWidth{};
      int sumHeight{};
      std::int64_t totalArea{};
      for (auto& item : items)
      {
        maxWidth = std::max(maxWidth, item.packWidth);
        maxHeight = std::max(maxHeight, item.packHeight);
        sumWidth += item.packWidth;
        sumHeight += item.packHeight;
        totalArea += (std::int64_t)item.packWidth * item.packHeight;
      }
      if (maxWidth <= 0 || maxHeight <= 0) return false;

      int bestSquareDelta = std::numeric_limits<int>::max();
      int bestArea = std::numeric_limits<int>::max();
      std::unordered_map<int, RectI> bestRects{};
      int bestWidth{};
      int bestHeight{};
      int startWidth = maxWidth;
      int endWidth = std::max(startWidth, sumWidth);
      int step = std::max(1, (endWidth - startWidth) / 512);

      for (int candidateWidth = startWidth; candidateWidth <= endWidth; candidateWidth += step)
      {
        int candidateHeightMin = std::max(maxHeight, (int)std::ceil((double)totalArea / candidateWidth));
        bool isValid{};
        int usedWidth{};
        int usedHeight{};
        std::unordered_map<int, RectI> candidateRects{};
        for (int candidateHeight = candidateHeightMin; candidateHeight <= sumHeight; ++candidateHeight)
        {
          MaxRectsPacker packer(candidateWidth, candidateHeight);
          candidateRects.clear();
          isValid = true;
          usedWidth = 0;
          usedHeight = 0;
          for (auto& item : items)
          {
            RectI rect{};
            if (!packer.insert(item.packWidth, item.packHeight, rect))
            {
              isValid = false;
              break;
            }
            candidateRects[item.regionId] = rect;
            usedWidth = std::max(usedWidth, rect.x + rect.w);
            usedHeight = std::max(usedHeight, rect.y + rect.h);
          }
          if (isValid) break;
        }
        if (!isValid) continue;

        int area = usedWidth * usedHeight;
        int squareDelta = std::abs(usedWidth - usedHeight);
        if (squareDelta < bestSquareDelta || (squareDelta == bestSquareDelta && area < bestArea))
        {
          bestSquareDelta = squareDelta;
          bestArea = area;
          bestWidth = usedWidth;
          bestHeight = usedHeight;
          bestRects = std::move(candidateRects);
          if (bestArea == totalArea && bestSquareDelta == 0) break;
        }
      }

      if (bestArea == std::numeric_limits<int>::max()) return false;
      packedWidth = bestWidth;
      packedHeight = bestHeight;
      packedRects = std::move(bestRects);
      return true;
    };

    auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id);
    auto texture = texture_get(id);
    if (!spritesheet || !texture || !texture->is_valid() || texture->pixels.empty()) return false;

    auto packingPadding = std::max(0, padding);
    std::vector<PackItem> items{};
    for (auto& region : spritesheet->children)
    {
      if (region.type != ElementType::REGION) continue;
      auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
      auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
      auto size = glm::max(maxPoint - minPoint, glm::ivec2(1));
      items.push_back({region.id, minPoint.x, minPoint.y, size.x, size.y, size.x + packingPadding * 2,
                       size.y + packingPadding * 2});
    }
    if (items.empty()) return false;

    std::sort(items.begin(), items.end(),
              [](const PackItem& a, const PackItem& b)
              {
                int areaA = a.width * a.height;
                int areaB = b.width * b.height;
                if (areaA != areaB) return areaA > areaB;
                return a.regionId < b.regionId;
              });

    int packedWidth{};
    int packedHeight{};
    std::unordered_map<int, RectI> packedRects{};
    if (!pack_regions(items, packedWidth, packedHeight, packedRects)) return false;
    if (packedWidth <= 0 || packedHeight <= 0) return false;

    std::vector<uint8_t> packedPixels((std::size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0);
    for (auto& item : items)
    {
      if (!packedRects.contains(item.regionId)) continue;
      auto destinationRect = packedRects.at(item.regionId);
      for (int y = 0; y < item.height; ++y)
        for (int x = 0; x < item.width; ++x)
        {
          int sourceX = item.srcX + x;
          int sourceY = item.srcY + y;
          int destinationX = destinationRect.x + packingPadding + x;
          int destinationY = destinationRect.y + packingPadding + y;
          if (sourceX < 0 || sourceY < 0 || sourceX >= texture->size.x || sourceY >= texture->size.y) continue;
          if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight)
            continue;
          auto sourceIndex = ((std::size_t)sourceY * texture->size.x + sourceX) * resource::texture::CHANNELS;
          auto destinationIndex =
              ((std::size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS;
          std::copy_n(texture->pixels.data() + sourceIndex, resource::texture::CHANNELS,
                      packedPixels.data() + destinationIndex);
        }
    }

    textures[id] = resource::Texture(packedPixels.data(), {packedWidth, packedHeight});
    for (auto& region : spritesheet->children)
      if (region.type == ElementType::REGION && packedRects.contains(region.id))
      {
        auto& rect = packedRects.at(region.id);
        region.crop = {rect.x + packingPadding, rect.y + packingPadding};
      }

    assets_sync(SPRITESHEETS);
    return true;
  }

  bool Document::spritesheets_merge(const std::set<int>& ids, bool isAppendRight, bool isMakeRegions,
                                    bool isMakePrimaryRegion, origin::Type regionOrigin)
  {
    if (ids.size() < 2) return false;
    auto baseId = *ids.begin();
    auto base = anm2.element_get(ElementType::SPRITESHEET, baseId);
    auto baseTexture = texture_get(baseId);
    if (!base || !baseTexture || !baseTexture->is_valid()) return false;
    for (auto id : ids)
      if (!anm2.element_get(ElementType::SPRITESHEET, id) || !texture_get(id) || !texture_get(id)->is_valid())
        return false;

    auto origin = regionOrigin == origin::ORIGIN_CENTER ? Origin::CENTER : Origin::TOP_LEFT;
    auto baseTextureSize = baseTexture->size;
    auto mergedTexture = *baseTexture;
    std::unordered_map<int, glm::ivec2> offsets{{baseId, {}}};

    for (auto id : ids)
    {
      if (id == baseId) continue;
      auto texture = texture_get(id);
      offsets[id] = isAppendRight ? glm::ivec2(mergedTexture.size.x, 0) : glm::ivec2(0, mergedTexture.size.y);
      mergedTexture = resource::Texture::merge_append(mergedTexture, *texture, isAppendRight);
    }
    textures[baseId] = std::move(mergedTexture);

    std::unordered_map<int, std::unordered_map<int, int>> regionIdMap{};
    if (isMakeRegions)
    {
      auto add_location_region = [&](int sourceId, glm::ivec2 crop, glm::ivec2 size)
      {
        auto source = anm2.element_get(ElementType::SPRITESHEET, sourceId);
        if (!source) return;
        auto region = element_make(ElementType::REGION);
        region.id = element_child_next_id_get(*base, ElementType::REGION);
        auto stem = path::to_utf8(source->path.stem());
        region.name = stem.empty() ? std::format("#{}", sourceId) : stem;
        region.crop = crop;
        region.size = size;
        region.pivot = origin == Origin::CENTER ? glm::vec2(size) * 0.5f : glm::vec2();
        region.origin = origin;
        base->children.push_back(region);
      };

      if (isMakePrimaryRegion) add_location_region(baseId, {}, baseTextureSize);
      for (auto id : ids)
      {
        if (id == baseId) continue;
        auto source = anm2.element_get(ElementType::SPRITESHEET, id);
        auto sourceTexture = texture_get(id);
        auto sheetOffset = offsets.at(id);
        add_location_region(id, sheetOffset, sourceTexture->size);
        for (auto& sourceRegion : source->children)
        {
          if (sourceRegion.type != ElementType::REGION) continue;
          auto destinationRegion = sourceRegion;
          destinationRegion.id = element_child_next_id_get(*base, ElementType::REGION);
          destinationRegion.crop += sheetOffset;
          base->children.push_back(destinationRegion);
          regionIdMap[id][sourceRegion.id] = destinationRegion.id;
        }
      }
    }

    std::unordered_map<int, int> layerSpritesheetBefore{};
    if (auto layers = anm2.element_get(ElementType::LAYERS))
      for (auto& layer : layers->children)
        if (layer.type == ElementType::LAYER_ELEMENT && ids.contains(layer.spritesheetId))
        {
          layerSpritesheetBefore[layer.id] = layer.spritesheetId;
          layer.spritesheetId = baseId;
        }

    if (auto animations = anm2.element_get(ElementType::ANIMATIONS))
      for (auto& animation : animations->children)
      {
        if (animation.type != ElementType::ANIMATION) continue;
        auto layerAnimations = element_child_first_get(animation, ElementType::LAYER_ANIMATIONS);
        if (!layerAnimations) continue;
        auto layer_animation_update = [&](auto&& self, Element& layerAnimation) -> void
        {
          if (layerAnimation.type == ElementType::GROUP)
          {
            for (auto& child : layerAnimation.children)
              self(self, child);
            return;
          }
          if (layerAnimation.type != ElementType::LAYER_ANIMATION ||
              !layerSpritesheetBefore.contains(layerAnimation.layerId))
            return;
          auto sourceSpritesheetId = layerSpritesheetBefore.at(layerAnimation.layerId);
          if (sourceSpritesheetId == baseId) return;
          for (auto& frame : layerAnimation.children)
          {
            if (frame.type != ElementType::FRAME || frame.regionId == -1) continue;
            if (isMakeRegions && regionIdMap.contains(sourceSpritesheetId) &&
                regionIdMap.at(sourceSpritesheetId).contains(frame.regionId))
              frame.regionId = regionIdMap.at(sourceSpritesheetId).at(frame.regionId);
            else
              frame.regionId = -1;
          }
        };
        for (auto& layerAnimation : layerAnimations->children)
          layer_animation_update(layer_animation_update, layerAnimation);
      }

    auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS);
    if (!spritesheets) return false;
    for (auto id : ids)
      if (id != baseId)
      {
        element_child_id_erase(*spritesheets, ElementType::SPRITESHEET, id);
        textures.erase(id);
      }

    assets_sync(ALL);
    return true;
  }

  void Document::scan_and_set_regions()
  {
    auto animations = anm2.element_get(ElementType::ANIMATIONS);
    if (animations)
    {
      for (auto& animation : animations->children)
      {
        if (animation.type != ElementType::ANIMATION) continue;
        auto layerAnimations = element_child_first_get(animation, ElementType::LAYER_ANIMATIONS);
        if (!layerAnimations) continue;
        auto layer_animation_update = [&](auto&& self, Element& layerAnimation) -> void
        {
          if (layerAnimation.type == ElementType::GROUP)
          {
            for (auto& child : layerAnimation.children)
              self(self, child);
            return;
          }
          if (layerAnimation.type != ElementType::LAYER_ANIMATION) return;
          auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, layerAnimation.layerId);
          auto spritesheet = layer ? anm2.element_get(ElementType::SPRITESHEET, layer->spritesheetId) : nullptr;
          if (!spritesheet) return;
          for (auto& frame : layerAnimation.children)
          {
            if (frame.type != ElementType::FRAME || frame.regionId != -1) continue;
            auto frameCrop = glm::ivec2(frame.crop);
            auto frameSize = glm::ivec2(frame.size);
            auto framePivot = glm::ivec2(frame.pivot);
            for (const auto& region : spritesheet->children)
              if (region.type == ElementType::REGION && glm::ivec2(region.crop) == frameCrop &&
                  glm::ivec2(region.size) == frameSize && glm::ivec2(region.pivot) == framePivot)
              {
                frame.regionId = region.id;
                break;
              }
          }
        };
        for (auto& layerAnimation : layerAnimations->children)
          layer_animation_update(layer_animation_update, layerAnimation);
      }
    }
  }

  bool Document::file_merge(const std::filesystem::path& path)
  {
    Anm2 source(path);
    if (!source.isValid) return false;

    auto remap_path = [&](const std::filesystem::path& original) -> std::filesystem::path
    {
      if (directory_get().empty()) return original;
      std::error_code ec{};
      std::filesystem::path absolute{};
      bool isAbsolute{};

      if (!original.empty())
      {
        if (original.is_absolute())
        {
          absolute = original;
          isAbsolute = true;
        }
        else
        {
          absolute = std::filesystem::weakly_canonical(path.parent_path() / original, ec);
          if (ec)
          {
            ec.clear();
            absolute = path.parent_path() / original;
          }
          isAbsolute = true;
        }
      }

      if (!isAbsolute) return original;
      auto relative = std::filesystem::relative(absolute, directory_get(), ec);
      if (!ec) return relative;
      return original.empty() ? absolute : original;
    };

    auto remap_id = [](const std::unordered_map<int, int>& remap, int id)
    {
      auto it = remap.find(id);
      return it == remap.end() ? -1 : it->second;
    };

    auto find_by_name = [](Element& container, ElementType type, const std::string& name)
    {
      for (auto& child : container.children)
        if (child.type == type && child.name == name) return &child;
      return (Element*)nullptr;
    };

    auto child_set = [](Element& container, Element child, ElementType type)
    {
      if (auto existing = element_child_first_get(container, type))
        *existing = std::move(child);
      else
        container.children.push_back(std::move(child));
    };

    auto track_find = [](Element& container, const Element& source)
    {
      auto find = [](auto&& self, Element& parent, const Element& source) -> Element*
      {
        for (auto& track : parent.children)
        {
          if (track.type == ElementType::GROUP)
            if (auto result = self(self, track, source)) return result;
          if (track.type != source.type) continue;
          if (track.type == ElementType::LAYER_ANIMATION && track.layerId == source.layerId) return &track;
          if (track.type == ElementType::NULL_ANIMATION && track.nullId == source.nullId) return &track;
        }
        return (Element*)nullptr;
      };
      return find(find, container, source);
    };

    auto track_container_get = [](Element& animation, ElementType type)
    {
      if (auto container = element_child_first_get(animation, type)) return container;
      animation.children.push_back(element_make(type));
      return &animation.children.back();
    };

    std::unordered_map<int, int> spritesheetRemap{};
    std::unordered_map<int, int> layerRemap{};
    std::unordered_map<int, int> nullRemap{};
    std::unordered_map<int, int> eventRemap{};
    std::unordered_map<int, int> soundRemap{};

    if (auto sourceSpritesheets = source.element_get(ElementType::SPRITESHEETS))
      if (auto destinationSpritesheets = anm2.element_get(ElementType::SPRITESHEETS))
        for (auto spritesheet : sourceSpritesheets->children)
        {
          if (spritesheet.type != ElementType::SPRITESHEET) continue;
          auto sourceId = spritesheet.id;
          spritesheet.id = element_child_next_id_get(*destinationSpritesheets, ElementType::SPRITESHEET);
          spritesheet.path = remap_path(spritesheet.path);
          destinationSpritesheets->children.push_back(spritesheet);
          spritesheetRemap[sourceId] = spritesheet.id;
        }

    if (auto sourceSounds = source.element_get(ElementType::SOUNDS))
      if (auto destinationSounds = anm2.element_get(ElementType::SOUNDS))
        for (auto sound : sourceSounds->children)
        {
          if (sound.type != ElementType::SOUND_ELEMENT) continue;
          auto sourceId = sound.id;
          sound.path = remap_path(sound.path);
          int destinationId{-1};
          for (auto& existing : destinationSounds->children)
            if (existing.type == ElementType::SOUND_ELEMENT && existing.path == sound.path)
            {
              destinationId = existing.id;
              sound.id = destinationId;
              existing = sound;
              break;
            }
          if (destinationId == -1)
          {
            destinationId = element_child_next_id_get(*destinationSounds, ElementType::SOUND_ELEMENT);
            sound.id = destinationId;
            destinationSounds->children.push_back(sound);
          }
          soundRemap[sourceId] = destinationId;
        }

    if (auto sourceLayers = source.element_get(ElementType::LAYERS))
      if (auto destinationLayers = anm2.element_get(ElementType::LAYERS))
        for (auto layer : sourceLayers->children)
        {
          if (layer.type != ElementType::LAYER_ELEMENT) continue;
          auto sourceId = layer.id;
          layer.spritesheetId = remap_id(spritesheetRemap, layer.spritesheetId);
          if (auto existing = find_by_name(*destinationLayers, ElementType::LAYER_ELEMENT, layer.name))
          {
            layer.id = existing->id;
            *existing = layer;
          }
          else
          {
            layer.id = element_child_next_id_get(*destinationLayers, ElementType::LAYER_ELEMENT);
            destinationLayers->children.push_back(layer);
          }
          layerRemap[sourceId] = layer.id;
        }

    if (auto sourceNulls = source.element_get(ElementType::NULLS))
      if (auto destinationNulls = anm2.element_get(ElementType::NULLS))
        for (auto null : sourceNulls->children)
        {
          if (null.type != ElementType::NULL_ELEMENT) continue;
          auto sourceId = null.id;
          if (auto existing = find_by_name(*destinationNulls, ElementType::NULL_ELEMENT, null.name))
          {
            null.id = existing->id;
            *existing = null;
          }
          else
          {
            null.id = element_child_next_id_get(*destinationNulls, ElementType::NULL_ELEMENT);
            destinationNulls->children.push_back(null);
          }
          nullRemap[sourceId] = null.id;
        }

    if (auto sourceEvents = source.element_get(ElementType::EVENTS))
      if (auto destinationEvents = anm2.element_get(ElementType::EVENTS))
        for (auto event : sourceEvents->children)
        {
          if (event.type != ElementType::EVENT_ELEMENT) continue;
          auto sourceId = event.id;
          if (auto existing = find_by_name(*destinationEvents, ElementType::EVENT_ELEMENT, event.name))
          {
            event.id = existing->id;
            *existing = event;
          }
          else
          {
            event.id = element_child_next_id_get(*destinationEvents, ElementType::EVENT_ELEMENT);
            destinationEvents->children.push_back(event);
          }
          eventRemap[sourceId] = event.id;
        }

    auto item_remap = [&](Element& item)
    {
      for (auto& frame : item.children)
      {
        if (frame.type != ElementType::FRAME && frame.type != ElementType::TRIGGER) continue;
        for (auto& soundId : frame.soundIds)
          soundId = remap_id(soundRemap, soundId);
        frame.eventId = remap_id(eventRemap, frame.eventId);
      }
    };

    auto track_tree_remap = [&](auto&& self, Element item, int itemType, Element& container, int parentGroupId = -1) -> void
    {
      auto trackType = itemType == LAYER ? ElementType::LAYER_ANIMATION : ElementType::NULL_ANIMATION;
      if (item.type == ElementType::GROUP)
      {
        auto group = element_make(ElementType::GROUP);
        group.id = item.id == -1 ? element_child_next_id_get(container, ElementType::GROUP) : item.id;
        group.name = item.name;
        group.isExpanded = item.isExpanded;
        container.children.push_back(group);
        for (auto child : item.children)
          self(self, child, itemType, container, group.id);
        return;
      }

      if (item.type != trackType) return;
      if (itemType == LAYER)
        item.layerId = remap_id(layerRemap, item.layerId);
      else
        item.nullId = remap_id(nullRemap, item.nullId);
      if ((itemType == LAYER && item.layerId < 0) || (itemType == NULL_ && item.nullId < 0)) return;
      if (parentGroupId != -1) item.groupId = parentGroupId;
      item_remap(item);
      container.children.push_back(item);
    };

    auto animation_build = [&](const Element& incoming)
    {
      auto animation = element_make(ElementType::ANIMATION);
      animation.name = incoming.name;
      animation.frameNum = incoming.frameNum;
      animation.isLoop = incoming.isLoop;

      if (auto root = element_child_first_get(incoming, ElementType::ROOT_ANIMATION))
      {
        auto item = *root;
        item_remap(item);
        animation.children.push_back(item);
      }

      if (auto layerAnimations = element_child_first_get(incoming, ElementType::LAYER_ANIMATIONS))
      {
        auto container = element_make(ElementType::LAYER_ANIMATIONS);
        for (auto item : layerAnimations->children)
          track_tree_remap(track_tree_remap, item, LAYER, container);
        animation.children.push_back(container);
      }

      if (auto nullAnimations = element_child_first_get(incoming, ElementType::NULL_ANIMATIONS))
      {
        auto container = element_make(ElementType::NULL_ANIMATIONS);
        for (auto item : nullAnimations->children)
          track_tree_remap(track_tree_remap, item, NULL_, container);
        animation.children.push_back(container);
      }

      if (auto triggers = element_child_first_get(incoming, ElementType::TRIGGERS))
      {
        auto item = *triggers;
        item_remap(item);
        animation.children.push_back(item);
      }

      return animation;
    };

    auto track_merge = [&](Element& destinationContainer, const Element& sourceTrack)
    {
      if (auto destinationTrack = track_find(destinationContainer, sourceTrack))
      {
        if (!sourceTrack.children.empty()) *destinationTrack = sourceTrack;
      }
      else
        destinationContainer.children.push_back(sourceTrack);
    };

    auto track_container_merge = [&](Element& destinationContainer, const Element& sourceContainer, ElementType trackType)
    {
      std::unordered_map<int, int> groupRemap{};
      for (auto item : sourceContainer.children)
      {
        if (item.type != ElementType::GROUP) continue;
        auto sourceGroupId = item.id;
        item.id = element_child_next_id_get(destinationContainer, ElementType::GROUP);
        item.children.clear();
        destinationContainer.children.push_back(item);
        groupRemap[sourceGroupId] = item.id;
      }

      for (auto item : sourceContainer.children)
      {
        if (item.type != trackType) continue;
        if (item.groupId != -1)
          item.groupId = groupRemap.contains(item.groupId) ? groupRemap.at(item.groupId) : -1;
        track_merge(destinationContainer, item);
      }
    };

    if (auto sourceAnimations = source.element_get(ElementType::ANIMATIONS))
      if (auto destinationAnimations = anm2.element_get(ElementType::ANIMATIONS))
      {
        for (const auto& incoming : sourceAnimations->children)
        {
          if (incoming.type != ElementType::ANIMATION) continue;
          auto processed = animation_build(incoming);
          auto destination = find_by_name(*destinationAnimations, ElementType::ANIMATION, processed.name);
          if (!destination)
          {
            destinationAnimations->children.push_back(processed);
            continue;
          }

          destination->frameNum = std::max(destination->frameNum, processed.frameNum);
          destination->isLoop = processed.isLoop;
          if (auto root = element_child_first_get(processed, ElementType::ROOT_ANIMATION);
              root && !root->children.empty())
            child_set(*destination, *root, ElementType::ROOT_ANIMATION);
          if (auto triggers = element_child_first_get(processed, ElementType::TRIGGERS);
              triggers && !triggers->children.empty())
            child_set(*destination, *triggers, ElementType::TRIGGERS);

          if (auto layerAnimations = element_child_first_get(processed, ElementType::LAYER_ANIMATIONS))
          {
            auto destinationLayerAnimations = track_container_get(*destination, ElementType::LAYER_ANIMATIONS);
            track_container_merge(*destinationLayerAnimations, *layerAnimations, ElementType::LAYER_ANIMATION);
          }

          if (auto nullAnimations = element_child_first_get(processed, ElementType::NULL_ANIMATIONS))
          {
            auto destinationNullAnimations = track_container_get(*destination, ElementType::NULL_ANIMATIONS);
            track_container_merge(*destinationNullAnimations, *nullAnimations, ElementType::NULL_ANIMATION);
          }

          destination->frameNum = std::max(destination->frameNum, animation_length_get(*destination));
        }

        if (destinationAnimations->defaultAnimation.empty() && !sourceAnimations->defaultAnimation.empty())
          destinationAnimations->defaultAnimation = sourceAnimations->defaultAnimation;
      }

    assets_sync(ALL);
    return true;
  }

  void Document::hash_set() { hash = anm2.hash(); }

  void Document::clean()
  {
    assets_sync();
    saveHash = anm2.hash();
    hash = saveHash;
    lastAutosaveTime = 0.0f;
    isForceDirty = false;
  }

  void Document::spritesheet_hashes_reset()
  {
    spritesheetHashes.clear();
    spritesheetSaveHashes.clear();
    if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS))
      for (auto& spritesheet : spritesheets->children)
        if (spritesheet.type == ElementType::SPRITESHEET)
        {
          auto currentHash = document::spritesheet_hash_get(spritesheet, texture_get(spritesheet.id));
          spritesheetHashes[spritesheet.id] = currentHash;
          spritesheetSaveHashes[spritesheet.id] = currentHash;
        }
  }

  void Document::spritesheet_hashes_sync()
  {
    std::set<int> validIds{};
    if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS))
      for (auto& spritesheet : spritesheets->children)
        if (spritesheet.type == ElementType::SPRITESHEET) validIds.insert(spritesheet.id);

    for (auto it = spritesheetHashes.begin(); it != spritesheetHashes.end();)
    {
      if (!validIds.contains(it->first))
        it = spritesheetHashes.erase(it);
      else
        ++it;
    }

    for (auto it = spritesheetSaveHashes.begin(); it != spritesheetSaveHashes.end();)
    {
      if (!validIds.contains(it->first))
        it = spritesheetSaveHashes.erase(it);
      else
        ++it;
    }

    if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS))
      for (auto& spritesheet : spritesheets->children)
        if (spritesheet.type == ElementType::SPRITESHEET)
        {
          auto currentHash = document::spritesheet_hash_get(spritesheet, texture_get(spritesheet.id));
          spritesheetHashes[spritesheet.id] = currentHash;
          if (!spritesheetSaveHashes.contains(spritesheet.id)) spritesheetSaveHashes[spritesheet.id] = currentHash;
        }
  }

  void Document::change(ChangeType type)
  {
    hash_set();
    assets_sync(type);

    auto events_set = [&]()
    {
      auto events = anm2.element_get(ElementType::EVENTS);
      event.labels_set(document::element_name_labels_get(events, ElementType::EVENT_ELEMENT, true),
                       document::element_ids_get(events, ElementType::EVENT_ELEMENT, true));
    };

    auto animations_set = [&]() { animation.labels_set(document::animation_labels_get(anm2)); };

    auto spritesheets_set = [&]()
    {
      spritesheet.labels_set(document::spritesheet_labels_get(anm2), document::spritesheet_ids_get(anm2));
      spritesheet_hashes_sync();
    };

    auto sounds_set = [&]() { sound.labels_set(document::sound_labels_get(anm2), document::sound_ids_get(anm2)); };

    auto regions_set = [&]()
    {
      regionBySpritesheet.clear();
      if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS))
        for (auto& spritesheet : spritesheets->children)
          if (spritesheet.type == ElementType::SPRITESHEET)
          {
            Storage storage{};
            storage.labels_set(document::element_name_labels_get(&spritesheet, ElementType::REGION, true),
                               document::element_ids_get(&spritesheet, ElementType::REGION, true));
            regionBySpritesheet.emplace(spritesheet.id, std::move(storage));
          }
    };

    switch (type)
    {
      case LAYERS:
        break;
      case NULLS:
        break;
      case EVENTS:
        events_set();
        break;
      case SPRITESHEETS:
        spritesheets_set();
        regions_set();
        break;
      case SOUNDS:
        sounds_set();
        break;
      case FRAMES:
        events_set();
        sounds_set();
        break;
      case ITEMS:
        spritesheets_set();
        break;
      case ANIMATIONS:
      case ALL:
        events_set();
        spritesheets_set();
        regions_set();
        animations_set();
        sounds_set();
        break;
      default:
        break;
    }
  }

  bool Document::is_dirty() const { return hash != saveHash; }
  bool Document::is_autosave_dirty() const { return hash != autosaveHash; }
  void Document::spritesheet_hash_update(int id)
  {
    auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id);
    if (!spritesheet) return;
    assets_sync(TEXTURES);
    spritesheetHashes[id] = document::spritesheet_hash_get(*spritesheet, texture_get(id));
  }

  void Document::spritesheet_hash_set_saved(int id)
  {
    auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, id);
    if (!spritesheet) return;
    assets_sync(TEXTURES);
    auto currentHash = document::spritesheet_hash_get(*spritesheet, texture_get(id));
    spritesheetHashes[id] = currentHash;
    spritesheetSaveHashes[id] = currentHash;
  }

  bool Document::spritesheet_is_dirty(int id)
  {
    if (!anm2.element_get(ElementType::SPRITESHEET, id)) return false;
    if (!spritesheetHashes.contains(id)) spritesheet_hash_update(id);
    auto saveIt = spritesheetSaveHashes.find(id);
    if (saveIt == spritesheetSaveHashes.end()) return false;
    return spritesheetHashes.at(id) != saveIt->second;
  }

  bool Document::spritesheet_any_dirty()
  {
    if (auto spritesheets = anm2.element_get(ElementType::SPRITESHEETS))
      for (auto& spritesheet : spritesheets->children)
        if (spritesheet.type == ElementType::SPRITESHEET && spritesheet_is_dirty(spritesheet.id)) return true;
    return false;
  }
  std::filesystem::path Document::directory_get() const { return path.parent_path(); }
  std::filesystem::path Document::filename_get() const { return path.filename(); }
  bool Document::is_valid() const { return isValid && !path.empty(); }

  void Document::command_run(Manager& manager, Command& command)
  {
    if (command.run) command.run(manager, *this);
  }

  std::vector<Reference> Document::layer_references_get()
  {
    std::set<Reference> selectedReferences = items.references;
    if (selectedReferences.empty())
      for (auto frameReference : frames.references)
      {
        frameReference.frameIndex = -1;
        selectedReferences.insert(frameReference);
      }
    if (selectedReferences.empty() && !frames.selection.empty())
      selectedReferences.insert({reference.animationIndex, reference.itemType, reference.itemID});
    if (selectedReferences.empty() && reference.itemType != NONE)
      selectedReferences.insert({reference.animationIndex, reference.itemType, reference.itemID});

    std::vector<Reference> result{};
    for (auto itemReference : selectedReferences)
    {
      itemReference.frameIndex = -1;
      if (itemReference.itemType != LAYER) return {};
      if (!anm2.element_get(itemReference.animationIndex, ItemType::LAYER, itemReference.itemID))
      {
        if (reference.animationIndex == itemReference.animationIndex ||
            !anm2.element_get(reference.animationIndex, ItemType::LAYER, itemReference.itemID))
          return {};
        itemReference.animationIndex = reference.animationIndex;
      }
      result.push_back(itemReference);
    }
    return result;
  }

  Element* Document::frame_get()
  {
    return anm2.element_get(reference.animationIndex, document::item_type_get(reference.itemType), reference.frameIndex,
                            reference.itemID);
  }

  Element* Document::item_get()
  {
    return anm2.element_get(reference.animationIndex, document::item_type_get(reference.itemType), reference.itemID);
  }
  Element* Document::animation_get() { return anm2.element_get(ElementType::ANIMATION, reference.animationIndex); }
  Element* Document::spritesheet_get() { return anm2.element_get(ElementType::SPRITESHEET, spritesheet.reference); }

  void Document::spritesheet_add(const std::filesystem::path& path)
  {
    spritesheets_add({path});
  }

  void Document::spritesheets_add(const std::vector<std::filesystem::path>& paths)
  {
    struct LoadedSpritesheet
    {
      std::filesystem::path relativePath{};
      resource::Texture texture{};
    };

    auto items = anm2.element_get(ElementType::SPRITESHEETS);
    if (!items) return;
    auto directory = directory_get();

    std::vector<LoadedSpritesheet> loaded{};
    for (auto& path : paths)
    {
      auto pathCopy = path;
      auto storagePath = path::backslash_handle(pathCopy);
      std::optional<WorkingDirectory> workingDirectory{};
      if (!storagePath.is_absolute()) workingDirectory.emplace(directory);
      auto loadPath = path::case_insensitive_find(storagePath);
      auto texture = resource::Texture(loadPath);
      if (!texture.is_valid())
      {
        auto pathUtf8 = path::to_utf8(pathCopy);
        toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED), std::make_format_args(pathUtf8)));
        logger.error(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED, anm2ed::ENGLISH),
                                  std::make_format_args(pathUtf8)));
        continue;
      }

      loaded.push_back({.relativePath = path::backslash_replace(path::make_relative(storagePath, directory)),
                        .texture = std::move(texture)});
    }
    if (loaded.empty()) return;

    snapshot(localize.get(EDIT_ADD_SPRITESHEET));

    std::set<int> added{};
    for (auto& loadedSpritesheet : loaded)
    {
      auto id = element_child_next_id_get(*items, ElementType::SPRITESHEET);
      auto spritesheet = element_make(ElementType::SPRITESHEET);
      spritesheet.id = id;
      spritesheet.path = loadedSpritesheet.relativePath;
      auto pathString = path::to_utf8(spritesheet.path);
      items->children.push_back(spritesheet);
      textures[id] = std::move(loadedSpritesheet.texture);
      texturePaths[id] = spritesheet.path;
      added.insert(id);
      this->spritesheet.reference = id;
      spritesheet_hash_set_saved(id);
      toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, pathString)));
      logger.info(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED, anm2ed::ENGLISH),
                               std::make_format_args(id, pathString)));
    }
    this->spritesheet.selection = added;
    change(Document::SPRITESHEETS);
  }

  void Document::sound_add(const std::filesystem::path& path)
  {
    sounds_add({path});
  }

  void Document::sounds_add(const std::vector<std::filesystem::path>& paths)
  {
    auto items = anm2.element_get(ElementType::SOUNDS);
    if (!items) return;
    auto directory = directory_get();

    std::vector<std::filesystem::path> validPaths{};
    for (auto& path : paths)
      if (!path.empty()) validPaths.push_back(path);
    if (validPaths.empty()) return;

    snapshot(localize.get(EDIT_ADD_SOUND));

    std::set<int> added{};
    for (auto& path : validPaths)
    {
      auto id = element_child_next_id_get(*items, ElementType::SOUND_ELEMENT);
      auto soundElement = element_make(ElementType::SOUND_ELEMENT);
      soundElement.id = id;
      auto loadPath = path::backslash_handle(path);
      auto relativePath = path::backslash_replace(path::make_relative(loadPath, directory));
      {
        WorkingDirectory workingDirectory(directory_get());
        soundElement.path = relativePath;
        sounds[id] = resource::Audio(path::case_insensitive_find(soundElement.path));
        soundPaths[id] = soundElement.path;
      }
      auto soundPath = path::to_utf8(soundElement.path);
      items->children.push_back(soundElement);
      added.insert(id);
      sound.reference = id;
      toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, soundPath)));
      logger.info(
          std::vformat(localize.get(TOAST_SOUND_INITIALIZED, anm2ed::ENGLISH), std::make_format_args(id, soundPath)));
    }
    sound.selection = added;
    change(Document::SOUNDS);
  }

  void Document::snapshot(const std::string& message)
  {
    this->message = message;
    snapshots.push(current);
  }

  void Document::undo()
  {
    if (!snapshots.undo()) return;
    document::restored_snapshot_sanitize(*this);
    toasts.push(std::vformat(localize.get(TOAST_UNDO), std::make_format_args(message)));
    logger.info(std::vformat(localize.get(TOAST_UNDO, anm2ed::ENGLISH), std::make_format_args(message)));
    change(Document::ALL);
  }

  void Document::redo()
  {
    if (!snapshots.redo()) return;
    document::restored_snapshot_sanitize(*this);
    toasts.push(std::vformat(localize.get(TOAST_REDO), std::make_format_args(message)));
    logger.info(std::vformat(localize.get(TOAST_REDO, anm2ed::ENGLISH), std::make_format_args(message)));
    change(Document::ALL);
  }

  bool Document::is_able_to_undo() { return !snapshots.undoStack.is_empty(); }
  bool Document::is_able_to_redo() { return !snapshots.redoStack.is_empty(); }
}
