/* 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): 
 *
 * pikanpointdeformationtool.c
 * Copyright (C) 2013 Marek Dvoroznak <dvoromar@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 <gegl.h>
#include <gegl-plugin.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include <npd/npd_common.h>

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

#include "tools-types.h"

#include "config/pikaguiconfig.h" /* playground */

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

#include "core/pika.h"
#include "core/pikadrawable.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/pikadisplayshell.h"
#include "display/pikacanvasbufferpreview.h"

#include "pikanpointdeformationtool.h"
#include "pikanpointdeformationoptions.h"
#include "pikatoolcontrol.h"
#include "pikatooloptions-gui.h"

#include "pika-intl.h"


//#define PIKA_NPD_DEBUG
#define PIKA_NPD_MAXIMUM_DEFORMATION_DELAY 100000 /* 100000 microseconds == 10 FPS */
#define PIKA_NPD_DRAW_INTERVAL                 50 /*     50 milliseconds == 20 FPS */


static void     pika_n_point_deformation_tool_start                   (PikaNPointDeformationTool *npd_tool,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_halt                    (PikaNPointDeformationTool *npd_tool);
static void     pika_n_point_deformation_tool_commit                  (PikaNPointDeformationTool *npd_tool);
static void     pika_n_point_deformation_tool_set_options             (PikaNPointDeformationTool *npd_tool,
                                                                       PikaNPointDeformationOptions *npd_options);
static void     pika_n_point_deformation_tool_options_notify          (PikaTool                  *tool,
                                                                       PikaToolOptions           *options,
                                                                       const GParamSpec          *pspec);
static void     pika_n_point_deformation_tool_button_press            (PikaTool                  *tool,
                                                                       const PikaCoords          *coords,
                                                                       guint32                    time,
                                                                       GdkModifierType            state,
                                                                       PikaButtonPressType        press_type,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_button_release          (PikaTool                  *tool,
                                                                       const PikaCoords          *coords,
                                                                       guint32                    time,
                                                                       GdkModifierType            state,
                                                                       PikaButtonReleaseType      release_type,
                                                                       PikaDisplay               *display);
static gboolean pika_n_point_deformation_tool_key_press               (PikaTool                  *tool,
                                                                       GdkEventKey               *kevent,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_modifier_key            (PikaTool                  *tool,
                                                                       GdkModifierType            key,
                                                                       gboolean                   press,
                                                                       GdkModifierType            state,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_cursor_update           (PikaTool                  *tool,
                                                                       const PikaCoords          *coords,
                                                                       GdkModifierType            state,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_draw                    (PikaDrawTool              *draw_tool);
static void     pika_n_point_deformation_tool_control                 (PikaTool                  *tool,
                                                                       PikaToolAction             action,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_oper_update             (PikaTool                  *tool,
                                                                       const PikaCoords          *coords,
                                                                       GdkModifierType            state,
                                                                       gboolean                   proximity,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_motion                  (PikaTool                  *tool,
                                                                       const PikaCoords          *coords,
                                                                       guint32                    time,
                                                                       GdkModifierType            state,
                                                                       PikaDisplay               *display);
static void     pika_n_point_deformation_tool_clear_selected_points_list
                                                                      (PikaNPointDeformationTool *npd_tool);
static gboolean pika_n_point_deformation_tool_add_cp_to_selection     (PikaNPointDeformationTool *npd_tool,
                                                                       NPDControlPoint           *cp);
static gboolean pika_n_point_deformation_tool_is_cp_in_area           (NPDControlPoint           *cp,
                                                                       gfloat                     x0,
                                                                       gfloat                     y0,
                                                                       gfloat                     x1,
                                                                       gfloat                     y1,
                                                                       gfloat                     offset_x,
                                                                       gfloat                     offset_y,
                                                                       gfloat                     cp_radius);
static void     pika_n_point_deformation_tool_remove_cp_from_selection
                                                                      (PikaNPointDeformationTool *npd_tool,
                                                                       NPDControlPoint           *cp);
static gpointer pika_n_point_deformation_tool_deform_thread_func      (gpointer                   data);
static gboolean pika_n_point_deformation_tool_canvas_update_timeout   (PikaNPointDeformationTool *npd_tool);
static void     pika_n_point_deformation_tool_perform_deformation     (PikaNPointDeformationTool *npd_tool);
static void     pika_n_point_deformation_tool_halt_threads            (PikaNPointDeformationTool *npd_tool);
static void     pika_n_point_deformation_tool_apply_deformation       (PikaNPointDeformationTool *npd_tool);

#ifdef PIKA_NPD_DEBUG
#define pika_npd_debug(x) g_printf x
#else
#define pika_npd_debug(x)
#endif

G_DEFINE_TYPE (PikaNPointDeformationTool, pika_n_point_deformation_tool,
               PIKA_TYPE_DRAW_TOOL)

#define parent_class pika_n_point_deformation_tool_parent_class


void
pika_n_point_deformation_tool_register (PikaToolRegisterCallback  callback,
                                        gpointer                  data)
{
  /* we should not know that "data" is a Gimp*, but what the heck this
   * is experimental playground stuff
   */
  if (PIKA_GUI_CONFIG (PIKA (data)->config)->playground_npd_tool)
    (* callback) (PIKA_TYPE_N_POINT_DEFORMATION_TOOL,
                  PIKA_TYPE_N_POINT_DEFORMATION_OPTIONS,
                  pika_n_point_deformation_options_gui,
                  0,
                  "pika-n-point-deformation-tool",
                  _("N-Point Deformation"),
                  _("N-Point Deformation Tool: Rubber-like deformation of "
                    "image using points"),
                  N_("_N-Point Deformation"), "<shift>N",
                  NULL, PIKA_HELP_TOOL_N_POINT_DEFORMATION,
                  PIKA_ICON_TOOL_N_POINT_DEFORMATION,
                  data);
}

static void
pika_n_point_deformation_tool_class_init (PikaNPointDeformationToolClass *klass)
{
  PikaToolClass     *tool_class      = PIKA_TOOL_CLASS (klass);
  PikaDrawToolClass *draw_tool_class = PIKA_DRAW_TOOL_CLASS (klass);

  tool_class->options_notify = pika_n_point_deformation_tool_options_notify;
  tool_class->button_press   = pika_n_point_deformation_tool_button_press;
  tool_class->button_release = pika_n_point_deformation_tool_button_release;
  tool_class->key_press      = pika_n_point_deformation_tool_key_press;
  tool_class->modifier_key   = pika_n_point_deformation_tool_modifier_key;
  tool_class->control        = pika_n_point_deformation_tool_control;
  tool_class->motion         = pika_n_point_deformation_tool_motion;
  tool_class->oper_update    = pika_n_point_deformation_tool_oper_update;
  tool_class->cursor_update  = pika_n_point_deformation_tool_cursor_update;

  draw_tool_class->draw      = pika_n_point_deformation_tool_draw;
}

static void
pika_n_point_deformation_tool_init (PikaNPointDeformationTool *npd_tool)
{
  PikaTool *tool = PIKA_TOOL (npd_tool);

  pika_tool_control_set_precision            (tool->control,
                                              PIKA_CURSOR_PRECISION_SUBPIXEL);
  pika_tool_control_set_tool_cursor          (tool->control,
                                              PIKA_TOOL_CURSOR_PERSPECTIVE);
  pika_tool_control_set_preserve             (tool->control, FALSE);
  pika_tool_control_set_wants_click          (tool->control, TRUE);
  pika_tool_control_set_wants_all_key_events (tool->control, TRUE);
  pika_tool_control_set_handle_empty_image   (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);
}

static void
pika_n_point_deformation_tool_control (PikaTool       *tool,
                                       PikaToolAction  action,
                                       PikaDisplay    *display)
{
  PikaNPointDeformationTool *npd_tool = PIKA_N_POINT_DEFORMATION_TOOL (tool);

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

    case PIKA_TOOL_ACTION_HALT:
      pika_n_point_deformation_tool_halt (npd_tool);
      break;

    case PIKA_TOOL_ACTION_COMMIT:
      pika_n_point_deformation_tool_commit (npd_tool);
      break;
    }

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

static void
pika_n_point_deformation_tool_start (PikaNPointDeformationTool *npd_tool,
                                     PikaDisplay               *display)
{
  PikaTool                     *tool = PIKA_TOOL (npd_tool);
  PikaNPointDeformationOptions *npd_options;
  PikaImage                    *image;
  GeglBuffer                   *source_buffer;
  GeglBuffer                   *preview_buffer;
  NPDModel                     *model;

  npd_options = PIKA_N_POINT_DEFORMATION_TOOL_GET_OPTIONS (npd_tool);

  pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, display);

  image = pika_display_get_image (display);

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

  npd_tool->active = TRUE;

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

  /* create GEGL graph */
  source_buffer  = pika_drawable_get_buffer (tool->drawables->data);

  preview_buffer = gegl_buffer_new (gegl_buffer_get_extent (source_buffer),
                                    babl_format ("cairo-ARGB32"));

  npd_tool->graph    = gegl_node_new ();

  npd_tool->source   = gegl_node_new_child (npd_tool->graph,
                                            "operation", "gegl:buffer-source",
                                            "buffer",    source_buffer,
                                            NULL);
  npd_tool->npd_node = gegl_node_new_child (npd_tool->graph,
                                            "operation", "gegl:npd",
                                            NULL);
  npd_tool->sink     = gegl_node_new_child (npd_tool->graph,
                                            "operation", "gegl:write-buffer",
                                            "buffer",    preview_buffer,
                                            NULL);

  gegl_node_link_many (npd_tool->source,
                       npd_tool->npd_node,
                       npd_tool->sink,
                       NULL);

  /* initialize some options */
  g_object_set (G_OBJECT (npd_options), "mesh-visible", TRUE, NULL);
  pika_n_point_deformation_options_set_sensitivity (npd_options, TRUE);

  /* compute and get model */
  gegl_node_process (npd_tool->npd_node);
  gegl_node_get (npd_tool->npd_node, "model", &model, NULL);

  npd_tool->model          = model;
  npd_tool->preview_buffer = preview_buffer;
  npd_tool->selected_cp    = NULL;
  npd_tool->hovering_cp    = NULL;
  npd_tool->selected_cps   = NULL;
  npd_tool->rubber_band    = FALSE;
  npd_tool->lattice_points = g_new (PikaVector2,
                                    5 * model->hidden_model->num_of_bones);

  pika_item_get_offset (PIKA_ITEM (tool->drawables->data),
                        &npd_tool->offset_x, &npd_tool->offset_y);
  pika_npd_debug (("offset: %f %f\n", npd_tool->offset_x, npd_tool->offset_y));

  pika_draw_tool_start (PIKA_DRAW_TOOL (npd_tool), display);

  pika_n_point_deformation_tool_perform_deformation (npd_tool);

  /* hide original image */
  pika_item_set_visible (PIKA_ITEM (tool->drawables->data), FALSE, FALSE);
  pika_image_flush (image);

  /* create and start a deformation thread */
  npd_tool->deform_thread =
    g_thread_new ("deform thread",
                  (GThreadFunc) pika_n_point_deformation_tool_deform_thread_func,
                  npd_tool);

  /* create and start canvas update timeout */
  npd_tool->draw_timeout_id =
    gdk_threads_add_timeout_full (G_PRIORITY_DEFAULT_IDLE,
                                  PIKA_NPD_DRAW_INTERVAL,
                                  (GSourceFunc) pika_n_point_deformation_tool_canvas_update_timeout,
                                  npd_tool,
                                  NULL);
}

static void
pika_n_point_deformation_tool_commit (PikaNPointDeformationTool *npd_tool)
{
  PikaTool *tool = PIKA_TOOL (npd_tool);

  if (npd_tool->active)
    {
      pika_n_point_deformation_tool_halt_threads (npd_tool);

      pika_tool_control_push_preserve (tool->control, TRUE);
      pika_n_point_deformation_tool_apply_deformation (npd_tool);
      pika_tool_control_pop_preserve (tool->control);

      /* show original/deformed image */
      pika_item_set_visible (PIKA_ITEM (tool->drawables->data), TRUE, FALSE);
      pika_image_flush (pika_display_get_image (tool->display));

      npd_tool->active = FALSE;
    }
}

static void
pika_n_point_deformation_tool_halt (PikaNPointDeformationTool *npd_tool)
{
  PikaTool                     *tool      = PIKA_TOOL (npd_tool);
  PikaDrawTool                 *draw_tool = PIKA_DRAW_TOOL (npd_tool);
  PikaNPointDeformationOptions *npd_options;

  npd_options = PIKA_N_POINT_DEFORMATION_TOOL_GET_OPTIONS (npd_tool);

  if (npd_tool->active)
    {
      pika_n_point_deformation_tool_halt_threads (npd_tool);

      /* show original/deformed image */
      pika_item_set_visible (PIKA_ITEM (tool->drawables->data), TRUE, FALSE);
      pika_image_flush (pika_display_get_image (tool->display));

      /* disable some options */
      pika_n_point_deformation_options_set_sensitivity (npd_options, FALSE);

      npd_tool->active = FALSE;
    }

  if (pika_draw_tool_is_active (draw_tool))
    pika_draw_tool_stop (draw_tool);

  pika_n_point_deformation_tool_clear_selected_points_list (npd_tool);

  g_clear_object (&npd_tool->graph);
  npd_tool->source   = NULL;
  npd_tool->npd_node = NULL;
  npd_tool->sink     = NULL;

  g_clear_object (&npd_tool->preview_buffer);
  g_clear_pointer (&npd_tool->lattice_points, g_free);

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

static void
pika_n_point_deformation_tool_set_options (PikaNPointDeformationTool    *npd_tool,
                                           PikaNPointDeformationOptions *npd_options)
{
  gegl_node_set (npd_tool->npd_node,
                 "square-size",       (gint) npd_options->square_size,
                 "rigidity",          (gint) npd_options->rigidity,
                 "asap-deformation",  npd_options->asap_deformation,
                 "mls-weights",       npd_options->mls_weights,
                 "mls-weights-alpha", npd_options->mls_weights_alpha,
                 NULL);
}

static void
pika_n_point_deformation_tool_options_notify (PikaTool         *tool,
                                              PikaToolOptions  *options,
                                              const GParamSpec *pspec)
{
  PikaNPointDeformationTool    *npd_tool    = PIKA_N_POINT_DEFORMATION_TOOL (tool);
  PikaNPointDeformationOptions *npd_options = PIKA_N_POINT_DEFORMATION_OPTIONS (options);
  PikaDrawTool                 *draw_tool   = PIKA_DRAW_TOOL (tool);

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

  if (! npd_tool->active)
    return;

  pika_draw_tool_pause (draw_tool);

  pika_npd_debug (("npd options notify\n"));
  pika_n_point_deformation_tool_set_options (npd_tool, npd_options);

  pika_draw_tool_resume (draw_tool);
}

static gboolean
pika_n_point_deformation_tool_key_press (PikaTool    *tool,
                                         GdkEventKey *kevent,
                                         PikaDisplay *display)
{
  PikaNPointDeformationTool *npd_tool = PIKA_N_POINT_DEFORMATION_TOOL (tool);

  switch (kevent->keyval)
    {
    case GDK_KEY_BackSpace:
      /* if there is at least one control point, remove last added
       *  control point
       */
      if (npd_tool->model                 &&
          npd_tool->model->control_points &&
          npd_tool->model->control_points->len > 0)
        {
          GArray          *cps = npd_tool->model->control_points;
          NPDControlPoint *cp  = &g_array_index (cps, NPDControlPoint,
                                                 cps->len - 1);

          pika_npd_debug (("removing last cp %p\n", cp));
          pika_n_point_deformation_tool_remove_cp_from_selection (npd_tool, cp);
          npd_remove_control_point (npd_tool->model, cp);
        }
      break;

    case GDK_KEY_Delete:
      if (npd_tool->model &&
          npd_tool->selected_cps)
        {
          /* if there is at least one selected control point, remove it */
          npd_remove_control_points (npd_tool->model, npd_tool->selected_cps);
          pika_n_point_deformation_tool_clear_selected_points_list (npd_tool);
        }
      break;

    case GDK_KEY_Return:
    case GDK_KEY_KP_Enter:
    case GDK_KEY_ISO_Enter:
      pika_tool_control (tool, PIKA_TOOL_ACTION_COMMIT, display);
      break;

    case GDK_KEY_Escape:
      pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, display);
      break;

    default:
      return FALSE;
  }

  return TRUE;
}

static void
pika_n_point_deformation_tool_modifier_key (PikaTool        *tool,
                                            GdkModifierType  key,
                                            gboolean         press,
                                            GdkModifierType  state,
                                            PikaDisplay     *display)
{
}

static void
pika_n_point_deformation_tool_cursor_update (PikaTool         *tool,
                                             const PikaCoords *coords,
                                             GdkModifierType   state,
                                             PikaDisplay      *display)
{
  PikaNPointDeformationTool *npd_tool = PIKA_N_POINT_DEFORMATION_TOOL (tool);
  PikaCursorModifier         modifier = PIKA_CURSOR_MODIFIER_PLUS;

  if (! npd_tool->active)
    {
      modifier = PIKA_CURSOR_MODIFIER_NONE;
    }
  else if (npd_tool->hovering_cp)
    {
      modifier = PIKA_CURSOR_MODIFIER_MOVE;
    }

  pika_tool_control_set_cursor_modifier (tool->control, modifier);

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

static void
pika_n_point_deformation_tool_clear_selected_points_list (PikaNPointDeformationTool *npd_tool)
{
  if (npd_tool->selected_cps)
    {
      g_list_free (npd_tool->selected_cps);
      npd_tool->selected_cps = NULL;
    }
}

static gboolean
pika_n_point_deformation_tool_add_cp_to_selection (PikaNPointDeformationTool *npd_tool,
                                                   NPDControlPoint           *cp)
{
  if (! g_list_find (npd_tool->selected_cps, cp))
    {
      npd_tool->selected_cps = g_list_append (npd_tool->selected_cps, cp);

      return TRUE;
    }

  return FALSE;
}

static void
pika_n_point_deformation_tool_remove_cp_from_selection (PikaNPointDeformationTool *npd_tool,
                                                        NPDControlPoint           *cp)
{
  npd_tool->selected_cps = g_list_remove (npd_tool->selected_cps, cp);
}

static void
pika_n_point_deformation_tool_button_press (PikaTool            *tool,
                                            const PikaCoords    *coords,
                                            guint32              time,
                                            GdkModifierType      state,
                                            PikaButtonPressType  press_type,
                                            PikaDisplay         *display)
{
  PikaNPointDeformationTool *npd_tool = PIKA_N_POINT_DEFORMATION_TOOL (tool);

  if (display != tool->display)
    {
      /* this is the first click on the drawable - just start the tool */
      pika_n_point_deformation_tool_start (npd_tool, display);
    }

  npd_tool->selected_cp = NULL;

  if (press_type == PIKA_BUTTON_PRESS_NORMAL)
    {
      NPDControlPoint *cp = npd_tool->hovering_cp;

      if (cp)
        {
          /* there is a control point at cursor's position */
          npd_tool->selected_cp = cp;

          if (! g_list_find (npd_tool->selected_cps, cp))
            {
              /* control point isn't selected, so we can add it to the
               * list of selected control points
               */

              if (! (state & pika_get_extend_selection_mask ()))
                {
                  /* <SHIFT> isn't pressed, so this isn't a
                   * multiselection - clear the list of selected
                   * control points
                   */
                  pika_n_point_deformation_tool_clear_selected_points_list (npd_tool);
                }

              pika_n_point_deformation_tool_add_cp_to_selection (npd_tool, cp);
            }
          else if (state & pika_get_extend_selection_mask ())
            {
              /* control point is selected and <SHIFT> is pressed -
               * remove control point from selected points
               */
              pika_n_point_deformation_tool_remove_cp_from_selection (npd_tool,
                                                                      cp);
            }
        }

      npd_tool->start_x = coords->x;
      npd_tool->start_y = coords->y;

      npd_tool->last_x = coords->x;
      npd_tool->last_y = coords->y;
    }

  pika_tool_control_activate (tool->control);
}

static gboolean
pika_n_point_deformation_tool_is_cp_in_area (NPDControlPoint *cp,
                                             gfloat           x0,
                                             gfloat           y0,
                                             gfloat           x1,
                                             gfloat           y1,
                                             gfloat           offset_x,
                                             gfloat           offset_y,
                                             gfloat           cp_radius)
{
  NPDPoint p = cp->point;

  p.x += offset_x;
  p.y += offset_y;

  return p.x >= x0 - cp_radius && p.x <= x1 + cp_radius &&
         p.y >= y0 - cp_radius && p.y <= y1 + cp_radius;
}

static void
pika_n_point_deformation_tool_button_release (PikaTool              *tool,
                                              const PikaCoords      *coords,
                                              guint32                time,
                                              GdkModifierType        state,
                                              PikaButtonReleaseType  release_type,
                                              PikaDisplay           *display)
{
  PikaNPointDeformationTool *npd_tool = PIKA_N_POINT_DEFORMATION_TOOL (tool);

  pika_tool_control_halt (tool->control);

  pika_draw_tool_pause (PIKA_DRAW_TOOL (npd_tool));

  if (release_type == PIKA_BUTTON_RELEASE_CLICK)
    {
      if (! npd_tool->hovering_cp)
        {
          NPDPoint p;

          pika_npd_debug (("cp doesn't exist, adding\n"));
          p.x = coords->x - npd_tool->offset_x;
          p.y = coords->y - npd_tool->offset_y;

          npd_add_control_point (npd_tool->model, &p);
        }
    }
  else if (release_type == PIKA_BUTTON_RELEASE_NORMAL)
    {
      if (npd_tool->rubber_band)
        {
          GArray *cps = npd_tool->model->control_points;
          gint    x0  = MIN (npd_tool->start_x, npd_tool->cursor_x);
          gint    y0  = MIN (npd_tool->start_y, npd_tool->cursor_y);
          gint    x1  = MAX (npd_tool->start_x, npd_tool->cursor_x);
          gint    y1  = MAX (npd_tool->start_y, npd_tool->cursor_y);
          gint    i;

          if (! (state & pika_get_extend_selection_mask ()))
            {
              /* <SHIFT> isn't pressed, so we want a clear selection */
              pika_n_point_deformation_tool_clear_selected_points_list (npd_tool);
            }

          for (i = 0; i < cps->len; i++)
            {
              NPDControlPoint *cp = &g_array_index (cps, NPDControlPoint, i);

              if (pika_n_point_deformation_tool_is_cp_in_area (cp,
                                                               x0, y0,
                                                               x1, y1,
                                                               npd_tool->offset_x,
                                                               npd_tool->offset_y,
                                                               npd_tool->cp_scaled_radius))
                {
                  /* control point is situated in an area defined by
                   * rubber band
                  */
                  pika_n_point_deformation_tool_add_cp_to_selection (npd_tool,
                                                                     cp);
                  pika_npd_debug (("%p selected\n", cp));
                }
            }
        }
    }
  else if (release_type == PIKA_BUTTON_RELEASE_CANCEL)
    {
      pika_npd_debug (("pika_button_release_cancel\n"));
    }

  npd_tool->rubber_band = FALSE;

  pika_draw_tool_resume (PIKA_DRAW_TOOL (npd_tool));
}

static void
pika_n_point_deformation_tool_oper_update (PikaTool         *tool,
                                           const PikaCoords *coords,
                                           GdkModifierType   state,
                                           gboolean          proximity,
                                           PikaDisplay      *display)
{
  PikaNPointDeformationTool *npd_tool  = PIKA_N_POINT_DEFORMATION_TOOL (tool);
  PikaDrawTool              *draw_tool = PIKA_DRAW_TOOL (tool);

  pika_draw_tool_pause (draw_tool);

  if (npd_tool->active)
    {
      NPDModel         *model = npd_tool->model;
      PikaDisplayShell *shell = pika_display_get_shell (display);
      NPDPoint          p;

      npd_tool->cp_scaled_radius = model->control_point_radius / shell->scale_x;

      p.x = coords->x - npd_tool->offset_x;
      p.y = coords->y - npd_tool->offset_y;

      npd_tool->hovering_cp =
        npd_get_control_point_with_radius_at (model,
                                              &p,
                                              npd_tool->cp_scaled_radius);
    }

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

  pika_draw_tool_resume (draw_tool);
}

static void
pika_n_point_deformation_tool_prepare_lattice (PikaNPointDeformationTool *npd_tool)
{
  NPDHiddenModel *hm     = npd_tool->model->hidden_model;
  PikaVector2    *points = npd_tool->lattice_points;
  gint            i, j;

  for (i = 0; i < hm->num_of_bones; i++)
    {
      NPDBone *bone = &hm->current_bones[i];

      for (j = 0; j < 4; j++)
        pika_vector2_set (&points[5 * i + j], bone->points[j].x, bone->points[j].y);
      pika_vector2_set (&points[5 * i + j], bone->points[0].x, bone->points[0].y);
    }
}

static void
pika_n_point_deformation_tool_draw_lattice (PikaNPointDeformationTool *npd_tool)
{
  PikaVector2 *points    = npd_tool->lattice_points;
  gint         n_squares = npd_tool->model->hidden_model->num_of_bones;
  gint         i;

  for (i = 0; i < n_squares; i++)
    pika_draw_tool_add_lines (PIKA_DRAW_TOOL (npd_tool),
                              &points[5 * i], 5, NULL, FALSE);
}

static void
pika_n_point_deformation_tool_draw (PikaDrawTool *draw_tool)
{
  PikaNPointDeformationTool    *npd_tool;
  PikaNPointDeformationOptions *npd_options;
  NPDModel                     *model;
  gint                          x0, y0, x1, y1;
  gint                          i;

  npd_tool    = PIKA_N_POINT_DEFORMATION_TOOL (draw_tool);
  npd_options = PIKA_N_POINT_DEFORMATION_TOOL_GET_OPTIONS (npd_tool);

  model = npd_tool->model;

  g_return_if_fail (model != NULL);

  /* draw lattice */
  if (npd_options->mesh_visible)
    pika_n_point_deformation_tool_draw_lattice (npd_tool);

  x0 = MIN (npd_tool->start_x, npd_tool->cursor_x);
  y0 = MIN (npd_tool->start_y, npd_tool->cursor_y);
  x1 = MAX (npd_tool->start_x, npd_tool->cursor_x);
  y1 = MAX (npd_tool->start_y, npd_tool->cursor_y);

  for (i = 0; i < model->control_points->len; i++)
    {
      NPDControlPoint *cp = &g_array_index (model->control_points,
                                            NPDControlPoint, i);
      NPDPoint         p  = cp->point;
      PikaHandleType   handle_type;

      p.x += npd_tool->offset_x;
      p.y += npd_tool->offset_y;

      handle_type = PIKA_HANDLE_CIRCLE;

      /* check if cursor is hovering over a control point or if a
       * control point is situated in an area defined by rubber band
       */
      if (cp == npd_tool->hovering_cp ||
          (npd_tool->rubber_band &&
           pika_n_point_deformation_tool_is_cp_in_area (cp,
                                                        x0, y0,
                                                        x1, y1,
                                                        npd_tool->offset_x,
                                                        npd_tool->offset_y,
                                                        npd_tool->cp_scaled_radius)))
        {
          handle_type = PIKA_HANDLE_FILLED_CIRCLE;
        }

      pika_draw_tool_add_handle (draw_tool,
                                 handle_type,
                                 p.x, p.y,
                                 PIKA_TOOL_HANDLE_SIZE_CIRCLE,
                                 PIKA_TOOL_HANDLE_SIZE_CIRCLE,
                                 PIKA_HANDLE_ANCHOR_CENTER);

      if (g_list_find (npd_tool->selected_cps, cp))
        {
          pika_draw_tool_add_handle (draw_tool,
                                     PIKA_HANDLE_SQUARE,
                                     p.x, p.y,
                                     PIKA_TOOL_HANDLE_SIZE_CIRCLE,
                                     PIKA_TOOL_HANDLE_SIZE_CIRCLE,
                                     PIKA_HANDLE_ANCHOR_CENTER);
        }
    }

  if (npd_tool->rubber_band)
    {
      /* draw a rubber band */
      pika_draw_tool_add_rectangle (draw_tool, FALSE,
                                    x0, y0, x1 - x0, y1 - y0);
    }

  if (npd_tool->preview_buffer)
    {
      PikaCanvasItem *item;

      item = pika_canvas_buffer_preview_new (pika_display_get_shell (draw_tool->display),
                                             npd_tool->preview_buffer);

      pika_draw_tool_add_preview (draw_tool, item);
      g_object_unref (item);
    }

  PIKA_DRAW_TOOL_CLASS (parent_class)->draw (draw_tool);
}

static void
pika_n_point_deformation_tool_motion (PikaTool         *tool,
                                      const PikaCoords *coords,
                                      guint32           time,
                                      GdkModifierType   state,
                                      PikaDisplay      *display)
{
  PikaNPointDeformationTool *npd_tool  = PIKA_N_POINT_DEFORMATION_TOOL (tool);
  PikaDrawTool              *draw_tool = PIKA_DRAW_TOOL (tool);

  pika_draw_tool_pause (draw_tool);

  if (npd_tool->selected_cp)
    {
      GList   *list;
      gdouble  shift_x = coords->x - npd_tool->last_x;
      gdouble  shift_y = coords->y - npd_tool->last_y;

      for (list = npd_tool->selected_cps; list; list = g_list_next (list))
        {
          NPDControlPoint *cp = list->data;

          cp->point.x += shift_x;
          cp->point.y += shift_y;
        }
    }
  else
    {
      /* activate a rubber band selection */
      npd_tool->rubber_band = TRUE;
    }

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

  npd_tool->last_x = coords->x;
  npd_tool->last_y = coords->y;

  pika_draw_tool_resume (draw_tool);
}

static gboolean
pika_n_point_deformation_tool_canvas_update_timeout (PikaNPointDeformationTool *npd_tool)
{
  if (! PIKA_TOOL (npd_tool)->drawables->data)
    return FALSE;

  pika_npd_debug (("canvas update thread\n"));

  pika_draw_tool_pause (PIKA_DRAW_TOOL(npd_tool));
  pika_draw_tool_resume (PIKA_DRAW_TOOL(npd_tool));

  pika_npd_debug (("canvas update thread stop\n"));

  return TRUE;
}

static gpointer
pika_n_point_deformation_tool_deform_thread_func (gpointer data)
{
  PikaNPointDeformationTool    *npd_tool = data;
  PikaNPointDeformationOptions *npd_options;
  guint64                       start, duration;

  npd_options = PIKA_N_POINT_DEFORMATION_TOOL_GET_OPTIONS (npd_tool);

  npd_tool->deformation_active = TRUE;

  while (npd_tool->deformation_active)
    {
      start = g_get_monotonic_time ();

      pika_n_point_deformation_tool_perform_deformation (npd_tool);

      if (npd_options->mesh_visible)
        pika_n_point_deformation_tool_prepare_lattice (npd_tool);

      duration = g_get_monotonic_time () - start;
      if (duration < PIKA_NPD_MAXIMUM_DEFORMATION_DELAY)
        {
          g_usleep (PIKA_NPD_MAXIMUM_DEFORMATION_DELAY - duration);
        }
    }

  pika_npd_debug (("deform thread exit\n"));

  return NULL;
}

static void
pika_n_point_deformation_tool_perform_deformation (PikaNPointDeformationTool *npd_tool)
{
  GObject *operation;

  g_object_get (npd_tool->npd_node,
                "gegl-operation", &operation,
                NULL);
  pika_npd_debug (("gegl_operation_invalidate\n"));
  gegl_operation_invalidate (GEGL_OPERATION (operation), NULL, FALSE);
  g_object_unref (operation);

  pika_npd_debug (("gegl_node_process\n"));
  gegl_node_process (npd_tool->sink);
}

static void
pika_n_point_deformation_tool_halt_threads (PikaNPointDeformationTool *npd_tool)
{
  if (! npd_tool->deformation_active)
    return;

  pika_npd_debug (("waiting for deform thread to finish\n"));
  npd_tool->deformation_active = FALSE;

  /* wait for deformation thread to finish its work */
  if (npd_tool->deform_thread)
    {
      g_thread_join (npd_tool->deform_thread);
      npd_tool->deform_thread = NULL;
    }

  /* remove canvas update timeout */
  if (npd_tool->draw_timeout_id)
    {
      g_source_remove (npd_tool->draw_timeout_id);
      npd_tool->draw_timeout_id = 0;
    }

  pika_npd_debug (("finished\n"));
}

static void
pika_n_point_deformation_tool_apply_deformation (PikaNPointDeformationTool *npd_tool)
{
  PikaTool                     *tool = PIKA_TOOL (npd_tool);
  PikaNPointDeformationOptions *npd_options;
  GeglBuffer                   *buffer;
  PikaImage                    *image;
  gint                          width, height, prev;

  npd_options = PIKA_N_POINT_DEFORMATION_TOOL_GET_OPTIONS (npd_tool);

  image  = pika_display_get_image (tool->display);
  buffer = pika_drawable_get_buffer (tool->drawables->data);

  width  = gegl_buffer_get_width  (buffer);
  height = gegl_buffer_get_height (buffer);

  prev = npd_options->rigidity;
  npd_options->rigidity = 0;
  pika_n_point_deformation_tool_set_options (npd_tool, npd_options);
  npd_options->rigidity = prev;

  pika_drawable_push_undo (tool->drawables->data, _("N-Point Deformation"), NULL,
                           0, 0, width, height);


  pika_gegl_apply_operation (NULL, NULL, _("N-Point Deformation"),
                             npd_tool->npd_node,
                             pika_drawable_get_buffer (tool->drawables->data),
                             NULL, FALSE);

  pika_drawable_update (tool->drawables->data,
                        0, 0, width, height);

  pika_projection_flush (pika_image_get_projection (image));
}

#undef pika_npd_debug
#ifdef PIKA_NPD_DEBUG
#undef PIKA_NPD_DEBUG
#endif
