/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * 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 "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "libpikaconfig/pikaconfig.h"

#include "core-types.h"

#include "pikacurve.h"
#include "pikacurve-load.h"
#include "pikacurve-save.h"
#include "pikaparamspecs.h"

#include "pika-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          pika_curve_config_iface_init (PikaConfigInterface *iface);

static void          pika_curve_finalize          (GObject          *object);
static void          pika_curve_set_property      (GObject          *object,
                                                   guint             property_id,
                                                   const GValue     *value,
                                                   GParamSpec       *pspec);
static void          pika_curve_get_property      (GObject          *object,
                                                   guint             property_id,
                                                   GValue           *value,
                                                   GParamSpec       *pspec);

static gint64        pika_curve_get_memsize       (PikaObject       *object,
                                                   gint64           *gui_size);

static void          pika_curve_get_preview_size  (PikaViewable     *viewable,
                                                   gint              size,
                                                   gboolean          popup,
                                                   gboolean          dot_for_dot,
                                                   gint             *width,
                                                   gint             *height);
static gboolean      pika_curve_get_popup_size    (PikaViewable     *viewable,
                                                   gint              width,
                                                   gint              height,
                                                   gboolean          dot_for_dot,
                                                   gint             *popup_width,
                                                   gint             *popup_height);
static PikaTempBuf * pika_curve_get_new_preview   (PikaViewable     *viewable,
                                                   PikaContext      *context,
                                                   gint              width,
                                                   gint              height);
static gchar       * pika_curve_get_description   (PikaViewable     *viewable,
                                                   gchar           **tooltip);

static void          pika_curve_dirty             (PikaData         *data);
static const gchar * pika_curve_get_extension     (PikaData         *data);
static void          pika_curve_data_copy         (PikaData         *data,
                                                   PikaData         *src_data);

static gboolean      pika_curve_serialize         (PikaConfig       *config,
                                                   PikaConfigWriter *writer,
                                                   gpointer          data);
static gboolean      pika_curve_deserialize       (PikaConfig       *config,
                                                   GScanner         *scanner,
                                                   gint              nest_level,
                                                   gpointer          data);
static gboolean      pika_curve_equal             (PikaConfig       *a,
                                                   PikaConfig       *b);
static void          _pika_curve_reset            (PikaConfig       *config);
static gboolean      pika_curve_config_copy       (PikaConfig       *src,
                                                   PikaConfig       *dest,
                                                   GParamFlags       flags);

static void          pika_curve_calculate         (PikaCurve        *curve);
static void          pika_curve_plot              (PikaCurve        *curve,
                                                   gint              p1,
                                                   gint              p2,
                                                   gint              p3,
                                                   gint              p4);


G_DEFINE_TYPE_WITH_CODE (PikaCurve, pika_curve, PIKA_TYPE_DATA,
                         G_IMPLEMENT_INTERFACE (PIKA_TYPE_CONFIG,
                                                pika_curve_config_iface_init))

#define parent_class pika_curve_parent_class


