/* 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
 *
 * Major improvements for interactivity
 * Copyright (C) 2014 Michael Henning <drawoc@darkrefraction.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

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

#include "libpikawidgets/pikawidgets.h"

#include "tools-types.h"

#include "config/pikaguiconfig.h"

#include "gegl/pika-gegl-utils.h"

#include "operations/pika-operation-config.h"

#include "operations/layer-modes/pika-layer-modes.h"

#include "core/pika.h"
#include "core/pikadrawable.h"
#include "core/pikadrawable-gradient.h"
#include "core/pikadrawablefilter.h"
#include "core/pikaerror.h"
#include "core/pikagradient.h"
#include "core/pikaimage.h"
#include "core/pikaprogress.h"
#include "core/pikaprojection.h"

#include "widgets/pikahelp-ids.h"
#include "widgets/pikawidgets-utils.h"

#include "display/pikadisplay.h"
#include "display/pikatoolline.h"

#include "pikagradientoptions.h"
#include "pikagradienttool.h"
#include "pikagradienttool-editor.h"
#include "pikatoolcontrol.h"
#include "pikatools-utils.h"

#include "pika-intl.h"


/*  local function prototypes  */

static void   pika_gradient_tool_dispose             (GObject               *object);

static gboolean pika_gradient_tool_initialize        (PikaTool              *tool,
                                                      PikaDisplay           *display,
                                                      GError               **error);
static void   pika_gradient_tool_control             (PikaTool              *tool,
                                                      PikaToolAction         action,
                                                      PikaDisplay           *display);
static void   pika_gradient_tool_button_press        (PikaTool              *tool,
                                                      const PikaCoords      *coords,
                                                      guint32                time,
                                                      GdkModifierType        state,
                                                      PikaButtonPressType    press_type,
                                                      PikaDisplay           *display);
static void   pika_gradient_tool_button_release      (PikaTool              *tool,
                                                      const PikaCoords      *coords,
                                                      guint32                time,
                                                      GdkModifierType        state,
                                                      PikaButtonReleaseType  release_type,
                                                      PikaDisplay           *display);
static void   pika_gradient_tool_motion              (PikaTool              *tool,
                                                      const PikaCoords      *coords,
                                                      guint32                time,
                                                      GdkModifierType        state,
                                                      PikaDisplay           *display);
static gboolean pika_gradient_tool_key_press         (PikaTool              *tool,
                                                      GdkEventKey           *kevent,
                                                      PikaDisplay           *display);
static void   pika_gradient_tool_modifier_key        (PikaTool              *tool,
                                                      GdkModifierType        key,
                                                      gboolean               press,
                                                      GdkModifierType        state,
                                                      PikaDisplay           *display);
static void   pika_gradient_tool_cursor_update       (PikaTool              *tool,
                                                      const PikaCoords      *coords,
                                                      GdkModifierType        state,
                                                      PikaDisplay           *display);
static const gchar * pika_gradient_tool_can_undo     (PikaTool              *tool,
                                                      PikaDisplay           *display);
static const gchar * pika_gradient_tool_can_redo     (PikaTool              *tool,
                                                      PikaDisplay           *display);
static gboolean  pika_gradient_tool_undo             (PikaTool              *tool,
                                                      PikaDisplay           *display);
static gboolean  pika_gradient_tool_redo             (PikaTool              *tool,
                                                      PikaDisplay           *display);
static void   pika_gradient_tool_options_notify      (PikaTool              *tool,
                                                      PikaToolOptions       *options,
                                                      const GParamSpec      *pspec);

static void   pika_gradient_tool_start               (PikaGradientTool         *gradient_tool,
                                                      const PikaCoords      *coords,
                                                      PikaDisplay           *display);
static void   pika_gradient_tool_halt                (PikaGradientTool         *gradient_tool);
static void   pika_gradient_tool_commit              (PikaGradientTool         *gradient_tool);

static void   pika_gradient_tool_line_changed        (PikaToolWidget        *widget,
                                                      PikaGradientTool         *gradient_tool);
static void   pika_gradient_tool_line_response       (PikaToolWidget        *widget,
                                                      gint                   response_id,
                                                      PikaGradientTool         *gradient_tool);

static void   pika_gradient_tool_precalc_shapeburst  (PikaGradientTool         *gradient_tool);

static void   pika_gradient_tool_create_graph        (PikaGradientTool         *gradient_tool);
static void   pika_gradient_tool_update_graph        (PikaGradientTool         *gradient_tool);

static void   pika_gradient_tool_fg_bg_changed       (PikaGradientTool         *gradient_tool);

static void   pika_gradient_tool_gradient_dirty      (PikaGradientTool         *gradient_tool);
static void   pika_gradient_tool_set_gradient        (PikaGradientTool         *gradient_tool,
                                                      PikaGradient          *gradient);

static gboolean pika_gradient_tool_is_shapeburst     (PikaGradientTool         *gradient_tool);

static void   pika_gradient_tool_create_filter       (PikaGradientTool         *gradient_tool,
                                                      PikaDrawable          *drawable);
