// Aseprite
// Copyright (C) 2019  Igara Studio S.A.
// Copyright (C) 2001-2015  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/util/autocrop.h"

#include "app/snap_to_grid.h"
#include "doc/algorithm/shrink_bounds.h"
#include "doc/image.h"
#include "doc/mask.h"
#include "doc/sprite.h"
#include "render/render.h"

#include <algorithm>
#include <memory>

namespace app {

using namespace doc;

namespace {

// We call a "solid border" when a specific color is repeated in every
// pixel of the image edge.  This will return isBorder1Solid=true if
// the top (when topBottomLookUp=true) or left edge (when
// topBottomLookUp=false) is a "solid border". In the case of
// isBorder2Solid is for the bottom (or right) edge.
bool analize_if_image_has_solid_borders(const Image* image,
                                        color_t& refColor,
                                        bool& isBorder1Solid,
                                        bool& isBorder2Solid,
                                        bool topBottomLookUp)
{
  isBorder1Solid = true;
  isBorder2Solid = true;

  int w = image->width();
  int h = image->height();
  const color_t probableRefColor1 = get_pixel(image, 0, 0);
  const color_t probableRefColor2 = get_pixel(image, w-1, h-1);
  color_t currentPixel;
  color_t transparentColor = image->maskColor();

  if (probableRefColor1 == transparentColor ||
      probableRefColor2 == transparentColor) {
    refColor = transparentColor;
    return false;
  }

  if (!topBottomLookUp)
    std::swap(w, h);

  for (int i=0; i<w; ++i) {
    if (topBottomLookUp)
      currentPixel = get_pixel(image, i, 0);
    else
      currentPixel = get_pixel(image, 0, i);
    if (currentPixel != probableRefColor1) {
      isBorder1Solid = false;
      if (currentPixel == transparentColor) {
        refColor = transparentColor;
        return false;
      }
    }
  }

  for (int i=0; i<w; ++i) {
    if (topBottomLookUp)
      currentPixel = get_pixel(image, i, h-1);
    else
      currentPixel = get_pixel(image, h-1, i);
    if (currentPixel != probableRefColor2) {
      isBorder2Solid = false;
      if (currentPixel == transparentColor) {
        refColor = transparentColor;
        return false;
      }
    }
  }
  return true;
}

} // anonymous namespace

bool get_shrink_rect(int *x1, int *y1, int *x2, int *y2,
                     Image *image, color_t refpixel)
{
#define SHRINK_SIDE(u_begin, u_op, u_final, u_add,              \
                    v_begin, v_op, v_final, v_add, U, V, var)   \
  do {                                                          \
    for (u = u_begin; u u_op u_final; u u_add) {                \
      for (v = v_begin; v v_op v_final; v v_add) {              \
        if (image->getPixel(U, V) != refpixel)                  \
          break;                                                \
      }                                                         \
      if (v == v_final)                                         \
        var;                                                    \
      else                                                      \
        break;                                                  \
    }                                                           \
  } while (0)

  int u, v;

  *x1 = 0;
  *y1 = 0;
  *x2 = image->width()-1;
  *y2 = image->height()-1;

  SHRINK_SIDE(0, <, image->width(), ++,
              0, <, image->height(), ++, u, v, (*x1)++);

  SHRINK_SIDE(0, <, image->height(), ++,
              0, <, image->width(), ++, v, u, (*y1)++);

  SHRINK_SIDE(image->width()-1, >, 0, --,
              0, <, image->height(), ++, u, v, (*x2)--);

  SHRINK_SIDE(image->height()-1, >, 0, --,
              0, <, image->width(), ++, v, u, (*y2)--);

  if ((*x1 > *x2) || (*y1 > *y2))
    return false;
  else
    return true;

#undef SHRINK_SIDE
}

bool get_shrink_rect2(int *x1, int *y1, int *x2, int *y2,
                      Image *image, Image *refimage)
{
#define SHRINK_SIDE(u_begin, u_op, u_final, u_add,              \
                    v_begin, v_op, v_final, v_add, U, V, var)   \
  do {                                                          \
    for (u = u_begin; u u_op u_final; u u_add) {                \
      for (v = v_begin; v v_op v_final; v v_add) {              \
        if (image->getPixel(U, V) != refimage->getPixel(U, V))  \
          break;                                                \
      }                                                         \
      if (v == v_final)                                         \
        var;                                                    \
      else                                                      \
        break;                                                  \
    }                                                           \
  } while (0)

  int u, v;

  *x1 = 0;
  *y1 = 0;
  *x2 = image->width()-1;
  *y2 = image->height()-1;

  SHRINK_SIDE(0, <, image->width(), ++,
              0, <, image->height(), ++, u, v, (*x1)++);

  SHRINK_SIDE(0, <, image->height(), ++,
              0, <, image->width(), ++, v, u, (*y1)++);

  SHRINK_SIDE(image->width()-1, >, 0, --,
              0, <, image->height(), ++, u, v, (*x2)--);

  SHRINK_SIDE(image->height()-1, >, 0, --,
              0, <, image->width(), ++, v, u, (*y2)--);

  if ((*x1 > *x2) || (*y1 > *y2))
    return false;
  else
    return true;

