// 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.

#include "app/color_utils.h"
#include "app/util/wrap_point.h"
#include "app/util/wrap_value.h"
#include "doc/blend_funcs.h"
#include "doc/blend_internals.h"
#include "doc/image_impl.h"
#include "doc/layer.h"
#include "doc/palette.h"
#include "doc/remap.h"
#include "doc/rgbmap.h"
#include "doc/sprite.h"
#include "filters/neighboring_pixels.h"
#include "gfx/hsv.h"
#include "gfx/rgb.h"
#include "render/dithering.h"
#include "render/gradient.h"

namespace app { namespace tools {

using namespace gfx;
using namespace filters;

class BaseInkProcessing {
public:
  virtual ~BaseInkProcessing() {}
  virtual void processScanline(int x1, int y, int x2, ToolLoop* loop) = 0;
  virtual void prepareForStrokes(ToolLoop* loop, Strokes& strokes) {}
  virtual void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) {}
  virtual void prepareVForPointShape(ToolLoop* loop, int y) {}
  virtual void prepareUForPointShapeWholeScanline(ToolLoop* loop, int x1) {}
  virtual void prepareUForPointShapeSlicedScanline(ToolLoop* loop, bool leftSlice, int x1) {}
};

class NoopInkProcessing : public BaseInkProcessing {
public:
  void processScanline(int x1, int y, int x2, ToolLoop* loop) override {}
};

typedef std::unique_ptr<BaseInkProcessing> InkProcessingPtr;

namespace {

//////////////////////////////////////////////////////////////////////
// Ink Processing
//////////////////////////////////////////////////////////////////////

template<typename Derived>
class InkProcessing : public BaseInkProcessing {
public:
  void processScanline(int x1, int y, int x2, ToolLoop* loop) override
  {
    int x;

    // Use mask
    if (loop->useMask()) {
      Point maskOrigin(loop->getMaskOrigin());
      const Rect& maskBounds(loop->getMask()->bounds());

      if ((y < maskOrigin.y) || (y >= maskOrigin.y + maskBounds.h))
        return;

      if (x1 < maskOrigin.x)
        x1 = maskOrigin.x;

      if (x2 > maskOrigin.x + maskBounds.w - 1)
        x2 = maskOrigin.x + maskBounds.w - 1;

      if (Image* bitmap = loop->getMask()->bitmap()) {
        static_cast<Derived*>(this)->initIterators(loop, x1, y);

        for (x = x1; x <= x2; ++x) {
          if (bitmap->getPixel(x - maskOrigin.x, y - maskOrigin.y))
            static_cast<Derived*>(this)->processPixel(x, y);

          static_cast<Derived*>(this)->moveIterators();
        }
        return;
      }
    }

    static_cast<Derived*>(this)->initIterators(loop, x1, y);
    for (x = x1; x <= x2; ++x) {
      static_cast<Derived*>(this)->processPixel(x, y);
      static_cast<Derived*>(this)->moveIterators();
    }
  }
};

template<typename Derived, typename ImageTraits>
class SimpleInkProcessing : public InkProcessing<Derived> {
public:
  void initIterators(ToolLoop* loop, int x1, int y)
  {
    m_dstAddress = (typename ImageTraits::address_t)loop->getDstImage()->getPixelAddress(x1, y);
  }

  void moveIterators() { ++m_dstAddress; }

protected:
  typename ImageTraits::address_t m_dstAddress;
};

template<typename Derived, typename ImageTraits>
class DoubleInkProcessing : public InkProcessing<Derived> {
public:
  void initIterators(ToolLoop* loop, int x1, int y)
  {
    m_srcAddress = (typename ImageTraits::address_t)loop->getSrcImage()->getPixelAddress(x1, y);
    m_dstAddress = (typename ImageTraits::address_t)loop->getDstImage()->getPixelAddress(x1, y);
  }

  void moveIterators()
  {
    ++m_srcAddress;
    ++m_dstAddress;
  }

protected:
  typename ImageTraits::address_t m_srcAddress;
  typename ImageTraits::address_t m_dstAddress;
};

//////////////////////////////////////////////////////////////////////
// Copy Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class CopyInkProcessing : public SimpleInkProcessing<CopyInkProcessing<ImageTraits>, ImageTraits> {
public:
  CopyInkProcessing(ToolLoop* loop) {}

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    m_color = loop->getPrimaryColor();

    if (loop->getLayer()->isBackground()) {
      switch (loop->sprite()->pixelFormat()) {
        case IMAGE_RGB:       m_color |= rgba_a_mask; break;
        case IMAGE_GRAYSCALE: m_color |= graya_a_mask; break;
      }
    }
  }

  void processPixel(int x, int y) { *this->m_dstAddress = m_color; }

private:
  color_t m_color;
};

//////////////////////////////////////////////////////////////////////
// LockAlpha Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class LockAlphaInkProcessing
  : public DoubleInkProcessing<LockAlphaInkProcessing<ImageTraits>, ImageTraits> {
public:
  LockAlphaInkProcessing(ToolLoop* loop) : m_opacity(loop->getOpacity()) {}

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    m_color = loop->getPrimaryColor();
  }

  void processPixel(int x, int y)
  {
    // Do nothing
  }

private:
  color_t m_color;
  const int m_opacity;
};

template<>
void LockAlphaInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  color_t result = rgba_blender_normal(*m_srcAddress, m_color, m_opacity);
  *m_dstAddress =
    doc::rgba(rgba_getr(result), rgba_getg(result), rgba_getb(result), rgba_geta(*m_srcAddress));
}

template<>
void LockAlphaInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  color_t result = graya_blender_normal(*m_srcAddress, m_color, m_opacity);
  *m_dstAddress = graya(graya_getv(result), graya_geta(*m_srcAddress));
}

template<>
class LockAlphaInkProcessing<IndexedTraits>
  : public DoubleInkProcessing<LockAlphaInkProcessing<IndexedTraits>, IndexedTraits> {
public:
  LockAlphaInkProcessing(ToolLoop* loop)
    : m_palette(loop->getPalette())
    , m_rgbmap(loop->getRgbMap())
    , m_opacity(loop->getOpacity())
    , m_maskIndex(loop->getLayer()->isBackground() ? -1 : loop->sprite()->transparentColor())
  {
  }

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    m_color = m_palette->getEntry(loop->getPrimaryColor());
  }

  void processPixel(int x, int y)
  {
    color_t c = *m_srcAddress;
    if (int(c) == m_maskIndex)
      c = m_palette->getEntry(c) & rgba_rgb_mask; // Alpha = 0
    else
      c = m_palette->getEntry(c);

    color_t result = rgba_blender_normal(c, m_color, m_opacity);
    // TODO should we use m_rgbmap->mapColor instead?
    *m_dstAddress = m_palette->findBestfit(rgba_getr(result),
                                           rgba_getg(result),
                                           rgba_getb(result),
                                           rgba_geta(c),
                                           m_maskIndex);
  }

