/* 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): 
 *
 * pikacagetool.c
 * Copyright (C) 2010 Michael Muré <batolettre@gmail.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 <string.h>

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

#include "libpikamath/pikamath.h"
#include "libpikawidgets/pikawidgets.h"

#include "tools-types.h"

#include "config/pikaguiconfig.h"

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

#include "operations/pikacageconfig.h"

#include "core/pika.h"
#include "core/pikadrawablefilter.h"
#include "core/pikaerror.h"
#include "core/pikaimage.h"
#include "core/pikaitem.h"
#include "core/pikaprogress.h"
#include "core/pikaprojection.h"

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

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

#include "pikacagetool.h"
#include "pikacageoptions.h"
#include "pikatoolcontrol.h"
#include "pikatools-utils.h"

#include "pika-intl.h"


/* XXX: if this state list is updated, in particular if for some reason,
   a new CAGE_STATE_* was to be inserted after CAGE_STATE_CLOSING, check
   if the function pika_cage_tool_is_complete() has to be updated.
   Current algorithm is that all DEFORM_* states are complete states,
   and all CAGE_* states are incomplete states. */
enum
{
  CAGE_STATE_INIT,
  CAGE_STATE_WAIT,
  CAGE_STATE_MOVE_HANDLE,
  CAGE_STATE_SELECTING,
  CAGE_STATE_CLOSING,
  DEFORM_STATE_WAIT,
  DEFORM_STATE_MOVE_HANDLE,
  DEFORM_STATE_SELECTING
};


static gboolean   pika_cage_tool_initialize         (PikaTool              *tool,
                                                     PikaDisplay           *display,
                                                     GError               **error);
static void       pika_cage_tool_control            (PikaTool              *tool,
                                                     PikaToolAction         action,
                                                     PikaDisplay           *display);
static void       pika_cage_tool_button_press       (PikaTool              *tool,
                                                     const PikaCoords      *coords,
                                                     guint32                time,
                                                     GdkModifierType        state,
                                                     PikaButtonPressType    press_type,
                                                     PikaDisplay           *display);
static void       pika_cage_tool_button_release     (PikaTool              *tool,
                                                     const PikaCoords      *coords,
                                                     guint32                time,
                                                     GdkModifierType        state,
                                                     PikaButtonReleaseType  release_type,
                                                     PikaDisplay           *display);
static void       pika_cage_tool_motion             (PikaTool              *tool,
                                                     const PikaCoords      *coords,
                                                     guint32                time,
                                                     GdkModifierType        state,
                                                     PikaDisplay           *display);
static gboolean   pika_cage_tool_key_press          (PikaTool              *tool,
                                                     GdkEventKey           *kevent,
                                                     PikaDisplay           *display);
static void       pika_cage_tool_cursor_update      (PikaTool              *tool,
                                                     const PikaCoords      *coords,
                                                     GdkModifierType        state,
                                                     PikaDisplay           *display);
static void       pika_cage_tool_oper_update        (PikaTool              *tool,
                                                     const PikaCoords      *coords,
                                                     GdkModifierType        state,
                                                     gboolean               proximity,
                                                     PikaDisplay           *display);
static void       pika_cage_tool_options_notify     (PikaTool              *tool,
                                                     PikaToolOptions       *options,
                                                     const GParamSpec      *pspec);

static void       pika_cage_tool_draw               (PikaDrawTool          *draw_tool);

static void       pika_cage_tool_start              (PikaCageTool          *ct,
                                                     PikaDisplay           *display);
static void       pika_cage_tool_halt               (PikaCageTool          *ct);
static void       pika_cage_tool_commit             (PikaCageTool          *ct);

static gint       pika_cage_tool_is_on_handle       (PikaCageTool          *ct,
                                                     PikaDrawTool          *draw_tool,
                                                     PikaDisplay           *display,
                                                     gdouble                x,
                                                     gdouble                y,
                                                     gint                   handle_size);
static gint       pika_cage_tool_is_on_edge         (PikaCageTool          *ct,
                                                     gdouble                x,
                                                     gdouble                y,
                                                     gint                   handle_size);

static gboolean   pika_cage_tool_is_complete        (PikaCageTool          *ct);
static void       pika_cage_tool_remove_last_handle (PikaCageTool          *ct);
static void       pika_cage_tool_compute_coef       (PikaCageTool          *ct);
static void       pika_cage_tool_create_filter      (PikaCageTool          *ct);
static void       pika_cage_tool_filter_flush       (PikaDrawableFilter    *filter,
                                                     PikaTool              *tool);
static void       pika_cage_tool_filter_update      (PikaCageTool          *ct);

static void       pika_cage_tool_create_render_node (PikaCageTool          *ct);
static void       pika_cage_tool_render_node_update (PikaCageTool          *ct);


G_DEFINE_TYPE (PikaCageTool, pika_cage_tool, PIKA_TYPE_DRAW_TOOL)