static void   pika_gradient_tool_filter_flush        (PikaDrawableFilter    *filter,
                                                      PikaTool              *tool);


G_DEFINE_TYPE (PikaGradientTool, pika_gradient_tool, PIKA_TYPE_DRAW_TOOL)

#define parent_class pika_gradient_tool_parent_class


void
pika_gradient_tool_register (PikaToolRegisterCallback  callback,
                             gpointer                  data)
{
  (* callback) (PIKA_TYPE_GRADIENT_TOOL,
                PIKA_TYPE_GRADIENT_OPTIONS,
                pika_gradient_options_gui,
                PIKA_CONTEXT_PROP_MASK_FOREGROUND |
                PIKA_CONTEXT_PROP_MASK_BACKGROUND |
                PIKA_CONTEXT_PROP_MASK_OPACITY    |
                PIKA_CONTEXT_PROP_MASK_PAINT_MODE |
                PIKA_CONTEXT_PROP_MASK_GRADIENT,
                "pika-gradient-tool",
                _("Gradient"),
                _("Gradient Tool: Fill selected area with a color gradient"),
                N_("Gra_dient"), "G",
                NULL, PIKA_HELP_TOOL_GRADIENT,
                PIKA_ICON_TOOL_GRADIENT,
                data);
}

static void
pika_gradient_tool_class_init (PikaGradientToolClass *klass)
{
  GObjectClass  *object_class = G_OBJECT_CLASS (klass);
  PikaToolClass *tool_class   = PIKA_TOOL_CLASS (klass);

  object_class->dispose      = pika_gradient_tool_dispose;

  tool_class->initialize     = pika_gradient_tool_initialize;
  tool_class->control        = pika_gradient_tool_control;
  tool_class->button_press   = pika_gradient_tool_button_press;
  tool_class->button_release = pika_gradient_tool_button_release;
  tool_class->motion         = pika_gradient_tool_motion;
  tool_class->key_press      = pika_gradient_tool_key_press;
  tool_class->modifier_key   = pika_gradient_tool_modifier_key;
  tool_class->cursor_update  = pika_gradient_tool_cursor_update;
  tool_class->can_undo       = pika_gradient_tool_can_undo;
  tool_class->can_redo       = pika_gradient_tool_can_redo;
  tool_class->undo           = pika_gradient_tool_undo;
  tool_class->redo           = pika_gradient_tool_redo;
  tool_class->options_notify = pika_gradient_tool_options_notify;
}

static void
pika_gradient_tool_init (PikaGradientTool *gradient_tool)
{
  PikaTool *tool = PIKA_TOOL (gradient_tool);

  pika_tool_control_set_scroll_lock        (tool->control, TRUE);
  pika_tool_control_set_preserve           (tool->control, FALSE);
  pika_tool_control_set_dirty_mask         (tool->control,
                                            PIKA_DIRTY_IMAGE           |
                                            PIKA_DIRTY_IMAGE_STRUCTURE |
                                            PIKA_DIRTY_DRAWABLE        |
                                            PIKA_DIRTY_ACTIVE_DRAWABLE);
  pika_tool_control_set_dirty_action       (tool->control,
                                            PIKA_TOOL_ACTION_COMMIT);
  pika_tool_control_set_wants_click        (tool->control, TRUE);
  pika_tool_control_set_wants_double_click (tool->control, TRUE);
  pika_tool_control_set_active_modifiers   (tool->control,
                                            PIKA_TOOL_ACTIVE_MODIFIERS_SEPARATE);
  pika_tool_control_set_precision          (tool->control,
                                            PIKA_CURSOR_PRECISION_SUBPIXEL);
  pika_tool_control_set_tool_cursor        (tool->control,
                                            PIKA_TOOL_CURSOR_GRADIENT);
  pika_tool_control_set_action_opacity     (tool->control,
                                            "context-opacity-set");
  pika_tool_control_set_action_object_1    (tool->control,
                                            "context-gradient-select-set");

  pika_draw_tool_set_default_status (PIKA_DRAW_TOOL (tool),
                                     _("Click-Drag to draw a gradient"));
}

