// Aseprite
// Copyright (C) 2019-2024  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.

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

#include "app/commands/cmd_flip.h"

#include "app/app.h"
#include "app/cmd/flip_mask.h"
#include "app/cmd/flip_masked_cel.h"
#include "app/cmd/set_cel_bounds.h"
#include "app/cmd/set_mask_position.h"
#include "app/cmd/trim_cel.h"
#include "app/commands/params.h"
#include "app/context_access.h"
#include "app/doc_api.h"
#include "app/doc_range.h"
#include "app/i18n/strings.h"
#include "app/modules/gui.h"
#include "app/tools/tool_box.h"
#include "app/tx.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/moving_pixels_state.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui/toolbar.h"
#include "app/util/expand_cel_canvas.h"
#include "app/util/range_utils.h"
#include "doc/algorithm/flip_image.h"
#include "doc/cel.h"
#include "doc/cels_range.h"
#include "doc/image.h"
#include "doc/layer.h"
#include "doc/mask.h"
#include "doc/sprite.h"
#include "gfx/size.h"


namespace app {

FlipCommand::FlipCommand()
  : Command(CommandId::Flip(), CmdRecordableFlag)
{
  m_flipMask = false;
  m_flipType = doc::algorithm::FlipHorizontal;
}

void FlipCommand::onLoadParams(const Params& params)
{
  std::string target = params.get("target");
  m_flipMask = (target == "mask");

  std::string orientation = params.get("orientation");
  m_flipType = (orientation == "vertical" ? doc::algorithm::FlipVertical:
                                            doc::algorithm::FlipHorizontal);
}

bool FlipCommand::onEnabled(Context* ctx)
{
  return ctx->checkFlags(ContextFlags::ActiveDocumentIsWritable);
}

void FlipCommand::onExecute(Context* ctx)
{
  Site site = ctx->activeSite();
  LockTimelineRange lockRange(App::instance()->timeline());

  CelList cels;
  if (m_flipMask) {
#ifdef ENABLE_UI
    // If we want to flip the visible mask we can go to
    // MovingPixelsState (even when the range is enabled, because now
    // PixelsMovement support ranges).
    if (site.document()->isMaskVisible() &&
        ctx->isUIAvailable()) {
      // Select marquee tool
      if (tools::Tool* tool = App::instance()->toolBox()
          ->getToolById(tools::WellKnownTools::RectangularMarquee)) {
        ToolBar::instance()->selectTool(tool);
        if (auto editor = Editor::activeEditor())
          editor->startFlipTransformation(m_flipType);
        return;
      }
    }
#endif

    auto range = site.range();
    if (range.enabled()) {
      cels = get_unique_cels_to_edit_pixels(site.sprite(), range);
    }
    else if (site.cel() &&
             site.layer() &&
             site.layer()->canEditPixels()) {
      cels.push_back(site.cel());
    }

    if (cels.empty()) {
#ifdef ENABLE_UI
      if (ctx->isUIAvailable()) {
        StatusBar::instance()->showTip(
          1000, Strings::statusbar_tips_all_layers_are_locked());
      }
#endif // ENABLE_UI
      return;
    }
  }
  // Flip the whole sprite (even locked layers)
  else {
    for (Cel* cel : site.sprite()->uniqueCels())
      cels.push_back(cel);
  }

  ContextWriter writer(ctx);
  Doc* document = writer.document();
  Sprite* sprite = writer.sprite();
  Tx tx(writer, friendlyName());
  DocApi api = document->getApi(tx);

  Mask* mask = document->mask();
  if (m_flipMask && document->isMaskVisible()) {
    Site site = *writer.site();

    for (Cel* cel : cels) {
      // TODO add support to flip masked part of a reference layer
      if (cel->layer()->isReference())
        continue;

      site.frame(cel->frame());
      site.layer(cel->layer());

      int x, y;
      Image* image = site.image(&x, &y);
      if (!image)
        continue;

      // When the mask is inside the cel, we can try to flip the
      // pixels inside the image.
      if (cel->bounds().contains(mask->bounds())) {
        gfx::Rect flipBounds = mask->bounds();
        flipBounds.offset(-x, -y);
        flipBounds &= image->bounds();
        if (flipBounds.isEmpty())
          continue;

        if (mask->bitmap() && !mask->isRectangular())
          tx(new cmd::FlipMaskedCel(cel, m_flipType));
        else
          api.flipImage(image, flipBounds, m_flipType);

        if (site.shouldTrimCel(cel))
          tx(new cmd::TrimCel(cel));
      }
      // When the mask is bigger than the cel bounds, we have to
      // expand the cel, make the flip, and shrink it again.
      else {
        gfx::Rect flipBounds = (sprite->bounds() & mask->bounds());
        if (flipBounds.isEmpty())
          continue;

        ExpandCelCanvas expand(
          site, cel->layer(),
          TiledMode::NONE, tx,
          ExpandCelCanvas::None);

        expand.validateDestCanvas(gfx::Region(flipBounds));

        if (mask->bitmap() && !mask->isRectangular())
          doc::algorithm::flip_image_with_mask(
            expand.getDestCanvas(), mask, m_flipType,
            document->bgColor(cel->layer()));
        else
          doc::algorithm::flip_image(
            expand.getDestCanvas(),
            flipBounds, m_flipType);

        expand.commit();
      }
    }
  }
  else {
    for (Cel* cel : cels) {
      Image* image = cel->image();

      // Flip reference layer cel
      if (cel->layer()->isReference()) {
        gfx::RectF bounds = cel->boundsF();

        if (m_flipType == doc::algorithm::FlipHorizontal)
          bounds.x = sprite->width() - bounds.w - bounds.x;
        if (m_flipType == doc::algorithm::FlipVertical)
          bounds.y = sprite->height() - bounds.h - bounds.y;

        tx(new cmd::SetCelBoundsF(cel, bounds));
      }
      else {
        api.setCelPosition
          (sprite, cel,
            (m_flipType == doc::algorithm::FlipHorizontal ?
            sprite->width() - image->width() - cel->x():
            cel->x()),
            (m_flipType == doc::algorithm::FlipVertical ?
            sprite->height() - image->height() - cel->y():
            cel->y()));
      }

      api.flipImage(image, image->bounds(), m_flipType);
    }
  }

  // Flip the mask.
  Image* maskBitmap = mask->bitmap();
  if (maskBitmap) {
    tx(new cmd::FlipMask(document, m_flipType));

    // Flip the mask position because the
    if (!m_flipMask)
      tx(
        new cmd::SetMaskPosition(
          document,
          gfx::Point(
            (m_flipType == doc::algorithm::FlipHorizontal ?
              sprite->width() - mask->bounds().x2():
              mask->bounds().x),
            (m_flipType == doc::algorithm::FlipVertical ?
              sprite->height() - mask->bounds().y2():
              mask->bounds().y))));
  }

  tx.commit();

#ifdef ENABLE_UI
  if (ctx->isUIAvailable())
    update_screen_for_document(document);
#endif
}

std::string FlipCommand::onGetFriendlyName() const
{
  std::string content;
  std::string orientation;

  if (m_flipMask)
    content = Strings::commands_Flip_Selection();
  else
    content = Strings::commands_Flip_Canvas();

  if (m_flipType == doc::algorithm::FlipHorizontal)
    orientation = Strings::commands_Flip_Horizontally();
  else
    orientation = Strings::commands_Flip_Vertically();

  return Strings::commands_Flip(content, orientation);
}

Command* CommandFactory::createFlipCommand()
{
  return new FlipCommand;
}

} // namespace app