#define parent_class pika_cage_tool_parent_class


void
pika_cage_tool_register (PikaToolRegisterCallback  callback,
                         gpointer                  data)
{
  (* callback) (PIKA_TYPE_CAGE_TOOL,
                PIKA_TYPE_CAGE_OPTIONS,
                pika_cage_options_gui,
                0,
                "pika-cage-tool",
                _("Cage Transform"),
                _("Cage Transform: Deform a selection with a cage"),
                N_("_Cage Transform"), "<shift>G",
                NULL, PIKA_HELP_TOOL_CAGE,
                PIKA_ICON_TOOL_CAGE,
                data);
}

static void
pika_cage_tool_class_init (PikaCageToolClass *klass)
{
  PikaToolClass     *tool_class      = PIKA_TOOL_CLASS (klass);
  PikaDrawToolClass *draw_tool_class = PIKA_DRAW_TOOL_CLASS (klass);

  tool_class->initialize     = pika_cage_tool_initialize;
  tool_class->control        = pika_cage_tool_control;
  tool_class->button_press   = pika_cage_tool_button_press;
  tool_class->button_release = pika_cage_tool_button_release;
  tool_class->key_press      = pika_cage_tool_key_press;
  tool_class->motion         = pika_cage_tool_motion;
  tool_class->cursor_update  = pika_cage_tool_cursor_update;
  tool_class->oper_update    = pika_cage_tool_oper_update;
  tool_class->options_notify = pika_cage_tool_options_notify;

  draw_tool_class->draw      = pika_cage_tool_draw;
}

static void
pika_cage_tool_init (PikaCageTool *self)
{
  PikaTool *tool = PIKA_TOOL (self);

  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_SELECTION       |
                                     PIKA_DIRTY_ACTIVE_DRAWABLE);
  pika_tool_control_set_wants_click (tool->control, TRUE);
  pika_tool_control_set_precision   (tool->control,
                                     PIKA_CURSOR_PRECISION_SUBPIXEL);
  pika_tool_control_set_tool_cursor (tool->control,
                                     PIKA_TOOL_CURSOR_PERSPECTIVE);

  self->config          = g_object_new (PIKA_TYPE_CAGE_CONFIG, NULL);
  self->hovering_handle = -1;
  self->tool_state      = CAGE_STATE_INIT;
}

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

  if (g_list_length (drawables) != 1)
    {
      if (g_list_length (drawables) > 1)
        g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED,
                                   _("Cannot modify multiple layers. Select only one layer."));
      else
        g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED, _("No selected drawables."));

      g_list_free (drawables);
      return FALSE;
    }

  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 item'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 active item is not visible."));
      return FALSE;
    }

  pika_cage_tool_start (PIKA_CAGE_TOOL (tool), display);

  return TRUE;
}

static void
pika_cage_tool_control (PikaTool       *tool,
                        PikaToolAction  action,
                        PikaDisplay    *display)
{
  PikaCageTool *ct = PIKA_CAGE_TOOL (tool);

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

    case PIKA_TOOL_ACTION_HALT:
      pika_cage_tool_halt (ct);
      break;

    case PIKA_TOOL_ACTION_COMMIT:
      pika_cage_tool_commit (ct);
      break;
    }

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