static void
pika_gradient_tool_dispose (GObject *object)
{
  PikaGradientTool *gradient_tool = PIKA_GRADIENT_TOOL (object);

  pika_gradient_tool_set_gradient (gradient_tool, NULL);

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

static gboolean
pika_gradient_tool_initialize (PikaTool     *tool,
                               PikaDisplay  *display,
                               GError      **error)
{
  PikaImage           *image       = pika_display_get_image (display);
  PikaGradientOptions *options     = PIKA_GRADIENT_TOOL_GET_OPTIONS (tool);
  PikaGuiConfig       *config      = PIKA_GUI_CONFIG (display->pika->config);
  PikaItem            *locked_item = NULL;
  GList               *drawables;
  PikaDrawable        *drawable;

  if (! PIKA_TOOL_CLASS (parent_class)->initialize (tool, display, error))
    {
      return FALSE;
    }

  drawables = pika_image_get_selected_drawables (image);
  if (g_list_length (drawables) != 1)
    {
      if (g_list_length (drawables) > 1)
        pika_tool_message_literal (tool, display,
                                   _("Cannot paint on multiple drawables. Select only one."));
      else
        pika_tool_message_literal (tool, display, _("No active drawables."));

      g_list_free (drawables);

      return FALSE;
    }

  /* Only a single drawable at this point. */
  drawable = drawables->data;
  g_list_free (drawables);

  if (pika_viewable_get_children (PIKA_VIEWABLE (drawable)))
    {
      g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED,
                           _("Cannot modify the pixels of layer groups."));
      return FALSE;
    }

  if (pika_item_is_content_locked (PIKA_ITEM (drawable), &locked_item))
    {
      g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED,
                           _("The selected layer's pixels are locked."));
      if (error)
        pika_tools_blink_lock_box (display->pika, locked_item);
      return FALSE;
    }

  if (! pika_item_is_visible (PIKA_ITEM (drawable)) &&
      ! config->edit_non_visible)
    {
      g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED,
                           _("The selected item is not visible."));
      return FALSE;
    }

  if (! pika_context_get_gradient (PIKA_CONTEXT (options)))
    {
      g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED,
                           _("No gradient available for use with this tool."));
      return FALSE;
    }

  return TRUE;
}

static void
pika_gradient_tool_control (PikaTool       *tool,
                            PikaToolAction  action,
                            PikaDisplay    *display)
{
  PikaGradientTool *gradient_tool = PIKA_GRADIENT_TOOL (tool);

  switch (action)
    {
    case PIKA_TOOL_ACTION_PAUSE:
    case PIKA_TOOL_ACTION_RESUME:
      break;

    case PIKA_TOOL_ACTION_HALT:
      pika_gradient_tool_halt (gradient_tool);
      break;

    case PIKA_TOOL_ACTION_COMMIT:
      pika_gradient_tool_commit (gradient_tool);
      break;
    }

  PIKA_TOOL_CLASS (parent_class)->control (tool, action, display);
}

static void
pika_gradient_tool_button_press (PikaTool            *tool,
                                 const PikaCoords    *coords,
                                 guint32              time,
                                 GdkModifierType      state,
                                 PikaButtonPressType  press_type,
                                 PikaDisplay         *display)
{
  PikaGradientTool *gradient_tool = PIKA_GRADIENT_TOOL (tool);

  if (tool->display && display != tool->display)
    pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display);

  if (! gradient_tool->widget)
    {
      pika_gradient_tool_start (gradient_tool, coords, display);

      pika_tool_widget_hover (gradient_tool->widget, coords, state, TRUE);
    }

  /* call start_edit() before widget_button_press(), because we need to record
   * the undo state before widget_button_press() potentially changes it.  note
   * that if widget_button_press() return FALSE, nothing changes and no undo
   * step is created.
   */
  if (press_type == PIKA_BUTTON_PRESS_NORMAL)
    pika_gradient_tool_editor_start_edit (gradient_tool);

  if (pika_tool_widget_button_press (gradient_tool->widget, coords, time, state,
                                     press_type))
    {
      gradient_tool->grab_widget = gradient_tool->widget;
    }

  if (press_type == PIKA_BUTTON_PRESS_NORMAL)
    pika_tool_control_activate (tool->control);
}

static void
pika_gradient_tool_button_release (PikaTool              *tool,
                                   const PikaCoords      *coords,
                                   guint32                time,
                                   GdkModifierType        state,
                                   PikaButtonReleaseType  release_type,
                                   PikaDisplay           *display)
{
  PikaGradientTool    *gradient_tool = PIKA_GRADIENT_TOOL (tool);
  PikaGradientOptions *options       = PIKA_GRADIENT_TOOL_GET_OPTIONS (tool);

  pika_tool_pop_status (tool, display);

  pika_tool_control_halt (tool->control);

  if (gradient_tool->grab_widget)
    {
      pika_tool_widget_button_release (gradient_tool->grab_widget,
                                       coords, time, state, release_type);
      gradient_tool->grab_widget = NULL;

      if (options->instant)
        {
          if (release_type == PIKA_BUTTON_RELEASE_CANCEL)
            pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, display);
          else
            pika_tool_control (tool, PIKA_TOOL_ACTION_COMMIT, display);
        }
    }

  if (! options->instant)
    {
      pika_gradient_tool_editor_end_edit (gradient_tool,
                                          release_type ==
                                          PIKA_BUTTON_RELEASE_CANCEL);
    }
}

static void
pika_gradient_tool_motion (PikaTool         *tool,
                           const PikaCoords *coords,
                           guint32           time,
                           GdkModifierType   state,
                           PikaDisplay      *display)
{
  PikaGradientTool *gradient_tool = PIKA_GRADIENT_TOOL (tool);

  if (gradient_tool->grab_widget)
    {
      pika_tool_widget_motion (gradient_tool->grab_widget, coords, time, state);
    }
}