private:
  const Palette* m_palette;
  const RgbMap* m_rgbmap;
  color_t m_color;
  const int m_opacity;
  const int m_maskIndex;
};

//////////////////////////////////////////////////////////////////////
// Transparent Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class TransparentInkProcessing
  : public DoubleInkProcessing<TransparentInkProcessing<ImageTraits>, ImageTraits> {
public:
  TransparentInkProcessing(ToolLoop* loop) { m_opacity = loop->getOpacity(); }

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    m_color = loop->getPrimaryColor();
  }

  void processPixel(int x, int y)
  {
    // Do nothing
  }

private:
  color_t m_color;
  int m_opacity;
};

template<>
void TransparentInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  *m_dstAddress = rgba_blender_normal(*m_srcAddress, m_color, m_opacity);
}

template<>
void TransparentInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  *m_dstAddress = graya_blender_normal(*m_srcAddress, m_color, m_opacity);
}

template<>
class TransparentInkProcessing<IndexedTraits>
  : public DoubleInkProcessing<TransparentInkProcessing<IndexedTraits>, IndexedTraits> {
public:
  TransparentInkProcessing(ToolLoop* loop)
    : m_palette(loop->getPalette())
    , m_rgbmap(loop->getRgbMap())
    , m_opacity(loop->getOpacity())
    , m_maskIndex(loop->getLayer()->isBackground() ? -1 : loop->sprite()->transparentColor())
    , m_colorIndex(loop->getFgColor())
  {
  }

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    m_color = m_palette->getEntry(loop->getPrimaryColor());
  }

  void processPixel(int x, int y)
  {
    if (m_colorIndex == m_maskIndex)
      return;

    color_t c = *m_srcAddress;
    if (int(c) == m_maskIndex)
      c = m_palette->getEntry(c) & rgba_rgb_mask; // Alpha = 0
    else
      c = m_palette->getEntry(c);

    c = rgba_blender_normal(c, m_color, m_opacity);
    *m_dstAddress = m_rgbmap->mapColor(c);
  }

private:
  const Palette* m_palette;
  const RgbMap* m_rgbmap;
  const int m_opacity;
  color_t m_color;
  const int m_maskIndex;
  int m_colorIndex;
};

//////////////////////////////////////////////////////////////////////
// Merge Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class MergeInkProcessing
  : public DoubleInkProcessing<MergeInkProcessing<ImageTraits>, ImageTraits> {
public:
  MergeInkProcessing(ToolLoop* loop) { m_opacity = loop->getOpacity(); }

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    m_color = loop->getPrimaryColor();
  }

  void processPixel(int x, int y)
  {
    // Do nothing
  }

private:
  color_t m_color;
  int m_opacity;
};

template<>
void MergeInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  *m_dstAddress = rgba_blender_merge(*m_srcAddress, m_color, m_opacity);
}

template<>
void MergeInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  *m_dstAddress = graya_blender_merge(*m_srcAddress, m_color, m_opacity);
}

template<>
class MergeInkProcessing<IndexedTraits>
  : public DoubleInkProcessing<MergeInkProcessing<IndexedTraits>, IndexedTraits> {
public:
  MergeInkProcessing(ToolLoop* loop)
    : m_palette(loop->getPalette())
    , m_rgbmap(loop->getRgbMap())
    , m_opacity(loop->getOpacity())
    , m_maskIndex(loop->getLayer()->isBackground() ? -1 : loop->sprite()->transparentColor())
  {
  }

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    m_color = (int(loop->getPrimaryColor()) == m_maskIndex ?
                 (m_palette->getEntry(loop->getPrimaryColor()) & rgba_rgb_mask) :
                 (m_palette->getEntry(loop->getPrimaryColor())));
  }

  void processPixel(int x, int y)
  {
    color_t c = *m_srcAddress;
    if (int(c) == m_maskIndex)
      c = m_palette->getEntry(c) & rgba_rgb_mask; // Alpha = 0
    else
      c = m_palette->getEntry(c);

    c = rgba_blender_merge(c, m_color, m_opacity);
    *m_dstAddress = m_rgbmap->mapColor(c);
  }

private:
  const Palette* m_palette;
  const RgbMap* m_rgbmap;
  const int m_opacity;
  const int m_maskIndex;
  color_t m_color;
};

//////////////////////////////////////////////////////////////////////
// Blur Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class BlurInkProcessing : public DoubleInkProcessing<BlurInkProcessing<ImageTraits>, ImageTraits> {
public:
  BlurInkProcessing(ToolLoop* loop) {}
  void processPixel(int x, int y)
  {
    // Do nothing (it's specialized for each case)
  }
};

template<>
class BlurInkProcessing<RgbTraits>
  : public DoubleInkProcessing<BlurInkProcessing<RgbTraits>, RgbTraits> {
public:
  BlurInkProcessing(ToolLoop* loop)
    : m_opacity(loop->getOpacity())
    , m_tiledMode(loop->getTiledMode())
    , m_srcImage(loop->getSrcImage())
  {
  }

  void processPixel(int x, int y)
  {
    m_area.reset();
    get_neighboring_pixels<RgbTraits>(m_srcImage, x, y, 3, 3, 1, 1, m_tiledMode, m_area);

    if (m_area.count > 0) {
      m_area.r /= m_area.count;
      m_area.g /= m_area.count;
      m_area.b /= m_area.count;
      m_area.a /= 9;
      *m_dstAddress = rgba_blender_merge(*m_srcAddress,
                                         doc::rgba(m_area.r, m_area.g, m_area.b, m_area.a),
                                         m_opacity);
    }
    else {
      *m_dstAddress = *m_srcAddress;
    }
  }

private:
  struct GetPixelsDelegate {
    int count, r, g, b, a;

    void reset() { count = r = g = b = a = 0; }

    void operator()(RgbTraits::pixel_t color)
    {
      if (rgba_geta(color) != 0) {
        r += rgba_getr(color);
        g += rgba_getg(color);
        b += rgba_getb(color);
        a += rgba_geta(color);
        ++count;
      }
    }
  };

  int m_opacity;
  TiledMode m_tiledMode;
  const Image* m_srcImage;
  GetPixelsDelegate m_area;
};

