/* 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 <stdlib.h>
#include <string.h> /* memcmp */

#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

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

#include "core-types.h"

#include "gimpcurve.h"
#include "gimpcurve-load.h"
#include "gimpcurve-save.h"
#include "gimpparamspecs.h"

#include "gimp-intl.h"


#define EPSILON 1e-6


enum
{
  PROP_0,
  PROP_CURVE_TYPE,
  PROP_N_POINTS,
  PROP_POINTS,
  PROP_POINT_TYPES,
  PROP_N_SAMPLES,
  PROP_SAMPLES,
  N_PROPS
};
static GParamSpec *obj_props[N_PROPS] = { NULL, };


/*  local function prototypes  */

static void          gimp_curve_config_iface_init (GimpConfigInterface *iface);

static void          gimp_curve_finalize          (GObject          *object);
static void          gimp_curve_set_property      (GObject          *object,
                                                   guint             property_id,
                                                   const GValue     *value,
                                                   GParamSpec       *pspec);
static void          gimp_curve_get_property      (GObject          *object,
                                                   guint             property_id,
                                                   GValue           *value,
                                                   GParamSpec       *pspec);

static gint64        gimp_curve_get_memsize       (GimpObject       *object,
                                                   gint64           *gui_size);

static void          gimp_curve_get_preview_size  (GimpViewable     *viewable,
                                                   gint              size,
                                                   gboolean          popup,
                                                   gboolean          dot_for_dot,
                                                   gint             *width,
                                                   gint             *height);
static gboolean      gimp_curve_get_popup_size    (GimpViewable     *viewable,
                                                   gint              width,
                                                   gint              height,
                                                   gboolean          dot_for_dot,
                                                   gint             *popup_width,
                                                   gint             *popup_height);
static GimpTempBuf * gimp_curve_get_new_preview   (GimpViewable     *viewable,
                                                   GimpContext      *context,
                                                   gint              width,
                                                   gint              height,
                                                   GeglColor        *fg_color);
static gchar       * gimp_curve_get_description   (GimpViewable     *viewable,
                                                   gchar           **tooltip);

static void          gimp_curve_dirty             (GimpData         *data);
static const gchar * gimp_curve_get_extension     (GimpData         *data);
static void          gimp_curve_data_copy         (GimpData         *data,
                                                   GimpData         *src_data);

static gboolean      gimp_curve_serialize         (GimpConfig       *config,
                                                   GimpConfigWriter *writer,
                                                   gpointer          data);
static gboolean      gimp_curve_deserialize       (GimpConfig       *config,
                                                   GScanner         *scanner,
                                                   gint              nest_level,
                                                   gpointer          data);
static gboolean      gimp_curve_equal             (GimpConfig       *a,
                                                   GimpConfig       *b);
static void          _gimp_curve_reset            (GimpConfig       *config);
static gboolean      gimp_curve_config_copy       (GimpConfig       *src,
                                                   GimpConfig       *dest,
                                                   GParamFlags       flags);

static void          gimp_curve_calculate         (GimpCurve        *curve);
static void          gimp_curve_plot              (GimpCurve        *curve,
                                                   gint              p1,
                                                   gint              p2,
                                                   gint              p3,
                                                   gint              p4);


G_DEFINE_TYPE_WITH_CODE (GimpCurve, gimp_curve, GIMP_TYPE_DATA,
                         G_IMPLEMENT_INTERFACE (GIMP_TYPE_CONFIG,
                                                gimp_curve_config_iface_init))

#define parent_class gimp_curve_parent_class