static gboolean
pika_gradient_tool_key_press (PikaTool    *tool,
                              GdkEventKey *kevent,
                              PikaDisplay *display)
{
  PikaGradientTool *gradient_tool = PIKA_GRADIENT_TOOL (tool);
  PikaDrawTool     *draw_tool     = PIKA_DRAW_TOOL (tool);
  gboolean          result;

  /* call start_edit() before widget_key_press(), because we need to record the
   * undo state before widget_key_press() potentially changes it.  note that if
   * widget_key_press() return FALSE, nothing changes and no undo step is
   * created.
   */
  if (display == draw_tool->display)
    pika_gradient_tool_editor_start_edit (gradient_tool);

  result = PIKA_TOOL_CLASS (parent_class)->key_press (tool, kevent, display);

  if (display == draw_tool->display)
    pika_gradient_tool_editor_end_edit (gradient_tool, FALSE);

  return result;
}

static void
pika_gradient_tool_modifier_key (PikaTool        *tool,
                                 GdkModifierType  key,
                                 gboolean         press,
                                 GdkModifierType  state,
                                 PikaDisplay     *display)
{
  PikaGradientOptions *options = PIKA_GRADIENT_TOOL_GET_OPTIONS (tool);

  if (key == pika_get_extend_selection_mask ())
    {
      if (options->instant_toggle &&
          gtk_widget_get_sensitive (options->instant_toggle))
        {
          g_object_set (options,
                        "instant", ! options->instant,
                        NULL);
        }
    }
}

static void
pika_gradient_tool_cursor_update (PikaTool         *tool,
                                  const PikaCoords *coords,
                                  GdkModifierType   state,
                                  PikaDisplay      *display)
{
  PikaGuiConfig *config    = PIKA_GUI_CONFIG (display->pika->config);
  PikaImage     *image     = pika_display_get_image (display);
  GList         *drawables = pika_image_get_selected_drawables (image);

  if (g_list_length (drawables) != 1                      ||
      pika_viewable_get_children (drawables->data)        ||
      pika_item_is_content_locked (drawables->data, NULL) ||
      ! (pika_item_is_visible (drawables->data) ||
         config->edit_non_visible))
    {
      pika_tool_set_cursor (tool, display,
                            pika_tool_control_get_cursor (tool->control),
                            pika_tool_control_get_tool_cursor (tool->control),
                            PIKA_CURSOR_MODIFIER_BAD);
      g_list_free (drawables);
      return;
    }
  g_list_free (drawables);

  PIKA_TOOL_CLASS (parent_class)->cursor_update (tool, coords, state, display);
}

static const gchar *
pika_gradient_tool_can_undo (PikaTool    *tool,
                             PikaDisplay *display)
{
  return pika_gradient_tool_editor_can_undo (PIKA_GRADIENT_TOOL (tool));
}

static const gchar *
pika_gradient_tool_can_redo (PikaTool    *tool,
                             PikaDisplay *display)
{
  return pika_gradient_tool_editor_can_redo (PIKA_GRADIENT_TOOL (tool));
}

static gboolean
pika_gradient_tool_undo (PikaTool    *tool,
                         PikaDisplay *display)
{
  return pika_gradient_tool_editor_undo (PIKA_GRADIENT_TOOL (tool));
}

static gboolean
pika_gradient_tool_redo (PikaTool    *tool,
                         PikaDisplay *display)
{
  return pika_gradient_tool_editor_redo (PIKA_GRADIENT_TOOL (tool));
}