template<>
class BlurInkProcessing<GrayscaleTraits>
  : public DoubleInkProcessing<BlurInkProcessing<GrayscaleTraits>, GrayscaleTraits> {
public:
  BlurInkProcessing(ToolLoop* loop)
    : m_opacity(loop->getOpacity())
    , m_tiledMode(loop->getTiledMode())
    , m_srcImage(loop->getSrcImage())
  {
  }

  void processPixel(int x, int y)
  {
    m_area.reset();
    get_neighboring_pixels<GrayscaleTraits>(m_srcImage, x, y, 3, 3, 1, 1, m_tiledMode, m_area);

    if (m_area.count > 0) {
      m_area.v /= m_area.count;
      m_area.a /= 9;
      *m_dstAddress = graya_blender_merge(*m_srcAddress, graya(m_area.v, m_area.a), m_opacity);
    }
    else {
      *m_dstAddress = *m_srcAddress;
    }
  }

private:
  struct GetPixelsDelegate {
    int count, v, a;

    void reset() { count = v = a = 0; }

    void operator()(GrayscaleTraits::pixel_t color)
    {
      if (graya_geta(color) > 0) {
        v += graya_getv(color);
        a += graya_geta(color);
        ++count;
      }
    }
  };

  int m_opacity;
  TiledMode m_tiledMode;
  const Image* m_srcImage;
  GetPixelsDelegate m_area;
};

template<>
class BlurInkProcessing<IndexedTraits>
  : public DoubleInkProcessing<BlurInkProcessing<IndexedTraits>, IndexedTraits> {
public:
  BlurInkProcessing(ToolLoop* loop)
    : m_palette(loop->getPalette())
    , m_rgbmap(loop->getRgbMap())
    , m_opacity(loop->getOpacity())
    , m_tiledMode(loop->getTiledMode())
    , m_srcImage(loop->getSrcImage())
    , m_area(loop->getPalette(),
             loop->getLayer()->isBackground() ? -1 : loop->sprite()->transparentColor())
  {
  }

  void processPixel(int x, int y)
  {
    m_area.reset();
    get_neighboring_pixels<IndexedTraits>(m_srcImage, x, y, 3, 3, 1, 1, m_tiledMode, m_area);

    if (m_area.count > 0) {
      m_area.r /= m_area.count;
      m_area.g /= m_area.count;
      m_area.b /= m_area.count;
      m_area.a /= 9;

      const color_t c = rgba_blender_merge(m_palette->getEntry(*m_srcAddress),
                                           doc::rgba(m_area.r, m_area.g, m_area.b, m_area.a),
                                           m_opacity);

      *m_dstAddress = m_rgbmap->mapColor(c);
    }
    else {
      *m_dstAddress = *m_srcAddress;
    }
  }

private:
  struct GetPixelsDelegate {
    const Palette* pal;
    int count, r, g, b, a;
    color_t maskColor;

    GetPixelsDelegate(const Palette* pal, color_t maskColor) : pal(pal), maskColor(maskColor) {}

    void reset() { count = r = g = b = a = 0; }

    void operator()(IndexedTraits::pixel_t color)
    {
      if (color == maskColor)
        return;

      uint32_t color32 = pal->getEntry(color);
      if (rgba_geta(color32) > 0) {
        r += rgba_getr(color32);
        g += rgba_getg(color32);
        b += rgba_getb(color32);
        a += rgba_geta(color32);
        ++count;
      }
    }
  };

  const Palette* m_palette;
  const RgbMap* m_rgbmap;
  int m_opacity;
  TiledMode m_tiledMode;
  const Image* m_srcImage;
  GetPixelsDelegate m_area;
};

//////////////////////////////////////////////////////////////////////
// Replace Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class ReplaceInkProcessing
  : public DoubleInkProcessing<ReplaceInkProcessing<ImageTraits>, ImageTraits> {
public:
  ReplaceInkProcessing(ToolLoop* loop)
  {
    m_color1 = loop->getPrimaryColor();
    m_color2 = loop->getSecondaryColor();
    if (loop->getLayer()->isBackground()) {
      switch (loop->sprite()->pixelFormat()) {
        case IMAGE_RGB:       m_color2 |= rgba_a_mask; break;
        case IMAGE_GRAYSCALE: m_color2 |= graya_a_mask; break;
      }
    }
    m_opacity = loop->getOpacity();
  }

  void processPixel(int x, int y)
  {
    // Do nothing (it's specialized for each case)
  }

private:
  color_t m_color1;
  color_t m_color2;
  int m_opacity;
};

template<>
void ReplaceInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  color_t src = (*m_srcAddress);

  // Colors (m_srcAddress and m_color1) match if:
  // * They are both completely transparent (alpha == 0)
  // * Or they are not transparent and the RGB values are the same
  if ((rgba_geta(src) == 0 && rgba_geta(m_color1) == 0) ||
      (rgba_geta(src) > 0 && rgba_geta(m_color1) > 0 &&
       ((src & rgba_rgb_mask) == (m_color1 & rgba_rgb_mask)))) {
    *m_dstAddress = rgba_blender_merge(src, m_color2, m_opacity);
  }
}

template<>
void ReplaceInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  color_t src = (*m_srcAddress);

  if ((graya_geta(src) == 0 && graya_geta(m_color1) == 0) ||
      (graya_geta(src) > 0 && graya_geta(m_color1) > 0 &&
       ((src & graya_v_mask) == (m_color1 & graya_v_mask)))) {
    *m_dstAddress = graya_blender_merge(src, m_color2, m_opacity);
  }
}

template<>
class ReplaceInkProcessing<IndexedTraits>
  : public DoubleInkProcessing<ReplaceInkProcessing<IndexedTraits>, IndexedTraits> {
public:
  ReplaceInkProcessing(ToolLoop* loop)
  {
    m_palette = loop->getPalette();
    m_rgbmap = loop->getRgbMap();
    m_color1 = loop->getPrimaryColor();
    m_color2 = loop->getSecondaryColor();
    m_opacity = loop->getOpacity();
    if (m_opacity < 255)
      m_color2 = m_palette->getEntry(m_color2);
  }

  void processPixel(int x, int y)
  {
    if (*m_srcAddress == m_color1) {
      if (m_opacity == 255)
        *m_dstAddress = m_color2;
      else {
        color_t c = rgba_blender_normal(m_palette->getEntry(*m_srcAddress), m_color2, m_opacity);

        *m_dstAddress = m_rgbmap->mapColor(c);
      }
    }
  }

private:
  const Palette* m_palette;
  const RgbMap* m_rgbmap;
  color_t m_color1;
  color_t m_color2;
  int m_opacity;
};

template<>
void ReplaceInkProcessing<TilemapTraits>::processPixel(int x, int y)
{
  color_t c = *m_srcAddress;
  *m_dstAddress = (c == m_color1 ? m_color2 : c);
}

