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

#include "config.h"

#include <string.h>

#include <gegl.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include "libgimpmath/gimpmath.h"
#include "libgimpcolor/gimpcolor.h"
#include "libgimpconfig/gimpconfig.h"
#include "libgimpwidgets/gimpwidgets.h"

#include "widgets-types.h"

#include "core/gimp.h"
#include "core/gimpcurve.h"
#include "core/gimpcurve-map.h"
#include "core/gimpmarshal.h"

#include "gimpclipboard.h"
#include "gimpcurveview.h"
#include "gimpwidgets-utils.h"


#define POINT_MAX_DISTANCE 16.0


enum
{
  PROP_0,
  PROP_GIMP,
  PROP_BASE_LINE,
  PROP_GRID_ROWS,
  PROP_GRID_COLUMNS,
  PROP_X_AXIS_LABEL,
  PROP_Y_AXIS_LABEL
};

enum
{
  SELECTION_CHANGED,
  CUT_CLIPBOARD,
  COPY_CLIPBOARD,
  PASTE_CLIPBOARD,
  LAST_SIGNAL
};


typedef struct
{
  GimpCurve *curve;
  GimpRGB    color;
  gboolean   color_set;
} BGCurve;


static void       gimp_curve_view_finalize              (GObject          *object);
static void       gimp_curve_view_dispose               (GObject          *object);
static void       gimp_curve_view_set_property          (GObject          *object,
                                                         guint             property_id,
                                                         const GValue     *value,
                                                         GParamSpec       *pspec);
static void       gimp_curve_view_get_property          (GObject          *object,
                                                         guint             property_id,
                                                         GValue           *value,
                                                         GParamSpec       *pspec);

static void       gimp_curve_view_style_set             (GtkWidget        *widget,
                                                         GtkStyle         *prev_style);
static gboolean   gimp_curve_view_expose                (GtkWidget        *widget,
                                                         GdkEventExpose   *event);
static gboolean   gimp_curve_view_button_press          (GtkWidget        *widget,
                                                         GdkEventButton   *bevent);
static gboolean   gimp_curve_view_button_release        (GtkWidget        *widget,
                                                         GdkEventButton   *bevent);
static gboolean   gimp_curve_view_motion_notify         (GtkWidget        *widget,
                                                         GdkEventMotion   *bevent);
static gboolean   gimp_curve_view_leave_notify          (GtkWidget        *widget,
                                                         GdkEventCrossing *cevent);
static gboolean   gimp_curve_view_key_press             (GtkWidget        *widget,
                                                         GdkEventKey      *kevent);

static void       gimp_curve_view_cut_clipboard         (GimpCurveView    *view);
static void       gimp_curve_view_copy_clipboard        (GimpCurveView    *view);
static void       gimp_curve_view_paste_clipboard       (GimpCurveView    *view);

static void       gimp_curve_view_curve_dirty           (GimpCurve        *curve,
                                                         GimpCurveView    *view);
static void       gimp_curve_view_curve_notify_n_points (GimpCurve        *curve,
                                                         GParamSpec       *pspec,
                                                         GimpCurveView    *view);

static void       gimp_curve_view_set_cursor            (GimpCurveView    *view,
                                                         gdouble           x,
                                                         gdouble           y);
static void       gimp_curve_view_unset_cursor          (GimpCurveView *view);


G_DEFINE_TYPE (GimpCurveView, gimp_curve_view,
               GIMP_TYPE_HISTOGRAM_VIEW)

#define parent_class gimp_curve_view_parent_class

static guint curve_view_signals[LAST_SIGNAL] = { 0 };