static void
pika_gradient_tool_options_notify (PikaTool         *tool,
                                   PikaToolOptions  *options,
                                   const GParamSpec *pspec)
{
  PikaContext   *context    = PIKA_CONTEXT (options);
  PikaGradientTool *gradient_tool = PIKA_GRADIENT_TOOL (tool);

  if (! strcmp (pspec->name, "gradient"))
    {
      pika_gradient_tool_set_gradient (gradient_tool, context->gradient);

      if (gradient_tool->filter)
        pika_drawable_filter_apply (gradient_tool->filter, NULL);
    }
  else if (gradient_tool->render_node &&
           gegl_node_find_property (gradient_tool->render_node, pspec->name))
    {
      /* Sync any property changes on the config object that match the op */
      GValue value = G_VALUE_INIT;

      g_value_init (&value, pspec->value_type);

      g_object_get_property (G_OBJECT (options), pspec->name, &value);
      gegl_node_set_property (gradient_tool->render_node, pspec->name, &value);

      g_value_unset (&value);

      if (! strcmp (pspec->name, "gradient-type"))
        {
          PikaRepeatMode   gradient_repeat;
          PikaRepeatMode   node_repeat;
          PikaGradientType gradient_type;

          gradient_repeat = PIKA_PAINT_OPTIONS (options)->gradient_options->gradient_repeat;
          gradient_type   = PIKA_GRADIENT_OPTIONS (options)->gradient_type;
          gegl_node_get (gradient_tool->render_node,
                         "gradient-repeat", &node_repeat,
                         NULL);

          if (gradient_type >= PIKA_GRADIENT_SHAPEBURST_ANGULAR)
            {
              /* These gradient types are only meant to work with repeat
               * value of "none" so these are the only ones where we
               * don't keep the render node and the gradient options in
               * sync.
               * We could instead reset the "gradient-repeat" value on
               * PikaGradientOptions, but I assume one would want to revert
               * back to the last set value if changing back the
               * gradient type. So instead we just make the option
               * insensitive (both in GUI and in render).
               */
              if (node_repeat != PIKA_REPEAT_NONE)
                gegl_node_set (gradient_tool->render_node,
                               "gradient-repeat", PIKA_REPEAT_NONE,
                               NULL);
            }
          else if (node_repeat != gradient_repeat)
            {
              gegl_node_set (gradient_tool->render_node,
                             "gradient-repeat", gradient_repeat,
                             NULL);
            }

          if (pika_gradient_tool_is_shapeburst (gradient_tool))
            pika_gradient_tool_precalc_shapeburst (gradient_tool);

          pika_gradient_tool_update_graph (gradient_tool);
        }

      pika_drawable_filter_apply (gradient_tool->filter, NULL);
    }
  else if (gradient_tool->render_node                    &&
           pika_gradient_tool_is_shapeburst (gradient_tool) &&
           g_strcmp0 (pspec->name, "distance-metric") == 0)
    {
      g_clear_object (&gradient_tool->dist_buffer);
      pika_gradient_tool_precalc_shapeburst (gradient_tool);
      pika_gradient_tool_update_graph (gradient_tool);
      pika_drawable_filter_apply (gradient_tool->filter, NULL);
    }
  else if (gradient_tool->filter &&
           ! strcmp (pspec->name, "opacity"))
    {
      pika_drawable_filter_set_opacity (gradient_tool->filter,
                                        pika_context_get_opacity (context));
    }
  else if (gradient_tool->filter &&
           ! strcmp (pspec->name, "paint-mode"))
    {
      pika_drawable_filter_set_mode (gradient_tool->filter,
                                     pika_context_get_paint_mode (context),
                                     PIKA_LAYER_COLOR_SPACE_AUTO,
                                     PIKA_LAYER_COLOR_SPACE_AUTO,
                                     pika_layer_mode_get_paint_composite_mode (
                                       pika_context_get_paint_mode (context)));
    }

  pika_gradient_tool_editor_options_notify (gradient_tool, options, pspec);
}

static void
pika_gradient_tool_start (PikaGradientTool *gradient_tool,
                          const PikaCoords *coords,
                          PikaDisplay      *display)
{
  PikaTool            *tool      = PIKA_TOOL (gradient_tool);
  PikaDisplayShell    *shell     = pika_display_get_shell (display);
  PikaImage           *image     = pika_display_get_image (display);
  GList               *drawables = pika_image_get_selected_drawables (image);
  PikaGradientOptions *options   = PIKA_GRADIENT_TOOL_GET_OPTIONS (gradient_tool);
  PikaContext         *context   = PIKA_CONTEXT (options);

  g_return_if_fail (g_list_length (drawables) == 1);

  if (options->instant_toggle)
    gtk_widget_set_sensitive (options->instant_toggle, FALSE);

  tool->display   = display;
  g_list_free (tool->drawables);
  tool->drawables = drawables;

  gradient_tool->start_x = coords->x;
  gradient_tool->start_y = coords->y;
  gradient_tool->end_x   = coords->x;
  gradient_tool->end_y   = coords->y;

  gradient_tool->widget = pika_tool_line_new (shell,
                                              gradient_tool->start_x,
                                              gradient_tool->start_y,
                                              gradient_tool->end_x,
                                              gradient_tool->end_y);

  g_object_set (gradient_tool->widget,
                "status-title", _("Gradient: "),
                NULL);

  pika_draw_tool_set_widget (PIKA_DRAW_TOOL (tool), gradient_tool->widget);

  g_signal_connect (gradient_tool->widget, "changed",
                    G_CALLBACK (pika_gradient_tool_line_changed),
                    gradient_tool);
  g_signal_connect (gradient_tool->widget, "response",
                    G_CALLBACK (pika_gradient_tool_line_response),
                    gradient_tool);

  g_signal_connect_swapped (context, "background-changed",
                            G_CALLBACK (pika_gradient_tool_fg_bg_changed),
                            gradient_tool);
  g_signal_connect_swapped (context, "foreground-changed",
                            G_CALLBACK (pika_gradient_tool_fg_bg_changed),
                            gradient_tool);

  pika_gradient_tool_create_filter (gradient_tool, tool->drawables->data);

  /* Initially sync all of the properties */
  pika_operation_config_sync_node (G_OBJECT (options),
                                   gradient_tool->render_node);

  /* We don't allow repeat values for some shapes. */
  if (options->gradient_type >= PIKA_GRADIENT_SHAPEBURST_ANGULAR)
    gegl_node_set (gradient_tool->render_node,
                   "gradient-repeat", PIKA_REPEAT_NONE,
                   NULL);

  /* Connect signal handlers for the gradient */
  pika_gradient_tool_set_gradient (gradient_tool, context->gradient);

  if (pika_gradient_tool_is_shapeburst (gradient_tool))
    pika_gradient_tool_precalc_shapeburst (gradient_tool);

  pika_draw_tool_start (PIKA_DRAW_TOOL (gradient_tool), display);

  pika_gradient_tool_editor_start (gradient_tool);
}