//////////////////////////////////////////////////////////////////////
// Jumble Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class JumbleInkProcessing
  : public DoubleInkProcessing<JumbleInkProcessing<ImageTraits>, ImageTraits> {
public:
  JumbleInkProcessing(ToolLoop* loop)
    : m_palette(loop->getPalette())
    , m_rgbmap(loop->getRgbMap())
    , m_speed(loop->getSpeed() / 4)
    , m_opacity(loop->getOpacity())
    , m_tiledMode(loop->getTiledMode())
    , m_srcImage(loop->getSrcImage())
    , m_srcImageWidth(m_srcImage->width())
    , m_srcImageHeight(m_srcImage->height())
  {
  }

  void processPixel(int x, int y)
  {
    // Do nothing (it's specialized for each case)
  }

private:
  void pickColorFromArea(int x, int y)
  {
    gfx::Point pt(x + (rand() % 3) - 1 - m_speed.x, y + (rand() % 3) - 1 - m_speed.y);

    pt = wrap_point(m_tiledMode, gfx::Size(m_srcImageWidth, m_srcImageHeight), pt, false);

    pt.x = std::clamp(pt.x, 0, m_srcImageWidth - 1);
    pt.y = std::clamp(pt.y, 0, m_srcImageHeight - 1);

    m_color = get_pixel(m_srcImage, pt.x, pt.y);
  }

  const Palette* m_palette;
  const RgbMap* m_rgbmap;
  Point m_speed;
  int m_opacity;
  TiledMode m_tiledMode;
  const Image* m_srcImage;
  int m_srcImageWidth;
  int m_srcImageHeight;
  color_t m_color;
};

template<>
void JumbleInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  pickColorFromArea(x, y);
  *m_dstAddress = rgba_blender_merge(*m_srcAddress, m_color, m_opacity);
}

template<>
void JumbleInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  pickColorFromArea(x, y);
  *m_dstAddress = graya_blender_merge(*m_srcAddress, m_color, m_opacity);
}

template<>
void JumbleInkProcessing<IndexedTraits>::processPixel(int x, int y)
{
  pickColorFromArea(x, y);

  color_t tc = (m_color != 0 ? m_palette->getEntry(m_color) : 0);
  color_t c =
    rgba_blender_merge(*m_srcAddress != 0 ? m_palette->getEntry(*m_srcAddress) : 0, tc, m_opacity);

  if (rgba_geta(c) >= 128)
    *m_dstAddress = m_rgbmap->mapColor(c);
  else
    *m_dstAddress = 0;
}

//////////////////////////////////////////////////////////////////////
// Shading Ink Helper used by ShadingInkProcessing and
// BrushShadingInkProcessing (it does only the shading of one pixel
// using some auxiliary structures)
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class PixelShadingInkHelper {
public:
  PixelShadingInkHelper(ToolLoop* loop)
  {
    static_assert(false && sizeof(ImageTraits), "Use specialized cases");
  }
};

template<>
class PixelShadingInkHelper<RgbTraits> {
public:
  using pixel_t = RgbTraits::pixel_t;

  PixelShadingInkHelper(ToolLoop* loop)
    : m_shadePalette(0, 1)
    , m_left(loop->getMouseButton() == ToolLoop::Left)
  {
    const Shade shade = loop->getShade();
    m_shadePalette.resize(shade.size());
    int i = 0;
    for (app::Color color : shade) {
      m_shadePalette.setEntry(i++, color_utils::color_for_layer(color, loop->getLayer()));
    }
  }

  int findIndex(uint8_t r, uint8_t g, uint8_t b, uint8_t a) const
  {
    return m_shadePalette.findExactMatch(r, g, b, a, -1);
  }

  pixel_t operator()(const pixel_t src) const
  {
    int i = findIndex(rgba_getr(src), rgba_getg(src), rgba_getb(src), rgba_geta(src));
    if (i < 0)
      return src;

    if (m_left) {
      if (i > 0)
        --i;
    }
    else {
      if (i < m_shadePalette.size() - 1)
        ++i;
    }
    return m_shadePalette.getEntry(i);
  }

private:
  Palette m_shadePalette;
  bool m_left;
};

template<>
class PixelShadingInkHelper<GrayscaleTraits> {
public:
  using pixel_t = GrayscaleTraits::pixel_t;

  PixelShadingInkHelper(ToolLoop* loop)
    : m_shadePalette(0, 1)
    , m_left(loop->getMouseButton() == ToolLoop::Left)
  {
    Shade shade = loop->getShade();
    m_shadePalette.resize(shade.size());

    // As the colors are going to a palette, we need RGB colors
    // (instead of Grayscale)
    const ColorTarget target((loop->getLayer()->isBackground() ? ColorTarget::BackgroundLayer :
                                                                 ColorTarget::TransparentLayer),
                             IMAGE_RGB,
                             0);

    int i = 0;
    for (app::Color color : shade) {
      m_shadePalette.setEntry(i++, color_utils::color_for_target(color, target));
    }
  }

  int findIndex(uint8_t r, uint8_t g, uint8_t b, uint8_t a) const
  {
    return m_shadePalette.findExactMatch(r, g, b, a, -1);
  }

  pixel_t operator()(const pixel_t src) const
  {
    int i = findIndex(graya_getv(src), graya_getv(src), graya_getv(src), graya_geta(src));
    if (i < 0)
      return src;

    if (m_left) {
      if (i > 0)
        --i;
    }
    else {
      if (i < m_shadePalette.size() - 1)
        ++i;
    }

    color_t rgba = m_shadePalette.getEntry(i);
    return graya(rgba_getr(rgba), rgba_geta(rgba));
  }

private:
  Palette m_shadePalette;
  bool m_left;
};

template<>
class PixelShadingInkHelper<IndexedTraits> {
public:
  using pixel_t = IndexedTraits::pixel_t;

  PixelShadingInkHelper(ToolLoop* loop)
    : m_palette(loop->getPalette())
    , m_remap(loop->getShadingRemap())
    , m_left(loop->getMouseButton() == ToolLoop::Left)
  {
  }

  int findIndex(uint8_t r, uint8_t g, uint8_t b, uint8_t a) const
  {
    return m_palette->findExactMatch(r, g, b, a, -1);
  }

  pixel_t operator()(pixel_t i) const
  {
    if (m_remap) {
      i = (*m_remap)[i];
    }
    else {
      if (m_left) {
        if (i > 0)
          --i;
      }
      else {
        if (i < m_palette->size() - 1)
          ++i;
      }
    }
    return i;
  }

private:
  const Palette* m_palette;
  const Remap* m_remap;
  bool m_left;
};

//////////////////////////////////////////////////////////////////////
// Shading Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class ShadingInkProcessing
  : public DoubleInkProcessing<ShadingInkProcessing<ImageTraits>, ImageTraits> {
public:
  using Base = DoubleInkProcessing<ShadingInkProcessing<ImageTraits>, ImageTraits>;

  ShadingInkProcessing(ToolLoop* loop) : m_shading(loop) {}
  void processPixel(int x, int y) { *Base::m_dstAddress = m_shading(*Base::m_srcAddress); }

private:
  PixelShadingInkHelper<ImageTraits> m_shading;
};

//////////////////////////////////////////////////////////////////////
// Gradient Ink
//////////////////////////////////////////////////////////////////////