static void
gimp_curve_view_class_init (GimpCurveViewClass *klass)
{
  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GtkBindingSet  *binding_set;

  object_class->finalize             = gimp_curve_view_finalize;
  object_class->dispose              = gimp_curve_view_dispose;
  object_class->set_property         = gimp_curve_view_set_property;
  object_class->get_property         = gimp_curve_view_get_property;

  widget_class->style_set            = gimp_curve_view_style_set;
  widget_class->expose_event         = gimp_curve_view_expose;
  widget_class->button_press_event   = gimp_curve_view_button_press;
  widget_class->button_release_event = gimp_curve_view_button_release;
  widget_class->motion_notify_event  = gimp_curve_view_motion_notify;
  widget_class->leave_notify_event   = gimp_curve_view_leave_notify;
  widget_class->key_press_event      = gimp_curve_view_key_press;

  klass->selection_changed           = NULL;
  klass->cut_clipboard               = gimp_curve_view_cut_clipboard;
  klass->copy_clipboard              = gimp_curve_view_copy_clipboard;
  klass->paste_clipboard             = gimp_curve_view_paste_clipboard;

  g_object_class_install_property (object_class, PROP_GIMP,
                                   g_param_spec_object ("gimp",
                                                        NULL, NULL,
                                                        GIMP_TYPE_GIMP,
                                                        GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_BASE_LINE,
                                   g_param_spec_boolean ("base-line",
                                                         NULL, NULL,
                                                         TRUE,
                                                         GIMP_PARAM_READWRITE |
                                                         G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_GRID_ROWS,
                                   g_param_spec_int ("grid-rows", NULL, NULL,
                                                     0, 100, 8,
                                                     GIMP_PARAM_READWRITE |
                                                     G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_GRID_COLUMNS,
                                   g_param_spec_int ("grid-columns", NULL, NULL,
                                                     0, 100, 8,
                                                     GIMP_PARAM_READWRITE |
                                                     G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_X_AXIS_LABEL,
                                   g_param_spec_string ("x-axis-label", NULL, NULL,
                                                        NULL,
                                                        GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_Y_AXIS_LABEL,
                                   g_param_spec_string ("y-axis-label", NULL, NULL,
                                                        NULL,
                                                        GIMP_PARAM_READWRITE));

  curve_view_signals[SELECTION_CHANGED] =
    g_signal_new ("selection-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpCurveViewClass, selection_changed),
                  NULL, NULL,
                  gimp_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

  curve_view_signals[CUT_CLIPBOARD] =
    g_signal_new ("cut-clipboard",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (GimpCurveViewClass, cut_clipboard),
                  NULL, NULL,
                  gimp_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

  curve_view_signals[COPY_CLIPBOARD] =
    g_signal_new ("copy-clipboard",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (GimpCurveViewClass, copy_clipboard),
                  NULL, NULL,
                  gimp_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

  curve_view_signals[PASTE_CLIPBOARD] =
    g_signal_new ("paste-clipboard",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (GimpCurveViewClass, paste_clipboard),
                  NULL, NULL,
                  gimp_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

  binding_set = gtk_binding_set_by_class (klass);

  gtk_binding_entry_add_signal (binding_set, GDK_KEY_x, GDK_CONTROL_MASK,
                                "cut-clipboard", 0);
  gtk_binding_entry_add_signal (binding_set, GDK_KEY_c, GDK_CONTROL_MASK,
                                "copy-clipboard", 0);
  gtk_binding_entry_add_signal (binding_set, GDK_KEY_v, GDK_CONTROL_MASK,
                                "paste-clipboard", 0);
}

static void
gimp_curve_view_init (GimpCurveView *view)
{
  view->curve       = NULL;
  view->selected    = -1;
  view->offset_x    = 0.0;
  view->offset_y    = 0.0;
  view->last_x      = 0.0;
  view->last_y      = 0.0;
  view->cursor_type = -1;
  view->xpos        = -1.0;
  view->cursor_x    = -1.0;
  view->cursor_y    = -1.0;
  view->range_x_min = 0.0;
  view->range_x_max = 1.0;
  view->range_y_min = 0.0;
  view->range_y_max = 1.0;

  view->x_axis_label = NULL;
  view->y_axis_label = NULL;

  gtk_widget_set_can_focus (GTK_WIDGET (view), TRUE);
  gtk_widget_add_events (GTK_WIDGET (view),
                         GDK_BUTTON_PRESS_MASK   |
                         GDK_BUTTON_RELEASE_MASK |
                         GDK_BUTTON1_MOTION_MASK |
                         GDK_POINTER_MOTION_MASK |
                         GDK_KEY_PRESS_MASK      |
                         GDK_LEAVE_NOTIFY_MASK);
}

static void
gimp_curve_view_finalize (GObject *object)
{
  GimpCurveView *view = GIMP_CURVE_VIEW (object);

  g_clear_object (&view->orig_curve);

  g_clear_object (&view->layout);
  g_clear_object (&view->cursor_layout);

  g_clear_pointer (&view->x_axis_label, g_free);
  g_clear_pointer (&view->y_axis_label, g_free);

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

static void
gimp_curve_view_dispose (GObject *object)
{
  GimpCurveView *view = GIMP_CURVE_VIEW (object);

  gimp_curve_view_set_curve (view, NULL, NULL);

  if (view->bg_curves)
    gimp_curve_view_remove_all_backgrounds (view);

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

static void
gimp_curve_view_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  GimpCurveView *view = GIMP_CURVE_VIEW (object);

  switch (property_id)
    {
    case PROP_GIMP:
      view->gimp = g_value_get_object (value); /* don't ref */
      break;
    case PROP_GRID_ROWS:
      view->grid_rows = g_value_get_int (value);
      break;
    case PROP_GRID_COLUMNS:
      view->grid_columns = g_value_get_int (value);
      break;
    case PROP_BASE_LINE:
      view->draw_base_line = g_value_get_boolean (value);
      break;
    case PROP_X_AXIS_LABEL:
      gimp_curve_view_set_x_axis_label (view, g_value_get_string (value));
      break;
    case PROP_Y_AXIS_LABEL:
      gimp_curve_view_set_y_axis_label (view, g_value_get_string (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_curve_view_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  GimpCurveView *view = GIMP_CURVE_VIEW (object);

  switch (property_id)
    {
    case PROP_GIMP:
      g_value_set_object (value, view->gimp);
      break;
    case PROP_GRID_ROWS:
      g_value_set_int (value, view->grid_rows);
      break;
    case PROP_GRID_COLUMNS:
      g_value_set_int (value, view->grid_columns);
      break;
    case PROP_BASE_LINE:
      g_value_set_boolean (value, view->draw_base_line);
      break;
    case PROP_X_AXIS_LABEL:
      g_value_set_string (value, view->x_axis_label);
      break;
    case PROP_Y_AXIS_LABEL:
      g_value_set_string (value, view->y_axis_label);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_curve_view_style_set (GtkWidget *widget,
                           GtkStyle  *prev_style)
{
  GimpCurveView *view = GIMP_CURVE_VIEW (widget);

  GTK_WIDGET_CLASS (parent_class)->style_set (widget, prev_style);

  g_clear_object (&view->layout);
  g_clear_object (&view->cursor_layout);
}

static void
gimp_curve_view_draw_grid (GimpCurveView *view,
                           cairo_t       *cr,
                           gint           width,
                           gint           height,
                           gint           border)
{
  gint i;

  for (i = 1; i < view->grid_rows; i++)
    {
      gint y = i * (height - 1) / view->grid_rows;

      if ((view->grid_rows % 2) == 0 && (i == view->grid_rows / 2))
        continue;

      cairo_move_to (cr, border,             border + y);
      cairo_line_to (cr, border + width - 1, border + y);
    }

  for (i = 1; i < view->grid_columns; i++)
    {
      gint x = i * (width - 1) / view->grid_columns;

      if ((view->grid_columns % 2) == 0 && (i == view->grid_columns / 2))
        continue;

      cairo_move_to (cr, border + x, border);
      cairo_line_to (cr, border + x, border + height - 1);
    }

  if (view->draw_base_line)
    {
      cairo_move_to (cr, border, border + height - 1);
      cairo_line_to (cr, border + width - 1, border);
    }

  cairo_set_line_width (cr, 0.6);
  cairo_stroke (cr);

  if ((view->grid_rows % 2) == 0)
    {
      gint y = (height - 1) / 2;

      cairo_move_to (cr, border,             border + y);
      cairo_line_to (cr, border + width - 1, border + y);
    }

  if ((view->grid_columns % 2) == 0)
    {
      gint x = (width - 1) / 2;

      cairo_move_to (cr, border + x, border);
      cairo_line_to (cr, border + x, border + height - 1);
    }

  cairo_set_line_width (cr, 1.0);
  cairo_stroke (cr);
}

static void
gimp_curve_view_draw_point (GimpCurveView *view,
                            cairo_t       *cr,
                            gint           i,
                            gint           width,
                            gint           height,
                            gint           border)
{
  gdouble x, y;

  gimp_curve_get_point (view->curve, i, &x, &y);

  y = 1.0 - y;

#define CIRCLE_RADIUS  3
#define DIAMOND_RADIUS (G_SQRT2 * CIRCLE_RADIUS)

  switch (gimp_curve_get_point_type (view->curve, i))
    {
    case GIMP_CURVE_POINT_SMOOTH:
      cairo_move_to (cr,
                     border + (gdouble) (width  - 1) * x + CIRCLE_RADIUS,
                     border + (gdouble) (height - 1) * y);
      cairo_arc     (cr,
                     border + (gdouble) (width  - 1) * x,
                     border + (gdouble) (height - 1) * y,
                     CIRCLE_RADIUS,
                     0, 2 * G_PI);
      break;

    case GIMP_CURVE_POINT_CORNER:
      cairo_move_to    (cr,
                        border + (gdouble) (width  - 1) * x,
                        border + (gdouble) (height - 1) * y - DIAMOND_RADIUS);
      cairo_line_to    (cr,
                        border + (gdouble) (width  - 1) * x + DIAMOND_RADIUS,
                        border + (gdouble) (height - 1) * y);
      cairo_line_to    (cr,
                        border + (gdouble) (width  - 1) * x,
                        border + (gdouble) (height - 1) * y + DIAMOND_RADIUS);
      cairo_line_to    (cr,
                        border + (gdouble) (width  - 1) * x - DIAMOND_RADIUS,
                        border + (gdouble) (height - 1) * y);
      cairo_close_path (cr);
      break;
    }
}

static void
gimp_curve_view_draw_curve (GimpCurveView *view,
                            cairo_t       *cr,
                            GimpCurve     *curve,
                            gint           width,
                            gint           height,
                            gint           border)
{
  gdouble x, y;
  gint    i;

  x = 0.0;
  y = 1.0 - gimp_curve_map_value (curve, 0.0);

  cairo_move_to (cr,
                 border + (gdouble) (width  - 1) * x,
                 border + (gdouble) (height - 1)* y);

  for (i = 1; i < 256; i++)
    {
      x = (gdouble) i / 255.0;
      y = 1.0 - gimp_curve_map_value (curve, x);

      cairo_line_to (cr,
                     border + (gdouble) (width  - 1) * x,
                     border + (gdouble) (height - 1) * y);
    }

  cairo_stroke (cr);
}

static gboolean
gimp_curve_view_expose (GtkWidget      *widget,
                        GdkEventExpose *event)
{
  GimpCurveView *view   = GIMP_CURVE_VIEW (widget);
  GdkWindow     *window = gtk_widget_get_window (widget);
  GtkStyle      *style  = gtk_widget_get_style (widget);
  GtkAllocation  allocation;
  cairo_t       *cr;
  GList         *list;
  gint           border;
  gint           width;
  gint           height;
  gint           layout_x;
  gint           layout_y;
  gdouble        x, y;
  gint           i;

  GTK_WIDGET_CLASS (parent_class)->expose_event (widget, event);

  if (! view->curve)
    return FALSE;

  gtk_widget_get_allocation (widget, &allocation);

  border = GIMP_HISTOGRAM_VIEW (view)->border_width;
  width  = allocation.width  - 2 * border;
  height = allocation.height - 2 * border;

  cr = gdk_cairo_create (gtk_widget_get_window (widget));

  gdk_cairo_region (cr, event->region);
  cairo_clip (cr);

  if (gtk_widget_has_focus (widget))
    {
      gtk_paint_focus (style, window,
                       gtk_widget_get_state (widget),
                       &event->area, widget, NULL,
                       border - 2, border - 2,
                       width + 4, height + 4);
    }

  cairo_set_line_width (cr, 1.0);
  cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
  cairo_translate (cr, 0.5, 0.5);

  /*  Draw the grid lines  */
  gdk_cairo_set_source_color (cr, &style->text_aa[GTK_STATE_NORMAL]);

  gimp_curve_view_draw_grid (view, cr, width, height, border);

  /*  Draw the axis labels  */

  if (view->x_axis_label)
    {
      if (! view->layout)
        view->layout = gtk_widget_create_pango_layout (widget, NULL);

      pango_layout_set_text (view->layout, view->x_axis_label, -1);
      pango_layout_get_pixel_size (view->layout, &layout_x, &layout_y);

      cairo_move_to (cr,
                     width - border - layout_x,
                     height - border - layout_y);

      gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_NORMAL]);
      pango_cairo_show_layout (cr, view->layout);
    }

  if (view->y_axis_label)
    {
      if (! view->layout)
        view->layout = gtk_widget_create_pango_layout (widget, NULL);

      pango_layout_set_text (view->layout, view->y_axis_label, -1);
      pango_layout_get_pixel_size (view->layout, &layout_x, &layout_y);

      cairo_save (cr);

      cairo_move_to (cr,
                     2 * border,
                     2 * border + layout_x);
      cairo_rotate (cr, - G_PI / 2);

      gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_NORMAL]);
      pango_cairo_show_layout (cr, view->layout);

      cairo_restore (cr);
    }


  /*  Draw the background curves  */
  for (list = view->bg_curves; list; list = g_list_next (list))
    {
      BGCurve *bg = list->data;

      if (bg->color_set)
        {
          cairo_set_source_rgba (cr,
                                 bg->color.r,
                                 bg->color.g,
                                 bg->color.b,
                                 0.5);
        }
      else
        {
          cairo_set_source_rgba (cr,
                                 style->text[GTK_STATE_NORMAL].red / 65535.0,
                                 style->text[GTK_STATE_NORMAL].green / 65535.0,
                                 style->text[GTK_STATE_NORMAL].blue / 65535.0,
                                 0.5);
        }

      gimp_curve_view_draw_curve (view, cr, bg->curve,
                                  width, height, border);
    }

  /*  Draw the curve  */
  if (view->curve_color)
    gimp_cairo_set_source_rgb (cr, view->curve_color);
  else
    gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_NORMAL]);

  gimp_curve_view_draw_curve (view, cr, view->curve,
                              width, height, border);

  /*  Draw the points  */
  if (gimp_curve_get_curve_type (view->curve) == GIMP_CURVE_SMOOTH)
    {
      gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_NORMAL]);

      /*  Draw the unselected points  */
      for (i = 0; i < view->curve->n_points; i++)
        {
          if (i == view->selected)
            continue;

          gimp_curve_view_draw_point (view, cr, i, width, height, border);
        }

      cairo_stroke (cr);

      /*  Draw the selected point  */
      if (view->selected != -1)
        {
          gimp_curve_view_draw_point (view, cr, view->selected,
                                      width, height, border);
          cairo_fill (cr);
       }
    }

  if (view->xpos >= 0.0)
    {
      gchar buf[32];

      gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_NORMAL]);

      /* draw the color line */
      cairo_move_to (cr,
                     border + ROUND ((gdouble) (width - 1) * view->xpos),
                     border + 1);
      cairo_line_to (cr,
                     border + ROUND ((gdouble) (width - 1) * view->xpos),
                     border + height - 1);
      cairo_stroke (cr);

      if (view->range_x_max == 255.0)
        {
          /*  stupid heuristic: special-case for 0..255  */

          g_snprintf (buf, sizeof (buf), "x:%3d",
                      (gint) (view->xpos *
                              (view->range_x_max - view->range_x_min) +
                              view->range_x_min));
        }
      else if (view->range_x_max == 100.0)
        {
          /*  and for 0..100  */

          g_snprintf (buf, sizeof (buf), "x:%0.2f",
                      view->xpos *
                      (view->range_x_max - view->range_x_min) +
                      view->range_x_min);
        }
      else
        {
          g_snprintf (buf, sizeof (buf), "x:%0.3f",
                      view->xpos *
                      (view->range_x_max - view->range_x_min) +
                      view->range_x_min);
        }

      if (! view->layout)
        view->layout = gtk_widget_create_pango_layout (widget, NULL);

      pango_layout_set_text (view->layout, buf, -1);
      pango_layout_get_pixel_size (view->layout, &layout_x, &layout_y);

      if (view->xpos < 0.5)
        layout_x = border;
      else
        layout_x = -(layout_x + border);

      cairo_move_to (cr,
                     border + (gdouble) width * view->xpos + layout_x,
                     border + height - border - layout_y);
      pango_cairo_show_layout (cr, view->layout);
    }

  if (view->cursor_x >= 0.0 && view->cursor_x <= 1.0 &&
      view->cursor_y >= 0.0 && view->cursor_y <= 1.0)
    {
      gchar  buf[32];
      gint   w, h;

      if (! view->cursor_layout)
        view->cursor_layout = gtk_widget_create_pango_layout (widget, NULL);

      if (view->range_x_max == 255.0 &&
          view->range_y_max == 255.0)
        {
          /*  stupid heuristic: special-case for 0..255  */

          g_snprintf (buf, sizeof (buf), "x:%3d y:%3d",
                      (gint) round (view->cursor_x *
                                    (view->range_x_max - view->range_x_min) +
                                    view->range_x_min),
                      (gint) round ((1.0 - view->cursor_y) *
                                    (view->range_y_max - view->range_y_min) +
                                    view->range_y_min));
        }
      else if (view->range_x_max == 100.0 &&
               view->range_y_max == 100.0)
        {
          /*  and for 0..100  */

          g_snprintf (buf, sizeof (buf), "x:%0.2f y:%0.2f",
                      view->cursor_x *
                      (view->range_x_max - view->range_x_min) +
                      view->range_x_min,
                      (1.0 - view->cursor_y) *
                      (view->range_y_max - view->range_y_min) +
                      view->range_y_min);
        }
      else
        {
          g_snprintf (buf, sizeof (buf), "x:%0.3f y:%0.3f",
                      view->cursor_x *
                      (view->range_x_max - view->range_x_min) +
                      view->range_x_min,
                      (1.0 - view->cursor_y) *
                      (view->range_y_max - view->range_y_min) +
                      view->range_y_min);
        }

      pango_layout_set_text (view->cursor_layout, buf, -1);
      pango_layout_get_pixel_extents (view->cursor_layout,
                                      NULL, &view->cursor_rect);

      x = border * 2 + 3;
      y = border * 2 + 3;
      w = view->cursor_rect.width;
      h = view->cursor_rect.height;

      if (view->x_axis_label)
        x += border + view->cursor_rect.height; /* coincidentially the right value */

      cairo_push_group (cr);

      gdk_cairo_set_source_color (cr, &style->text[GTK_STATE_NORMAL]);
      cairo_rectangle (cr, x + 0.5, y + 0.5, w, h);
      cairo_fill_preserve (cr);

      cairo_set_line_width (cr, 6);
      cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
      cairo_stroke (cr);

      gdk_cairo_set_source_color (cr, &style->base[GTK_STATE_NORMAL]);
      cairo_move_to (cr, x, y);
      pango_cairo_show_layout (cr, view->cursor_layout);

      cairo_pop_group_to_source (cr);
      cairo_paint_with_alpha (cr, 0.6);
    }

  cairo_destroy (cr);

  return FALSE;
}

static void
set_cursor (GimpCurveView *view,
            GdkCursorType  new_cursor)
{
  if (new_cursor != view->cursor_type)
    {
      GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (view));
      GdkCursor  *cursor  = gdk_cursor_new_for_display (display, new_cursor);

      gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (view)), cursor);
      gdk_cursor_unref (cursor);

      view->cursor_type = new_cursor;
    }
}

static gboolean
gimp_curve_view_button_press (GtkWidget      *widget,
                              GdkEventButton *bevent)
{
  GimpCurveView *view  = GIMP_CURVE_VIEW (widget);
  GimpCurve     *curve = view->curve;
  GtkAllocation  allocation;
  gint           border;
  gint           width, height;
  gdouble        x;
  gdouble        y;
  gint           point;
  gdouble        point_x;
  gdouble        point_y;

  if (! curve || bevent->button != 1)
    return TRUE;

  gtk_widget_get_allocation (widget, &allocation);

  border = GIMP_HISTOGRAM_VIEW (view)->border_width;
  width  = allocation.width  - 2 * border;
  height = allocation.height - 2 * border;

  x = (gdouble) (bevent->x - border) / (gdouble) width;
  y = (gdouble) (bevent->y - border) / (gdouble) height;

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

  view->grabbed = TRUE;

  view->orig_curve = GIMP_CURVE (gimp_data_duplicate (GIMP_DATA (curve)));

  set_cursor (view, GDK_TCROSS);

  switch (gimp_curve_get_curve_type (curve))
    {
    case GIMP_CURVE_SMOOTH:
      point = gimp_curve_get_closest_point (curve, x, 1.0 - y,
                                            POINT_MAX_DISTANCE /
                                            MAX (width, height));

      if (point < 0)
        {
          GimpCurvePointType type = GIMP_CURVE_POINT_SMOOTH;

          if (bevent->state & gimp_get_constrain_behavior_mask ())
            y = 1.0 - gimp_curve_map_value (view->orig_curve, x);

          if (view->selected >= 0)
            type = gimp_curve_get_point_type (curve, view->selected);

          point = gimp_curve_add_point (curve, x, 1.0 - y);

          gimp_curve_set_point_type (curve, point, type);
        }

      if (point > 0)
        gimp_curve_get_point (curve, point - 1, &view->leftmost, NULL);
      else
        view->leftmost = -1.0;

      if (point < gimp_curve_get_n_points (curve) - 1)
        gimp_curve_get_point (curve, point + 1, &view->rightmost, NULL);
      else
        view->rightmost = 2.0;

      gimp_curve_view_set_selected (view, point);

      gimp_curve_get_point (curve, point, &point_x, &point_y);

      view->offset_x = point_x         - x;
      view->offset_y = (1.0 - point_y) - y;

      view->point_type = gimp_curve_get_point_type (curve, point);
      break;

    case GIMP_CURVE_FREE:
      view->last_x = x;
      view->last_y = y;

      gimp_curve_set_curve (curve, x, 1.0 - y);
      break;
    }

  if (! gtk_widget_has_focus (widget))
    gtk_widget_grab_focus (widget);

  return TRUE;
}

static gboolean
gimp_curve_view_button_release (GtkWidget      *widget,
                                GdkEventButton *bevent)
{
  GimpCurveView *view = GIMP_CURVE_VIEW (widget);

  if (bevent->button != 1)
    return TRUE;

  g_clear_object (&view->orig_curve);

  view->offset_x = 0.0;
  view->offset_y = 0.0;

  view->grabbed = FALSE;

  set_cursor (view, GDK_FLEUR);

  return TRUE;
}

static gboolean
gimp_curve_view_motion_notify (GtkWidget      *widget,
                               GdkEventMotion *mevent)
{
  GimpCurveView  *view       = GIMP_CURVE_VIEW (widget);
  GimpCurve      *curve      = view->curve;
  GtkAllocation   allocation;
  GdkCursorType   new_cursor = GDK_X_CURSOR;
  gint            border;
  gint            width, height;
  gdouble         x;
  gdouble         y;
  gdouble         point_x;
  gdouble         point_y;
  gint            point;

  if (! curve)
    return TRUE;

  gtk_widget_get_allocation (widget, &allocation);

  border = GIMP_HISTOGRAM_VIEW (view)->border_width;
  width  = allocation.width  - 2 * border;
  height = allocation.height - 2 * border;

  x = (gdouble) (mevent->x - border) / (gdouble) width;
  y = (gdouble) (mevent->y - border) / (gdouble) height;

  x += view->offset_x;
  y += view->offset_y;

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

  switch (gimp_curve_get_curve_type (curve))
    {
    case GIMP_CURVE_SMOOTH:
      if (! view->grabbed) /*  If no point is grabbed...  */
        {
          point = gimp_curve_get_closest_point (curve, x, 1.0 - y,
                                                POINT_MAX_DISTANCE /
                                                MAX (width, height));

          if (point >= 0)
            {
              gimp_curve_get_point (curve, point, &point_x, &point_y);

              new_cursor = GDK_FLEUR;

              x = point_x;
              y = 1.0 - point_y;
            }
          else
            {
              new_cursor = GDK_TCROSS;

              if (mevent->state & gimp_get_constrain_behavior_mask ())
                y = 1.0 - gimp_curve_map_value (view->curve, x);
            }
        }
      else /*  Else, drag the grabbed point  */
        {
          new_cursor = GDK_TCROSS;

          if (mevent->state & gimp_get_constrain_behavior_mask ())
            y = 1.0 - gimp_curve_map_value (view->orig_curve, x);

          gimp_data_freeze (GIMP_DATA (curve));

          if (x > view->leftmost && x < view->rightmost)
            {
              if (view->selected < 0)
                {
                  gimp_curve_view_set_selected (
                    view,
                    gimp_curve_add_point (curve, x, 1.0 - y));

                  gimp_curve_set_point_type (curve,
                                             view->selected, view->point_type);
                }
              else
                {
                  gimp_curve_set_point (curve, view->selected, x, 1.0 - y);
                }
            }
          else
            {
              if (view->selected >= 0)
                {
                  gimp_curve_delete_point (curve, view->selected);

                  gimp_curve_view_set_selected (view, -1);
                }
            }

          gimp_data_thaw (GIMP_DATA (curve));
        }
      break;

    case GIMP_CURVE_FREE:
      if (view->grabbed)
        {
          gint    n_samples = gimp_curve_get_n_samples (curve);
          gdouble x1, x2;
          gdouble y1, y2;

          if (view->last_x > x)
            {
              x1 = x;
              x2 = view->last_x;
              y1 = y;
              y2 = view->last_y;
            }
          else
            {
              x1 = view->last_x;
              x2 = x;
              y1 = view->last_y;
              y2 = y;
            }

          if (x2 != x1)
            {
              gint from = ROUND (x1 * (gdouble) (n_samples - 1));
              gint to   = ROUND (x2 * (gdouble) (n_samples - 1));
              gint i;

              gimp_data_freeze (GIMP_DATA (curve));

              for (i = from; i <= to; i++)
                {
                  gdouble xpos = (gdouble) i / (gdouble) (n_samples - 1);
                  gdouble ypos = (y1 + ((y2 - y1) * (xpos - x1)) / (x2 - x1));

                  xpos = CLAMP (xpos, 0.0, 1.0);
                  ypos = CLAMP (ypos, 0.0, 1.0);

                  gimp_curve_set_curve (curve, xpos, 1.0 - ypos);
                }

              gimp_data_thaw (GIMP_DATA (curve));
            }
          else
            {
              gimp_curve_set_curve (curve, x, 1.0 - y);
            }

          view->last_x = x;
          view->last_y = y;
        }

      if (mevent->state & GDK_BUTTON1_MASK)
        new_cursor = GDK_TCROSS;
      else
        new_cursor = GDK_PENCIL;

      break;
    }

  set_cursor (view, new_cursor);

  gimp_curve_view_set_cursor (view, x, y);

  return TRUE;
}

static gboolean
gimp_curve_view_leave_notify (GtkWidget        *widget,
                              GdkEventCrossing *cevent)
{
  GimpCurveView *view = GIMP_CURVE_VIEW (widget);

  gimp_curve_view_unset_cursor (view);

  return TRUE;
}

static gboolean
gimp_curve_view_key_press (GtkWidget   *widget,
                           GdkEventKey *kevent)
{
  GimpCurveView *view    = GIMP_CURVE_VIEW (widget);
  GimpCurve     *curve   = view->curve;
  gboolean       handled = FALSE;

  if (! view->grabbed                                        &&
      curve                                                  &&
      gimp_curve_get_curve_type (curve) == GIMP_CURVE_SMOOTH &&
      view->selected                    >= 0)
    {
      gint    i = view->selected;
      gdouble x, y;

      gimp_curve_get_point (curve, i, NULL, &y);

      switch (kevent->keyval)
        {
        case GDK_KEY_Left:
          for (i = i - 1; i >= 0 && ! handled; i--)
            {
              gimp_curve_get_point (curve, i, &x, NULL);

              if (x >= 0.0)
                {
                  gimp_curve_view_set_selected (view, i);

                  handled = TRUE;
                }
            }
          break;

        case GDK_KEY_Right:
          for (i = i + 1; i < curve->n_points && ! handled; i++)
            {
              gimp_curve_get_point (curve, i, &x, NULL);

              if (x >= 0.0)
                {
                  gimp_curve_view_set_selected (view, i);

                  handled = TRUE;
                }
            }
          break;

        case GDK_KEY_Up:
          if (y < 1.0)
            {
              y = y + (kevent->state & GDK_SHIFT_MASK ?
                       (16.0 / 255.0) : (1.0 / 255.0));

              gimp_curve_move_point (curve, i, CLAMP (y, 0.0, 1.0));

              handled = TRUE;
            }
          break;

        case GDK_KEY_Down:
          if (y > 0)
            {
              y = y - (kevent->state & GDK_SHIFT_MASK ?
                       (16.0 / 255.0) : (1.0 / 255.0));

              gimp_curve_move_point (curve, i, CLAMP (y, 0.0, 1.0));

              handled = TRUE;
            }
          break;

        case GDK_KEY_Delete:
          gimp_curve_delete_point (curve, i);
          break;

        default:
          break;
        }
    }

  if (handled)
    {
      set_cursor (view, GDK_TCROSS);

      return TRUE;
    }

  return GTK_WIDGET_CLASS (parent_class)->key_press_event (widget, kevent);
}

static void
gimp_curve_view_cut_clipboard (GimpCurveView *view)
{
  g_printerr ("%s\n", G_STRFUNC);

  if (! view->curve || ! view->gimp)
    {
      gtk_widget_error_bell (GTK_WIDGET (view));
      return;
    }

  gimp_curve_view_copy_clipboard (view);

  gimp_curve_reset (view->curve, FALSE);
}

static void
gimp_curve_view_copy_clipboard (GimpCurveView *view)
{
  GimpCurve *copy;

  g_printerr ("%s\n", G_STRFUNC);

  if (! view->curve || ! view->gimp)
    {
      gtk_widget_error_bell (GTK_WIDGET (view));
      return;
    }

  copy = GIMP_CURVE (gimp_data_duplicate (GIMP_DATA (view->curve)));
  gimp_clipboard_set_curve (view->gimp, copy);
  g_object_unref (copy);
}

static void
gimp_curve_view_paste_clipboard (GimpCurveView *view)
{
  GimpCurve *copy;

  g_printerr ("%s\n", G_STRFUNC);

  if (! view->curve || ! view->gimp)
    {
      gtk_widget_error_bell (GTK_WIDGET (view));
      return;
    }

  copy = gimp_clipboard_get_curve (view->gimp);

  if (copy)
    {
      gimp_config_copy (GIMP_CONFIG (copy),
                        GIMP_CONFIG (view->curve), 0);
      g_object_unref (copy);
    }
}

static void
gimp_curve_view_curve_dirty (GimpCurve     *curve,
                             GimpCurveView *view)
{
  gtk_widget_queue_draw (GTK_WIDGET (view));
}

static void
gimp_curve_view_curve_notify_n_points (GimpCurve     *curve,
                                       GParamSpec    *pspec,
                                       GimpCurveView *view)
{
  gimp_curve_view_set_selected (view, -1);
}


/*  public functions  */

GtkWidget *
gimp_curve_view_new (void)
{
  return g_object_new (GIMP_TYPE_CURVE_VIEW, NULL);
}

void
gimp_curve_view_set_curve (GimpCurveView *view,
                           GimpCurve     *curve,
                           const GimpRGB *color)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));
  g_return_if_fail (curve == NULL || GIMP_IS_CURVE (curve));

  if (view->curve == curve)
    return;

  if (view->curve)
    {
      g_signal_handlers_disconnect_by_func (view->curve,
                                            gimp_curve_view_curve_dirty,
                                            view);
      g_signal_handlers_disconnect_by_func (view->curve,
                                            gimp_curve_view_curve_notify_n_points,
                                            view);
      g_object_unref (view->curve);
    }

  view->curve = curve;

  if (view->curve)
    {
      g_object_ref (view->curve);
      g_signal_connect (view->curve, "dirty",
                        G_CALLBACK (gimp_curve_view_curve_dirty),
                        view);
      g_signal_connect (view->curve, "notify::n-points",
                        G_CALLBACK (gimp_curve_view_curve_notify_n_points),
                        view);
    }

  if (view->curve_color)
    g_free (view->curve_color);

  if (color)
    view->curve_color = g_memdup (color, sizeof (GimpRGB));
  else
    view->curve_color = NULL;

  gimp_curve_view_set_selected (view, -1);

  gtk_widget_queue_draw (GTK_WIDGET (view));
}

