/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include <gio/gio.h>
#include <gegl.h>

#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"

extern "C"
{

#include "gimp-gegl-types.h"

#include "gimp-babl.h"
#include "gimp-gegl-loops.h"
#include "gimp-gegl-mask-combine.h"


#define EPSILON 1e-6

#define PIXELS_PER_THREAD \
  (/* each thread costs as much as */ 64.0 * 64.0 /* pixels */)


gboolean
gimp_gegl_mask_combine_rect (GeglBuffer     *mask,
                             GimpChannelOps  op,
                             gint            x,
                             gint            y,
                             gint            w,
                             gint            h)
{
  GeglRectangle rect;
  gfloat        value;

  g_return_val_if_fail (GEGL_IS_BUFFER (mask), FALSE);

  if (! gegl_rectangle_intersect (&rect,
                                  GEGL_RECTANGLE (x, y, w, h),
                                  gegl_buffer_get_abyss (mask)))
    {
      return FALSE;
    }

  switch (op)
    {
    case GIMP_CHANNEL_OP_REPLACE:
    case GIMP_CHANNEL_OP_ADD:
      value = 1.0f;
      break;

    case GIMP_CHANNEL_OP_SUBTRACT:
      value = 0.0f;
      break;

    case GIMP_CHANNEL_OP_INTERSECT:
      return TRUE;
    }

  gegl_buffer_set_color_from_pixel (mask, &rect, &value,
                                    babl_format ("Y float"));

  return TRUE;
}

gboolean
gimp_gegl_mask_combine_ellipse (GeglBuffer     *mask,
                                GimpChannelOps  op,
                                gint            x,
                                gint            y,
                                gint            w,
                                gint            h,
                                gboolean        antialias)
{
  return gimp_gegl_mask_combine_ellipse_rect (mask, op, x, y, w, h,
                                              w / 2.0, h / 2.0, antialias);
}

gboolean
gimp_gegl_mask_combine_ellipse_rect (GeglBuffer     *mask,
                                     GimpChannelOps  op,
                                     gint            x,
                                     gint            y,
                                     gint            w,
                                     gint            h,
                                     gdouble         rx,
                                     gdouble         ry,
                                     gboolean        antialias)
{
  GeglRectangle  rect;
  const Babl    *format;
  gint           bpp;
  gfloat         one_f = 1.0f;
  gpointer       one;
  gdouble        cx;
  gdouble        cy;
  gint           left;
  gint           right;
  gint           top;
  gint           bottom;

  g_return_val_if_fail (GEGL_IS_BUFFER (mask), FALSE);

  if (rx <= EPSILON || ry <= EPSILON)
    return gimp_gegl_mask_combine_rect (mask, op, x, y, w, h);

  left   = x;
  right  = x + w;
  top    = y;
  bottom = y + h;

  cx = (left + right)  / 2.0;
  cy = (top  + bottom) / 2.0;

  rx = MIN (rx, w / 2.0);
  ry = MIN (ry, h / 2.0);

  if (! gegl_rectangle_intersect (&rect,
                                  GEGL_RECTANGLE (x, y, w, h),
                                  gegl_buffer_get_abyss (mask)))
    {
      return FALSE;
    }

  format = gegl_buffer_get_format (mask);

  if (antialias)
    {
      format = gimp_babl_format_change_component_type (
        format, GIMP_COMPONENT_TYPE_FLOAT);
    }

  bpp = babl_format_get_bytes_per_pixel (format);
  one = g_alloca (bpp);

  babl_process (babl_fish ("Y float", format), &one_f, one, 1);

  /* coordinate-system transforms.  (x, y) coordinates are in the image
   * coordinate-system, and (u, v) coordinates are in a coordinate-system
   * aligned with the center of one of the elliptic corners, with the positive
   * directions pointing away from the rectangle.  when converting from (x, y)
   * to (u, v), we use the closest elliptic corner.
   */
  auto x_to_u = [=] (gdouble x)
  {
    if (x < cx)
      return (left + rx) - x;
    else
      return x - (right - rx);
  };

  auto y_to_v = [=] (gdouble y)
  {
    if (y < cy)
      return (top + ry) - y;
    else
      return y - (bottom - ry);
  };

  auto u_to_x_left = [=] (gdouble u)
  {
    return (left + rx) - u;
  };

  auto u_to_x_right = [=] (gdouble u)
  {
    return (right - rx) + u;
  };

  /* intersection of a horizontal line with the ellipse */
  auto v_to_u = [=] (gdouble v)
  {
    if (v > 0.0)
      return sqrt (MAX (SQR (rx) - SQR (rx * v / ry), 0.0));
    else
      return rx;
  };

  /* intersection of a vertical line with the ellipse */
  auto u_to_v = [=] (gdouble u)
  {
    if (u > 0.0)
      return sqrt (MAX (SQR (ry) - SQR (ry * u / rx), 0.0));
    else
      return ry;
  };

  /* signed, normalized distance of a point from the ellipse's circumference.
   * the sign of the result determines if the point is inside (positive) or
   * outside (negative) the ellipse.  the result is normalized to the cross-
   * section length of a pixel, in the direction of the closest point along the
   * ellipse.
   *
   * we use the following method to approximate the distance: pass horizontal
   * and vertical lines at the given point, P, and find their (positive) points
   * of intersection with the ellipse, A and B.  the segment AB is an
   * approximation of the corresponding elliptic arc (see bug #147836).  find
   * the closest point, C, to P, along the segment AB.  find the (positive)
   * point of intersection, Q, of the line PC and the ellipse.  Q is an
   * approximation for the closest point to P along the ellipse, and the
   * approximated distance is the distance from P to Q.
   */
  auto ellipse_distance = [=] (gdouble u,
                               gdouble v)
  {
    gdouble du;
    gdouble dv;
    gdouble t;
    gdouble a, b, c;
    gdouble d;

    u = MAX (u, 0.0);
    v = MAX (v, 0.0);

    du = v_to_u (v) - u;
    dv = u_to_v (u) - v;

    t = SQR (du) / (SQR (du) + SQR (dv));

    du *= 1.0 - t;
    dv *= t;

    v  *= rx / ry;
    dv *= rx / ry;

    a = SQR (du) + SQR (dv);
    b = u * du + v * dv;
    c = SQR (u) + SQR (v) - SQR (rx);

    if (a <= EPSILON)
      return 0.0;

    if (c < 0.0)
      t = (-b + sqrt (MAX (SQR (b) - a * c, 0.0))) / a;
    else
      t = (-b - sqrt (MAX (SQR (b) - a * c, 0.0))) / a;

    dv *= ry / rx;

    d = sqrt (SQR (du * t) + SQR (dv * t));

    if (c > 0.0)
      d = -d;

    d /= sqrt (SQR (MIN (du / dv, dv / du)) + 1.0);

    return d;
  };

  /* anti-aliased value of a pixel */
  auto pixel_value = [=] (gint x,
                          gint y)
  {
    gdouble u = x_to_u (x + 0.5);
    gdouble v = y_to_v (y + 0.5);
    gdouble d = ellipse_distance (u, v);

    /* use the distance of the pixel's center from the ellipse to approximate
     * the coverage
     */
    d = CLAMP (0.5 + d, 0.0, 1.0);

    /* we're at the horizontal boundary of an elliptic corner */
    if (u < 0.5)
      d = d * (0.5 + u) + (0.5 - u);

    /* we're at the vertical boundary of an elliptic corner */
    if (v < 0.5)
      d = d * (0.5 + v) + (0.5 - v);

    /* opposite horizontal corners intersect the pixel */
    if (x == (right - 1) - (x - left))
      d = 2.0 * d - 1.0;

    /* opposite vertical corners intersect the pixel */
    if (y == (bottom - 1) - (y - top))
      d = 2.0 * d - 1.0;

    return d;
  };

  auto ellipse_range = [=] (gdouble  y,
                            gdouble *x0,
                            gdouble *x1)
  {
    gdouble u = v_to_u (y_to_v (y));

    *x0 = u_to_x_left  (u);
    *x1 = u_to_x_right (u);
  };

  auto fill0 = [=] (gpointer dest,
                    gint     n)
  {
    switch (op)
      {
      case GIMP_CHANNEL_OP_REPLACE:
      case GIMP_CHANNEL_OP_INTERSECT:
        memset (dest, 0, bpp * n);
        break;

      case GIMP_CHANNEL_OP_ADD:
      case GIMP_CHANNEL_OP_SUBTRACT:
        break;
      }

    return (gpointer) ((guint8 *) dest + bpp * n);
  };

  auto fill1 = [=] (gpointer dest,
                    gint     n)
  {
    switch (op)
      {
      case GIMP_CHANNEL_OP_REPLACE:
      case GIMP_CHANNEL_OP_ADD:
        gegl_memset_pattern (dest, one, bpp, n);
        break;

      case GIMP_CHANNEL_OP_SUBTRACT:
        memset (dest, 0, bpp * n);
        break;

      case GIMP_CHANNEL_OP_INTERSECT:
        break;
      }

    return (gpointer) ((guint8 *) dest + bpp * n);
  };

  auto set = [=] (gpointer dest,
                  gfloat   value)
  {
    gfloat *p = (gfloat *) dest;

    switch (op)
      {
      case GIMP_CHANNEL_OP_REPLACE:
        *p = value;
        break;

      case GIMP_CHANNEL_OP_ADD:
        *p = MIN (*p + value, 1.0);
        break;

      case GIMP_CHANNEL_OP_SUBTRACT:
        *p = MAX (*p - value, 0.0);
        break;

      case GIMP_CHANNEL_OP_INTERSECT:
        *p = MIN (*p, value);
        break;
      }

    return (gpointer) (p + 1);
  };

  gegl_parallel_distribute_area (
    &rect, PIXELS_PER_THREAD,
    [=] (const GeglRectangle *area)
    {
      GeglBufferIterator *iter;

      iter = gegl_buffer_iterator_new (
        mask, area, 0, format,
        op == GIMP_CHANNEL_OP_REPLACE ? GEGL_ACCESS_WRITE :
                                        GEGL_ACCESS_READWRITE,
        GEGL_ABYSS_NONE, 1);

      while (gegl_buffer_iterator_next (iter))
        {
          const GeglRectangle *roi = &iter->items[0].roi;
          gpointer             d   = iter->items[0].data;
          gdouble              tx0, ty0;
          gdouble              tx1, ty1;
          gdouble              x0;
          gdouble              x1;
          gint                 y;

          /* tile bounds */
          tx0 = roi->x;
          ty0 = roi->y;

          tx1 = roi->x + roi->width;
          ty1 = roi->y + roi->height;

          if (! antialias)
            {
              tx0 += 0.5;
              ty0 += 0.5;

              tx1 -= 0.5;
              ty1 -= 0.5;
            }

          /* if the tile is fully inside/outside the ellipse, fill it with 1/0,
           * respectively, and skip the rest.
           */
          ellipse_range (ty0, &x0, &x1);

          if (tx0 >= x0 && tx1 <= x1)
            {
              ellipse_range (ty1, &x0, &x1);

              if (tx0 >= x0 && tx1 <= x1)
                {
                  fill1 (d, iter->length);

                  continue;
                }
            }
          else if (tx1 < x0 || tx0 > x1)
            {
              ellipse_range (ty1, &x0, &x1);

              if (tx1 < x0 || tx0 > x1)
                {
                  if ((ty0 - cy) * (ty1 - cy) >= 0.0)
                    {
                      fill0 (d, iter->length);

                      continue;
                    }
                }
            }

          for (y = roi->y; y < roi->y + roi->height; y++)
            {
              gint a, b;

              if (antialias)
                {
                  gdouble v  = y_to_v (y + 0.5);
                  gdouble u0 = v_to_u (v - 0.5);
                  gdouble u1 = v_to_u (v + 0.5);
                  gint    x;

                  a = floor (u_to_x_left (u0)) - roi->x;
                  a = CLAMP (a, 0, roi->width);

                  b = ceil  (u_to_x_left (u1)) - roi->x;
                  b = CLAMP (b, a, roi->width);

                  d = fill0 (d, a);

                  for (x = roi->x + a; x < roi->x + b; x++)
                    d = set (d, pixel_value (x, y));

                  a = floor (u_to_x_right (u1)) - roi->x;
                  a = CLAMP (a, b, roi->width);

                  d = fill1 (d, a - b);

                  b = ceil  (u_to_x_right (u0)) - roi->x;
                  b = CLAMP (b, a, roi->width);

                  for (x = roi->x + a; x < roi->x + b; x++)
                    d = set (d, pixel_value (x, y));

                  d = fill0 (d, roi->width - b);
                }
              else
                {
                  ellipse_range (y + 0.5, &x0, &x1);

                  a = ceil  (x0 - 0.5) - roi->x;
                  a = CLAMP (a, 0, roi->width);

                  b = floor (x1 + 0.5) - roi->x;
                  b = CLAMP (b, 0, roi->width);

                  d = fill0 (d, a);
                  d = fill1 (d, b - a);
                  d = fill0 (d, roi->width - b);
                }
            }
        }
    });

  return TRUE;
}

gboolean
gimp_gegl_mask_combine_buffer (GeglBuffer     *mask,
                               GeglBuffer     *add_on,
                               GimpChannelOps  op,
                               gint            off_x,
                               gint            off_y)
{
  GeglRectangle  mask_rect;
  GeglRectangle  add_on_rect;
  const Babl    *mask_format;
  const Babl    *add_on_format;

  g_return_val_if_fail (GEGL_IS_BUFFER (mask), FALSE);
  g_return_val_if_fail (GEGL_IS_BUFFER (add_on), FALSE);

  if (! gegl_rectangle_intersect (&mask_rect,
                                  GEGL_RECTANGLE (
                                    off_x, off_y,
                                    gegl_buffer_get_width  (add_on),
                                    gegl_buffer_get_height (add_on)),
                                  gegl_buffer_get_abyss (mask)))
    {
      return FALSE;
    }

  add_on_rect    = mask_rect;
  add_on_rect.x -= off_x;
  add_on_rect.y -= off_y;

  mask_format   = gegl_buffer_get_format (mask);
  add_on_format = gegl_buffer_get_format (add_on);

  if (op == GIMP_CHANNEL_OP_REPLACE                                          &&
      (gimp_babl_is_bounded (gimp_babl_format_get_precision (add_on_format)) ||
       gimp_babl_is_bounded (gimp_babl_format_get_precision (mask_format))))
    {
      /*  See below: this additional hack is only needed for the
       *  gimp-channel-combine-masks procedure, it's the only place that
       *  allows to combine arbitrary channels with each other.
       */
      gegl_buffer_set_format (
        add_on,
        gimp_babl_format_change_linear (
          add_on_format, gimp_babl_format_get_linear (mask_format)));

      gimp_gegl_buffer_copy (add_on, &add_on_rect, GEGL_ABYSS_NONE,
                             mask,   &mask_rect);

      gegl_buffer_set_format (add_on, NULL);

      return TRUE;
    }

  /*  This is a hack: all selections/layer masks/channels are always
   *  linear except for channels in 8-bit images. We don't want these
   *  "Y' u8" to be converted to "Y float" because that would cause a
   *  gamma canversion and give unexpected results for
   *  "add/subtract/etc channel from selection". Instead, use all
   *  channel values "as-is", which makes no differce except in the
   *  8-bit case where we need it.
   *
   *  See https://bugzilla.gnome.org/show_bug.cgi?id=791519
   */
  mask_format = gimp_babl_format_change_component_type (
    mask_format, GIMP_COMPONENT_TYPE_FLOAT);

  add_on_format = gimp_babl_format_change_component_type (
    add_on_format, GIMP_COMPONENT_TYPE_FLOAT);

  gegl_parallel_distribute_area (
    &mask_rect, PIXELS_PER_THREAD,
    [=] (const GeglRectangle *mask_area)
    {
      GeglBufferIterator *iter;
      GeglRectangle       add_on_area;

      add_on_area    = *mask_area;
      add_on_area.x -= off_x;
      add_on_area.y -= off_y;

      iter = gegl_buffer_iterator_new (mask, mask_area, 0,
                                       mask_format,
                                       op == GIMP_CHANNEL_OP_REPLACE ?
                                         GEGL_ACCESS_WRITE :
                                         GEGL_ACCESS_READWRITE,
                                       GEGL_ABYSS_NONE, 2);

      gegl_buffer_iterator_add (iter, add_on, &add_on_area, 0,
                                add_on_format,
                                GEGL_ACCESS_READ, GEGL_ABYSS_NONE);

      auto process = [=] (auto value)
      {
        while (gegl_buffer_iterator_next (iter))
          {
            gfloat       *mask_data   = (gfloat       *) iter->items[0].data;
            const gfloat *add_on_data = (const gfloat *) iter->items[1].data;
            gint          count       = iter->length;

            while (count--)
              {
                const gfloat val = value (mask_data, add_on_data);

                *mask_data = CLAMP (val, 0.0f, 1.0f);

                add_on_data++;
                mask_data++;
              }
          }
      };

      switch (op)
        {
        case GIMP_CHANNEL_OP_REPLACE:
          process ([] (const gfloat *mask,
                       const gfloat *add_on)
                   {
                     return *add_on;
                   });
          break;

        case GIMP_CHANNEL_OP_ADD:
          process ([] (const gfloat *mask,
                       const gfloat *add_on)
                   {
                     return *mask + *add_on;
                   });
          break;

        case GIMP_CHANNEL_OP_SUBTRACT:
          process ([] (const gfloat *mask,
                       const gfloat *add_on)
                   {
                     return *mask - *add_on;
                   });
          break;

        case GIMP_CHANNEL_OP_INTERSECT:
          process ([] (const gfloat *mask,
                       const gfloat *add_on)
                   {
                     return MIN (*mask, *add_on);
                   });
          break;
        }
    });

  return TRUE;
}

} /* extern "C" */