static ImageBufferPtr tmpGradientBuffer; // TODO non-thread safe

class GradientRenderer {
public:
  GradientRenderer(ToolLoop* loop) : m_tiledMode(loop->getTiledMode())
  {
    if (!tmpGradientBuffer)
      tmpGradientBuffer.reset(new ImageBuffer(1));

    m_tmpImage.reset(Image::create(IMAGE_RGB,
                                   loop->getDstImage()->width(),
                                   loop->getDstImage()->height(),
                                   tmpGradientBuffer));
    m_tmpImage->clear(0);
  }

  void renderRgbaGradient(ToolLoop* loop,
                          Strokes& strokes,
                          // RGBA colors
                          color_t c0,
                          color_t c1)
  {
    if (strokes.empty() || strokes[0].size() < 2) {
      m_tmpImage->clear(0);
      return;
    }

    const gfx::Point u = strokes[0].firstPoint().toPoint();
    const gfx::Point v = strokes[0].lastPoint().toPoint();

    // The image position for the gradient depends on the first user
    // click. The gradient depends on the first clicked tile.
    gfx::Point imgPos(0, 0);
    if (int(m_tiledMode) & int(TiledMode::X_AXIS)) {
      const int w = loop->sprite()->width();
      imgPos.x = u.x / w;
      imgPos.x *= w;
    }
    if (int(m_tiledMode) & int(TiledMode::Y_AXIS)) {
      const int h = loop->sprite()->height();
      imgPos.y = u.y / h;
      imgPos.y *= h;
    }

    render::render_rgba_gradient(m_tmpImage.get(),
                                 imgPos,
                                 u,
                                 v,
                                 c0,
                                 c1,
                                 loop->getDitheringMatrix(),
                                 loop->getGradientType());
  }

protected:
  ImageRef m_tmpImage;
  RgbTraits::address_t m_tmpAddress;
  TiledMode m_tiledMode;
};

template<typename ImageTraits>
class GradientInkProcessing
  : public GradientRenderer,
    public DoubleInkProcessing<GradientInkProcessing<ImageTraits>, ImageTraits> {
public:
  typedef DoubleInkProcessing<GradientInkProcessing<ImageTraits>, ImageTraits> base;

  GradientInkProcessing(ToolLoop* loop)
    : GradientRenderer(loop)
    , m_opacity(loop->getOpacity())
    , m_palette(loop->getPalette())
    , m_rgbmap(loop->getRgbMap())
    , m_maskIndex(loop->getLayer()->isBackground() ? -1 : loop->sprite()->transparentColor())
  {
  }

  void processScanline(int x1, int y, int x2, ToolLoop* loop) override
  {
    m_tmpAddress = (RgbTraits::address_t)m_tmpImage->getPixelAddress(x1, y);
    base::processScanline(x1, y, x2, loop);
  }

  void prepareForStrokes(ToolLoop* loop, Strokes& strokes) override
  {
    // Do nothing
  }

  void processPixel(int x, int y)
  {
    // Do nothing (it's specialized for each case)
  }

private:
  const int m_opacity;
  const Palette* m_palette;
  const RgbMap* m_rgbmap;
  const int m_maskIndex;
};

template<>
void GradientInkProcessing<RgbTraits>::prepareForStrokes(ToolLoop* loop, Strokes& strokes)
{
  color_t c0 = loop->getPrimaryColor();
  color_t c1 = loop->getSecondaryColor();

  renderRgbaGradient(loop, strokes, c0, c1);
}

template<>
void GradientInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  *m_dstAddress = rgba_blender_normal(*m_srcAddress, *m_tmpAddress, m_opacity);
  ++m_tmpAddress;
}

template<>
void GradientInkProcessing<GrayscaleTraits>::prepareForStrokes(ToolLoop* loop, Strokes& strokes)
{
  color_t c0 = loop->getPrimaryColor();
  color_t c1 = loop->getSecondaryColor();
  int v0 = int(doc::graya_getv(c0));
  int a0 = int(doc::graya_geta(c0));
  int v1 = int(doc::graya_getv(c1));
  int a1 = int(doc::graya_geta(c1));
  c0 = doc::rgba(v0, v0, v0, a0);
  c1 = doc::rgba(v1, v1, v1, a1);

  renderRgbaGradient(loop, strokes, c0, c1);
}

template<>
void GradientInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  doc::color_t c = *m_tmpAddress;
  int a = doc::rgba_geta(c);
  int v = doc::rgba_getr(c);

  *m_dstAddress = graya_blender_normal(*m_srcAddress, doc::graya(v, a), m_opacity);
  ++m_tmpAddress;
}

template<>
void GradientInkProcessing<IndexedTraits>::prepareForStrokes(ToolLoop* loop, Strokes& strokes)
{
  color_t c0 = m_palette->getEntry(loop->getPrimaryColor());
  color_t c1 = m_palette->getEntry(loop->getSecondaryColor());

  renderRgbaGradient(loop, strokes, c0, c1);
}

template<>
void GradientInkProcessing<IndexedTraits>::processPixel(int x, int y)
{
  doc::color_t c = *m_tmpAddress;
  doc::color_t c0 = *m_srcAddress;
  if (int(c0) == m_maskIndex)
    c0 = m_palette->getEntry(c0) & rgba_rgb_mask; // Alpha = 0
  else
    c0 = m_palette->getEntry(c0);
  c = rgba_blender_normal(c0, c, m_opacity);

  *m_dstAddress = m_rgbmap->mapColor(c);

  ++m_tmpAddress;
}

//////////////////////////////////////////////////////////////////////
// Xor Ink
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class XorInkProcessing : public DoubleInkProcessing<XorInkProcessing<ImageTraits>, ImageTraits> {
public:
  XorInkProcessing(ToolLoop* loop) {}
  void processPixel(int x, int y) {}
};

template<>
void XorInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  *m_dstAddress = rgba_blender_neg_bw(*m_srcAddress, 0, 255);
}

template<>
void XorInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  *m_dstAddress = graya_blender_neg_bw(*m_srcAddress, 0, 255);
}

template<>
class XorInkProcessing<IndexedTraits>
  : public DoubleInkProcessing<XorInkProcessing<IndexedTraits>, IndexedTraits> {
public:
  XorInkProcessing(ToolLoop* loop) : m_palette(loop->getPalette()), m_rgbmap(loop->getRgbMap()) {}

  void processPixel(int x, int y)
  {
    color_t c = rgba_blender_neg_bw(m_palette->getEntry(*m_srcAddress), 0, 255);
    *m_dstAddress = m_rgbmap->mapColor(c);
  }

private:
  const Palette* m_palette;
  const RgbMap* m_rgbmap;
};

//////////////////////////////////////////////////////////////////////
// Brush Ink - Base
//////////////////////////////////////////////////////////////////////