GimpCurve *
gimp_curve_view_get_curve (GimpCurveView *view)
{
  g_return_val_if_fail (GIMP_IS_CURVE_VIEW (view), NULL);

  return view->curve;
}

void
gimp_curve_view_add_background (GimpCurveView *view,
                                GimpCurve     *curve,
                                const GimpRGB *color)
{
  GList   *list;
  BGCurve *bg;

  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));
  g_return_if_fail (GIMP_IS_CURVE (curve));

  for (list = view->bg_curves; list; list = g_list_next (list))
    {
      bg = list->data;

      g_return_if_fail (curve != bg->curve);
    }

  bg = g_slice_new0 (BGCurve);

  bg->curve = g_object_ref (curve);

  if (color)
    {
      bg->color     = *color;
      bg->color_set = TRUE;
    }

  g_signal_connect (bg->curve, "dirty",
                    G_CALLBACK (gimp_curve_view_curve_dirty),
                    view);

  view->bg_curves = g_list_append (view->bg_curves, bg);

  gtk_widget_queue_draw (GTK_WIDGET (view));
}

void
gimp_curve_view_remove_background (GimpCurveView *view,
                                   GimpCurve     *curve)
{
  GList *list;

  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));
  g_return_if_fail (GIMP_IS_CURVE (curve));

  for (list = view->bg_curves; list; list = g_list_next (list))
    {
      BGCurve *bg = list->data;

      if (bg->curve == curve)
        {
          g_signal_handlers_disconnect_by_func (bg->curve,
                                                gimp_curve_view_curve_dirty,
                                                view);
          g_object_unref (bg->curve);

          view->bg_curves = g_list_remove (view->bg_curves, bg);

          g_slice_free (BGCurve, bg);

          gtk_widget_queue_draw (GTK_WIDGET (view));

          break;
        }
    }

  if (! list)
    g_return_if_reached ();
}