static void
gimp_curve_class_init (GimpCurveClass *klass)
{
  GObjectClass      *object_class      = G_OBJECT_CLASS (klass);
  GimpObjectClass   *gimp_object_class = GIMP_OBJECT_CLASS (klass);
  GimpViewableClass *viewable_class    = GIMP_VIEWABLE_CLASS (klass);
  GimpDataClass     *data_class        = GIMP_DATA_CLASS (klass);
  GParamSpec        *array_spec;

  object_class->finalize            = gimp_curve_finalize;
  object_class->set_property        = gimp_curve_set_property;
  object_class->get_property        = gimp_curve_get_property;

  gimp_object_class->get_memsize    = gimp_curve_get_memsize;

  viewable_class->default_icon_name = "FIXME icon name";
  viewable_class->get_preview_size  = gimp_curve_get_preview_size;
  viewable_class->get_popup_size    = gimp_curve_get_popup_size;
  viewable_class->get_new_preview   = gimp_curve_get_new_preview;
  viewable_class->get_description   = gimp_curve_get_description;

  data_class->dirty                 = gimp_curve_dirty;
  data_class->save                  = gimp_curve_save;
  data_class->get_extension         = gimp_curve_get_extension;
  data_class->copy                  = gimp_curve_data_copy;

  obj_props[PROP_CURVE_TYPE] =
      g_param_spec_enum ("curve-type",
                         "Curve Type",
                         "The curve type",
                         GIMP_TYPE_CURVE_TYPE,
                         GIMP_CURVE_SMOOTH,
                         GIMP_CONFIG_PARAM_FLAGS);

  obj_props[PROP_N_POINTS] =
      g_param_spec_int ("n-points",
                        "Number of Points",
                        "The number of points",
                        0, G_MAXINT, 0,
                        /* for backward compatibility */
                        GIMP_CONFIG_PARAM_IGNORE | GIMP_CONFIG_PARAM_FLAGS);

  array_spec = g_param_spec_double ("point", NULL, NULL,
                                    -1.0, 1.0, 0.0, GIMP_PARAM_READWRITE);
  obj_props[PROP_POINTS] =
      gimp_param_spec_value_array ("points",
                                   NULL, NULL,
                                   array_spec,
                                   GIMP_CONFIG_PARAM_FLAGS);

  array_spec = g_param_spec_enum ("point-type", NULL, NULL,
                                  GIMP_TYPE_CURVE_POINT_TYPE,
                                  GIMP_CURVE_POINT_SMOOTH,
                                  GIMP_PARAM_READWRITE);
  obj_props[PROP_POINT_TYPES] =
      gimp_param_spec_value_array ("point-types",
                                   NULL, NULL,
                                   array_spec,
                                   GIMP_CONFIG_PARAM_FLAGS);

  obj_props[PROP_N_SAMPLES] =
      g_param_spec_int ("n-samples",
                        "Number of Samples",
                        "The number of samples",
                        256, 256, 256,
                        GIMP_CONFIG_PARAM_FLAGS);

  array_spec = g_param_spec_double ("sample", NULL, NULL,
                                    0.0, 1.0, 0.0, GIMP_PARAM_READWRITE);
  obj_props[PROP_SAMPLES] =
      gimp_param_spec_value_array ("samples",
                                   NULL, NULL,
                                   array_spec,
                                   GIMP_CONFIG_PARAM_FLAGS);

  g_object_class_install_properties (object_class, N_PROPS, obj_props);
}

static void
gimp_curve_config_iface_init (GimpConfigInterface *iface)
{
  iface->serialize   = gimp_curve_serialize;
  iface->deserialize = gimp_curve_deserialize;
  iface->equal       = gimp_curve_equal;
  iface->reset       = _gimp_curve_reset;
  iface->copy        = gimp_curve_config_copy;
}

static void
gimp_curve_init (GimpCurve *curve)
{
  curve->n_points  = 0;
  curve->points    = NULL;
  curve->n_samples = 0;
  curve->samples   = NULL;
  curve->identity  = FALSE;
}