// TODO In all cases where we get the brush index and use that index
//      in m_palette->getEntry(index), the color is converted to the
//      sprite palette or to the grayscale palette (for grayscale
//      sprites), we might want to save the original palette in the
//      brush to use that one in these cases (not sure if this does
//      applies when we select a new foreground/background color in
//      the color bar and the brush color changes, or if this should
//      be a new optional flag/parameter to save on each brush)
template<typename ImageTraits>
class BrushInkProcessingBase
  : public DoubleInkProcessing<BrushInkProcessingBase<ImageTraits>, ImageTraits> {
public:
  BrushInkProcessingBase(ToolLoop* loop)
  {
    m_fgColor = loop->getPrimaryColor();
    m_bgColor = loop->getSecondaryColor();
    m_palette = loop->getPalette();
    m_brush = loop->getBrush();
    m_brushImage = (m_brush->patternImage() ? m_brush->patternImage() : m_brush->image());
    m_brushMask = m_brush->maskBitmap();
    m_patternAlign = m_brush->pattern();
    m_opacity = loop->getOpacity();
    m_width = m_brush->bounds().w;
    m_height = m_brush->bounds().h;
    m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % m_width;
    m_v = (m_brush->patternOrigin().y - loop->getCelOrigin().y) % m_height;

    if (loop->sprite()->colorMode() == ColorMode::INDEXED) {
      if (loop->getLayer()->isTransparent())
        m_transparentColor = loop->sprite()->transparentColor();
      else
        m_transparentColor = -1;
    }
    else
      m_transparentColor = 0;
  }

  void prepareForPointShape(ToolLoop* loop, bool firstPoint, int x, int y) override
  {
    if (m_patternAlign != BrushPattern::ALIGNED_TO_SRC) {
      // Case: during painting process with PaintBucket Tool
      if (loop->getPointShape()->isFloodFill()) {
        m_u = x - m_brush->bounds().w / 2;
        m_v = y - m_brush->bounds().h / 2;
      }
      // Case: during brush preview of PaintBucket Tool
      else if (loop->getController()->isOnePoint()) {
        m_u = 0;
        m_v = 0;
      }
      else {
        m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x) %
              m_width;
        m_v = ((m_brush->patternOrigin().y % loop->sprite()->height()) - loop->getCelOrigin().y) %
              m_height;
      }
    }
  }

  void prepareVForPointShape(ToolLoop* loop, int y) override
  {
    if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
      m_v = (m_brush->patternOrigin().y - loop->getCelOrigin().y) % m_height;
      if (m_v < 0)
        m_v += m_height;
    }
    else {
      int spriteH = loop->sprite()->height();
      if (y / spriteH > 0)
        // 'y' is outside of the center tile.
        m_v = (m_brush->patternOrigin().y + m_height - (y / spriteH) * spriteH) % m_height;
      else
        // 'y' is inside of the center tile.
        m_v = ((m_brush->patternOrigin().y % spriteH) - loop->getCelOrigin().y) % m_height;
    }
  }

  void prepareUForPointShapeWholeScanline(ToolLoop* loop, int x1) override
  {
    if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
      m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % m_width;
      if (m_u < 0)
        m_u += m_height;
    }
    else {
      m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x) %
            m_width;
      if (x1 / loop->sprite()->width() > 0)
        m_u = (m_brush->patternOrigin().x + m_width -
               (x1 / loop->sprite()->width()) * loop->sprite()->width()) %
              m_width;
    }
  }

  void prepareUForPointShapeSlicedScanline(ToolLoop* loop, bool leftSlice, int x1) override
  {
    if (m_patternAlign == doc::BrushPattern::ALIGNED_TO_SRC) {
      m_u = (m_brush->patternOrigin().x - loop->getCelOrigin().x) % m_width;
      if (m_u < 0)
        m_u += m_height;
      return;
    }
    else {
      if (leftSlice)
        m_u = ((m_brush->patternOrigin().x % loop->sprite()->width()) - loop->getCelOrigin().x) %
              m_width;
      else
        m_u = (m_brush->patternOrigin().x + m_width -
               (x1 / loop->sprite()->width() + 1) * loop->sprite()->width()) %
              m_width;
    }
  }

  bool preProcessPixel(int x, int y, color_t* result)
  {
    // Do nothing
    return true;
  }

  // TODO Remove this virtual function in some way. At the moment we
  //      need it because InkProcessing expects that its Derived
  //      template parameter has a processPixel() member function.
  virtual void processPixel(int x, int y)
  {
    // Do nothing
  }

protected:
  bool alignPixelPoint(int& x0, int& y0)
  {
    int x = (x0 - m_u) % m_width;
    int y = (y0 - m_v) % m_height;
    if (x < 0)
      x = m_width - ((-x) % m_width);
    if (y < 0)
      y = m_height - ((-y) % m_height);

    if (m_brushMask && !get_pixel_fast<BitmapTraits>(m_brushMask, x, y))
      return false;

    if (m_brush->patternImage()) {
      const int w = m_brush->patternImage()->width();
      const int h = m_brush->patternImage()->height();
      x = x0 % w;
      y = y0 % h;
      if (x < 0)
        x = w - ((-x) % w);
      if (y < 0)
        y = h - ((-y) % h);
    }

    x0 = x;
    y0 = y;
    return true;
  }

  color_t m_fgColor;
  color_t m_bgColor;
  const Palette* m_palette;
  const Brush* m_brush;
  const Image* m_brushImage;
  const Image* m_brushMask;
  BrushPattern m_patternAlign;
  int m_opacity;
  int m_u, m_v, m_width, m_height;
  // When we have a image brush from an INDEXED sprite, we need to know
  // which is the background color in order to translate to transparent color
  // in a RGBA sprite.
  color_t m_transparentColor;
};

template<>
bool BrushInkProcessingBase<RgbTraits>::preProcessPixel(int x, int y, color_t* result)
{
  if (!alignPixelPoint(x, y))
    return false;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);

      // We blend the previous image brush pixel with a pixel from the
      // image preview (*m_dstAddress). Yes, dstImage, in that way we
      // can overlap image brush self printed areas (auto compose
      // colors). Doing this, we avoid eraser action of the pixels
      // with alpha <255 in the image brush.
      c = rgba_blender_normal(*m_dstAddress, c, m_opacity);
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (m_transparentColor == c)
        c = 0;
      else
        c = m_palette->getEntry(c);
      c = rgba_blender_normal(*m_dstAddress, c, m_opacity);
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      c = doc::rgba(graya_getv(c), graya_getv(c), graya_getv(c), graya_geta(c));
      c = rgba_blender_normal(*m_dstAddress, c, m_opacity);
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_fgColor : m_bgColor;
      break;
    }
    default: ASSERT(false); return false;
  }
  *result = c;
  return true;
}