static void
pika_curve_class_init (PikaCurveClass *klass)
{
  GObjectClass      *object_class      = G_OBJECT_CLASS (klass);
  PikaObjectClass   *pika_object_class = PIKA_OBJECT_CLASS (klass);
  PikaViewableClass *viewable_class    = PIKA_VIEWABLE_CLASS (klass);
  PikaDataClass     *data_class        = PIKA_DATA_CLASS (klass);
  GParamSpec        *array_spec;

  object_class->finalize            = pika_curve_finalize;
  object_class->set_property        = pika_curve_set_property;
  object_class->get_property        = pika_curve_get_property;

  pika_object_class->get_memsize    = pika_curve_get_memsize;

  viewable_class->default_icon_name = "FIXME icon name";
  viewable_class->get_preview_size  = pika_curve_get_preview_size;
  viewable_class->get_popup_size    = pika_curve_get_popup_size;
  viewable_class->get_new_preview   = pika_curve_get_new_preview;
  viewable_class->get_description   = pika_curve_get_description;

  data_class->dirty                 = pika_curve_dirty;
  data_class->save                  = pika_curve_save;
  data_class->get_extension         = pika_curve_get_extension;
  data_class->copy                  = pika_curve_data_copy;

  obj_props[PROP_CURVE_TYPE] =
      g_param_spec_enum ("curve-type",
                         "Curve Type",
                         "The curve type",
                         PIKA_TYPE_CURVE_TYPE,
                         PIKA_CURVE_SMOOTH,
                         PIKA_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 */
                        PIKA_CONFIG_PARAM_IGNORE | PIKA_CONFIG_PARAM_FLAGS);

  array_spec = g_param_spec_double ("point", NULL, NULL,
                                    -1.0, 1.0, 0.0, PIKA_PARAM_READWRITE);
  obj_props[PROP_POINTS] =
      pika_param_spec_value_array ("points",
                                   NULL, NULL,
                                   array_spec,
                                   PIKA_CONFIG_PARAM_FLAGS);

  array_spec = g_param_spec_enum ("point-type", NULL, NULL,
                                  PIKA_TYPE_CURVE_POINT_TYPE,
                                  PIKA_CURVE_POINT_SMOOTH,
                                  PIKA_PARAM_READWRITE);
  obj_props[PROP_POINT_TYPES] =
      pika_param_spec_value_array ("point-types",
                                   NULL, NULL,
                                   array_spec,
                                   PIKA_CONFIG_PARAM_FLAGS);

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

  array_spec = g_param_spec_double ("sample", NULL, NULL,
                                    0.0, 1.0, 0.0, PIKA_PARAM_READWRITE);
  obj_props[PROP_SAMPLES] =
      pika_param_spec_value_array ("samples",
                                   NULL, NULL,
                                   array_spec,
                                   PIKA_CONFIG_PARAM_FLAGS);

  g_object_class_install_properties (object_class, N_PROPS, obj_props);
}

static void
pika_curve_config_iface_init (PikaConfigInterface *iface)
{
  iface->serialize   = pika_curve_serialize;
  iface->deserialize = pika_curve_deserialize;
  iface->equal       = pika_curve_equal;
  iface->reset       = _pika_curve_reset;
  iface->copy        = pika_curve_config_copy;
}

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