static void
gimp_curve_finalize (GObject *object)
{
  GimpCurve *curve = GIMP_CURVE (object);

  g_clear_pointer (&curve->points,  g_free);
  curve->n_points = 0;

  g_clear_pointer (&curve->samples, g_free);
  curve->n_samples = 0;

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

static void
gimp_curve_set_property (GObject      *object,
                         guint         property_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  GimpCurve *curve = GIMP_CURVE (object);

  switch (property_id)
    {
    case PROP_CURVE_TYPE:
      gimp_curve_set_curve_type (curve, g_value_get_enum (value));
      break;

    case PROP_N_POINTS:
      /* ignored */
      break;

    case PROP_POINTS:
      {
        GimpValueArray *array = g_value_get_boxed (value);
        GimpCurvePoint *points;
        gint            length;
        gint            n_points;
        gint            i;

        if (! array)
          {
            gimp_curve_clear_points (curve);

            break;
          }

        length = gimp_value_array_length (array) / 2;

        n_points = 0;
        points   = g_new0 (GimpCurvePoint, length);

        for (i = 0; i < length; i++)
          {
            GValue *x = gimp_value_array_index (array, i * 2);
            GValue *y = gimp_value_array_index (array, i * 2 + 1);

            /* for backward compatibility */
            if (g_value_get_double (x) < 0.0)
              continue;

            points[n_points].x = CLAMP (g_value_get_double (x), 0.0, 1.0);
            points[n_points].y = CLAMP (g_value_get_double (y), 0.0, 1.0);

            if (n_points > 0)
              {
                points[n_points].x = MAX (points[n_points].x,
                                          points[n_points - 1].x);
              }

            if (n_points < curve->n_points)
              points[n_points].type = curve->points[n_points].type;
            else
              points[n_points].type = GIMP_CURVE_POINT_SMOOTH;

            n_points++;
          }

        g_free (curve->points);

        curve->n_points = n_points;
        curve->points   = points;

        g_object_notify_by_pspec (object, obj_props[PROP_N_POINTS]);
        g_object_notify_by_pspec (object, obj_props[PROP_POINT_TYPES]);
      }
      break;

    case PROP_POINT_TYPES:
      {
        GimpValueArray *array = g_value_get_boxed (value);
        GimpCurvePoint *points;
        gint            length;
        gdouble         x     = 0.0;
        gdouble         y     = 0.0;
        gint            i;

        if (! array)
          {
            gimp_curve_clear_points (curve);

            break;
          }

        length = gimp_value_array_length (array);

        points = g_new0 (GimpCurvePoint, length);

        for (i = 0; i < length; i++)
          {
            GValue *type = gimp_value_array_index (array, i);

            points[i].type = g_value_get_enum (type);

            if (i < curve->n_points)
              {
                x = curve->points[i].x;
                y = curve->points[i].y;
              }

            points[i].x = x;
            points[i].y = y;
          }

        g_free (curve->points);

        curve->n_points = length;
        curve->points   = points;

        g_object_notify_by_pspec (object, obj_props[PROP_N_POINTS]);
        g_object_notify_by_pspec (object, obj_props[PROP_POINTS]);
      }
      break;

    case PROP_N_SAMPLES:
      gimp_curve_set_n_samples (curve, g_value_get_int (value));
      break;

    case PROP_SAMPLES:
      {
        GimpValueArray *array = g_value_get_boxed (value);
        gint            length;
        gint            i;

        if (! array)
          break;

        length = gimp_value_array_length (array);

        for (i = 0; i < curve->n_samples && i < length; i++)
          {
            GValue *v = gimp_value_array_index (array, i);

            curve->samples[i] = CLAMP (g_value_get_double (v), 0.0, 1.0);
          }
      }
      break;

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

static void
gimp_curve_get_property (GObject    *object,
                         guint       property_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  GimpCurve *curve = GIMP_CURVE (object);

  switch (property_id)
    {
    case PROP_CURVE_TYPE:
      g_value_set_enum (value, curve->curve_type);
      break;

    case PROP_N_POINTS:
      g_value_set_int (value, curve->n_points);
      break;

    case PROP_POINTS:
      {
        GimpValueArray *array = gimp_value_array_new (curve->n_points * 2);
        GValue          v     = G_VALUE_INIT;
        gint            i;

        g_value_init (&v, G_TYPE_DOUBLE);

        for (i = 0; i < curve->n_points; i++)
          {
            g_value_set_double (&v, curve->points[i].x);
            gimp_value_array_append (array, &v);

            g_value_set_double (&v, curve->points[i].y);
            gimp_value_array_append (array, &v);
          }

        g_value_unset (&v);

        g_value_take_boxed (value, array);
      }
      break;

    case PROP_POINT_TYPES:
      {
        GimpValueArray *array = gimp_value_array_new (curve->n_points);
        GValue          v     = G_VALUE_INIT;
        gint            i;

        g_value_init (&v, GIMP_TYPE_CURVE_POINT_TYPE);

        for (i = 0; i < curve->n_points; i++)
          {
            g_value_set_enum (&v, curve->points[i].type);
            gimp_value_array_append (array, &v);
          }

        g_value_unset (&v);

        g_value_take_boxed (value, array);
      }
      break;

    case PROP_N_SAMPLES:
      g_value_set_int (value, curve->n_samples);
      break;

    case PROP_SAMPLES:
      {
        GimpValueArray *array = gimp_value_array_new (curve->n_samples);
        GValue          v     = G_VALUE_INIT;
        gint            i;

        g_value_init (&v, G_TYPE_DOUBLE);

        for (i = 0; i < curve->n_samples; i++)
          {
            g_value_set_double (&v, curve->samples[i]);
            gimp_value_array_append (array, &v);
          }

        g_value_unset (&v);

        g_value_take_boxed (value, array);
      }
      break;

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

static gint64
gimp_curve_get_memsize (GimpObject *object,
                        gint64     *gui_size)
{
  GimpCurve *curve   = GIMP_CURVE (object);
  gint64     memsize = 0;

  memsize += curve->n_points  * sizeof (GimpCurvePoint);
  memsize += curve->n_samples * sizeof (gdouble);

  return memsize + GIMP_OBJECT_CLASS (parent_class)->get_memsize (object,
                                                                  gui_size);
}

static void
gimp_curve_get_preview_size (GimpViewable *viewable,
                             gint          size,
                             gboolean      popup,
                             gboolean      dot_for_dot,
                             gint         *width,
                             gint         *height)
{
  *width  = size;
  *height = size;
}

static gboolean
gimp_curve_get_popup_size (GimpViewable *viewable,
                           gint          width,
                           gint          height,
                           gboolean      dot_for_dot,
                           gint         *popup_width,
                           gint         *popup_height)
{
  *popup_width  = width  * 2;
  *popup_height = height * 2;

  return TRUE;
}

static GimpTempBuf *
gimp_curve_get_new_preview (GimpViewable *viewable,
                            GimpContext  *context,
                            gint          width,
                            gint          height,
                            GeglColor    *fg_color G_GNUC_UNUSED)
{
  return NULL;
}

static gchar *
gimp_curve_get_description (GimpViewable  *viewable,
                            gchar        **tooltip)
{
  GimpCurve *curve = GIMP_CURVE (viewable);

  return g_strdup_printf ("%s", gimp_object_get_name (curve));
}

static void
gimp_curve_dirty (GimpData *data)
{
  GimpCurve *curve = GIMP_CURVE (data);

  curve->identity = FALSE;

  gimp_curve_calculate (curve);

  GIMP_DATA_CLASS (parent_class)->dirty (data);
}

static const gchar *
gimp_curve_get_extension (GimpData *data)
{
  return GIMP_CURVE_FILE_EXTENSION;
}

static void
gimp_curve_data_copy (GimpData *data,
                      GimpData *src_data)
{
  gimp_data_freeze (data);

  gimp_config_copy (GIMP_CONFIG (src_data),
                    GIMP_CONFIG (data), 0);

  gimp_data_thaw (data);
}

static gboolean
gimp_curve_serialize (GimpConfig       *config,
                      GimpConfigWriter *writer,
                      gpointer          data)
{
  return gimp_config_serialize_properties (config, writer);
}

static gboolean
gimp_curve_deserialize (GimpConfig *config,
                        GScanner   *scanner,
                        gint        nest_level,
                        gpointer    data)
{
  gboolean success;

  success = gimp_config_deserialize_properties (config, scanner, nest_level);

  GIMP_CURVE (config)->identity = FALSE;

  return success;
}

static gboolean
gimp_curve_equal (GimpConfig *a,
                  GimpConfig *b)
{
  GimpCurve *a_curve = GIMP_CURVE (a);
  GimpCurve *b_curve = GIMP_CURVE (b);

  if (a_curve->curve_type != b_curve->curve_type)
    return FALSE;

  if (a_curve->n_points != b_curve->n_points ||
      memcmp (a_curve->points, b_curve->points,
              sizeof (GimpCurvePoint) * a_curve->n_points))
    {
      return FALSE;
    }

  if (a_curve->n_samples != b_curve->n_samples ||
      memcmp (a_curve->samples, b_curve->samples,
              sizeof (gdouble) * a_curve->n_samples))
    {
      return FALSE;
    }

  return TRUE;
}

static void
_gimp_curve_reset (GimpConfig *config)
{
  gimp_curve_reset (GIMP_CURVE (config), TRUE);
}

static gboolean
gimp_curve_config_copy (GimpConfig  *src,
                        GimpConfig  *dest,
                        GParamFlags  flags)
{
  GimpCurve *src_curve  = GIMP_CURVE (src);
  GimpCurve *dest_curve = GIMP_CURVE (dest);

  /* make sure the curve type is copied *before* the points, so that we don't
   * overwrite the copied points when changing the type
   */
  dest_curve->curve_type = src_curve->curve_type;

  gimp_config_sync (G_OBJECT (src), G_OBJECT (dest), flags);

  dest_curve->identity = src_curve->identity;

  gimp_data_dirty (GIMP_DATA (dest));

  return TRUE;
}


/*  public functions  */

GimpData *
gimp_curve_new (const gchar *name)
{
  g_return_val_if_fail (name != NULL, NULL);
  g_return_val_if_fail (*name != '\0', NULL);

  return g_object_new (GIMP_TYPE_CURVE,
                       "name", name,
                       NULL);
}

GimpData *
gimp_curve_get_standard (void)
{
  static GimpData *standard_curve = NULL;

  if (! standard_curve)
    {
      standard_curve = gimp_curve_new ("Standard");

      gimp_data_clean (standard_curve);
      gimp_data_make_internal (standard_curve,
                               "gimp-curve-standard");

      g_object_ref (standard_curve);
    }

  return standard_curve;
}

void
gimp_curve_reset (GimpCurve *curve,
                  gboolean   reset_type)
{
  gint i;

  g_return_if_fail (GIMP_IS_CURVE (curve));

  g_object_freeze_notify (G_OBJECT (curve));

  for (i = 0; i < curve->n_samples; i++)
    curve->samples[i] = (gdouble) i / (gdouble) (curve->n_samples - 1);

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_SAMPLES]);

  g_free (curve->points);

  curve->n_points = 2;
  curve->points   = g_new0 (GimpCurvePoint, 2);

  curve->points[0].x    = 0.0;
  curve->points[0].y    = 0.0;
  curve->points[0].type = GIMP_CURVE_POINT_SMOOTH;

  curve->points[1].x    = 1.0;
  curve->points[1].y    = 1.0;
  curve->points[1].type = GIMP_CURVE_POINT_SMOOTH;

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_N_POINTS]);
  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINTS]);
  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINT_TYPES]);

  if (reset_type)
    {
      curve->curve_type = GIMP_CURVE_SMOOTH;
      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_CURVE_TYPE]);
    }

  curve->identity = TRUE;

  g_object_thaw_notify (G_OBJECT (curve));

  gimp_data_dirty (GIMP_DATA (curve));
}