static void
pika_gradient_tool_halt (PikaGradientTool *gradient_tool)
{
  PikaTool            *tool    = PIKA_TOOL (gradient_tool);
  PikaGradientOptions *options = PIKA_GRADIENT_TOOL_GET_OPTIONS (gradient_tool);
  PikaContext         *context = PIKA_CONTEXT (options);

  pika_gradient_tool_editor_halt (gradient_tool);

  if (gradient_tool->graph)
    {
      g_clear_object (&gradient_tool->graph);
      gradient_tool->render_node    = NULL;
#if 0
      gradient_tool->subtract_node  = NULL;
      gradient_tool->divide_node    = NULL;
#endif
      gradient_tool->dist_node      = NULL;
    }

  g_clear_object (&gradient_tool->dist_buffer);

  if (gradient_tool->filter)
    {
      pika_tool_control_push_preserve (tool->control, TRUE);

      pika_drawable_filter_abort (gradient_tool->filter);
      g_object_unref (gradient_tool->filter);
      gradient_tool->filter = NULL;

      pika_tool_control_pop_preserve (tool->control);

      pika_image_flush (pika_display_get_image (tool->display));
    }

  pika_gradient_tool_set_tentative_gradient (gradient_tool, NULL);

  g_signal_handlers_disconnect_by_func (context,
                                        G_CALLBACK (pika_gradient_tool_fg_bg_changed),
                                        gradient_tool);

  if (tool->display)
    pika_tool_pop_status (tool, tool->display);

  if (pika_draw_tool_is_active (PIKA_DRAW_TOOL (gradient_tool)))
    pika_draw_tool_stop (PIKA_DRAW_TOOL (gradient_tool));

  pika_draw_tool_set_widget (PIKA_DRAW_TOOL (tool), NULL);
  g_clear_object (&gradient_tool->widget);

  tool->display   = NULL;
  g_list_free (tool->drawables);
  tool->drawables = NULL;

  if (options->instant_toggle)
    gtk_widget_set_sensitive (options->instant_toggle, TRUE);
}

static void
pika_gradient_tool_commit (PikaGradientTool *gradient_tool)
{
  PikaTool *tool = PIKA_TOOL (gradient_tool);

  if (gradient_tool->filter)
    {
      /* halt the editor before committing the filter so that the image-flush
       * idle source is removed, to avoid flushing the image, and hence
       * restarting the projection rendering, while applying the filter.
       */
      pika_gradient_tool_editor_halt (gradient_tool);

      pika_tool_control_push_preserve (tool->control, TRUE);

      pika_drawable_filter_commit (gradient_tool->filter,
                                   PIKA_PROGRESS (tool), FALSE);
      g_clear_object (&gradient_tool->filter);

      pika_tool_control_pop_preserve (tool->control);

      pika_image_flush (pika_display_get_image (tool->display));
    }
}

static void
pika_gradient_tool_line_changed (PikaToolWidget   *widget,
                                 PikaGradientTool *gradient_tool)
{
  gdouble  start_x;
  gdouble  start_y;
  gdouble  end_x;
  gdouble  end_y;
  gboolean update = FALSE;

  g_object_get (widget,
                "x1", &start_x,
                "y1", &start_y,
                "x2", &end_x,
                "y2", &end_y,
                NULL);

  if (start_x != gradient_tool->start_x ||
      start_y != gradient_tool->start_y ||
      end_x   != gradient_tool->end_x   ||
      end_y   != gradient_tool->end_y)
    {
      gradient_tool->start_x = start_x;
      gradient_tool->start_y = start_y;
      gradient_tool->end_x   = end_x;
      gradient_tool->end_y   = end_y;

      update = TRUE;
    }

  if (pika_gradient_tool_editor_line_changed (gradient_tool))
    update = TRUE;

  if (update)
    {
      pika_gradient_tool_update_graph (gradient_tool);
      pika_drawable_filter_apply (gradient_tool->filter, NULL);
    }
}

static void
pika_gradient_tool_line_response (PikaToolWidget *widget,
                                  gint            response_id,
                                  PikaGradientTool  *gradient_tool)
{
  PikaTool *tool = PIKA_TOOL (gradient_tool);

  switch (response_id)
    {
    case PIKA_TOOL_WIDGET_RESPONSE_CONFIRM:
      pika_tool_control (tool, PIKA_TOOL_ACTION_COMMIT, tool->display);
      break;

    case PIKA_TOOL_WIDGET_RESPONSE_CANCEL:
      pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display);
      break;
    }
}

