// Aseprite Render Library
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.

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

#include "render/gradient.h"

#include "base/vector2d.h"
#include "doc/image.h"
#include "doc/image_impl.h"
#include "render/dithering_matrix.h"

namespace render {

void render_rgba_gradient(
  doc::Image* img,
  const gfx::Point imgPos,
  const gfx::Point p0,
  const gfx::Point p1,
  doc::color_t c0,
  doc::color_t c1,
  const render::DitheringMatrix& matrix,
  const GradientType type)
{
  switch (type) {
    case GradientType::Linear:
      render_rgba_linear_gradient(img, imgPos, p0, p1, c0, c1, matrix);
      break;
    case GradientType::Radial:
      render_rgba_radial_gradient(img, imgPos, p0, p1, c0, c1, matrix);
      break;
  }
}

void render_rgba_linear_gradient(
  doc::Image* img,
  const gfx::Point imgPos,
  const gfx::Point p0,
  const gfx::Point p1,
  doc::color_t c0,
  doc::color_t c1,
  const render::DitheringMatrix& matrix)
{
  ASSERT(img->pixelFormat() == doc::IMAGE_RGB);
  if (img->pixelFormat() != doc::IMAGE_RGB) {
    return;
  }

  // If there is no vector defining the gradient (just one point),
  // the "gradient" will be just "c0"
  if (p0 == p1) {
    img->clear(c0);
    return;
  }

  base::Vector2d<double>
    u(p0.x, p0.y),
    v(p1.x, p1.y), w;
  w = v - u;
  const double wmag = w.magnitude();
  w = w.normalize();

  // As we use non-premultiplied RGB values, we need correct RGB
  // values on each stop. So in case that one color has alpha=0
  // (complete transparent), use the RGB values of the
  // non-transparent color in the other stop point.
  if (doc::rgba_geta(c0) == 0 &&
      doc::rgba_geta(c1) != 0) {
    c0 = (c1 & doc::rgba_rgb_mask);
  }
  else if (doc::rgba_geta(c0) != 0 &&
           doc::rgba_geta(c1) == 0) {
    c1 = (c0 & doc::rgba_rgb_mask);
  }

  const uint8_t r0 = doc::rgba_getr(c0);
  const uint8_t g0 = doc::rgba_getg(c0);
  const uint8_t b0 = doc::rgba_getb(c0);
  const uint8_t a0 = doc::rgba_geta(c0);

  const uint8_t r1 = doc::rgba_getr(c1);
  const uint8_t g1 = doc::rgba_getg(c1);
  const uint8_t b1 = doc::rgba_getb(c1);
  const uint8_t a1 = doc::rgba_geta(c1);

  doc::LockImageBits<doc::RgbTraits> bits(img);
  auto it = bits.begin();
  const int width = img->width();
  const int height = img->height();

  if (matrix.rows() == 1 && matrix.cols() == 1) {
    for (int y=0; y<height; ++y) {
      for (int x=0; x<width; ++x, ++it) {
        base::Vector2d<double> q(imgPos.x+x,
                                 imgPos.y+y);
        q -= u;
        double f = (q * w) / wmag;

        doc::color_t c;
        if (f < 0.0) c = c0;
        else if (f > 1.0) c = c1;
        else {
          c = doc::rgba(int(r0 + f*(r1-r0) + 1e-7),
                        int(g0 + f*(g1-g0) + 1e-7),
                        int(b0 + f*(b1-b0) + 1e-7),
                        int(a0 + f*(a1-a0) + 1e-7));
        }

        *it = c;
      }
    }
  }
  else {
    for (int y=0; y<height; ++y) {
      for (int x=0; x<width; ++x, ++it) {
        base::Vector2d<double> q(imgPos.x+x,
                                 imgPos.y+y);
        q -= u;
        double f = (q * w) / wmag;

        *it = (f*(matrix.maxValue()+2) < matrix(y, x)+1 ? c0: c1);
      }
    }
  }
}

void render_rgba_radial_gradient(
  doc::Image* img,
  const gfx::Point imgPos,
  const gfx::Point p0,
  const gfx::Point p1,
  doc::color_t c0,
  doc::color_t c1,
  const render::DitheringMatrix& matrix)
{
  ASSERT(img->pixelFormat() == doc::IMAGE_RGB);
  if (img->pixelFormat() != doc::IMAGE_RGB) {
    return;
  }

  base::Vector2d<double>
    u(p0.x, p0.y),
    v(p1.x, p1.y), w;
  w = (v - u) / 2;

  // If there is no vector defining the gradient (just one point),
  // the "gradient" will be just a solid color ("c1")
  if (std::fabs(w.x) <= 0.000001 ||
      std::fabs(w.y) <= 0.000001) {
    img->clear(c1);
    return;
  }

  // As we use non-premultiplied RGB values, we need correct RGB
  // values on each stop. So in case that one color has alpha=0
  // (complete transparent), use the RGB values of the
  // non-transparent color in the other stop point.
  if (doc::rgba_geta(c0) == 0 &&
      doc::rgba_geta(c1) != 0) {
    c0 = (c1 & doc::rgba_rgb_mask);
  }
  else if (doc::rgba_geta(c0) != 0 &&
           doc::rgba_geta(c1) == 0) {
    c1 = (c0 & doc::rgba_rgb_mask);
  }

  const uint8_t r0 = doc::rgba_getr(c0);
  const uint8_t g0 = doc::rgba_getg(c0);
  const uint8_t b0 = doc::rgba_getb(c0);
  const uint8_t a0 = doc::rgba_geta(c0);

  const uint8_t r1 = doc::rgba_getr(c1);
  const uint8_t g1 = doc::rgba_getg(c1);
  const uint8_t b1 = doc::rgba_getb(c1);
  const uint8_t a1 = doc::rgba_geta(c1);

  doc::LockImageBits<doc::RgbTraits> bits(img);
  auto it = bits.begin();
  const int width = img->width();
  const int height = img->height();

  if (matrix.rows() == 1 && matrix.cols() == 1) {
    for (int y=0; y<height; ++y) {
      for (int x=0; x<width; ++x, ++it) {
        base::Vector2d<double> q(imgPos.x+x,
                                 imgPos.y+y);
        q -= (u+v)/2;
        q.x /= std::fabs(w.x);
        q.y /= std::fabs(w.y);
        double f = std::sqrt(q.x*q.x + q.y*q.y);

        doc::color_t c;
        if (f < 0.0) c = c0;
        else if (f > 1.0) c = c1;
        else {
          c = doc::rgba(int(r0 + f*(r1-r0) + 1e-7),
                        int(g0 + f*(g1-g0) + 1e-7),
                        int(b0 + f*(b1-b0) + 1e-7),
                        int(a0 + f*(a1-a0) + 1e-7));
        }

        *it = c;
      }
    }
  }
  else {
    for (int y=0; y<height; ++y) {
      for (int x=0; x<width; ++x, ++it) {
        base::Vector2d<double> q(imgPos.x+x,
                                 imgPos.y+y);
        q -= (u+v)/2;
        q.x /= std::fabs(w.x);
        q.y /= std::fabs(w.y);
        double f = std::sqrt(q.x*q.x + q.y*q.y);

        *it = (f*(matrix.maxValue()+2) < matrix(y, x)+1 ? c0: c1);
      }
    }
  }
}

template<typename ImageTraits>
static void create_dithering_pattern_templ(
  doc::Image* pattern,
  const render::DitheringMatrix& matrix,
  const float f,
  const doc::color_t c0,
  const doc::color_t c1)
{
  const int w = pattern->width();
  const int h = pattern->height();

  doc::LockImageBits<ImageTraits> dstBits(pattern);
  auto dst = dstBits.begin();
  for (int y=0; y<h; ++y) {
    for (int x=0; x<w; ++x, ++dst)
      *dst = (f*(matrix.maxValue()+2) < matrix(y, x)+1 ? c0: c1);
  }
}

void convert_bitmap_brush_to_dithering_brush(
  doc::Brush* brush,
  const doc::PixelFormat pixelFormat,
  const render::DitheringMatrix& matrix,
  const float f,
  const doc::color_t c0,
  const doc::color_t c1)
{
  // Create a pattern
  doc::ImageRef pattern(
    doc::Image::create(pixelFormat,
                       matrix.cols(), matrix.rows()));

  switch (pixelFormat) {
    case doc::IMAGE_RGB:
      create_dithering_pattern_templ<doc::RgbTraits>(
        pattern.get(), matrix, f, c0, c1);
      break;
    case doc::IMAGE_GRAYSCALE:
      create_dithering_pattern_templ<doc::GrayscaleTraits>(
        pattern.get(), matrix, f, c0, c1);
      break;
    case doc::IMAGE_INDEXED:
      create_dithering_pattern_templ<doc::IndexedTraits>(
        pattern.get(), matrix, f, c0, c1);
      break;
  }

  doc::ImageRef copy(doc::Image::createCopy(brush->image()));
  brush->setImage(copy.get(), copy.get());
  brush->setPatternImage(pattern);
  brush->setPattern(doc::BrushPattern::PAINT_BRUSH);
}

} // namespace render