static void
pika_cage_tool_button_press (PikaTool            *tool,
                             const PikaCoords    *coords,
                             guint32              time,
                             GdkModifierType      state,
                             PikaButtonPressType  press_type,
                             PikaDisplay         *display)
{
  PikaCageTool *ct        = PIKA_CAGE_TOOL (tool);
  PikaDrawTool *draw_tool = PIKA_DRAW_TOOL (tool);
  gint          handle    = -1;
  gint          edge      = -1;

  pika_tool_control_activate (tool->control);

  if (ct->config)
    {
      handle = pika_cage_tool_is_on_handle (ct,
                                            draw_tool,
                                            display,
                                            coords->x,
                                            coords->y,
                                            PIKA_TOOL_HANDLE_SIZE_CIRCLE);
      edge = pika_cage_tool_is_on_edge (ct,
                                        coords->x,
                                        coords->y,
                                        PIKA_TOOL_HANDLE_SIZE_CIRCLE);
    }

  ct->movement_start_x = coords->x;
  ct->movement_start_y = coords->y;

  switch (ct->tool_state)
    {
    case CAGE_STATE_INIT:
      /* No handle yet, we add the first one and switch the tool to
       * moving handle state.
       */
      pika_cage_config_add_cage_point (ct->config,
                                       coords->x - ct->offset_x,
                                       coords->y - ct->offset_y);
      pika_cage_config_select_point (ct->config, 0);
      ct->tool_state = CAGE_STATE_MOVE_HANDLE;
      break;

    case CAGE_STATE_WAIT:
      if (handle == -1 && edge <= 0)
        {
          /* User clicked on the background, we add a new handle
           * and move it
           */
          pika_cage_config_add_cage_point (ct->config,
                                           coords->x - ct->offset_x,
                                           coords->y - ct->offset_y);
          pika_cage_config_select_point (ct->config,
                                         pika_cage_config_get_n_points (ct->config) - 1);
          ct->tool_state = CAGE_STATE_MOVE_HANDLE;
        }
      else if (handle == 0 && pika_cage_config_get_n_points (ct->config) > 2)
        {
          /* User clicked on the first handle, we wait for
           * release for closing the cage and switching to
           * deform if possible
           */
          pika_cage_config_select_point (ct->config, 0);
          ct->tool_state = CAGE_STATE_CLOSING;
        }
      else if (handle >= 0)
        {
          /* User clicked on a handle, so we move it */

          if (state & pika_get_extend_selection_mask ())
            {
              /* Multiple selection */

              pika_cage_config_toggle_point_selection (ct->config, handle);
            }
          else
            {
              /* New selection */

              if (! pika_cage_config_point_is_selected (ct->config, handle))
                {
                  pika_cage_config_select_point (ct->config, handle);
                }
            }

          ct->tool_state = CAGE_STATE_MOVE_HANDLE;
        }
      else if (edge > 0)
        {
          /* User clicked on an edge, we add a new handle here and select it */

          pika_cage_config_insert_cage_point (ct->config, edge,
                                              coords->x, coords->y);
          pika_cage_config_select_point (ct->config, edge);
          ct->tool_state = CAGE_STATE_MOVE_HANDLE;
        }
      break;

    case DEFORM_STATE_WAIT:
      if (handle == -1)
        {
          /* User clicked on the background, we start a rubber band
           * selection
           */
          ct->selection_start_x = coords->x;
          ct->selection_start_y = coords->y;
          ct->tool_state = DEFORM_STATE_SELECTING;
        }

      if (handle >= 0)
        {
          /* User clicked on a handle, so we move it */

          if (state & pika_get_extend_selection_mask ())
            {
              /* Multiple selection */

              pika_cage_config_toggle_point_selection (ct->config, handle);
            }
          else
            {
              /* New selection */

              if (! pika_cage_config_point_is_selected (ct->config, handle))
                {
                  pika_cage_config_select_point (ct->config, handle);
                }
            }

          ct->tool_state = DEFORM_STATE_MOVE_HANDLE;
        }
      break;
    }
}

void
pika_cage_tool_button_release (PikaTool              *tool,
                               const PikaCoords      *coords,
                               guint32                time,
                               GdkModifierType        state,
                               PikaButtonReleaseType  release_type,
                               PikaDisplay           *display)
{
  PikaCageTool    *ct      = PIKA_CAGE_TOOL (tool);
  PikaCageOptions *options = PIKA_CAGE_TOOL_GET_OPTIONS (ct);

  pika_draw_tool_pause (PIKA_DRAW_TOOL (ct));

  pika_tool_control_halt (tool->control);

  if (release_type == PIKA_BUTTON_RELEASE_CANCEL)
    {
      /* Cancelling */

      switch (ct->tool_state)
        {
        case CAGE_STATE_CLOSING:
          ct->tool_state = CAGE_STATE_WAIT;
          break;

        case CAGE_STATE_MOVE_HANDLE:
          pika_cage_config_remove_last_cage_point (ct->config);
          ct->tool_state = CAGE_STATE_WAIT;
          break;

        case CAGE_STATE_SELECTING:
          ct->tool_state = CAGE_STATE_WAIT;
          break;

        case DEFORM_STATE_MOVE_HANDLE:
          pika_cage_tool_filter_update (ct);
          ct->tool_state = DEFORM_STATE_WAIT;
          break;

        case DEFORM_STATE_SELECTING:
          ct->tool_state = DEFORM_STATE_WAIT;
          break;
        }

      pika_cage_config_reset_displacement (ct->config);
    }
  else
    {
      /* Normal release */

      switch (ct->tool_state)
        {
        case CAGE_STATE_CLOSING:
          ct->dirty_coef = TRUE;
          pika_cage_config_commit_displacement (ct->config);

          if (release_type == PIKA_BUTTON_RELEASE_CLICK)
            g_object_set (options, "cage-mode", PIKA_CAGE_MODE_DEFORM, NULL);
          break;

        case CAGE_STATE_MOVE_HANDLE:
          ct->dirty_coef = TRUE;
          ct->tool_state = CAGE_STATE_WAIT;
          pika_cage_config_commit_displacement (ct->config);
          break;

        case CAGE_STATE_SELECTING:
          {
            GeglRectangle area =
              { MIN (ct->selection_start_x, coords->x) - ct->offset_x,
                MIN (ct->selection_start_y, coords->y) - ct->offset_y,
                ABS (ct->selection_start_x - coords->x),
                ABS (ct->selection_start_y - coords->y) };

            if (state & pika_get_extend_selection_mask ())
              {
                pika_cage_config_select_add_area (ct->config,
                                                  PIKA_CAGE_MODE_CAGE_CHANGE,
                                                  area);
              }
            else
              {
                pika_cage_config_select_area (ct->config,
                                              PIKA_CAGE_MODE_CAGE_CHANGE,
                                              area);
              }

            ct->tool_state = CAGE_STATE_WAIT;
          }
          break;

        case DEFORM_STATE_MOVE_HANDLE:
          ct->tool_state = DEFORM_STATE_WAIT;
          pika_cage_config_commit_displacement (ct->config);
          gegl_node_set (ct->cage_node,
                         "config", ct->config,
                         NULL);
          pika_cage_tool_filter_update (ct);
          break;

        case DEFORM_STATE_SELECTING:
          {
            GeglRectangle area =
              { MIN (ct->selection_start_x, coords->x) - ct->offset_x,
                MIN (ct->selection_start_y, coords->y) - ct->offset_y,
                ABS (ct->selection_start_x - coords->x),
                ABS (ct->selection_start_y - coords->y) };

            if (state & pika_get_extend_selection_mask ())
              {
                pika_cage_config_select_add_area (ct->config,
                                                  PIKA_CAGE_MODE_DEFORM, area);
              }
            else
              {
                pika_cage_config_select_area (ct->config,
                                              PIKA_CAGE_MODE_DEFORM, area);
              }

            ct->tool_state = DEFORM_STATE_WAIT;
          }
          break;
        }
    }

  pika_draw_tool_resume (PIKA_DRAW_TOOL (tool));
}

