/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * Largely based on gimpdrawable-gradient.c
 *
 * gimpoperationgradient.c
 * Copyright (C) 2014 Michael Henning <drawoc@darkrefraction.com>
 *
 * 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 <cairo.h>
#include <gegl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include "libgimpcolor/gimpcolor.h"
#include "libgimpmath/gimpmath.h"

#include "operations-types.h"

#include "core/gimpgradient.h"

#include "gimpoperationgradient.h"


#define GRADIENT_CACHE_N_SUPERSAMPLES 4
#define GRADIENT_CACHE_MAX_SIZE       ((1 << 20) / (sizeof (gdouble) * 4))


enum
{
  PROP_0,
  PROP_CONTEXT,
  PROP_GRADIENT,
  PROP_START_X,
  PROP_START_Y,
  PROP_END_X,
  PROP_END_Y,
  PROP_GRADIENT_TYPE,
  PROP_GRADIENT_REPEAT,
  PROP_OFFSET,
  PROP_GRADIENT_REVERSE,
  PROP_GRADIENT_BLEND_COLOR_SPACE,
  PROP_SUPERSAMPLE,
  PROP_SUPERSAMPLE_DEPTH,
  PROP_SUPERSAMPLE_THRESHOLD,
  PROP_DITHER
};

typedef struct
{
  GimpGradient                *gradient;
  gboolean                     reverse;
  GimpGradientBlendColorSpace  blend_color_space;
  gdouble                     *gradient_cache;
  gint                         gradient_cache_size;
  GimpGradientSegment         *last_seg;
  gdouble                      offset;
  gdouble                      sx, sy;
  GimpGradientType             gradient_type;
  gdouble                      dist;
  gdouble                      vec[2];
  GimpRepeatMode               repeat;
  GeglSampler                 *dist_sampler;
} RenderBlendData;


typedef struct
{
  gfloat        *data;
  GeglRectangle  roi;
  GRand         *dither_rand;
} PutPixelData;


/*  local function prototypes  */

static void            gimp_operation_gradient_dispose           (GObject               *gobject);
static void            gimp_operation_gradient_finalize          (GObject               *gobject);
static void            gimp_operation_gradient_get_property      (GObject               *object,
                                                                  guint                  property_id,
                                                                  GValue                *value,
                                                                  GParamSpec            *pspec);
static void            gimp_operation_gradient_set_property      (GObject               *object,
                                                                  guint                  property_id,
                                                                  const GValue          *value,
                                                                  GParamSpec            *pspec);

static void            gimp_operation_gradient_prepare           (GeglOperation         *operation);

static GeglRectangle   gimp_operation_gradient_get_bounding_box  (GeglOperation         *operation);