void
gimp_curve_set_curve_type (GimpCurve     *curve,
                           GimpCurveType  curve_type)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));

  if (curve->curve_type != curve_type)
    {
      gimp_data_freeze (GIMP_DATA (curve));

      g_object_freeze_notify (G_OBJECT (curve));

      curve->curve_type = curve_type;

      if (curve_type == GIMP_CURVE_SMOOTH)
        {
          gint i;

          g_free (curve->points);

          /*  pick some points from the curve and make them control
           *  points
           */
          curve->n_points = 9;
          curve->points   = g_new0 (GimpCurvePoint, 9);

          for (i = 0; i < curve->n_points; i++)
            {
              gint sample = i * (curve->n_samples - 1) / (curve->n_points - 1);

              curve->points[i].x    = (gdouble) sample /
                                      (gdouble) (curve->n_samples - 1);
              curve->points[i].y    = curve->samples[sample];
              curve->points[i].type = GIMP_CURVE_POINT_SMOOTH;
            }

          g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_N_POINTS]);
          g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINTS]);
          g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINT_TYPES]);
        }
      else
        {
          gimp_curve_clear_points (curve);
        }

      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_CURVE_TYPE]);

      g_object_thaw_notify (G_OBJECT (curve));

      gimp_data_thaw (GIMP_DATA (curve));
    }
}