template<>
bool BrushInkProcessingBase<GrayscaleTraits>::preProcessPixel(int x, int y, color_t* result)
{
  if (!alignPixelPoint(x, y))
    return false;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      c = graya(rgba_luma(c), rgba_geta(c));
      c = graya_blender_normal(*m_dstAddress, c, m_opacity);
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (m_transparentColor == c)
        c = 0;
      else
        c = m_palette->getEntry(c);
      c = graya(rgba_luma(c), rgba_geta(c));
      c = graya_blender_normal(*m_dstAddress, c, m_opacity);
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      c = graya_blender_normal(*m_dstAddress, c, m_opacity);
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_fgColor : m_bgColor;
      break;
    }
    default: ASSERT(false); return false;
  }
  *result = c;
  return true;
}

template<>
bool BrushInkProcessingBase<IndexedTraits>::preProcessPixel(int x, int y, color_t* result)
{
  if (!alignPixelPoint(x, y))
    return false;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      color_t d = m_palette->getEntry(*m_dstAddress);
      c = rgba_blender_normal(d, c, m_opacity);
      c = m_palette->findBestfit(rgba_getr(c),
                                 rgba_getg(c),
                                 rgba_getb(c),
                                 rgba_geta(c),
                                 m_transparentColor);
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (c == m_transparentColor)
        return false;

      color_t f = m_palette->getEntry(c);

      // Keep original index in special opaque case
      if (rgba_geta(f) == 255 && m_opacity == 255)
        break;

      color_t b = m_palette->getEntry(*m_dstAddress);
      c = rgba_blender_normal(b, f, m_opacity);
      c = m_palette->findBestfit(rgba_getr(c),
                                 rgba_getg(c),
                                 rgba_getb(c),
                                 rgba_geta(c),
                                 m_transparentColor);
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      color_t b = m_palette->getEntry(*m_dstAddress);
      b = graya(rgba_luma(b), rgba_geta(b));
      c = graya_blender_normal(b, c, m_opacity);
      c = m_palette->findBestfit(graya_getv(c),
                                 graya_getv(c),
                                 graya_getv(c),
                                 graya_geta(c),
                                 m_transparentColor);
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_fgColor : m_bgColor;
      break;
    }
    default: ASSERT(false); return false;
  }
  if (c != m_transparentColor) {
    *result = c;
    return true;
  }
  return false;
}

//////////////////////////////////////////////////////////////////////
// Brush Ink - Simple ink type
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class BrushSimpleInkProcessing : public BrushInkProcessingBase<ImageTraits> {
public:
  BrushSimpleInkProcessing(ToolLoop* loop) : BrushInkProcessingBase<ImageTraits>(loop) {}

  void processPixel(int x, int y) override
  {
    // Do nothing
  }
};

template<>
void BrushSimpleInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  color_t c;
  if (preProcessPixel(x, y, &c))
    *m_dstAddress = c;
}

template<>
void BrushSimpleInkProcessing<IndexedTraits>::processPixel(int x, int y)
{
  color_t c;
  if (preProcessPixel(x, y, &c))
    *m_dstAddress = c;
}

template<>
void BrushSimpleInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  color_t c;
  if (preProcessPixel(x, y, &c))
    *m_dstAddress = c;
}

//////////////////////////////////////////////////////////////////////
// Brush Ink - Lock Alpha ink type
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class BrushLockAlphaInkProcessing : public BrushInkProcessingBase<ImageTraits> {
public:
  BrushLockAlphaInkProcessing(ToolLoop* loop) : BrushInkProcessingBase<ImageTraits>(loop) {}

  void processPixel(int x, int y) override
  {
    // Do nothing
  }
};

template<>
void BrushLockAlphaInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  color_t c;
  if (preProcessPixel(x, y, &c))
    *m_dstAddress = doc::rgba(rgba_getr(c), rgba_getg(c), rgba_getb(c), rgba_geta(*m_srcAddress));
}

template<>
void BrushLockAlphaInkProcessing<IndexedTraits>::processPixel(int x, int y)
{
  if (*m_srcAddress != m_transparentColor) {
    color_t c;
    if (preProcessPixel(x, y, &c))
      *m_dstAddress = c;
  }
}

template<>
void BrushLockAlphaInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  color_t c;
  if (preProcessPixel(x, y, &c))
    *m_dstAddress = graya(graya_getv(c), graya_geta(*m_srcAddress));
}

//////////////////////////////////////////////////////////////////////
// Brush Ink - Eraser Tool
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class BrushEraserInkProcessing : public BrushInkProcessingBase<ImageTraits> {
public:
  BrushEraserInkProcessing(ToolLoop* loop) : BrushInkProcessingBase<ImageTraits>(loop) {}

  void processPixel(int x, int y) override
  {
    // Do nothing
  }
};

template<>
void BrushEraserInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      int t;
      c = doc::rgba(rgba_getr(*m_srcAddress),
                    rgba_getg(*m_srcAddress),
                    rgba_getb(*m_srcAddress),
                    MUL_UN8(rgba_geta(*m_dstAddress), 255 - rgba_geta(c), t));
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (m_transparentColor == c)
        c = 0;
      else
        c = m_palette->getEntry(c);
      int t;
      c = doc::rgba(rgba_getr(*m_srcAddress),
                    rgba_getg(*m_srcAddress),
                    rgba_getb(*m_srcAddress),
                    MUL_UN8(rgba_geta(*m_dstAddress), 255 - rgba_geta(c), t));
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      int t;
      c = doc::rgba(rgba_getr(*m_srcAddress),
                    rgba_getg(*m_srcAddress),
                    rgba_getb(*m_srcAddress),
                    MUL_UN8(rgba_geta(*m_dstAddress), 255 - graya_geta(c), t));
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_bgColor : *m_srcAddress;
      break;
    }
    default: ASSERT(false); return;
  }
  *m_dstAddress = c;
  ;
}

template<>
void BrushEraserInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      int t;
      c = graya(graya_getv(*m_srcAddress),
                MUL_UN8(graya_geta(*m_dstAddress), 255 - rgba_geta(c), t));
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (m_transparentColor == c)
        c = 0;
      else {
        c = m_palette->getEntry(c);
      }
      int t;
      c = graya(graya_getv(*m_srcAddress),
                MUL_UN8(graya_geta(*m_dstAddress), 255 - rgba_geta(c), t));
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      int t;
      c = graya(graya_getv(*m_srcAddress),
                MUL_UN8(graya_geta(*m_dstAddress), 255 - graya_geta(c), t));
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_bgColor : *m_srcAddress;
      break;
    }
    default: ASSERT(false); return;
  }
  *m_dstAddress = c;
}