static gdouble         gradient_calc_conical_sym_factor          (gdouble                dist,
                                                                  gdouble               *axis,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_conical_asym_factor         (gdouble                dist,
                                                                  gdouble               *axis,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_square_factor               (gdouble                dist,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_radial_factor               (gdouble                dist,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_linear_factor               (gdouble                dist,
                                                                  gdouble               *vec,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_bilinear_factor             (gdouble                dist,
                                                                  gdouble               *vec,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_spiral_factor               (gdouble                dist,
                                                                  gdouble               *axis,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y,
                                                                  gboolean               clockwise);

static gdouble         gradient_calc_shapeburst_angular_factor   (GeglSampler           *dist_sampler,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_shapeburst_spherical_factor (GeglSampler           *dist_sampler,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);
static gdouble         gradient_calc_shapeburst_dimpled_factor   (GeglSampler           *dist_sampler,
                                                                  gdouble                offset,
                                                                  gdouble                x,
                                                                  gdouble                y);

static void            gradient_render_pixel                     (gdouble                x,
                                                                  gdouble                y,
                                                                  gdouble               *color,
                                                                  gpointer               render_data);

static void            gradient_put_pixel                        (gint                   x,
                                                                  gint                   y,
                                                                  gdouble               *color,
                                                                  gpointer               put_pixel_data);

static void            gradient_dither_pixel                     (gdouble               *color,
                                                                  GRand                 *dither_rand,
                                                                  gfloat                *dest);

static gboolean        gimp_operation_gradient_process           (GeglOperation         *operation,
                                                                  GeglBuffer            *input,
                                                                  GeglBuffer            *output,
                                                                  const GeglRectangle   *result,
                                                                  gint                   level);

static void            gimp_operation_gradient_invalidate_cache  (GimpOperationGradient *self);
static void            gimp_operation_gradient_validate_cache    (GimpOperationGradient *self);


G_DEFINE_TYPE (GimpOperationGradient, gimp_operation_gradient,
               GEGL_TYPE_OPERATION_FILTER)

#define parent_class gimp_operation_gradient_parent_class


static void
gimp_operation_gradient_class_init (GimpOperationGradientClass *klass)
{
  GObjectClass             *object_class    = G_OBJECT_CLASS (klass);
  GeglOperationClass       *operation_class = GEGL_OPERATION_CLASS (klass);
  GeglOperationFilterClass *filter_class    = GEGL_OPERATION_FILTER_CLASS (klass);

  object_class->dispose             = gimp_operation_gradient_dispose;
  object_class->finalize            = gimp_operation_gradient_finalize;
  object_class->set_property        = gimp_operation_gradient_set_property;
  object_class->get_property        = gimp_operation_gradient_get_property;

  operation_class->prepare          = gimp_operation_gradient_prepare;
  operation_class->get_bounding_box = gimp_operation_gradient_get_bounding_box;

  filter_class->process             = gimp_operation_gradient_process;

  gegl_operation_class_set_keys (operation_class,
                                 "name",        "gimp:gradient",
                                 "categories",  "gimp",
                                 "description", "GIMP Gradient operation",
                                 NULL);

  g_object_class_install_property (object_class, PROP_CONTEXT,
                                   g_param_spec_object ("context",
                                                        "Context",
                                                        "A GimpContext",
                                                        GIMP_TYPE_OBJECT,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_GRADIENT,
                                   g_param_spec_object ("gradient",
                                                        "Gradient",
                                                        "A GimpGradient to render",
                                                        GIMP_TYPE_OBJECT,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_START_X,
                                   g_param_spec_double ("start-x",
                                                        "Start X",
                                                        "X coordinate of the first point",
                                                        -G_MAXDOUBLE, G_MAXDOUBLE, 0,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_START_Y,
                                   g_param_spec_double ("start-y",
                                                        "Start Y",
                                                        "Y coordinate of the first point",
                                                        -G_MAXDOUBLE, G_MAXDOUBLE, 0,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_END_X,
                                   g_param_spec_double ("end-x",
                                                        "End X",
                                                        "X coordinate of the second point",
                                                        -G_MAXDOUBLE, G_MAXDOUBLE, 200,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_END_Y,
                                   g_param_spec_double ("end-y",
                                                        "End Y",
                                                        "Y coordinate of the second point",
                                                        -G_MAXDOUBLE, G_MAXDOUBLE, 200,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_GRADIENT_TYPE,
                                   g_param_spec_enum ("gradient-type",
                                                      "Gradient Type",
                                                      "The type of gradient to render",
                                                      GIMP_TYPE_GRADIENT_TYPE,
                                                      GIMP_GRADIENT_LINEAR,
                                                      G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_GRADIENT_REPEAT,
                                   g_param_spec_enum ("gradient-repeat",
                                                      "Repeat mode",
                                                      "Repeat mode",
                                                      GIMP_TYPE_REPEAT_MODE,
                                                      GIMP_REPEAT_NONE,
                                                      G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_OFFSET,
                                   g_param_spec_double ("offset",
                                                        "Offset",
                                                        "Offset relates to the starting and ending coordinates "
                                                        "specified for the blend. This parameter is mode dependent.",
                                                        0, G_MAXDOUBLE, 0,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_GRADIENT_REVERSE,
                                   g_param_spec_boolean ("gradient-reverse",
                                                         "Reverse",
                                                         "Reverse the gradient",
                                                         FALSE,
                                                         G_PARAM_READWRITE |
                                                         G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_GRADIENT_BLEND_COLOR_SPACE,
                                   g_param_spec_enum ("gradient-blend-color-space",
                                                      "Blend Color Space",
                                                      "Which color space to use when blending RGB gradient segments",
                                                      GIMP_TYPE_GRADIENT_BLEND_COLOR_SPACE,
                                                      GIMP_GRADIENT_BLEND_RGB_PERCEPTUAL,
                                                      G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_SUPERSAMPLE,
                                   g_param_spec_boolean ("supersample",
                                                         "Supersample",
                                                         "Do adaptive supersampling",
                                                         FALSE,
                                                         G_PARAM_READWRITE |
                                                         G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_SUPERSAMPLE_DEPTH,
                                   g_param_spec_int ("supersample-depth",
                                                     "Max depth",
                                                     "Maximum recursion levels for supersampling",
                                                     1, 9, 3,
                                                     G_PARAM_READWRITE |
                                                     G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_SUPERSAMPLE_THRESHOLD,
                                   g_param_spec_double ("supersample-threshold",
                                                        "Threshold",
                                                        "Supersampling threshold",
                                                        0, 4, 0.20,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_DITHER,
                                   g_param_spec_boolean ("dither",
                                                         "Dither",
                                                         "Use dithering to reduce banding",
                                                         FALSE,
                                                         G_PARAM_READWRITE |
                                                         G_PARAM_CONSTRUCT));
}

static void
gimp_operation_gradient_init (GimpOperationGradient *self)
{
  g_mutex_init (&self->gradient_cache_mutex);
}

static void
gimp_operation_gradient_dispose (GObject *object)
{
  GimpOperationGradient *self = GIMP_OPERATION_GRADIENT (object);

  gimp_operation_gradient_invalidate_cache (self);

  g_clear_object (&self->gradient);
  g_clear_object (&self->context);

  G_OBJECT_CLASS (parent_class)->dispose (object);
}

static void
gimp_operation_gradient_finalize (GObject *object)
{
  GimpOperationGradient *self = GIMP_OPERATION_GRADIENT (object);

  g_mutex_clear (&self->gradient_cache_mutex);

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gimp_operation_gradient_get_property (GObject    *object,
                                      guint       property_id,
                                      GValue     *value,
                                      GParamSpec *pspec)
{
 GimpOperationGradient *self = GIMP_OPERATION_GRADIENT (object);

  switch (property_id)
    {
    case PROP_CONTEXT:
      g_value_set_object (value, self->context);
      break;

    case PROP_GRADIENT:
      g_value_set_object (value, self->gradient);
      break;

    case PROP_START_X:
      g_value_set_double (value, self->start_x);
      break;

    case PROP_START_Y:
      g_value_set_double (value, self->start_y);
      break;

    case PROP_END_X:
      g_value_set_double (value, self->end_x);
      break;

    case PROP_END_Y:
      g_value_set_double (value, self->end_y);
      break;

    case PROP_GRADIENT_TYPE:
      g_value_set_enum (value, self->gradient_type);
      break;

    case PROP_GRADIENT_REPEAT:
      g_value_set_enum (value, self->gradient_repeat);
      break;

    case PROP_OFFSET:
      g_value_set_double (value, self->offset);
      break;

    case PROP_GRADIENT_REVERSE:
      g_value_set_boolean (value, self->gradient_reverse);
      break;

    case PROP_GRADIENT_BLEND_COLOR_SPACE:
      g_value_set_enum (value, self->gradient_blend_color_space);
      break;

    case PROP_SUPERSAMPLE:
      g_value_set_boolean (value, self->supersample);
      break;

    case PROP_SUPERSAMPLE_DEPTH:
      g_value_set_int (value, self->supersample_depth);
      break;

    case PROP_SUPERSAMPLE_THRESHOLD:
      g_value_set_double (value, self->supersample_threshold);
      break;

    case PROP_DITHER:
      g_value_set_boolean (value, self->dither);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_operation_gradient_set_property (GObject      *object,
                                      guint         property_id,
                                      const GValue *value,
                                      GParamSpec   *pspec)
{
  GimpOperationGradient *self = GIMP_OPERATION_GRADIENT (object);

  switch (property_id)
    {
    case PROP_CONTEXT:
      g_set_object (&self->context, g_value_get_object (value));
      break;

    case PROP_GRADIENT:
      {
        GimpGradient *gradient = g_value_dup_object (value);

        g_clear_object (&self->gradient);

        if (gradient)
          {
            if (gimp_gradient_has_fg_bg_segments (gradient))
              self->gradient = gimp_gradient_flatten (gradient, self->context);
            else
              self->gradient = g_object_ref (gradient);
          }

        gimp_operation_gradient_invalidate_cache (self);
        g_clear_object (&gradient);
      }
      break;

    case PROP_START_X:
      self->start_x = g_value_get_double (value);

      gimp_operation_gradient_invalidate_cache (self);
      break;

    case PROP_START_Y:
      self->start_y = g_value_get_double (value);

      gimp_operation_gradient_invalidate_cache (self);
      break;

    case PROP_END_X:
      self->end_x = g_value_get_double (value);

      gimp_operation_gradient_invalidate_cache (self);
      break;

    case PROP_END_Y:
      self->end_y = g_value_get_double (value);

      gimp_operation_gradient_invalidate_cache (self);
      break;

    case PROP_GRADIENT_TYPE:
      self->gradient_type = g_value_get_enum (value);
      break;

    case PROP_GRADIENT_REPEAT:
      self->gradient_repeat = g_value_get_enum (value);
      break;

    case PROP_OFFSET:
      self->offset = g_value_get_double (value);
      break;

    case PROP_GRADIENT_REVERSE:
      self->gradient_reverse = g_value_get_boolean (value);

      gimp_operation_gradient_invalidate_cache (self);
      break;

    case PROP_GRADIENT_BLEND_COLOR_SPACE:
      self->gradient_blend_color_space = g_value_get_enum (value);

      gimp_operation_gradient_invalidate_cache (self);
      break;

    case PROP_SUPERSAMPLE:
      self->supersample = g_value_get_boolean (value);
      break;

    case PROP_SUPERSAMPLE_DEPTH:
      self->supersample_depth = g_value_get_int (value);
      break;

    case PROP_SUPERSAMPLE_THRESHOLD:
      self->supersample_threshold = g_value_get_double (value);
      break;

    case PROP_DITHER:
      self->dither = g_value_get_boolean (value);
      break;

   default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_operation_gradient_prepare (GeglOperation *operation)
{
  gegl_operation_set_format (operation, "output", babl_format ("R'G'B'A float"));
}

static GeglRectangle
gimp_operation_gradient_get_bounding_box (GeglOperation *operation)
{
  return gegl_rectangle_infinite_plane ();
}

static gdouble
gradient_calc_conical_sym_factor (gdouble  dist,
                                  gdouble *axis,
                                  gdouble  offset,
                                  gdouble  x,
                                  gdouble  y)
{
  if (dist == 0.0)
    {
      return 0.0;
    }
  else if ((x != 0) || (y != 0))
    {
      gdouble vec[2];
      gdouble r;
      gdouble rat;

      /* Calculate offset from the start in pixels */

      r = sqrt (SQR (x) + SQR (y));

      vec[0] = x / r;
      vec[1] = y / r;

      rat = axis[0] * vec[0] + axis[1] * vec[1]; /* Dot product */

      if (rat > 1.0)
        rat = 1.0;
      else if (rat < -1.0)
        rat = -1.0;

      /* This cool idea is courtesy Josh MacDonald,
       * Ali Rahimi --- two more XCF losers.  */

      rat = acos (rat) / G_PI;
      rat = pow (rat, (offset / 10.0) + 1.0);

      return CLAMP (rat, 0.0, 1.0);
    }
  else
    {
      return 0.5;
    }
}

static gdouble
gradient_calc_conical_asym_factor (gdouble  dist,
                                   gdouble *axis,
                                   gdouble  offset,
                                   gdouble  x,
                                   gdouble  y)
{
  if (dist == 0.0)
    {
      return 0.0;
    }
  else if (x != 0 || y != 0)
    {
      gdouble ang0, ang1;
      gdouble ang;
      gdouble rat;

      ang0 = atan2 (axis[0], axis[1]) + G_PI;

      ang1 = atan2 (x, y) + G_PI;

      ang = ang1 - ang0;

      if (ang < 0.0)
        ang += (2.0 * G_PI);

      rat = ang / (2.0 * G_PI);
      rat = pow (rat, (offset / 10.0) + 1.0);

      return CLAMP (rat, 0.0, 1.0);
    }
  else
    {
      return 0.5; /* We are on middle point */
    }
}

static gdouble
gradient_calc_square_factor (gdouble dist,
                             gdouble offset,
                             gdouble x,
                             gdouble y)
{
  if (dist == 0.0)
    {
      return 0.0;
    }
  else
    {
      gdouble r;
      gdouble rat;

      /* Calculate offset from start as a value in [0, 1] */

      offset = offset / 100.0;

      r   = MAX (fabs (x), fabs (y));
      rat = r / dist;

      if (rat < offset)
        return 0.0;
      else if (offset == 1.0)
        return (rat >= 1.0) ? 1.0 : 0.0;
      else
        return (rat - offset) / (1.0 - offset);
    }
}

static gdouble
gradient_calc_radial_factor (gdouble dist,
                             gdouble offset,
                             gdouble x,
                             gdouble y)
{
  if (dist == 0.0)
    {
      return 0.0;
    }
  else
    {
      gdouble r;
      gdouble rat;

      /* Calculate radial offset from start as a value in [0, 1] */

      offset = offset / 100.0;

      r   = sqrt (SQR (x) + SQR (y));
      rat = r / dist;

      if (rat < offset)
        return 0.0;
      else if (offset == 1.0)
        return (rat >= 1.0) ? 1.0 : 0.0;
      else
        return (rat - offset) / (1.0 - offset);
    }
}

static gdouble
gradient_calc_linear_factor (gdouble  dist,
                             gdouble *vec,
                             gdouble  offset,
                             gdouble  x,
                             gdouble  y)
{
  if (dist == 0.0)
    {
      return 0.0;
    }
  else
    {
      gdouble r;
      gdouble rat;

      offset = offset / 100.0;

      r   = vec[0] * x + vec[1] * y;
      rat = r / dist;

      if (rat >= 0.0 && rat < offset)
        return 0.0;
      else if (offset == 1.0)
        return (rat >= 1.0) ? 1.0 : 0.0;
      else if (rat < 0.0)
        return rat / (1.0 - offset);
      else
        return (rat - offset) / (1.0 - offset);
    }
}

static gdouble
gradient_calc_bilinear_factor (gdouble  dist,
                               gdouble *vec,
                               gdouble  offset,
                               gdouble  x,
                               gdouble  y)
{
  if (dist == 0.0)
    {
      return 0.0;
    }
  else
    {
      gdouble r;
      gdouble rat;

      /* Calculate linear offset from the start line outward */

      offset = offset / 100.0;

      r   = vec[0] * x + vec[1] * y;
      rat = r / dist;

      if (fabs (rat) < offset)
        return 0.0;
      else if (offset == 1.0)
        return (rat == 1.0) ? 1.0 : 0.0;
      else
        return (fabs (rat) - offset) / (1.0 - offset);
    }
}

static gdouble
gradient_calc_spiral_factor (gdouble   dist,
                             gdouble  *axis,
                             gdouble   offset,
                             gdouble   x,
                             gdouble   y,
                             gboolean  clockwise)
{
  if (dist == 0.0)
    {
      return 0.0;
    }
  else if (x != 0.0 || y != 0.0)
    {
      gdouble ang0, ang1;
      gdouble ang;
      double  r;

      offset = offset / 100.0;

      ang0 = atan2 (axis[0], axis[1]) + G_PI;
      ang1 = atan2 (x, y) + G_PI;

      if (clockwise)
        ang = ang1 - ang0;
      else
        ang = ang0 - ang1;

      if (ang < 0.0)
        ang += (2.0 * G_PI);

      r = sqrt (SQR (x) + SQR (y)) / dist;

      return fmod (ang / (2.0 * G_PI) + r + offset, 1.0);
    }
  else
    {
      return 0.5 ; /* We are on the middle point */
    }
}

static gdouble
gradient_calc_shapeburst_angular_factor (GeglSampler *dist_sampler,
                                         gdouble      offset,
                                         gdouble      x,
                                         gdouble      y)
{
  gfloat value;

  offset = offset / 100.0;

  gegl_sampler_get (dist_sampler, x, y, NULL, &value, GEGL_ABYSS_NONE);

  value = 1.0 - value;

  if (value < offset)
    value = 0.0;
  else if (offset == 1.0)
    value = (value >= 1.0) ? 1.0 : 0.0;
  else
    value = (value - offset) / (1.0 - offset);

  return value;
}


static gdouble
gradient_calc_shapeburst_spherical_factor (GeglSampler *dist_sampler,
                                           gdouble      offset,
                                           gdouble      x,
                                           gdouble      y)
{
  gfloat value;

  offset = 1.0 - offset / 100.0;

  gegl_sampler_get (dist_sampler, x, y, NULL, &value, GEGL_ABYSS_NONE);

  if (value > offset)
    value = 1.0;
  else if (offset == 0.0)
    value = (value <= 0.0) ? 0.0 : 1.0;
  else
    value = value / offset;

  value = 1.0 - sin (0.5 * G_PI * value);

  return value;
}


static gdouble
gradient_calc_shapeburst_dimpled_factor (GeglSampler *dist_sampler,
                                         gdouble      offset,
                                         gdouble      x,
                                         gdouble      y)
{
  gfloat value;

  offset = 1.0 - offset / 100.0;

  gegl_sampler_get (dist_sampler, x, y, NULL, &value, GEGL_ABYSS_NONE);

  if (value > offset)
    value = 1.0;
  else if (offset == 0.0)
    value = (value <= 0.0) ? 0.0 : 1.0;
  else
    value = value / offset;

  value = cos (0.5 * G_PI * value);

  return value;
}

static void
gradient_render_pixel (gdouble   x,
                       gdouble   y,
                       gdouble  *rgb,
                       gpointer  render_data)
{
  RenderBlendData *rbd = render_data;
  gdouble          factor;

  /*  we want to calculate the color at the pixel's center  */
  x += 0.5;
  y += 0.5;

  /* Calculate blending factor */

  switch (rbd->gradient_type)
    {
    case GIMP_GRADIENT_LINEAR:
      factor = gradient_calc_linear_factor (rbd->dist,
                                            rbd->vec, rbd->offset,
                                            x - rbd->sx, y - rbd->sy);
      break;

    case GIMP_GRADIENT_BILINEAR:
      factor = gradient_calc_bilinear_factor (rbd->dist,
                                              rbd->vec, rbd->offset,
                                              x - rbd->sx, y - rbd->sy);
      break;

    case GIMP_GRADIENT_RADIAL:
      factor = gradient_calc_radial_factor (rbd->dist,
                                            rbd->offset,
                                            x - rbd->sx, y - rbd->sy);
      break;

    case GIMP_GRADIENT_SQUARE:
      factor = gradient_calc_square_factor (rbd->dist, rbd->offset,
                                            x - rbd->sx, y - rbd->sy);
      break;

    case GIMP_GRADIENT_CONICAL_SYMMETRIC:
      factor = gradient_calc_conical_sym_factor (rbd->dist,
                                                 rbd->vec, rbd->offset,
                                                 x - rbd->sx, y - rbd->sy);
      break;

    case GIMP_GRADIENT_CONICAL_ASYMMETRIC:
      factor = gradient_calc_conical_asym_factor (rbd->dist,
                                                  rbd->vec, rbd->offset,
                                                  x - rbd->sx, y - rbd->sy);
      break;

    case GIMP_GRADIENT_SHAPEBURST_ANGULAR:
      factor = gradient_calc_shapeburst_angular_factor (rbd->dist_sampler,
                                                        rbd->offset,
                                                        x, y);
      break;

    case GIMP_GRADIENT_SHAPEBURST_SPHERICAL:
      factor = gradient_calc_shapeburst_spherical_factor (rbd->dist_sampler,
                                                          rbd->offset,
                                                          x, y);
      break;

    case GIMP_GRADIENT_SHAPEBURST_DIMPLED:
      factor = gradient_calc_shapeburst_dimpled_factor (rbd->dist_sampler,
                                                        rbd->offset,
                                                        x, y);
      break;

    case GIMP_GRADIENT_SPIRAL_CLOCKWISE:
      factor = gradient_calc_spiral_factor (rbd->dist,
                                            rbd->vec, rbd->offset,
                                            x - rbd->sx, y - rbd->sy, TRUE);
      break;

    case GIMP_GRADIENT_SPIRAL_ANTICLOCKWISE:
      factor = gradient_calc_spiral_factor (rbd->dist,
                                            rbd->vec, rbd->offset,
                                            x - rbd->sx, y - rbd->sy, FALSE);
      break;

    default:
      g_return_if_reached ();
      break;
    }

  /* Adjust for repeat */

  switch (rbd->repeat)
    {
    case GIMP_REPEAT_NONE:
      break;

    case GIMP_REPEAT_SAWTOOTH:
      factor = factor - floor (factor);
      break;

    case GIMP_REPEAT_TRIANGULAR:
      {
        guint ifactor;

        if (factor < 0.0)
          factor = -factor;

        ifactor = (guint) factor;
        factor = factor - floor (factor);

        if (ifactor & 1)
          factor = 1.0 - factor;
      }
      break;

    case GIMP_REPEAT_TRUNCATE:
      if (factor < 0.0 || factor > 1.0)
        {
          for (gint i = 0; i < 4; i++)
            rgb[i] = 0.0;
          return;
        }
      break;
    }

  /* Blend the colors */

  if (rbd->gradient_cache)
    {
      gint index;

      factor = CLAMP (factor, 0.0, 1.0);
      index  = ROUND (factor * (rbd->gradient_cache_size - 1));

      for (gint i = 0; i < 4; i++)
        rgb[i] = rbd->gradient_cache[(index * 4) + i];
    }
  else
    {
      GeglColor *color = NULL;

      rbd->last_seg = gimp_gradient_get_color_at (rbd->gradient, NULL,
                                                  rbd->last_seg, factor,
                                                  rbd->reverse,
                                                  rbd->blend_color_space,
                                                  &color);
      if (color)
        gegl_color_get_pixel (color, babl_format ("R'G'B'A double"), rgb);

      g_clear_object (&color);
    }
}

static void
gradient_put_pixel (gint      x,
                    gint      y,
                    gdouble  *color,
                    gpointer  put_pixel_data)
{
  PutPixelData *ppd   = put_pixel_data;
  const gint    index = (y - ppd->roi.y) * ppd->roi.width + (x - ppd->roi.x);
  gfloat       *dest  = ppd->data + 4 * index;

  if (ppd->dither_rand)
    {
      gradient_dither_pixel (color, ppd->dither_rand, dest);

      dest += 4;
    }
  else
    {
      *dest++ = color[0];
      *dest++ = color[1];
      *dest++ = color[2];
      *dest++ = color[3];
    }
}

static void
gradient_dither_pixel (gdouble *color,
                       GRand   *dither_rand,
                       gfloat  *dest)
{
  gfloat r, g, b, a;
  guint  i;

  i = g_rand_int (dither_rand);

  if ((color[0] == color[1]) && (color[0] == color[2]))
    {
      gdouble dither = (gdouble) (i & 0xff) / 256.0 / 256.0 - 0.5 / 256.0;
      r              = color[0] + dither;
      g              = color[1] + dither;
      b              = color[2] + dither;
    }
  else
    {
      r = color[0] + (gdouble) (i & 0xff) / 256.0 / 256.0 - 0.5 / 256.0;
      i >>= 8;
      g = color[1] + (gdouble) (i & 0xff) / 256.0 / 256.0 - 0.5 / 256.0;
      i >>= 8;
      b = color[2] + (gdouble) (i & 0xff) / 256.0 / 256.0 - 0.5 / 256.0;
      i >>= 8;
    }

  if (color[3] > 0.0 && color[3] < 1.0)
    a = color[3] + (gdouble) (i & 0xff) / 256.0 / 256.0 - 0.5 / 256.0;
  else
    a = color[3];

  *dest++ = CLAMP (r, 0.0, 1.0);
  *dest++ = CLAMP (g, 0.0, 1.0);
  *dest++ = CLAMP (b, 0.0, 1.0);
  *dest++ = CLAMP (a, 0.0, 1.0);
}

static gboolean
gimp_operation_gradient_process (GeglOperation       *operation,
                                 GeglBuffer          *input,
                                 GeglBuffer          *output,
                                 const GeglRectangle *result,
                                 gint                 level)
{
  GimpOperationGradient *self = GIMP_OPERATION_GRADIENT (operation);

  const gdouble sx = self->start_x;
  const gdouble sy = self->start_y;
  const gdouble ex = self->end_x;
  const gdouble ey = self->end_y;

  RenderBlendData rbd = { 0, };

  GeglBufferIterator *iter;
  GeglRectangle      *roi;
  GRand              *dither_rand = NULL;

  if (! self->gradient)
    return TRUE;

  gimp_operation_gradient_validate_cache (self);

  rbd.gradient            = self->gradient;
  rbd.reverse             = self->gradient_reverse;
  rbd.blend_color_space   = self->gradient_blend_color_space;
  rbd.gradient_cache      = self->gradient_cache;
  rbd.gradient_cache_size = self->gradient_cache_size;

  /* Calculate type-specific parameters */

  switch (self->gradient_type)
    {
    case GIMP_GRADIENT_RADIAL:
      rbd.dist = sqrt (SQR (ex - sx) + SQR (ey - sy));
      break;

    case GIMP_GRADIENT_SQUARE:
      rbd.dist = MAX (fabs (ex - sx), fabs (ey - sy));
      break;

    case GIMP_GRADIENT_CONICAL_SYMMETRIC:
    case GIMP_GRADIENT_CONICAL_ASYMMETRIC:
    case GIMP_GRADIENT_SPIRAL_CLOCKWISE:
    case GIMP_GRADIENT_SPIRAL_ANTICLOCKWISE:
    case GIMP_GRADIENT_LINEAR:
    case GIMP_GRADIENT_BILINEAR:
      rbd.dist = sqrt (SQR (ex - sx) + SQR (ey - sy));

      if (rbd.dist > 0.0)
        {
          rbd.vec[0] = (ex - sx) / rbd.dist;
          rbd.vec[1] = (ey - sy) / rbd.dist;
        }

      break;

    case GIMP_GRADIENT_SHAPEBURST_ANGULAR:
    case GIMP_GRADIENT_SHAPEBURST_SPHERICAL:
    case GIMP_GRADIENT_SHAPEBURST_DIMPLED:
      rbd.dist = sqrt (SQR (ex - sx) + SQR (ey - sy));
      rbd.dist_sampler = gegl_buffer_sampler_new_at_level (
        input, babl_format ("Y float"), GEGL_SAMPLER_NEAREST, level);
      break;

    default:
      g_return_val_if_reached (FALSE);
      break;
    }

  /* Initialize render data */

  rbd.offset        = self->offset;
  rbd.sx            = self->start_x;
  rbd.sy            = self->start_y;
  rbd.gradient_type = self->gradient_type;
  rbd.repeat        = self->gradient_repeat;

  /* Render the gradient! */

  iter = gegl_buffer_iterator_new (output, result, 0,
                                   babl_format ("R'G'B'A float"),
                                   GEGL_ACCESS_WRITE, GEGL_ABYSS_NONE, 1);
  roi = &iter->items[0].roi;

  if (self->dither)
    dither_rand = g_rand_new ();

  if (self->supersample)
    {
      PutPixelData ppd;

      ppd.dither_rand = dither_rand;

      while (gegl_buffer_iterator_next (iter))
        {
          ppd.data = iter->items[0].data;
          ppd.roi  = *roi;

          gimp_adaptive_supersample_area (roi->x, roi->y,
                                          roi->x + roi->width  - 1,
                                          roi->y + roi->height - 1,
                                          self->supersample_depth,
                                          self->supersample_threshold,
                                          gradient_render_pixel, &rbd,
                                          gradient_put_pixel, &ppd,
                                          NULL,
                                          NULL);
        }
    }
  else
    {
      while (gegl_buffer_iterator_next (iter))
        {
          gfloat *dest = iter->items[0].data;
          gint    endx = roi->x + roi->width;
          gint    endy = roi->y + roi->height;
          gint    x, y;

          if (dither_rand)
            {
              for (y = roi->y; y < endy; y++)
                for (x = roi->x; x < endx; x++)
                  {
                    gdouble color[4] = { 0.0, 0.0, 0.0, 1.0 };

                    gradient_render_pixel (x, y, color, &rbd);
                    gradient_dither_pixel (color, dither_rand, dest);

                    dest += 4;
                  }
            }
          else
            {
              for (y = roi->y; y < endy; y++)
                for (x = roi->x; x < endx; x++)
                  {
                    gdouble color[4] = { 0.0, 0.0, 0.0, 1.0 };

                    gradient_render_pixel (x, y, color, &rbd);

                    *dest++ = color[0];
                    *dest++ = color[1];
                    *dest++ = color[2];
                    *dest++ = color[3];
                  }
            }
        }
    }

  if (self->dither)
    g_rand_free (dither_rand);

  g_clear_object (&rbd.dist_sampler);

  return TRUE;
}

static void
gimp_operation_gradient_invalidate_cache (GimpOperationGradient *self)
{
  g_clear_pointer (&self->gradient_cache, g_free);
}

static void
gimp_operation_gradient_validate_cache (GimpOperationGradient *self)
{
  GimpGradientSegment *last_seg = NULL;
  gint                 cache_size;
  gdouble              pixel[4];
  gint                 i, j;

  if (! self->gradient)
    return;

  g_mutex_lock (&self->gradient_cache_mutex);

  if (self->gradient_cache)
    {
      g_mutex_unlock (&self->gradient_cache_mutex);

      return;
    }

  cache_size = ceil (hypot (self->start_x - self->end_x,
                            self->start_y - self->end_y)) *
               GRADIENT_CACHE_N_SUPERSAMPLES;

  /*  have at least two values in the cache  */
  cache_size = MAX (cache_size, 2);

  /*  don't use a cache if its necessary size is too big  */
  if (cache_size > GRADIENT_CACHE_MAX_SIZE)
    {
      g_mutex_unlock (&self->gradient_cache_mutex);

      return;
    }

  self->gradient_cache      = g_new0 (gdouble, cache_size * 4);
  self->gradient_cache_size = cache_size;

  for (i = 0; i < self->gradient_cache_size; i++)
    {
      GeglColor *color  = NULL;
      gdouble    factor = (gdouble) i / (gdouble) (self->gradient_cache_size - 1);

      last_seg = gimp_gradient_get_color_at (self->gradient, NULL, last_seg,
                                             factor,
                                             self->gradient_reverse,
                                             self->gradient_blend_color_space,
                                             &color);
      if (color)
        {
          gegl_color_get_pixel (color, babl_format ("R'G'B'A double"), pixel);
          for (j = 0; j < 4; j++)
            self->gradient_cache[i * 4 + j] = pixel[j];
        }

      g_clear_object (&color);
    }

  g_mutex_unlock (&self->gradient_cache_mutex);
}