GimpCurveType
gimp_curve_get_curve_type (GimpCurve *curve)
{
  g_return_val_if_fail (GIMP_IS_CURVE (curve), GIMP_CURVE_SMOOTH);

  return curve->curve_type;
}

gint
gimp_curve_get_n_points (GimpCurve *curve)
{
  g_return_val_if_fail (GIMP_IS_CURVE (curve), 0);

  return curve->n_points;
}

void
gimp_curve_set_n_samples (GimpCurve *curve,
                          gint       n_samples)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));
  g_return_if_fail (n_samples >= 256);
  g_return_if_fail (n_samples <= 4096);

  if (n_samples != curve->n_samples)
    {
      gint i;

      g_object_freeze_notify (G_OBJECT (curve));

      curve->n_samples = n_samples;
      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_N_SAMPLES]);

      curve->samples = g_renew (gdouble, curve->samples, curve->n_samples);

      for (i = 0; i < curve->n_samples; i++)
        curve->samples[i] = (gdouble) i / (gdouble) (curve->n_samples - 1);

      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_SAMPLES]);

      if (curve->curve_type == GIMP_CURVE_FREE)
        curve->identity = TRUE;

      g_object_thaw_notify (G_OBJECT (curve));
    }
}

gint
gimp_curve_get_n_samples (GimpCurve *curve)
{
  g_return_val_if_fail (GIMP_IS_CURVE (curve), 0);

  return curve->n_samples;
}