void
gimp_curve_view_remove_all_backgrounds (GimpCurveView *view)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));

  while (view->bg_curves)
    {
      BGCurve *bg = view->bg_curves->data;

      g_signal_handlers_disconnect_by_func (bg->curve,
                                            gimp_curve_view_curve_dirty,
                                            view);
      g_object_unref (bg->curve);

      view->bg_curves = g_list_remove (view->bg_curves, bg);

      g_slice_free (BGCurve, bg);
    }

  gtk_widget_queue_draw (GTK_WIDGET (view));
}

void
gimp_curve_view_set_selected (GimpCurveView *view,
                              gint           selected)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));

  if (selected != view->selected)
    {
      view->selected = selected;

      g_signal_emit (view, curve_view_signals[SELECTION_CHANGED], 0);

      gtk_widget_queue_draw (GTK_WIDGET (view));
    }
}

gint
gimp_curve_view_get_selected (GimpCurveView *view)
{
  g_return_val_if_fail (GIMP_IS_CURVE_VIEW (view), -1);

  if (view->curve && view->selected < gimp_curve_get_n_points (view->curve))
    return view->selected;
  else
    return -1;
}

void
gimp_curve_view_set_range_x (GimpCurveView *view,
                             gdouble        min,
                             gdouble        max)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));

  view->range_x_min = min;
  view->range_x_max = max;

  gtk_widget_queue_draw (GTK_WIDGET (view));
}