static void
pika_cage_tool_motion (PikaTool         *tool,
                       const PikaCoords *coords,
                       guint32           time,
                       GdkModifierType   state,
                       PikaDisplay      *display)
{
  PikaCageTool    *ct       = PIKA_CAGE_TOOL (tool);
  PikaCageOptions *options  = PIKA_CAGE_TOOL_GET_OPTIONS (ct);

  pika_draw_tool_pause (PIKA_DRAW_TOOL (tool));

  ct->cursor_x = coords->x;
  ct->cursor_y = coords->y;

  switch (ct->tool_state)
    {
    case CAGE_STATE_MOVE_HANDLE:
    case CAGE_STATE_CLOSING:
    case DEFORM_STATE_MOVE_HANDLE:
      pika_cage_config_add_displacement (ct->config,
                                         options->cage_mode,
                                         ct->cursor_x - ct->movement_start_x,
                                         ct->cursor_y - ct->movement_start_y);
      break;
    }

  pika_draw_tool_resume (PIKA_DRAW_TOOL (tool));
}

static gboolean
pika_cage_tool_key_press (PikaTool    *tool,
                          GdkEventKey *kevent,
                          PikaDisplay *display)
{
  PikaCageTool *ct = PIKA_CAGE_TOOL (tool);

  if (! ct->config)
    return FALSE;

  switch (kevent->keyval)
    {
    case GDK_KEY_BackSpace:
      if (ct->tool_state == CAGE_STATE_WAIT)
        {
          if (pika_cage_config_get_n_points (ct->config) != 0)
            pika_cage_tool_remove_last_handle (ct);
        }
      else if (ct->tool_state == DEFORM_STATE_WAIT)
        {
          pika_cage_config_remove_selected_points (ct->config);

          /* if the cage have less than 3 handles, we reopen it */
          if (pika_cage_config_get_n_points (ct->config) <= 2)
            {
              ct->tool_state = CAGE_STATE_WAIT;
            }

          pika_cage_tool_compute_coef (ct);
          pika_cage_tool_render_node_update (ct);
        }
      return TRUE;

    case GDK_KEY_Return:
    case GDK_KEY_KP_Enter:
    case GDK_KEY_ISO_Enter:
      if (! pika_cage_tool_is_complete (ct) &&
          pika_cage_config_get_n_points (ct->config) > 2)
        {
          g_object_set (pika_tool_get_options (tool),
                        "cage-mode", PIKA_CAGE_MODE_DEFORM,
                        NULL);
        }
      else if (ct->tool_state == DEFORM_STATE_WAIT)
        {
          pika_tool_control (tool, PIKA_TOOL_ACTION_COMMIT, display);
        }
      return TRUE;

    case GDK_KEY_Escape:
      pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, display);
      return TRUE;

    default:
      break;
    }

  return FALSE;
}