#undef SHRINK_SIDE
}

// A simple method to trim an image we have used in the past is
// selecting the top-left corner pixel as the "refColor" in
// shrink_bounds() algorithm (whatever that color is, transparent
// or non-transparent).
//
// Now we have changed it to other heuristic: we look if there are
// borders in the sprite with a solid color ("solid border"),
// i.e. a color repeating in every pixel of that specific side
// (left, top, right, or bottom).
//
// If we find a transparent pixel at the edges of the sprite, we
// automatically set "refColor" as the transparent color and it
// goes directly to shrink_bounds() function. Because this mean
// that we are in a transparent layer and the transparent color is
// the one that must be trimmed.
//
// The other case is when borders don't contain the transparent
// color, we search for a "solid border" (top border first), then
// it checks the opposite border (bottom border), then:
//
// 1) If the opposite border is equal to the first border,
//    the color of both borders will be the "refColor".
// 2) If the color of the opposite border is solid, BUT different to
//    the first border we will need the user intervention to select a
//    valid refColor (in this case the function returns false, which
//    means that we cannot automatically trim the image).
// 3) If opposite border contains differents colors, we choose the
//    first border color as "refColor".
// 4) It repeats the analysis with the left and right edges.
//
// If no border has solid color, trimSprite() does nothing.
bool get_best_refcolor_for_trimming(
  doc::Image* image,
  color_t& refColor)
{
  const color_t probableRefColor1 = get_pixel(image, 0, 0);
  const color_t probableRefColor2 = get_pixel(image, image->width()-1, image->height()-1);
  bool isBorder1Solid = true;
  bool isBorder2Solid = true;

  refColor = probableRefColor1;

  if (analize_if_image_has_solid_borders(
        image, refColor,
        isBorder1Solid,
        isBorder2Solid,
        true)) {                // Analize top vs. bottom borders
    // Here, we know that analize_if_image_has_solid_borders() did not
    // find transparent pixels on top and bottom borders.
    if (!isBorder1Solid &&
        isBorder2Solid) {
      refColor = probableRefColor2;
    }
    else if (isBorder1Solid &&
             isBorder2Solid &&
             probableRefColor1 != probableRefColor2) {
      // Both border are solid but with different colors, so the
      // decision should be asked to the user.
      return false;
    }

    if (analize_if_image_has_solid_borders(
          image, refColor,
          isBorder1Solid,
          isBorder2Solid,
          false)) {                // Analize left vs. right borders
      if (!isBorder1Solid &&
          isBorder2Solid)
        refColor = probableRefColor2;
      else if (isBorder1Solid &&
               isBorder2Solid &&
               probableRefColor1 != probableRefColor2)
        // Both border are solid but with different colors, so the
        // decision should be asked to the user.
        return false;
    }
  }
  return true;
}

gfx::Rect get_trimmed_bounds(
  const doc::Sprite* sprite,
  const bool byGrid)
{
  gfx::Rect bounds;

  std::unique_ptr<Image> image_wrap(Image::create(sprite->spec()));
  Image* image = image_wrap.get();

  render::Render render;

  for (frame_t frame(0); frame<sprite->totalFrames(); ++frame) {
    render.renderSprite(image, sprite, frame);

    gfx::Rect frameBounds;
    doc::color_t refColor;
    if (get_best_refcolor_for_trimming(image, refColor) &&
        doc::algorithm::shrink_bounds(image, refColor, nullptr, frameBounds)) {
      bounds = bounds.createUnion(frameBounds);
    }

    // TODO merge this code with the code in DocExporter::captureSamples()
    if (byGrid) {
      const gfx::Rect& gridBounds = sprite->gridBounds();
      gfx::Point posTopLeft =
        snap_to_grid(gridBounds,
                     bounds.origin(),
                     PreferSnapTo::FloorGrid);
      gfx::Point posBottomRight =
        snap_to_grid(gridBounds,
                     bounds.point2(),
                     PreferSnapTo::CeilGrid);
      bounds = gfx::Rect(posTopLeft, posBottomRight);
    }
  }
  return bounds;
}

} // namespace app