gint
gimp_curve_get_point_at (GimpCurve *curve,
                         gdouble    x)
{
  gint    closest_point = -1;
  gdouble distance      = EPSILON;
  gint    i;

  g_return_val_if_fail (GIMP_IS_CURVE (curve), -1);

  for (i = 0; i < curve->n_points; i++)
    {
      gdouble point_distance;

      point_distance = fabs (x - curve->points[i].x);

      if (point_distance <= distance)
        {
          closest_point = i;
          distance      = point_distance;
        }
    }

  return closest_point;
}

gint
gimp_curve_get_closest_point (GimpCurve *curve,
                              gdouble    x,
                              gdouble    y,
                              gdouble    max_distance)
{
  gint    closest_point = -1;
  gdouble distance2     = G_MAXDOUBLE;
  gint    i;

  g_return_val_if_fail (GIMP_IS_CURVE (curve), -1);

  if (max_distance >= 0.0)
    distance2 = SQR (max_distance);

  for (i = curve->n_points - 1; i >= 0; i--)
    {
      gdouble point_distance2;

      point_distance2 = SQR (x - curve->points[i].x) +
                        SQR (y - curve->points[i].y);

      if (point_distance2 <= distance2)
        {
          closest_point = i;
          distance2     = point_distance2;
        }
    }

  return closest_point;
}

gint
gimp_curve_add_point (GimpCurve *curve,
                      gdouble    x,
                      gdouble    y)
{
  GimpCurvePoint *points;
  gint            point;

  g_return_val_if_fail (GIMP_IS_CURVE (curve), -1);

  if (curve->curve_type == GIMP_CURVE_FREE)
    return -1;

  x = CLAMP (x, 0.0, 1.0);
  y = CLAMP (y, 0.0, 1.0);

  for (point = 0; point < curve->n_points; point++)
    {
      if (curve->points[point].x > x)
        break;
    }

  points = g_new0 (GimpCurvePoint, curve->n_points + 1);

  memcpy (points,         curve->points,
          point * sizeof (GimpCurvePoint));
  memcpy (points + point + 1, curve->points + point,
          (curve->n_points - point) * sizeof (GimpCurvePoint));

  points[point].x    = x;
  points[point].y    = y;
  points[point].type = GIMP_CURVE_POINT_SMOOTH;

  g_free (curve->points);

  curve->n_points++;
  curve->points = points;

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_N_POINTS]);
  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINTS]);
  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINT_TYPES]);

  gimp_data_dirty (GIMP_DATA (curve));

  return point;
}

void
gimp_curve_delete_point (GimpCurve *curve,
                         gint       point)
{
  GimpCurvePoint *points;

  g_return_if_fail (GIMP_IS_CURVE (curve));
  g_return_if_fail (point >= 0 && point < curve->n_points);

  points = g_new0 (GimpCurvePoint, curve->n_points - 1);

  memcpy (points,         curve->points,
          point * sizeof (GimpCurvePoint));
  memcpy (points + point, curve->points + point + 1,
          (curve->n_points - point - 1) * sizeof (GimpCurvePoint));

  g_free (curve->points);

  curve->n_points--;
  curve->points = points;

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_N_POINTS]);
  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINTS]);
  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINT_TYPES]);

  gimp_data_dirty (GIMP_DATA (curve));
}

void
gimp_curve_set_point (GimpCurve *curve,
                      gint       point,
                      gdouble    x,
                      gdouble    y)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));
  g_return_if_fail (point >= 0 && point < curve->n_points);

  curve->points[point].x = CLAMP (x, 0.0, 1.0);
  curve->points[point].y = CLAMP (y, 0.0, 1.0);

  if (point > 0)
    curve->points[point].x = MAX (x, curve->points[point - 1].x);

  if (point < curve->n_points - 1)
    curve->points[point].x = MIN (x, curve->points[point + 1].x);

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINTS]);

  gimp_data_dirty (GIMP_DATA (curve));
}

void
gimp_curve_move_point (GimpCurve *curve,
                       gint       point,
                       gdouble    y)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));
  g_return_if_fail (point >= 0 && point < curve->n_points);

  curve->points[point].y = CLAMP (y, 0.0, 1.0);

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINTS]);

  gimp_data_dirty (GIMP_DATA (curve));
}

