// Aseprite
// Copyright (C) 2019-2025  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

// Uncomment this in case you want to debug range ops
// #define TRACE_RANGE_OPS

#ifdef HAVE_CONFIG_H
  #include "config.h"
#endif

#include "app/doc_range_ops.h"

#include "app/app.h"
#include "app/context_access.h"
#include "app/doc_api.h"
#include "app/doc_range.h"
#include "app/transaction.h"
#include "app/tx.h"
#include "doc/layer.h"
#include "doc/sprite.h"

#include <stdexcept>

#ifdef TRACE_RANGE_OPS
  #include <iostream>
#endif

namespace app {

enum Op { Move, Copy };

static void move_or_copy_cels(DocApi& api,
                              Op op,
                              const LayerList& srcLayers,
                              const LayerList& dstLayers,
                              const SelectedFrames& srcFrames,
                              const SelectedFrames& dstFrames)
{
  ASSERT(srcLayers.size() == dstLayers.size());

  for (layer_t i = 0; i < srcLayers.size(); ++i) {
    auto srcFrame = srcFrames.begin();
    auto dstFrame = dstFrames.begin();
    auto srcFrameEnd = srcFrames.end();
    auto dstFrameEnd = dstFrames.end();

    for (; srcFrame != srcFrameEnd && dstFrame != dstFrameEnd; ++srcFrame, ++dstFrame) {
      if (i >= 0 && i < srcLayers.size() && srcLayers[i]->isImage()) {
        LayerImage* srcLayer = static_cast<LayerImage*>(srcLayers[i]);

        if (i < dstLayers.size() && dstLayers[i]->isImage()) {
          LayerImage* dstLayer = static_cast<LayerImage*>(dstLayers[i]);

#ifdef TRACE_RANGE_OPS
          std::clog << (op == Move ? "Moving" : "Copying") << " cel " << srcLayer->name() << "["
                    << *srcFrame << "]"
                    << " into " << dstLayer->name() << "[" << *dstFrame << "]\n";
#endif

          switch (op) {
            case Move: api.moveCel(srcLayer, *srcFrame, dstLayer, *dstFrame); break;
            case Copy: api.copyCel(srcLayer, *srcFrame, dstLayer, *dstFrame); break;
          }
        }
        // All cels moved from a image layer and dropped in other kind
        // of layer (e.g. a group) will be discarded/deleted.
        else if (op == Move) {
          api.clearCel(srcLayer, *srcFrame);
        }
      }
    }
  }
}

static DocRange move_or_copy_frames(DocApi& api,
                                    Op op,
                                    Sprite* sprite,
                                    const DocRange& srcRange,
                                    frame_t dstFrame,
                                    const DocRangePlace place,
                                    const TagsHandling tagsHandling)
{
  const SelectedFrames& srcFrames = srcRange.selectedFrames();

#ifdef TRACE_RANGE_OPS
  std::clog << "move_or_copy_frames frames[";
  for (auto srcFrame : srcFrames) {
    std::clog << srcFrame << ", ";
  }
  std::clog << "] "
            << (place == kDocRangeBefore ? "before" :
                place == kDocRangeAfter  ? "after" :
                                           "as first child")
            << " " << dstFrame << "\n";
#endif

  auto srcFrame = srcFrames.begin();
  auto srcFrameEnd = srcFrames.end();
  frame_t srcDelta = 0;
  frame_t firstCopiedBlock = 0;
  frame_t dstBeforeFrame = (place == kDocRangeBefore ? dstFrame : dstFrame + 1);

  for (; srcFrame != srcFrameEnd; ++srcFrame) {
    frame_t fromFrame = (*srcFrame) + srcDelta;

    switch (op) {
      case Move:
        if ((*srcFrame) >= dstBeforeFrame) {
          srcDelta = 0;
          fromFrame = *srcFrame;
        }
        break;

      case Copy:
        if (fromFrame >= dstBeforeFrame - 1 && firstCopiedBlock) {
          srcDelta += firstCopiedBlock;
          fromFrame += firstCopiedBlock;
          firstCopiedBlock = 0;
        }
        break;
    }

#ifdef TRACE_RANGE_OPS
    std::clog << " [";
    for (frame_t i = 0; i <= sprite->lastFrame(); ++i) {
      std::clog << (sprite->frameDuration(i) - 1);
    }
    std::clog << "] => " << (op == Move ? "Move" : "Copy") << " " << (*srcFrame) << "+"
              << (srcDelta) << (place == kDocRangeBefore ? " before " : " after ") << dstFrame
              << " => ";
#endif

    switch (op) {
      case Move:
        api.moveFrame(sprite,
                      fromFrame,
                      dstFrame,
                      (place == kDocRangeBefore ? kDropBeforeFrame : kDropAfterFrame),
                      tagsHandling);

        if (fromFrame < dstBeforeFrame - 1) {
          --srcDelta;
        }
        else if (fromFrame > dstBeforeFrame - 1) {
          ++dstBeforeFrame;
          ++dstFrame;
        }
        break;

      case Copy:
        api.copyFrame(sprite,
                      fromFrame,
                      dstFrame,
                      (place == kDocRangeBefore ? kDropBeforeFrame : kDropAfterFrame),
                      tagsHandling);

        if (fromFrame < dstBeforeFrame - 1) {
          ++firstCopiedBlock;
        }
        else if (fromFrame >= dstBeforeFrame - 1) {
          ++srcDelta;
        }
        ++dstBeforeFrame;
        ++dstFrame;
        break;
    }

#ifdef TRACE_RANGE_OPS
    std::clog << " [";
    for (frame_t i = 0; i <= sprite->lastFrame(); ++i) {
      std::clog << (sprite->frameDuration(i) - 1);
    }
    std::clog << "]\n";
#endif
  }

  DocRange result;
  if (!srcRange.selectedLayers().empty())
    result.selectLayers(srcRange.selectedLayers());
  result.startRange(nullptr, dstBeforeFrame - srcFrames.size(), DocRange::kFrames);
  result.endRange(nullptr, dstBeforeFrame - 1);
  return result;
}

static bool has_child(LayerGroup* parent, Layer* child)
{
  for (auto c : parent->layers()) {
    if (c == child)
      return true;
    else if (c->isGroup() && has_child(static_cast<LayerGroup*>(c), child))
      return true;
  }
  return false;
}

static DocRange drop_range_op(Doc* doc,
                              const Op op,
                              const DocRange& from,
                              DocRangePlace place,
                              const TagsHandling tagsHandling,
                              DocRange to)
{
  // Convert "first child" operation into a insert after last child.
  LayerGroup* parent = nullptr;
  if (to.type() == DocRange::kLayers && !to.selectedLayers().empty()) {
    if (place == kDocRangeFirstChild && (*to.selectedLayers().begin())->isGroup()) {
      place = kDocRangeAfter;
      parent = static_cast<LayerGroup*>((*to.selectedLayers().begin()));

      to.clearRange();
      to.startRange(parent->lastLayer(), -1, DocRange::kLayers);
      to.endRange(parent->lastLayer(), -1);
    }
    else {
      parent = (*to.selectedLayers().begin())->parent();
    }

    // Check that we're not moving a group inside itself
    for (auto moveThis : from.selectedLayers()) {
      if (moveThis == parent ||
          (moveThis->isGroup() && has_child(static_cast<LayerGroup*>(moveThis), parent)))
        return from;
    }
  }

  if (place != kDocRangeBefore && place != kDocRangeAfter) {
    ASSERT(false);
    throw std::invalid_argument("Invalid 'place' argument");
  }

  Sprite* sprite = doc->sprite();

  // Check noop/trivial/do nothing cases, i.e., move a range to the same place.
  // Also check invalid cases, like moving a Background layer.
  switch (from.type()) {
    case DocRange::kCels:
      if (from == to)
        return from;
      break;

    case DocRange::kFrames:
      if (op == Move) {
        // Simple cases with one continuos range of frames that are a
        // no-op.
        if ((from.selectedFrames().ranges() == 1) &&
            ((to.firstFrame() >= from.firstFrame() && to.lastFrame() <= from.lastFrame()) ||
             (place == kDocRangeBefore && to.firstFrame() == from.lastFrame() + 1) ||
             (place == kDocRangeAfter && to.lastFrame() == from.firstFrame() - 1)) &&
            // If there are tags, this might not be a no-op
            (sprite->tags().empty() || tagsHandling == kDontAdjustTags)) {
          return from;
        }
      }
      break;

    case DocRange::kLayers:
      if (op == Move) {
        SelectedLayers srcSelLayers = from.selectedLayers();
        SelectedLayers dstSelLayers = to.selectedLayers();
        LayerList srcLayers = srcSelLayers.toBrowsableLayerList();
        LayerList dstLayers = dstSelLayers.toBrowsableLayerList();
        ASSERT(!srcLayers.empty());
        if (srcLayers.empty())
          return from;

        // dstLayers can be nullptr when we insert the first child in
        // a group.

        // Check no-ops when we move layers at the same level (all
        // layers with the same parent), all adjacents, and which are
        // moved to the same place.
        if (!dstSelLayers.empty() && srcSelLayers.hasSameParent() && dstSelLayers.hasSameParent() &&
            are_layers_adjacent(srcLayers) && are_layers_adjacent(dstLayers)) {
          for (Layer* srcLayer : srcLayers)
            if (dstSelLayers.contains(srcLayer))
              return from;

          if ((place == kDocRangeBefore && dstLayers.front() == srcLayers.back()->getNext()) ||
              (place == kDocRangeAfter && dstLayers.back() == srcLayers.front()->getPrevious()))
            return from;
        }

        // We cannot move the background
        for (Layer* layer : srcSelLayers)
          if (layer->isBackground())
            throw std::runtime_error("The background layer cannot be moved");
      }

      // Before background
      if (place == kDocRangeBefore) {
        for (Layer* background : to.selectedLayers()) {
          if (background && background->isBackground())
            throw std::runtime_error(
              "You cannot move or copy something below the background layer");
        }
      }
      break;
  }

  const char* undoLabel = NULL;
  switch (op) {
    case Move: undoLabel = "Move Range"; break;
    case Copy: undoLabel = "Copy Range"; break;
    default:   ASSERT(false); throw std::invalid_argument("Invalid 'op' argument");
  }
  DocRange resultRange;

  {
    const app::Context* context = static_cast<app::Context*>(doc->context());
    const ContextReader reader(context);
    ContextWriter writer(reader);
    Tx tx(writer, undoLabel, ModifyDocument);
    DocApi api = doc->getApi(tx);

    // TODO Try to add the range with just one call to DocApi
    // methods, to avoid generating a lot of cmd::SetCelFrame (see
    // DocApi::setCelFramePosition() function).

    switch (from.type()) {
      case DocRange::kCels: {
        LayerList allLayers = sprite->allBrowsableLayers();
        if (allLayers.empty())
          break;

        LayerList srcLayers = from.selectedLayers().toBrowsableLayerList();
        LayerList dstLayers = to.selectedLayers().toBrowsableLayerList();
        if (srcLayers.empty() || dstLayers.empty())
          throw std::invalid_argument("You need to specify a non-empty cels range");

        if (find_layer_index(allLayers, srcLayers.front()) <
            find_layer_index(allLayers, dstLayers.front())) {
          std::reverse(srcLayers.begin(), srcLayers.end());
          std::reverse(dstLayers.begin(), dstLayers.end());
        }

        if (from.firstFrame() < to.firstFrame()) {
          auto srcFrames = from.selectedFrames().makeReverse();
          auto dstFrames = to.selectedFrames().makeReverse();

          move_or_copy_cels(api, op, srcLayers, dstLayers, srcFrames, dstFrames);
        }
        else {
          const auto& srcFrames = from.selectedFrames();
          const auto& dstFrames = to.selectedFrames();

          move_or_copy_cels(api, op, srcLayers, dstLayers, srcFrames, dstFrames);
        }

        resultRange = to;
        break;
      }

      case DocRange::kFrames: {
        frame_t dstFrame;
        if (place == kDocRangeBefore)
          dstFrame = to.firstFrame();
        else
          dstFrame = to.lastFrame();

        resultRange = move_or_copy_frames(api, op, sprite, from, dstFrame, place, tagsHandling);
        break;
      }

      case DocRange::kLayers: {
        LayerList allLayers = sprite->allBrowsableLayers();
        if (allLayers.empty())
          break;

        LayerList srcLayers = from.selectedLayers().toBrowsableLayerList();
        LayerList dstLayers = to.selectedLayers().toBrowsableLayerList();
        ASSERT(!srcLayers.empty());

        switch (op) {
          case Move:
            if (place == kDocRangeBefore) {
              Layer* beforeThis = (!dstLayers.empty() ? dstLayers.front() : nullptr);
              Layer* afterThis = nullptr;

              for (Layer* srcLayer : srcLayers) {
                if (afterThis)
                  api.restackLayerAfter(srcLayer, parent, afterThis);
                else
                  api.restackLayerBefore(srcLayer, parent, beforeThis);

                afterThis = srcLayer;
              }
            }
            else if (place == kDocRangeAfter) {
              Layer* afterThis = (!dstLayers.empty() ? dstLayers.back() : nullptr);
              for (Layer* srcLayer : srcLayers) {
                api.restackLayerAfter(srcLayer, parent, afterThis);
                afterThis = srcLayer;
              }
            }

            // Same set of layers than the "from" range
            resultRange = from;
            break;

          case Copy: {
            if (place == kDocRangeBefore) {
              Layer* beforeThis = (!dstLayers.empty() ? dstLayers.front() : nullptr);
              for (Layer* srcLayer : srcLayers) {
                Layer* copiedLayer = api.duplicateLayerBefore(srcLayer, parent, beforeThis);

                resultRange.startRange(copiedLayer, -1, DocRange::kLayers);
                resultRange.endRange(copiedLayer, -1);
              }
            }
            else if (place == kDocRangeAfter) {
              std::reverse(srcLayers.begin(), srcLayers.end());

              Layer* afterThis = (!dstLayers.empty() ? dstLayers.back() : nullptr);
              for (Layer* srcLayer : srcLayers) {
                Layer* copiedLayer = api.duplicateLayerAfter(srcLayer, parent, afterThis);

                resultRange.startRange(copiedLayer, -1, DocRange::kLayers);
                resultRange.endRange(copiedLayer, -1);
              }
            }
            break;
          }
        }
        break;
      }
    }

    if (resultRange.type() != DocRange::kNone)
      tx.setNewDocRange(resultRange);

    tx.commit();
  }

  return resultRange;
}

DocRange move_range(Doc* doc,
                    const DocRange& from,
                    const DocRange& to,
                    const DocRangePlace place,
                    const TagsHandling tagsHandling)
{
  return drop_range_op(doc, Move, from, place, tagsHandling, DocRange(to));
}

DocRange copy_range(Doc* doc,
                    const DocRange& from,
                    const DocRange& to,
                    const DocRangePlace place,
                    const TagsHandling tagsHandling)
{
  return drop_range_op(doc, Copy, from, place, tagsHandling, DocRange(to));
}

void reverse_frames(Doc* doc, const DocRange& range)
{
  const app::Context* context = static_cast<app::Context*>(doc->context());
  const ContextReader reader(context);
  ContextWriter writer(reader);
  Tx tx(writer, "Reverse Frames");
  DocApi api = doc->getApi(tx);
  Sprite* sprite = doc->sprite();
  LayerList layers;
  frame_t frameBegin, frameEnd;
  bool moveFrames = false;
  bool swapCels = false;

  switch (range.type()) {
    case DocRange::kCels:
      frameBegin = range.firstFrame();
      frameEnd = range.lastFrame();
      layers = range.selectedLayers().toBrowsableLayerList();
      swapCels = true;
      break;
    case DocRange::kFrames:
      frameBegin = range.firstFrame();
      frameEnd = range.lastFrame();
      layers = sprite->allLayers();
      moveFrames = true;
      break;
    case DocRange::kLayers:
      frameBegin = frame_t(0);
      frameEnd = sprite->totalFrames() - 1;
      layers = range.selectedLayers().toBrowsableLayerList();
      swapCels = true;
      break;
  }

  if (moveFrames) {
    for (frame_t frameRev = frameEnd + 1; frameRev > frameBegin; --frameRev) {
      api.moveFrame(sprite, frameBegin, frameRev, kDropBeforeFrame, kDontAdjustTags);
    }
  }
  else if (swapCels) {
    for (Layer* layer : layers) {
      if (!layer->isImage())
        continue;

      for (frame_t frame = frameBegin, frameRev = frameEnd;
           frame != (frameBegin + frameEnd) / 2 + 1;
           ++frame, --frameRev) {
        if (frame == frameRev)
          continue;

        LayerImage* imageLayer = static_cast<LayerImage*>(layer);
        api.swapCel(imageLayer, frame, frameRev);
      }
    }
  }

  tx.setNewDocRange(range);
  tx.commit();
}

} // namespace app