static void
pika_cage_tool_oper_update (PikaTool         *tool,
                            const PikaCoords *coords,
                            GdkModifierType   state,
                            gboolean          proximity,
                            PikaDisplay      *display)
{
  PikaCageTool *ct        = PIKA_CAGE_TOOL (tool);
  PikaDrawTool *draw_tool = PIKA_DRAW_TOOL (tool);

  if (ct->config)
    {
      ct->hovering_handle = pika_cage_tool_is_on_handle (ct,
                                                         draw_tool,
                                                         display,
                                                         coords->x,
                                                         coords->y,
                                                         PIKA_TOOL_HANDLE_SIZE_CIRCLE);

      ct->hovering_edge = pika_cage_tool_is_on_edge (ct,
                                                     coords->x,
                                                     coords->y,
                                                     PIKA_TOOL_HANDLE_SIZE_CIRCLE);
    }

  pika_draw_tool_pause (draw_tool);

  ct->cursor_x = coords->x;
  ct->cursor_y = coords->y;

  pika_draw_tool_resume (draw_tool);
}

static void
pika_cage_tool_cursor_update (PikaTool         *tool,
                              const PikaCoords *coords,
                              GdkModifierType   state,
                              PikaDisplay      *display)
{
  PikaCageTool       *ct       = PIKA_CAGE_TOOL (tool);
  PikaCageOptions    *options  = PIKA_CAGE_TOOL_GET_OPTIONS (ct);
  PikaGuiConfig      *config   = PIKA_GUI_CONFIG (display->pika->config);
  PikaCursorModifier  modifier = PIKA_CURSOR_MODIFIER_PLUS;

  if (tool->display)
    {
      if (ct->hovering_handle != -1)
        {
          modifier = PIKA_CURSOR_MODIFIER_MOVE;
        }
      else if (ct->hovering_edge  != -1 &&
               options->cage_mode == PIKA_CAGE_MODE_CAGE_CHANGE)
        {
          modifier = PIKA_CURSOR_MODIFIER_PLUS;
        }
      else
        {
          if (pika_cage_tool_is_complete (ct))
            modifier = PIKA_CURSOR_MODIFIER_BAD;
        }
    }
  else
    {
      PikaImage    *image     = pika_display_get_image (display);
      GList        *drawables = pika_image_get_selected_drawables (image);
      PikaDrawable *drawable  = NULL;

      if (g_list_length (drawables) == 1)
        drawable = drawables->data;
      g_list_free (drawables);

      if (! drawable                                               ||
          pika_viewable_get_children (PIKA_VIEWABLE (drawable))    ||
          pika_item_is_content_locked (PIKA_ITEM (drawable), NULL) ||
          ! (pika_item_is_visible (PIKA_ITEM (drawable)) ||
             config->edit_non_visible))
        {
          modifier = PIKA_CURSOR_MODIFIER_BAD;
        }
    }

  pika_tool_control_set_cursor_modifier (tool->control, modifier);

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

static void
pika_cage_tool_options_notify (PikaTool         *tool,
                               PikaToolOptions  *options,
                               const GParamSpec *pspec)
{
  PikaCageTool *ct = PIKA_CAGE_TOOL (tool);

  PIKA_TOOL_CLASS (parent_class)->options_notify (tool, options, pspec);

  if (! tool->display)
    return;

  pika_draw_tool_pause (PIKA_DRAW_TOOL (tool));

  if (strcmp (pspec->name, "cage-mode") == 0)
    {
      PikaCageMode mode;

      g_object_get (options,
                    "cage-mode", &mode,
                    NULL);

      if (mode == PIKA_CAGE_MODE_DEFORM)
        {
          /* switch to deform mode */

          if (pika_cage_config_get_n_points (ct->config) > 2)
            {
              pika_cage_config_reset_displacement (ct->config);
              pika_cage_config_reverse_cage_if_needed (ct->config);
              pika_tool_push_status (tool, tool->display,
                                     _("Press ENTER to commit the transform"));
              ct->tool_state = DEFORM_STATE_WAIT;

              if (! ct->render_node)
                {
                  pika_cage_tool_create_render_node (ct);
                }

              if (ct->dirty_coef)
                {
                  pika_cage_tool_compute_coef (ct);
                  pika_cage_tool_render_node_update (ct);
                }

              if (! ct->filter)
                pika_cage_tool_create_filter (ct);

              pika_cage_tool_filter_update (ct);
            }
          else
            {
              g_object_set (options,
                            "cage-mode", PIKA_CAGE_MODE_CAGE_CHANGE,
                            NULL);
            }
        }
      else
        {
          /* switch to edit mode */
          if (ct->filter)
            {
              pika_drawable_filter_abort (ct->filter);

              pika_tool_pop_status (tool, tool->display);
              ct->tool_state = CAGE_STATE_WAIT;
            }
        }
    }
  else if (strcmp  (pspec->name, "fill-plain-color") == 0)
    {
      if (ct->tool_state == DEFORM_STATE_WAIT)
        {
          pika_cage_tool_render_node_update (ct);
          pika_cage_tool_filter_update (ct);
        }
    }

  pika_draw_tool_resume (PIKA_DRAW_TOOL (tool));
}

static void
pika_cage_tool_draw (PikaDrawTool *draw_tool)
{
  PikaCageTool    *ct        = PIKA_CAGE_TOOL (draw_tool);
  PikaCageOptions *options   = PIKA_CAGE_TOOL_GET_OPTIONS (ct);
  PikaCageConfig  *config    = ct->config;
  PikaCanvasGroup *stroke_group;
  gint             n_vertices;
  gint             i;
  PikaHandleType   handle;

  n_vertices = pika_cage_config_get_n_points (config);

  if (n_vertices == 0)
    return;

  if (ct->tool_state == CAGE_STATE_INIT)
    return;

  stroke_group = pika_draw_tool_add_stroke_group (draw_tool);

  pika_draw_tool_push_group (draw_tool, stroke_group);

  /* If needed, draw line to the cursor. */
  if (! pika_cage_tool_is_complete (ct))
    {
      PikaVector2 last_point;

      last_point = pika_cage_config_get_point_coordinate (ct->config,
                                                          options->cage_mode,
                                                          n_vertices - 1);

      pika_draw_tool_add_line (draw_tool,
                               last_point.x + ct->offset_x,
                               last_point.y + ct->offset_y,
                               ct->cursor_x,
                               ct->cursor_y);
    }

  pika_draw_tool_pop_group (draw_tool);

  /* Draw the cage with handles. */
  for (i = 0; i < n_vertices; i++)
    {
      PikaCanvasItem *item;
      PikaVector2     point1, point2;

      point1 = pika_cage_config_get_point_coordinate (ct->config,
                                                      options->cage_mode,
                                                      i);
      point1.x += ct->offset_x;
      point1.y += ct->offset_y;

      if (i > 0 || pika_cage_tool_is_complete (ct))
        {
          gint index_point2;

          if (i == 0)
            index_point2 = n_vertices - 1;
          else
            index_point2 = i - 1;

          point2 = pika_cage_config_get_point_coordinate (ct->config,
                                                          options->cage_mode,
                                                          index_point2);
          point2.x += ct->offset_x;
          point2.y += ct->offset_y;

          if (i != ct->hovering_edge ||
              pika_cage_tool_is_complete (ct))
            {
              pika_draw_tool_push_group (draw_tool, stroke_group);
            }

          item = pika_draw_tool_add_line (draw_tool,
                                          point1.x,
                                          point1.y,
                                          point2.x,
                                          point2.y);

          if (i == ct->hovering_edge &&
              ! pika_cage_tool_is_complete (ct))
            {
              pika_canvas_item_set_highlight (item, TRUE);
            }
          else
            {
              pika_draw_tool_pop_group (draw_tool);
            }
        }

      if (pika_cage_config_point_is_selected (ct->config, i))
        {
          if (i == ct->hovering_handle)
            handle = PIKA_HANDLE_FILLED_SQUARE;
          else
            handle = PIKA_HANDLE_SQUARE;
        }
      else
        {
          if (i == ct->hovering_handle)
            handle = PIKA_HANDLE_FILLED_CIRCLE;
          else
            handle = PIKA_HANDLE_CIRCLE;
        }

      item = pika_draw_tool_add_handle (draw_tool,
                                        handle,
                                        point1.x,
                                        point1.y,
                                        PIKA_TOOL_HANDLE_SIZE_CIRCLE,
                                        PIKA_TOOL_HANDLE_SIZE_CIRCLE,
                                        PIKA_HANDLE_ANCHOR_CENTER);

      if (i == ct->hovering_handle)
        pika_canvas_item_set_highlight (item, TRUE);
    }

  if (ct->tool_state == DEFORM_STATE_SELECTING ||
      ct->tool_state == CAGE_STATE_SELECTING)
    {
      pika_draw_tool_add_rectangle (draw_tool,
                                    FALSE,
                                    MIN (ct->selection_start_x, ct->cursor_x),
                                    MIN (ct->selection_start_y, ct->cursor_y),
                                    ABS (ct->selection_start_x - ct->cursor_x),
                                    ABS (ct->selection_start_y - ct->cursor_y));
    }
}

static void
pika_cage_tool_start (PikaCageTool *ct,
                      PikaDisplay  *display)
{
  PikaTool     *tool     = PIKA_TOOL (ct);
  PikaImage    *image    = pika_display_get_image (display);
  GList        *drawables = pika_image_get_selected_drawables (image);

  g_return_if_fail (g_list_length (drawables) == 1);

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

  g_clear_object (&ct->config);

  g_clear_object (&ct->coef);
  ct->dirty_coef = TRUE;

  if (ct->filter)
    {
      pika_drawable_filter_abort (ct->filter);
      g_clear_object (&ct->filter);
    }

  if (ct->render_node)
    {
      g_clear_object (&ct->render_node);
      ct->coef_node = NULL;
      ct->cage_node = NULL;
    }

  ct->config          = g_object_new (PIKA_TYPE_CAGE_CONFIG, NULL);
  ct->hovering_handle = -1;
  ct->hovering_edge   = -1;
  ct->tool_state      = CAGE_STATE_INIT;

  /* Setting up cage offset to convert the cage point coords to
   * drawable coords
   */
  pika_item_get_offset (PIKA_ITEM (tool->drawables->data),
                        &ct->offset_x, &ct->offset_y);

  pika_draw_tool_start (PIKA_DRAW_TOOL (ct), display);
}

static void
pika_cage_tool_halt (PikaCageTool *ct)
{
  PikaTool *tool = PIKA_TOOL (ct);

  g_clear_object (&ct->config);
  g_clear_object (&ct->coef);
  g_clear_object (&ct->render_node);
  ct->coef_node = NULL;
  ct->cage_node = NULL;

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

      pika_drawable_filter_abort (ct->filter);
      g_clear_object (&ct->filter);

      pika_tool_control_pop_preserve (tool->control);

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

  tool->display   = NULL;
  g_list_free (tool->drawables);
  tool->drawables = NULL;
  ct->tool_state  = CAGE_STATE_INIT;

  g_object_set (pika_tool_get_options (tool),
                "cage-mode", PIKA_CAGE_MODE_CAGE_CHANGE,
                NULL);
}

static void
pika_cage_tool_commit (PikaCageTool *ct)
{
  if (ct->filter)
    {
      PikaTool *tool = PIKA_TOOL (ct);

      pika_tool_control_push_preserve (tool->control, TRUE);

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

      pika_tool_control_pop_preserve (tool->control);

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

static gint
pika_cage_tool_is_on_handle (PikaCageTool *ct,
                             PikaDrawTool *draw_tool,
                             PikaDisplay  *display,
                             gdouble       x,
                             gdouble       y,
                             gint          handle_size)
{
  PikaCageOptions *options = PIKA_CAGE_TOOL_GET_OPTIONS (ct);
  PikaCageConfig  *config  = ct->config;
  gdouble          dist    = G_MAXDOUBLE;
  gint             i;
  PikaVector2      cage_point;
  guint            n_cage_vertices;

  g_return_val_if_fail (PIKA_IS_CAGE_TOOL (ct), -1);

  n_cage_vertices = pika_cage_config_get_n_points (config);

  if (n_cage_vertices == 0)
    return -1;

  for (i = 0; i < n_cage_vertices; i++)
    {
      cage_point = pika_cage_config_get_point_coordinate (config,
                                                          options->cage_mode,
                                                          i);
      cage_point.x += ct->offset_x;
      cage_point.y += ct->offset_y;

      dist = pika_draw_tool_calc_distance_square (PIKA_DRAW_TOOL (draw_tool),
                                                  display,
                                                  x, y,
                                                  cage_point.x,
                                                  cage_point.y);

      if (dist <= SQR (handle_size / 2))
        return i;
    }

  return -1;
}

static gint
pika_cage_tool_is_on_edge (PikaCageTool *ct,
                           gdouble       x,
                           gdouble       y,
                           gint          handle_size)
{
  PikaCageOptions *options = PIKA_CAGE_TOOL_GET_OPTIONS (ct);
  PikaCageConfig  *config  = ct->config;
  gint             i;
  guint            n_cage_vertices;
  PikaVector2      A, B, C, AB, BC, AC;
  gdouble          lAB, lBC, lAC, lEB, lEC;

  g_return_val_if_fail (PIKA_IS_CAGE_TOOL (ct), -1);

  n_cage_vertices = pika_cage_config_get_n_points (config);

  if (n_cage_vertices < 2)
    return -1;

  A = pika_cage_config_get_point_coordinate (config,
                                             options->cage_mode,
                                             n_cage_vertices-1);
  B = pika_cage_config_get_point_coordinate (config,
                                             options->cage_mode,
                                             0);
  C.x = x;
  C.y = y;

  for (i = 0; i < n_cage_vertices; i++)
    {
      pika_vector2_sub (&AB, &A, &B);
      pika_vector2_sub (&BC, &B, &C);
      pika_vector2_sub (&AC, &A, &C);

      lAB = pika_vector2_length (&AB);
      lBC = pika_vector2_length (&BC);
      lAC = pika_vector2_length (&AC);
      lEB = lAB / 2 + (SQR (lBC) - SQR (lAC)) / (2 * lAB);
      lEC = sqrt (SQR (lBC) - SQR (lEB));

      if ((lEC < handle_size / 2) && (ABS (SQR (lBC) - SQR (lAC)) <= SQR (lAB)))
        return i;

      A = B;
      B = pika_cage_config_get_point_coordinate (config,
                                                 options->cage_mode,
                                                 (i+1) % n_cage_vertices);
    }

  return -1;
}

static gboolean
pika_cage_tool_is_complete (PikaCageTool *ct)
{
  return (ct->tool_state > CAGE_STATE_CLOSING);
}

static void
pika_cage_tool_remove_last_handle (PikaCageTool *ct)
{
  PikaCageConfig *config = ct->config;

  pika_draw_tool_pause (PIKA_DRAW_TOOL (ct));

  pika_cage_config_remove_last_cage_point (config);

  pika_draw_tool_resume (PIKA_DRAW_TOOL (ct));
}

static void
pika_cage_tool_compute_coef (PikaCageTool *ct)
{
  PikaCageConfig *config = ct->config;
  PikaProgress   *progress;
  const Babl     *format;
  GeglNode       *gegl;
  GeglNode       *input;
  GeglNode       *output;
  GeglProcessor  *processor;
  GeglBuffer     *buffer;
  gdouble         value;

  progress = pika_progress_start (PIKA_PROGRESS (ct), FALSE,
                                  _("Computing Cage Coefficients"));

  g_clear_object (&ct->coef);

  format = babl_format_n (babl_type ("float"),
                          pika_cage_config_get_n_points (config) * 2);


  gegl = gegl_node_new ();

  input = gegl_node_new_child (gegl,
                               "operation", "pika:cage-coef-calc",
                               "config",    ct->config,
                               NULL);

  output = gegl_node_new_child (gegl,
                                "operation", "gegl:buffer-sink",
                                "buffer",    &buffer,
                                "format",    format,
                                NULL);

  gegl_node_link (input, output);

  processor = gegl_node_new_processor (output, NULL);

  while (gegl_processor_work (processor, &value))
    {
      if (progress)
        pika_progress_set_value (progress, value);
    }

  if (progress)
    pika_progress_end (progress);

  g_object_unref (processor);

  ct->coef = buffer;
  g_object_unref (gegl);

  ct->dirty_coef = FALSE;
}

static void
pika_cage_tool_create_render_node (PikaCageTool *ct)
{
  PikaCageOptions *options = PIKA_CAGE_TOOL_GET_OPTIONS (ct);
  GeglNode        *render;
  GeglNode        *input;
  GeglNode        *output;

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

  ct->render_node = gegl_node_new ();

  input  = gegl_node_get_input_proxy  (ct->render_node, "input");
  output = gegl_node_get_output_proxy (ct->render_node, "output");

  ct->coef_node = gegl_node_new_child (ct->render_node,
                                       "operation", "gegl:buffer-source",
                                       "buffer",    ct->coef,
                                       NULL);

  ct->cage_node = gegl_node_new_child (ct->render_node,
                                       "operation",        "pika:cage-transform",
                                       "config",           ct->config,
                                       "fill-plain-color", options->fill_plain_color,
                                       NULL);

  render = gegl_node_new_child (ct->render_node,
                                "operation", "gegl:map-absolute",
                                NULL);

  gegl_node_link (input, ct->cage_node);

  gegl_node_connect (ct->coef_node, "output",
                     ct->cage_node, "aux");

  gegl_node_link (input, render);

  gegl_node_connect (ct->cage_node, "output",
                     render,        "aux");

  gegl_node_link (render, output);

  pika_gegl_progress_connect (ct->cage_node, PIKA_PROGRESS (ct),
                              _("Cage Transform"));
}

static void
pika_cage_tool_render_node_update (PikaCageTool *ct)
{
  PikaCageOptions *options  = PIKA_CAGE_TOOL_GET_OPTIONS (ct);
  gboolean         fill;
  GeglBuffer      *buffer;

  gegl_node_get (ct->cage_node,
                 "fill-plain-color", &fill,
                 NULL);

  if (fill != options->fill_plain_color)
    {
      gegl_node_set (ct->cage_node,
                     "fill-plain-color", options->fill_plain_color,
                     NULL);
    }

  gegl_node_get (ct->coef_node,
                 "buffer", &buffer,
                 NULL);

  if (buffer != ct->coef)
    {
      gegl_node_set (ct->coef_node,
                     "buffer", ct->coef,
                     NULL);
    }

  if (buffer)
    g_object_unref (buffer);
}

static void
pika_cage_tool_create_filter (PikaCageTool *ct)
{
  if (! ct->render_node)
    pika_cage_tool_create_render_node (ct);

  ct->filter = pika_drawable_filter_new (PIKA_TOOL (ct)->drawables->data,
                                         _("Cage transform"),
                                         ct->render_node,
                                         PIKA_ICON_TOOL_CAGE);
  pika_drawable_filter_set_region (ct->filter, PIKA_FILTER_REGION_DRAWABLE);

  g_signal_connect (ct->filter, "flush",
                    G_CALLBACK (pika_cage_tool_filter_flush),
                    ct);
}

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

  pika_projection_flush (pika_image_get_projection (image));
}

static void
pika_cage_tool_filter_update (PikaCageTool *ct)
{
  pika_drawable_filter_apply (ct->filter, NULL);
}
