#include "document.h"

#include <utility>

#include "log.h"
#include "toast.h"

using namespace anm2ed::anm2;
using namespace anm2ed::imgui;
using namespace anm2ed::types;

using namespace glm;

namespace anm2ed
{
  Document::Document(Anm2& anm2, const std::string& path)
  {
    this->anm2 = std::move(anm2);
    this->path = path;
    clean();
    change(Document::ALL);
  }

  Document::Document(const std::string& path, bool isNew, std::string* errorString)
  {
    if (isNew)
    {
      anm2 = anm2::Anm2();
      if (!save(path)) return;
    }
    else
    {
      anm2 = Anm2(path, errorString);
      if (errorString && !errorString->empty()) return;
    }

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

  Document::Document(Document&& other) noexcept
      : path(std::move(other.path)), snapshots(std::move(other.snapshots)), current(snapshots.current),
        anm2(current.anm2), reference(current.reference), playback(current.playback), animation(current.animation),
        merge(current.merge), event(current.event), layer(current.layer), null(current.null), sound(current.sound),
        spritesheet(current.spritesheet), frames(current.frames), message(current.message),
        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), isOpen(other.isOpen),
        isForceDirty(other.isForceDirty), isAnimationPreviewSet(other.isAnimationPreviewSet),
        isSpritesheetEditorSet(other.isSpritesheetEditorSet)
  {
  }

  Document& Document::operator=(Document&& other) noexcept
  {
    if (this != &other)
    {
      path = std::move(other.path);
      snapshots = std::move(other.snapshots);
      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;
      isOpen = other.isOpen;
      isForceDirty = other.isForceDirty;
      isAnimationPreviewSet = other.isAnimationPreviewSet;
      isSpritesheetEditorSet = other.isSpritesheetEditorSet;
    }
    return *this;
  }

  bool Document::save(const std::string& path, std::string* errorString)
  {
    this->path = !path.empty() ? path : this->path.string();

    if (anm2.serialize(this->path.string(), errorString))
    {
      toasts.info(std::format("Saved document to: {}", this->path.string()));
      clean();
      return true;
    }
    else if (errorString)
      toasts.warning(std::format("Could not save document to: {} ({})", this->path.string(), *errorString));

    return false;
  }

  std::filesystem::path Document::autosave_path_get()
  {
    return directory_get() / std::string("." + filename_get().string() + ".autosave");
  }

  std::filesystem::path Document::path_from_autosave_get(std::filesystem::path& path)
  {
    auto fileName = path.filename().string();
    if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin());

    auto restorePath = path.parent_path() / fileName;
    restorePath.replace_extension("");

    return path;
  }

  bool Document::autosave(std::string* errorString)
  {
    auto autosavePath = autosave_path_get();
    if (anm2.serialize(autosavePath.string(), errorString))
    {
      autosaveHash = hash;
      lastAutosaveTime = 0.0f;
      toasts.info("Autosaving...");
      logger.info(std::format("Autosaved document to: {}", autosavePath.string()));
      return true;
    }
    else if (errorString)
      toasts.warning(std::format("Could not autosave document to: {} ({})", autosavePath.string(), *errorString));

    return false;
  }

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

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

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

    auto layers_set = [&]() { layer.unused = anm2.layers_unused(); };
    auto nulls_set = [&]() { null.unused = anm2.nulls_unused(); };
    auto events_set = [&]()
    {
      event.unused = anm2.events_unused();
      event.labels_set(anm2.event_labels_get());
    };

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

    auto spritesheets_set = [&]()
    {
      spritesheet.unused = anm2.spritesheets_unused();
      spritesheet.labels_set(anm2.spritesheet_labels_get());
    };

    auto sounds_set = [&]()
    {
      sound.unused = anm2.sounds_unused();
      sound.labels_set(anm2.sound_labels_get());

      for (auto& animation : anm2.animations.items)
        for (auto& trigger : animation.triggers.frames)
          if (!anm2.content.sounds.contains(trigger.soundID)) trigger.soundID = -1;
    };

    switch (type)
    {
      case LAYERS:
        layers_set();
        break;
      case NULLS:
        nulls_set();
        break;
      case EVENTS:
        events_set();
        break;
      case SPRITESHEETS:
        spritesheets_set();
        break;
      case SOUNDS:
        sounds_set();
        break;
      case ANIMATIONS:
        animations_set();
        break;
      case FRAMES:
        events_set();
        sounds_set();
        break;
      case ALL:
        layers_set();
        nulls_set();
        events_set();
        spritesheets_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; }
  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 !path.empty(); }

  anm2::Frame* Document::frame_get()
  {
    return anm2.frame_get(reference.animationIndex, reference.itemType, reference.frameIndex, reference.itemID);
  }

  anm2::Item* Document::item_get()
  {
    return anm2.item_get(reference.animationIndex, reference.itemType, reference.itemID);
  }
  anm2::Animation* Document::animation_get() { return anm2.animation_get(reference.animationIndex); }
  anm2::Spritesheet* Document::spritesheet_get() { return anm2.spritesheet_get(spritesheet.reference); }

  void Document::spritesheet_add(const std::string& path)
  {
    auto add = [&]()
    {
      int id{};
      if (anm2.spritesheet_add(directory_get().string(), path, id))
      {
        anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id];
        this->spritesheet.selection = {id};
        this->spritesheet.reference = id;
        toasts.info(std::format("Initialized spritesheet #{}: {}", id, spritesheet.path.string()));
      }
      else
        toasts.error(std::format("Failed to initialize spritesheet: {}", path));
    };

    DOCUMENT_EDIT_PTR(this, "Add Spritesheet", Document::SPRITESHEETS, add());
  }

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

  void Document::undo()
  {
    snapshots.undo();
    toasts.info(std::format("Undo: {}", message));
    change(Document::ALL);
  }

  void Document::redo()
  {
    snapshots.redo();
    toasts.info(std::format("Redo: {}", 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(); }
}