void
gimp_curve_get_point (GimpCurve *curve,
                      gint       point,
                      gdouble   *x,
                      gdouble   *y)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));
  g_return_if_fail (point >= 0 && point < curve->n_points);

  if (x) *x = curve->points[point].x;
  if (y) *y = curve->points[point].y;
}

void
gimp_curve_set_point_type (GimpCurve          *curve,
                           gint                point,
                           GimpCurvePointType  type)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));
  g_return_if_fail (point >= 0 && point < curve->n_points);

  curve->points[point].type = type;

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINT_TYPES]);

  gimp_data_dirty (GIMP_DATA (curve));
}

GimpCurvePointType
gimp_curve_get_point_type (GimpCurve *curve,
                           gint       point)
{
  g_return_val_if_fail (GIMP_IS_CURVE (curve), GIMP_CURVE_POINT_SMOOTH);
  g_return_val_if_fail (point >= 0 && point < curve->n_points, GIMP_CURVE_POINT_SMOOTH);

  return curve->points[point].type;
}

void
gimp_curve_clear_points (GimpCurve *curve)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));

  if (curve->points)
    {
      g_clear_pointer (&curve->points, g_free);
      curve->n_points = 0;

      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_N_POINTS]);
      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINTS]);
      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_POINT_TYPES]);

      gimp_data_dirty (GIMP_DATA (curve));
    }
}

gdouble
gimp_curve_get_sample (GimpCurve *curve,
                       gdouble    x)
{
  g_return_val_if_fail (GIMP_IS_CURVE (curve), 0);
  g_return_val_if_fail (curve->curve_type == GIMP_CURVE_FREE, 0);
  g_return_val_if_fail (x >= 0 && x <= 1.0, 0);

  return curve->samples[ROUND (x * (gdouble) (curve->n_samples - 1))];
}

void
gimp_curve_set_sample (GimpCurve *curve,
                       gdouble    x,
                       gdouble    y)
{
  g_return_if_fail (GIMP_IS_CURVE (curve));
  g_return_if_fail (x >= 0 && x <= 1.0);
  g_return_if_fail (y >= 0 && y <= 1.0);

  if (curve->curve_type == GIMP_CURVE_SMOOTH)
    return;

  curve->samples[ROUND (x * (gdouble) (curve->n_samples - 1))] = y;

  g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_SAMPLES]);

  gimp_data_dirty (GIMP_DATA (curve));
}

/**
 * gimp_curve_is_identity:
 * @curve: a #GimpCurve object
 *
 * If this function returns %TRUE, then the curve maps each value to
 * itself. If it returns %FALSE, then this assumption can not be made.
 *
 * Returns: %TRUE if the curve is an identity mapping, %FALSE otherwise.
 **/
gboolean
gimp_curve_is_identity (GimpCurve *curve)
{
  g_return_val_if_fail (GIMP_IS_CURVE (curve), FALSE);

  return curve->identity;
}

void
gimp_curve_get_uchar (GimpCurve *curve,
                      gint       n_samples,
                      guchar    *samples)
{
  gint i;

  g_return_if_fail (GIMP_IS_CURVE (curve));
  /* FIXME: support n_samples != curve->n_samples */
  g_return_if_fail (n_samples == curve->n_samples);
  g_return_if_fail (samples != NULL);

  for (i = 0; i < curve->n_samples; i++)
    samples[i] = curve->samples[i] * 255.999;
}


/*  private functions  */

static void
gimp_curve_calculate (GimpCurve *curve)
{
  gint i;
  gint p1, p2, p3, p4;

  if (gimp_data_is_frozen (GIMP_DATA (curve)))
    return;

  switch (curve->curve_type)
    {
    case GIMP_CURVE_SMOOTH:
      /*  Initialize boundary curve points */
      if (curve->n_points > 0)
        {
          GimpCurvePoint point;
          gint           boundary;

          point    = curve->points[0];
          boundary = ROUND (point.x * (gdouble) (curve->n_samples - 1));

          for (i = 0; i < boundary; i++)
            curve->samples[i] = point.y;

          point    = curve->points[curve->n_points - 1];
          boundary = ROUND (point.x * (gdouble) (curve->n_samples - 1));

          for (i = boundary; i < curve->n_samples; i++)
            curve->samples[i] = point.y;
        }

      for (i = 0; i < curve->n_points - 1; i++)
        {
          p1 = MAX (i - 1, 0);
          p2 = i;
          p3 = i + 1;
          p4 = MIN (i + 2, curve->n_points - 1);

          if (curve->points[p2].type == GIMP_CURVE_POINT_CORNER)
            p1 = p2;

          if (curve->points[p3].type == GIMP_CURVE_POINT_CORNER)
            p4 = p3;

          gimp_curve_plot (curve, p1, p2, p3, p4);
        }

      /* ensure that the control points are used exactly */
      for (i = 0; i < curve->n_points; i++)
        {
          gdouble x = curve->points[i].x;
          gdouble y = curve->points[i].y;

          curve->samples[ROUND (x * (gdouble) (curve->n_samples - 1))] = y;
        }

      g_object_notify_by_pspec (G_OBJECT (curve), obj_props[PROP_SAMPLES]);
      break;

    case GIMP_CURVE_FREE:
      break;
    }
}