template<>
void BrushEraserInkProcessing<IndexedTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      c = m_palette->findBestfit(rgba_getr(c),
                                 rgba_getg(c),
                                 rgba_getb(c),
                                 rgba_geta(c),
                                 m_transparentColor);
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      c = m_palette->findBestfit(graya_getv(c),
                                 graya_getv(c),
                                 graya_getv(c),
                                 graya_geta(c),
                                 m_transparentColor);
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_fgColor : m_bgColor;
      break;
    }
    default: ASSERT(false); return;
  }
  if (c != m_transparentColor) {
    *m_dstAddress = m_transparentColor;
  }
}

//////////////////////////////////////////////////////////////////////
// Brush Ink - Shading ink type
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class BrushShadingInkProcessing : public BrushInkProcessingBase<ImageTraits> {
public:
  using pixel_t = typename ImageTraits::pixel_t;

  BrushShadingInkProcessing(ToolLoop* loop)
    : BrushInkProcessingBase<ImageTraits>(loop)
    , m_shading(loop)
  {
  }

  void processPixel(int x, int y) override
  {
    // Do nothing
  }

private:
  PixelShadingInkHelper<ImageTraits> m_shading;
};

template<>
void BrushShadingInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      auto c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      int iBrush = m_shading.findIndex(rgba_getr(c), rgba_getg(c), rgba_getb(c), rgba_geta(c));
      if (rgba_geta(c) != 0 && iBrush >= 0)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    case IMAGE_INDEXED: {
      auto c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (m_transparentColor != c)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    case IMAGE_GRAYSCALE: {
      auto c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      if (graya_geta(c) != 0)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    default: ASSERT(false); return;
  }
};

template<>
void BrushShadingInkProcessing<IndexedTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      auto c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      int iBrush = m_shading.findIndex(rgba_getr(c), rgba_getg(c), rgba_getb(c), rgba_geta(c));
      if (rgba_geta(c) != 0 && iBrush >= 0)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    case IMAGE_INDEXED: {
      auto c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (m_transparentColor != c)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    case IMAGE_GRAYSCALE: {
      auto c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      if (graya_geta(c) != 0)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    default: ASSERT(false); return;
  }
};

template<>
void BrushShadingInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      auto c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      int iBrush = m_shading.findIndex(rgba_getr(c), rgba_getg(c), rgba_getb(c), rgba_geta(c));
      if (rgba_geta(c) != 0 && iBrush >= 0)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    case IMAGE_INDEXED: {
      auto c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (m_transparentColor != c)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    case IMAGE_GRAYSCALE: {
      auto c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      if (graya_geta(c) != 0)
        *m_dstAddress = m_shading(*m_srcAddress);
      break;
    }
    default: ASSERT(false); return;
  }
};

//////////////////////////////////////////////////////////////////////
// Brush Ink - Copy Alpha+Color ink type
//////////////////////////////////////////////////////////////////////

template<typename ImageTraits>
class BrushCopyInkProcessing : public BrushInkProcessingBase<ImageTraits> {
public:
  BrushCopyInkProcessing(ToolLoop* loop) : BrushInkProcessingBase<ImageTraits>(loop) {}

  void processPixel(int x, int y) override
  {
    // Do nothing
  }
};

template<>
void BrushCopyInkProcessing<RgbTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      if (rgba_geta(c) == 0)
        return;
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (c == m_transparentColor) {
        *m_dstAddress = *m_srcAddress;
        return;
      }
      c = m_palette->getEntry(c);
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      if (graya_geta(c) == 0)
        return;
      c = doc::rgba(graya_getv(c), graya_getv(c), graya_getv(c), graya_geta(c));
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_fgColor : m_bgColor;
      break;
    }
    default: ASSERT(false); return;
  }
  *m_dstAddress = c;
}

template<>
void BrushCopyInkProcessing<IndexedTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      if (rgba_geta(c) == 0)
        return;
      c = m_palette->findBestfit(rgba_getr(c),
                                 rgba_getg(c),
                                 rgba_getb(c),
                                 rgba_geta(c),
                                 m_transparentColor);
      if (c == 0)
        c = *m_srcAddress;
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (c == m_transparentColor)
        c = *m_srcAddress;
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      if (graya_geta(c) == 0)
        return;
      c = m_palette->findBestfit(graya_getv(c),
                                 graya_getv(c),
                                 graya_getv(c),
                                 graya_geta(c),
                                 m_transparentColor);
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_fgColor : m_bgColor;
      break;
    }
    default: ASSERT(false); return;
  }
  *m_dstAddress = c;
}

template<>
void BrushCopyInkProcessing<GrayscaleTraits>::processPixel(int x, int y)
{
  if (!alignPixelPoint(x, y))
    return;

  color_t c;
  switch (m_brushImage->pixelFormat()) {
    case IMAGE_RGB: {
      c = get_pixel_fast<RgbTraits>(m_brushImage, x, y);
      if (rgba_geta(c) == 0)
        return;
      c = graya(rgba_luma(c), rgba_geta(c));
      break;
    }
    case IMAGE_INDEXED: {
      c = get_pixel_fast<IndexedTraits>(m_brushImage, x, y);
      if (c == m_transparentColor) {
        *m_dstAddress = *m_srcAddress;
        return;
      }
      c = m_palette->getEntry(c);
      c = graya(rgba_luma(c), rgba_geta(c));
      break;
    }
    case IMAGE_GRAYSCALE: {
      c = get_pixel_fast<GrayscaleTraits>(m_brushImage, x, y);
      if (graya_geta(c) == 0)
        return;
      break;
    }
    case IMAGE_BITMAP: {
      // TODO In which circuntance is possible this case?
      c = get_pixel_fast<BitmapTraits>(m_brushImage, x, y);
      c = c ? m_fgColor : m_bgColor;
      break;
    }
    default: ASSERT(false); return;
  }
  *m_dstAddress = c;
}

//////////////////////////////////////////////////////////////////////

template<template<typename> class T>
BaseInkProcessing* get_ink_proc(ToolLoop* loop)
{
  switch (loop->sprite()->pixelFormat()) {
    case IMAGE_RGB:       return new T<RgbTraits>(loop);
    case IMAGE_GRAYSCALE: return new T<GrayscaleTraits>(loop);
    case IMAGE_INDEXED:   return new T<IndexedTraits>(loop);
  }
  ASSERT(false);
  return nullptr;
}

template<template<typename> class T>
BaseInkProcessing* get_ink_proc2(ToolLoop* loop)
{
  switch (loop->getDstImage()->pixelFormat()) {
    case IMAGE_RGB:       return new T<RgbTraits>(loop);
    case IMAGE_GRAYSCALE: return new T<GrayscaleTraits>(loop);
    case IMAGE_INDEXED:   return new T<IndexedTraits>(loop);
    case IMAGE_TILEMAP:   return new T<TilemapTraits>(loop);
  }
  ASSERT(false);
  return nullptr;
}

} // anonymous namespace
}} // namespace app::tools