void
gimp_curve_view_set_range_y (GimpCurveView *view,
                             gdouble        min,
                             gdouble        max)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));

  view->range_y_min = min;
  view->range_y_max = max;

  gtk_widget_queue_draw (GTK_WIDGET (view));
}

void
gimp_curve_view_set_xpos (GimpCurveView *view,
                          gdouble        x)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));

  view->xpos = x;

  gtk_widget_queue_draw (GTK_WIDGET (view));
}

void
gimp_curve_view_set_x_axis_label (GimpCurveView *view,
                                  const gchar   *label)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));

  if (view->x_axis_label)
    g_free (view->x_axis_label);

  view->x_axis_label = g_strdup (label);

  g_object_notify (G_OBJECT (view), "x-axis-label");

  gtk_widget_queue_draw (GTK_WIDGET (view));
}

void
gimp_curve_view_set_y_axis_label (GimpCurveView *view,
                                  const gchar   *label)
{
  g_return_if_fail (GIMP_IS_CURVE_VIEW (view));

  if (view->y_axis_label)
    g_free (view->y_axis_label);

  view->y_axis_label = g_strdup (label);

  g_object_notify (G_OBJECT (view), "y-axis-label");

  gtk_widget_queue_draw (GTK_WIDGET (view));
}


/*  private functions  */

static void
gimp_curve_view_set_cursor (GimpCurveView *view,
                            gdouble        x,
                            gdouble        y)
{
  view->cursor_x = x;
  view->cursor_y = y;

  /* TODO: only invalidate the cursor label area */
  gtk_widget_queue_draw (GTK_WIDGET (view));
}

static void
gimp_curve_view_unset_cursor (GimpCurveView *view)
{
  view->cursor_x = -1.0;
  view->cursor_y = -1.0;

  /* TODO: only invalidate the cursor label area */
  gtk_widget_queue_draw (GTK_WIDGET (view));
}