/*
 * This function calculates the curve values between the control points
 * p2 and p3, taking the potentially existing neighbors p1 and p4 into
 * account.
 *
 * This function uses a cubic bezier curve for the individual segments and
 * calculates the necessary intermediate control points depending on the
 * neighbor curve control points.
 */
static void
gimp_curve_plot (GimpCurve *curve,
                 gint       p1,
                 gint       p2,
                 gint       p3,
                 gint       p4)
{
  gint    i;
  gdouble x0, x3;
  gdouble y0, y1, y2, y3;
  gdouble dx, dy;
  gdouble slope;

  /* the outer control points for the bezier curve. */
  x0 = curve->points[p2].x;
  y0 = curve->points[p2].y;
  x3 = curve->points[p3].x;
  y3 = curve->points[p3].y;

  /*
   * the x values of the inner control points are fixed at
   * x1 = 2/3*x0 + 1/3*x3   and  x2 = 1/3*x0 + 2/3*x3
   * this ensures that the x values increase linearly with the
   * parameter t and enables us to skip the calculation of the x
   * values altogether - just calculate y(t) evenly spaced.
   */

  dx = x3 - x0;
  dy = y3 - y0;

  if (dx <= EPSILON)
    {
      gint index;

      index = ROUND (x0 * (gdouble) (curve->n_samples - 1));

      curve->samples[index] = y3;

      return;
    }

  if (p1 == p2 && p3 == p4)
    {
      /* No information about the neighbors,
       * calculate y1 and y2 to get a straight line
       */
      y1 = y0 + dy / 3.0;
      y2 = y0 + dy * 2.0 / 3.0;
    }
  else if (p1 == p2 && p3 != p4)
    {
      /* only the right neighbor is available. Make the tangent at the
       * right endpoint parallel to the line between the left endpoint
       * and the right neighbor. Then point the tangent at the left towards
       * the control handle of the right tangent, to ensure that the curve
       * does not have an inflection point.
       */
      slope = (curve->points[p4].y - y0) / (curve->points[p4].x - x0);

      y2 = y3 - slope * dx / 3.0;
      y1 = y0 + (y2 - y0) / 2.0;
    }
  else if (p1 != p2 && p3 == p4)
    {
      /* see previous case */
      slope = (y3 - curve->points[p1].y) / (x3 - curve->points[p1].x);

      y1 = y0 + slope * dx / 3.0;
      y2 = y3 + (y1 - y3) / 2.0;
    }
  else /* (p1 != p2 && p3 != p4) */
    {
      /* Both neighbors are available. Make the tangents at the endpoints
       * parallel to the line between the opposite endpoint and the adjacent
       * neighbor.
       */
      slope = (y3 - curve->points[p1].y) / (x3 - curve->points[p1].x);

      y1 = y0 + slope * dx / 3.0;

      slope = (curve->points[p4].y - y0) / (curve->points[p4].x - x0);

      y2 = y3 - slope * dx / 3.0;
    }

  /*
   * finally calculate the y(t) values for the given bezier values. We can
   * use homogeneously distributed values for t, since x(t) increases linearly.
   */
  for (i = 0; i <= ROUND (dx * (gdouble) (curve->n_samples - 1)); i++)
    {
      gdouble y, t;
      gint    index;

      t = i / dx / (gdouble) (curve->n_samples - 1);
      y =     y0 * (1-t) * (1-t) * (1-t) +
          3 * y1 * (1-t) * (1-t) * t     +
          3 * y2 * (1-t) * t     * t     +
              y3 * t     * t     * t;

      index = i + ROUND (x0 * (gdouble) (curve->n_samples - 1));

      if (index < curve->n_samples)
        curve->samples[index] = CLAMP (y, 0.0, 1.0);
    }
}