static void
pika_curve_finalize (GObject *object)
{
  PikaCurve *curve = PIKA_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
pika_curve_set_property (GObject      *object,
                         guint         property_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  PikaCurve *curve = PIKA_CURVE (object);

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

    case PROP_N_POINTS:
      /* ignored */
      break;

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

        if (! array)
          {
            pika_curve_clear_points (curve);

            break;
          }

        length = pika_value_array_length (array) / 2;

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

        for (i = 0; i < length; i++)
          {
            GValue *x = pika_value_array_index (array, i * 2);
            GValue *y = pika_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 = PIKA_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:
      {
        PikaValueArray *array = g_value_get_boxed (value);
        PikaCurvePoint *points;
        gint            length;
        gdouble         x     = 0.0;
        gdouble         y     = 0.0;
        gint            i;

        if (! array)
          {
            pika_curve_clear_points (curve);

            break;
          }

        length = pika_value_array_length (array);

        points = g_new0 (PikaCurvePoint, length);

        for (i = 0; i < length; i++)
          {
            GValue *type = pika_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:
      pika_curve_set_n_samples (curve, g_value_get_int (value));
      break;

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

        if (! array)
          break;

        length = pika_value_array_length (array);

        for (i = 0; i < curve->n_samples && i < length; i++)
          {
            GValue *v = pika_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
pika_curve_get_property (GObject    *object,
                         guint       property_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  PikaCurve *curve = PIKA_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:
      {
        PikaValueArray *array = pika_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);
            pika_value_array_append (array, &v);

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

        g_value_unset (&v);

        g_value_take_boxed (value, array);
      }
      break;

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

        g_value_init (&v, PIKA_TYPE_CURVE_POINT_TYPE);

        for (i = 0; i < curve->n_points; i++)
          {
            g_value_set_enum (&v, curve->points[i].type);
            pika_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:
      {
        PikaValueArray *array = pika_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]);
            pika_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
pika_curve_get_memsize (PikaObject *object,
                        gint64     *gui_size)
{
  PikaCurve *curve   = PIKA_CURVE (object);
  gint64     memsize = 0;

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

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

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

static gboolean
pika_curve_get_popup_size (PikaViewable *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 PikaTempBuf *
pika_curve_get_new_preview (PikaViewable *viewable,
                            PikaContext  *context,
                            gint          width,
                            gint          height)
{
  return NULL;
}

static gchar *
pika_curve_get_description (PikaViewable  *viewable,
                            gchar        **tooltip)
{
  PikaCurve *curve = PIKA_CURVE (viewable);

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

static void
pika_curve_dirty (PikaData *data)
{
  PikaCurve *curve = PIKA_CURVE (data);

  curve->identity = FALSE;

  pika_curve_calculate (curve);

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

static const gchar *
pika_curve_get_extension (PikaData *data)
{
  return PIKA_CURVE_FILE_EXTENSION;
}

static void
pika_curve_data_copy (PikaData *data,
                      PikaData *src_data)
{
  pika_data_freeze (data);

  pika_config_copy (PIKA_CONFIG (src_data),
                    PIKA_CONFIG (data), 0);

  pika_data_thaw (data);
}

static gboolean
pika_curve_serialize (PikaConfig       *config,
                      PikaConfigWriter *writer,
                      gpointer          data)
{
  return pika_config_serialize_properties (config, writer);
}

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

  success = pika_config_deserialize_properties (config, scanner, nest_level);

  PIKA_CURVE (config)->identity = FALSE;

  return success;
}

static gboolean
pika_curve_equal (PikaConfig *a,
                  PikaConfig *b)
{
  PikaCurve *a_curve = PIKA_CURVE (a);
  PikaCurve *b_curve = PIKA_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 (PikaCurvePoint) * 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
_pika_curve_reset (PikaConfig *config)
{
  pika_curve_reset (PIKA_CURVE (config), TRUE);
}

static gboolean
pika_curve_config_copy (PikaConfig  *src,
                        PikaConfig  *dest,
                        GParamFlags  flags)
{
  PikaCurve *src_curve  = PIKA_CURVE (src);
  PikaCurve *dest_curve = PIKA_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;

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

  dest_curve->identity = src_curve->identity;

  pika_data_dirty (PIKA_DATA (dest));

  return TRUE;
}


/*  public functions  */

PikaData *
pika_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 (PIKA_TYPE_CURVE,
                       "name", name,
                       NULL);
}

PikaData *
pika_curve_get_standard (void)
{
  static PikaData *standard_curve = NULL;

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

      pika_data_clean (standard_curve);
      pika_data_make_internal (standard_curve,
                               "pika-curve-standard");

      g_object_ref (standard_curve);
    }

  return standard_curve;
}

void
pika_curve_reset (PikaCurve *curve,
                  gboolean   reset_type)
{
  gint i;

  g_return_if_fail (PIKA_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 (PikaCurvePoint, 2);

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

  curve->points[1].x    = 1.0;
  curve->points[1].y    = 1.0;
  curve->points[1].type = PIKA_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 = PIKA_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));

  pika_data_dirty (PIKA_DATA (curve));
}

void
pika_curve_set_curve_type (PikaCurve     *curve,
                           PikaCurveType  curve_type)
{
  g_return_if_fail (PIKA_IS_CURVE (curve));

  if (curve->curve_type != curve_type)
    {
      pika_data_freeze (PIKA_DATA (curve));

      g_object_freeze_notify (G_OBJECT (curve));

      curve->curve_type = curve_type;

      if (curve_type == PIKA_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 (PikaCurvePoint, 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 = PIKA_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
        {
          pika_curve_clear_points (curve);
        }

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

      g_object_thaw_notify (G_OBJECT (curve));

      pika_data_thaw (PIKA_DATA (curve));
    }
}

PikaCurveType
pika_curve_get_curve_type (PikaCurve *curve)
{
  g_return_val_if_fail (PIKA_IS_CURVE (curve), PIKA_CURVE_SMOOTH);

  return curve->curve_type;
}

gint
pika_curve_get_n_points (PikaCurve *curve)
{
  g_return_val_if_fail (PIKA_IS_CURVE (curve), 0);

  return curve->n_points;
}

void
pika_curve_set_n_samples (PikaCurve *curve,
                          gint       n_samples)
{
  g_return_if_fail (PIKA_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 == PIKA_CURVE_FREE)
        curve->identity = TRUE;

      g_object_thaw_notify (G_OBJECT (curve));
    }
}

gint
pika_curve_get_n_samples (PikaCurve *curve)
{
  g_return_val_if_fail (PIKA_IS_CURVE (curve), 0);

  return curve->n_samples;
}

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

  g_return_val_if_fail (PIKA_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
pika_curve_get_closest_point (PikaCurve *curve,
                              gdouble    x,
                              gdouble    y,
                              gdouble    max_distance)
{
  gint    closest_point = -1;
  gdouble distance2     = G_MAXDOUBLE;
  gint    i;

  g_return_val_if_fail (PIKA_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
pika_curve_add_point (PikaCurve *curve,
                      gdouble    x,
                      gdouble    y)
{
  PikaCurvePoint *points;
  gint            point;

  g_return_val_if_fail (PIKA_IS_CURVE (curve), -1);

  if (curve->curve_type == PIKA_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 (PikaCurvePoint, curve->n_points + 1);

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

  points[point].x    = x;
  points[point].y    = y;
  points[point].type = PIKA_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]);

  pika_data_dirty (PIKA_DATA (curve));

  return point;
}

void
pika_curve_delete_point (PikaCurve *curve,
                         gint       point)
{
  PikaCurvePoint *points;

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

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

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

  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]);

  pika_data_dirty (PIKA_DATA (curve));
}

void
pika_curve_set_point (PikaCurve *curve,
                      gint       point,
                      gdouble    x,
                      gdouble    y)
{
  g_return_if_fail (PIKA_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]);

  pika_data_dirty (PIKA_DATA (curve));
}

void
pika_curve_move_point (PikaCurve *curve,
                       gint       point,
                       gdouble    y)
{
  g_return_if_fail (PIKA_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]);

  pika_data_dirty (PIKA_DATA (curve));
}

void
pika_curve_get_point (PikaCurve *curve,
                      gint       point,
                      gdouble   *x,
                      gdouble   *y)
{
  g_return_if_fail (PIKA_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
pika_curve_set_point_type (PikaCurve          *curve,
                           gint                point,
                           PikaCurvePointType  type)
{
  g_return_if_fail (PIKA_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]);

  pika_data_dirty (PIKA_DATA (curve));
}

PikaCurvePointType
pika_curve_get_point_type (PikaCurve *curve,
                           gint       point)
{
  g_return_val_if_fail (PIKA_IS_CURVE (curve), PIKA_CURVE_POINT_SMOOTH);
  g_return_val_if_fail (point >= 0 && point < curve->n_points, PIKA_CURVE_POINT_SMOOTH);

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

void
pika_curve_clear_points (PikaCurve *curve)
{
  g_return_if_fail (PIKA_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]);

      pika_data_dirty (PIKA_DATA (curve));
    }
}

void
pika_curve_set_curve (PikaCurve *curve,
                      gdouble    x,
                      gdouble    y)
{
  g_return_if_fail (PIKA_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 == PIKA_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]);

  pika_data_dirty (PIKA_DATA (curve));
}

/**
 * pika_curve_is_identity:
 * @curve: a #PikaCurve 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
pika_curve_is_identity (PikaCurve *curve)
{
  g_return_val_if_fail (PIKA_IS_CURVE (curve), FALSE);

  return curve->identity;
}

void
pika_curve_get_uchar (PikaCurve *curve,
                      gint       n_samples,
                      guchar    *samples)
{
  gint i;

  g_return_if_fail (PIKA_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
pika_curve_calculate (PikaCurve *curve)
{
  gint i;
  gint p1, p2, p3, p4;

  if (pika_data_is_frozen (PIKA_DATA (curve)))
    return;

  switch (curve->curve_type)
    {
    case PIKA_CURVE_SMOOTH:
      /*  Initialize boundary curve points */
      if (curve->n_points > 0)
        {
          PikaCurvePoint 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 == PIKA_CURVE_POINT_CORNER)
            p1 = p2;

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

          pika_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 PIKA_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
pika_curve_plot (PikaCurve *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);
    }
}