static void
pika_gradient_tool_precalc_shapeburst (PikaGradientTool *gradient_tool)
{
  PikaGradientOptions *options = PIKA_GRADIENT_TOOL_GET_OPTIONS (gradient_tool);
  PikaTool            *tool    = PIKA_TOOL (gradient_tool);
  gint                 x, y, width, height;

  if (gradient_tool->dist_buffer || ! tool->drawables)
    return;

  g_return_if_fail (g_list_length (tool->drawables) == 1);

  if (! pika_item_mask_intersect (PIKA_ITEM (tool->drawables->data),
                                  &x, &y, &width, &height))
    return;

  gradient_tool->dist_buffer =
    pika_drawable_gradient_shapeburst_distmap (tool->drawables->data,
                                               options->distance_metric,
                                               GEGL_RECTANGLE (x, y, width, height),
                                               PIKA_PROGRESS (gradient_tool));

  if (gradient_tool->dist_node)
    gegl_node_set (gradient_tool->dist_node,
                   "buffer", gradient_tool->dist_buffer,
                   NULL);

  pika_progress_end (PIKA_PROGRESS (gradient_tool));
}


/* gegl graph stuff */

static void
pika_gradient_tool_create_graph (PikaGradientTool *gradient_tool)
{
  PikaGradientOptions *options = PIKA_GRADIENT_TOOL_GET_OPTIONS (gradient_tool);
  PikaContext         *context = PIKA_CONTEXT (options);
  GeglNode            *output;

  /* render_node is not supposed to be recreated */
  g_return_if_fail (gradient_tool->graph == NULL);

  gradient_tool->graph = gegl_node_new ();

  gradient_tool->dist_node =
    gegl_node_new_child (gradient_tool->graph,
                         "operation", "gegl:buffer-source",
                         "buffer",    gradient_tool->dist_buffer,
                         NULL);

#if 0
  gradient_tool->subtract_node =
    gegl_node_new_child (gradient_tool->graph,
                         "operation", "gegl:subtract",
                         NULL);

  gradient_tool->divide_node =
    gegl_node_new_child (gradient_tool->graph,
                         "operation", "gegl:divide",
                         NULL);
#endif

  gradient_tool->render_node =
    gegl_node_new_child (gradient_tool->graph,
                         "operation", "pika:gradient",
                         "context", context,
                         NULL);

  output = gegl_node_get_output_proxy (gradient_tool->graph, "output");

  gegl_node_link_many (gradient_tool->dist_node,
#if 0
                       gradient_tool->subtract_node,
                       gradient_tool->divide_node,
#endif
                       gradient_tool->render_node,
                       output,
                       NULL);

  pika_gegl_node_set_underlying_operation (gradient_tool->graph,
                                           gradient_tool->render_node);

  pika_gradient_tool_update_graph (gradient_tool);
}

static void
pika_gradient_tool_update_graph (PikaGradientTool *gradient_tool)
{
  PikaTool            *tool    = PIKA_TOOL (gradient_tool);
  PikaGradientOptions *options = PIKA_GRADIENT_TOOL_GET_OPTIONS (gradient_tool);
  gint                 off_x, off_y;

  pika_item_get_offset (PIKA_ITEM (tool->drawables->data), &off_x, &off_y);

#if 0
  if (pika_gradient_tool_is_shapeburst (gradient_tool))
    {
      gfloat start, end;

      gegl_buffer_get (gradient_tool->dist_buffer,
                       GEGL_RECTANGLE (gradient_tool->start_x - off_x,
                                       gradient_tool->start_y - off_y,
                                       1, 1),
                       1.0, babl_format("Y float"), &start,
                       GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);

      gegl_buffer_get (gradient_tool->dist_buffer,
                       GEGL_RECTANGLE (gradient_tool->end_x - off_x,
                                       gradient_tool->end_y - off_y,
                                       1, 1),
                       1.0, babl_format("Y float"), &end,
                       GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);

      if (start != end)
        {
          gegl_node_set (gradient_tool->subtract_node,
                         "value", (gdouble) start,
                         NULL);
          gegl_node_set (gradient_tool->divide_node,
                         "value", (gdouble) (end - start),
                         NULL);
        }
    }
  else
#endif
    {
      GeglRectangle roi;
      gdouble       start_x, start_y;
      gdouble       end_x,   end_y;

      pika_item_mask_intersect (PIKA_ITEM (tool->drawables->data),
                                &roi.x, &roi.y, &roi.width, &roi.height);

      start_x = gradient_tool->start_x - off_x;
      start_y = gradient_tool->start_y - off_y;
      end_x   = gradient_tool->end_x   - off_x;
      end_y   = gradient_tool->end_y   - off_y;

      pika_drawable_gradient_adjust_coords (tool->drawables->data,
                                            options->gradient_type,
                                            &roi,
                                            &start_x, &start_y, &end_x, &end_y);

      gegl_node_set (gradient_tool->render_node,
                     "start-x", start_x,
                     "start-y", start_y,
                     "end-x",   end_x,
                     "end-y",   end_y,
                     NULL);
    }
}

