// 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/app.h"
#include "app/cmd/set_cel_bounds.h"
#include "app/commands/cmd_rotate.h"
#include "app/commands/params.h"
#include "app/doc_api.h"
#include "app/i18n/strings.h"
#include "app/modules/gui.h"
#include "app/sprite_job.h"
#include "app/tools/tool_box.h"
#include "app/tx.h"
#include "app/ui/editor/editor.h"
#include "app/ui/status_bar.h"
#include "app/ui/timeline/timeline.h"
#include "app/ui/toolbar.h"
#include "base/convert_to.h"
#include "doc/cel.h"
#include "doc/cels_range.h"
#include "doc/image.h"
#include "doc/mask.h"
#include "doc/sprite.h"

namespace app {

class RotateJob : public SpriteJob {
  int m_angle;
  CelList m_cels;
  bool m_rotateSprite;

public:
  RotateJob(Context* ctx,
            Doc* doc,
            const std::string& jobName,
            int angle,
            const CelList& cels,
            const bool rotateSprite,
            const bool showProgress)
    : SpriteJob(ctx, doc, jobName, showProgress)
    , m_cels(cels)
    , m_rotateSprite(rotateSprite)
  {
    m_angle = angle;
  }

protected:
  template<typename T>
  void rotate_rect(gfx::RectT<T>& newBounds)
  {
    const gfx::RectT<T> bounds = newBounds;
    switch (m_angle) {
      case 180:
        newBounds.x = sprite()->width() - bounds.x - bounds.w;
        newBounds.y = sprite()->height() - bounds.y - bounds.h;
        break;
      case 90:
        newBounds.x = sprite()->height() - bounds.y - bounds.h;
        newBounds.y = bounds.x;
        newBounds.w = bounds.h;
        newBounds.h = bounds.w;
        break;
      case -90:
        newBounds.x = bounds.y;
        newBounds.y = sprite()->width() - bounds.x - bounds.w;
        newBounds.w = bounds.h;
        newBounds.h = bounds.w;
        break;
    }
  }

  // [working thread]
  void onSpriteJob(Tx& tx) override
  {
    DocApi api = document()->getApi(tx);

    // 1) Rotate cel positions
    for (Cel* cel : m_cels) {
      Image* image = cel->image();
      if (!image)
        continue;

      if (cel->layer()->isReference()) {
        gfx::RectF bounds = cel->boundsF();
        rotate_rect(bounds);
        if (cel->boundsF() != bounds)
          tx(new cmd::SetCelBoundsF(cel, bounds));
      }
      else {
        gfx::Rect bounds = cel->bounds();
        rotate_rect(bounds);
        if (bounds.origin() != cel->bounds().origin())
          api.setCelPosition(sprite(), cel, bounds.x, bounds.y);
      }
    }

    // 2) Rotate images
    int i = 0;
    for (Cel* cel : m_cels) {
      Image* image = cel->image();
      if (image) {
        ImageRef new_image(Image::create(image->pixelFormat(),
                                         m_angle == 180 ? image->width() : image->height(),
                                         m_angle == 180 ? image->height() : image->width()));
        new_image->setMaskColor(image->maskColor());

        doc::rotate_image(image, new_image.get(), m_angle);
        api.replaceImage(sprite(), cel->imageRef(), new_image);
      }

      jobProgress((float)i / m_cels.size());
      ++i;

      // cancel all the operation?
      if (isCanceled())
        return; // Tx destructor will undo all operations
    }

    // rotate mask
    if (document()->isMaskVisible()) {
      Mask* origMask = document()->mask();
      std::unique_ptr<Mask> new_mask(new Mask());
      const gfx::Rect& origBounds = origMask->bounds();
      int x = 0, y = 0;

      switch (m_angle) {
        case 180:
          x = sprite()->width() - origBounds.x - origBounds.w;
          y = sprite()->height() - origBounds.y - origBounds.h;
          break;
        case 90:
          x = sprite()->height() - origBounds.y - origBounds.h;
          y = origBounds.x;
          break;
        case -90:
          x = origBounds.y;
          y = sprite()->width() - origBounds.x - origBounds.w;
          break;
      }

      // create the new rotated mask
      new_mask->replace(gfx::Rect(x,
                                  y,
                                  m_angle == 180 ? origBounds.w : origBounds.h,
                                  m_angle == 180 ? origBounds.h : origBounds.w));
      doc::rotate_image(origMask->bitmap(), new_mask->bitmap(), m_angle);

      // Copy new mask
      api.copyToCurrentMask(new_mask.get());
    }

    // change the sprite's size
    if (m_rotateSprite && m_angle != 180)
      api.setSpriteSize(sprite(), sprite()->height(), sprite()->width());
  }
};

RotateCommand::RotateCommand() : Command(CommandId::Rotate())
{
  m_ui = true;
  m_flipMask = false;
  m_angle = 0;
}

void RotateCommand::onLoadParams(const Params& params)
{
  if (params.has_param("ui"))
    m_ui = params.get_as<bool>("ui");
  else
    m_ui = true;

  std::string target = params.get("target");
  m_flipMask = (target == "mask");

  if (params.has_param("angle")) {
    m_angle = strtol(params.get("angle").c_str(), NULL, 10);
  }
}

bool RotateCommand::onEnabled(Context* context)
{
  // Because we use the toolbar & editor to transform the selection, this won't work without a UI
  if (m_flipMask && !context->isUIAvailable())
    return false;

  return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
                             ContextFlags::HasActiveSprite);
}

void RotateCommand::onExecute(Context* context)
{
  {
    Site site = context->activeSite();
    Doc* doc = site.document();
    CelList cels;
    bool rotateSprite = false;

    Timeline* timeline = App::instance()->timeline();
    LockTimelineRange lockRange(timeline);

    // Flip the mask or current cel
    if (m_flipMask) {
      // If we want to rotate the visible mask, we can go to
      // MovingPixelsState (even when the range is enabled, because
      // now PixelsMovement support ranges).
      if (doc->isMaskVisible()) {
        // Select marquee tool
        if (tools::Tool* tool = App::instance()->toolBox()->getToolById(
              tools::WellKnownTools::RectangularMarquee)) {
          ToolBar::instance()->selectTool(tool);
          if (auto editor = Editor::activeEditor())
            editor->startSelectionTransformation(gfx::Point(0, 0), m_angle);
          return;
        }
      }

      cels = site.selectedUniqueCelsToEditPixels();
      if (cels.empty()) {
        StatusBar::instance()->showTip(1000, Strings::statusbar_tips_all_layers_are_locked());
        return;
      }
    }
    // Flip the whole sprite (even locked layers)
    else if (site.sprite()) {
      cels = site.sprite()->uniqueCels().toList();
      rotateSprite = true;
    }

    {
      RotateJob job(context, doc, friendlyName(), m_angle, cels, rotateSprite, m_ui);
      job.startJob();
      job.waitJob();
    }
    update_screen_for_document(doc);
  }
}

std::string RotateCommand::onGetFriendlyName() const
{
  std::string content;
  if (m_flipMask)
    content = Strings::commands_Rotate_Selection();
  else
    content = Strings::commands_Rotate_Sprite();
  return Strings::commands_Rotate(content, base::convert_to<std::string>(m_angle));
}

Command* CommandFactory::createRotateCommand()
{
  return new RotateCommand;
}

} // namespace app