static void
pika_gradient_tool_fg_bg_changed (PikaGradientTool *gradient_tool)
{
  if (! gradient_tool->filter || ! gradient_tool->gradient)
    return;

  if (pika_gradient_has_fg_bg_segments (gradient_tool->gradient))
    {
      /* Set a property on the node. Otherwise it will cache and refuse to update */
      gegl_node_set (gradient_tool->render_node,
                     "gradient", gradient_tool->gradient,
                     NULL);

      /* Update the filter */
      pika_drawable_filter_apply (gradient_tool->filter, NULL);

      pika_gradient_tool_editor_fg_bg_changed (gradient_tool);
    }
}

static void
pika_gradient_tool_gradient_dirty (PikaGradientTool *gradient_tool)
{
  if (! gradient_tool->filter)
    return;

  if (! gradient_tool->tentative_gradient)
    {
      /* Set a property on the node. Otherwise it will cache and refuse to update */
      gegl_node_set (gradient_tool->render_node,
                     "gradient", gradient_tool->gradient,
                     NULL);

      /* Update the filter */
      pika_drawable_filter_apply (gradient_tool->filter, NULL);
    }

  pika_gradient_tool_editor_gradient_dirty (gradient_tool);
}

static void
pika_gradient_tool_set_gradient (PikaGradientTool *gradient_tool,
                                 PikaGradient     *gradient)
{
  if (gradient_tool->gradient)
    g_signal_handlers_disconnect_by_func (gradient_tool->gradient,
                                          G_CALLBACK (pika_gradient_tool_gradient_dirty),
                                          gradient_tool);


  g_set_object (&gradient_tool->gradient, gradient);

  if (gradient_tool->gradient)
    {
      g_signal_connect_swapped (gradient_tool->gradient, "dirty",
                                G_CALLBACK (pika_gradient_tool_gradient_dirty),
                                gradient_tool);

      if (gradient_tool->render_node)
        gegl_node_set (gradient_tool->render_node,
                       "gradient", gradient_tool->gradient,
                       NULL);
    }

  pika_gradient_tool_editor_gradient_changed (gradient_tool);
}

static gboolean
pika_gradient_tool_is_shapeburst (PikaGradientTool *gradient_tool)
{
  PikaGradientOptions *options = PIKA_GRADIENT_TOOL_GET_OPTIONS (gradient_tool);

  return options->gradient_type >= PIKA_GRADIENT_SHAPEBURST_ANGULAR &&
         options->gradient_type <= PIKA_GRADIENT_SHAPEBURST_DIMPLED;
}


/* image map stuff */

static void
pika_gradient_tool_create_filter (PikaGradientTool *gradient_tool,
                                  PikaDrawable     *drawable)
{
  PikaGradientOptions *options = PIKA_GRADIENT_TOOL_GET_OPTIONS (gradient_tool);
  PikaContext         *context = PIKA_CONTEXT (options);

  if (! gradient_tool->graph)
    pika_gradient_tool_create_graph (gradient_tool);

  gradient_tool->filter = pika_drawable_filter_new (drawable,
                                                 C_("undo-type", "Gradient"),
                                                 gradient_tool->graph,
                                                 PIKA_ICON_TOOL_GRADIENT);

  pika_drawable_filter_set_region (gradient_tool->filter,
                                   PIKA_FILTER_REGION_DRAWABLE);
  pika_drawable_filter_set_opacity (gradient_tool->filter,
                                    pika_context_get_opacity (context));
  pika_drawable_filter_set_mode (gradient_tool->filter,
                                 pika_context_get_paint_mode (context),
                                 PIKA_LAYER_COLOR_SPACE_AUTO,
                                 PIKA_LAYER_COLOR_SPACE_AUTO,
                                 pika_layer_mode_get_paint_composite_mode (
                                   pika_context_get_paint_mode (context)));

  g_signal_connect (gradient_tool->filter, "flush",
                    G_CALLBACK (pika_gradient_tool_filter_flush),
                    gradient_tool);
}

static void
pika_gradient_tool_filter_flush (PikaDrawableFilter *filter,
                                 PikaTool           *tool)
{
  PikaImage *image = pika_display_get_image (tool->display);

  pika_projection_flush (pika_image_get_projection (image));
}


/*  protected functions  */


void
pika_gradient_tool_set_tentative_gradient (PikaGradientTool *gradient_tool,
                                           PikaGradient     *gradient)
{
  g_return_if_fail (PIKA_IS_GRADIENT_TOOL (gradient_tool));
  g_return_if_fail (gradient == NULL || PIKA_IS_GRADIENT (gradient));

  if (g_set_object (&gradient_tool->tentative_gradient, gradient))
    {
      if (gradient_tool->render_node)
        {
          gegl_node_set (gradient_tool->render_node,
                         "gradient", gradient ? gradient : gradient_tool->gradient,
                         NULL);

          pika_drawable_filter_apply (gradient_tool->filter, NULL);
        }
    }
}
