/* 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
 *
 * pikastroke.c
 * Copyright (C) 2002 Simon Budig  <simon@gimp.org>
 *
 * 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 <cairo.h>
#include <gegl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"

#include "vectors-types.h"

#include "core/pika-memsize.h"
#include "core/pikacoords.h"
#include "core/pikaparamspecs.h"
#include "core/pika-transform-utils.h"

#include "pikaanchor.h"
#include "pikastroke.h"

enum
{
  PROP_0,
  PROP_CONTROL_POINTS,
  PROP_CLOSED
};

/* Prototypes */

static void    pika_stroke_set_property              (GObject      *object,
                                                      guint         property_id,
                                                      const GValue *value,
                                                      GParamSpec   *pspec);
static void    pika_stroke_get_property              (GObject      *object,
                                                      guint         property_id,
                                                      GValue       *value,
                                                      GParamSpec   *pspec);
static void    pika_stroke_finalize                  (GObject      *object);

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

static PikaAnchor * pika_stroke_real_anchor_get      (PikaStroke       *stroke,
                                                      const PikaCoords *coord);
static PikaAnchor * pika_stroke_real_anchor_get_next (PikaStroke       *stroke,
                                                      const PikaAnchor *prev);
static void         pika_stroke_real_anchor_select   (PikaStroke       *stroke,
                                                      PikaAnchor       *anchor,
                                                      gboolean          selected,
                                                      gboolean          exclusive);
static void    pika_stroke_real_anchor_move_relative (PikaStroke       *stroke,
                                                      PikaAnchor       *anchor,
                                                      const PikaCoords *delta,
                                                      PikaAnchorFeatureType feature);
static void    pika_stroke_real_anchor_move_absolute (PikaStroke       *stroke,
                                                      PikaAnchor       *anchor,
                                                      const PikaCoords *delta,
                                                      PikaAnchorFeatureType feature);
static void         pika_stroke_real_anchor_convert  (PikaStroke       *stroke,
                                                      PikaAnchor       *anchor,
                                                      PikaAnchorFeatureType  feature);
static void         pika_stroke_real_anchor_delete   (PikaStroke       *stroke,
                                                      PikaAnchor       *anchor);
static gboolean     pika_stroke_real_point_is_movable
                                              (PikaStroke            *stroke,
                                               PikaAnchor            *predec,
                                               gdouble                position);
static void         pika_stroke_real_point_move_relative
                                              (PikaStroke            *stroke,
                                               PikaAnchor            *predec,
                                               gdouble                position,
                                               const PikaCoords      *deltacoord,
                                               PikaAnchorFeatureType  feature);
static void         pika_stroke_real_point_move_absolute
                                              (PikaStroke            *stroke,
                                               PikaAnchor            *predec,
                                               gdouble                position,
                                               const PikaCoords      *coord,
                                               PikaAnchorFeatureType  feature);

static void         pika_stroke_real_close           (PikaStroke       *stroke);
static PikaStroke * pika_stroke_real_open            (PikaStroke       *stroke,
                                                      PikaAnchor       *end_anchor);
static gboolean     pika_stroke_real_anchor_is_insertable
                                                     (PikaStroke       *stroke,
                                                      PikaAnchor       *predec,
                                                      gdouble           position);
static PikaAnchor * pika_stroke_real_anchor_insert   (PikaStroke       *stroke,
                                                      PikaAnchor       *predec,
                                                      gdouble           position);

static gboolean     pika_stroke_real_is_extendable   (PikaStroke       *stroke,
                                                      PikaAnchor       *neighbor);

static PikaAnchor * pika_stroke_real_extend (PikaStroke           *stroke,
                                             const PikaCoords     *coords,
                                             PikaAnchor           *neighbor,
                                             PikaVectorExtendMode  extend_mode);

gboolean     pika_stroke_real_connect_stroke (PikaStroke          *stroke,
                                              PikaAnchor          *anchor,
                                              PikaStroke          *extension,
                                              PikaAnchor          *neighbor);


static gboolean     pika_stroke_real_is_empty        (PikaStroke       *stroke);
static gboolean     pika_stroke_real_reverse         (PikaStroke       *stroke);
static gboolean     pika_stroke_real_shift_start     (PikaStroke       *stroke,
                                                      PikaAnchor       *anchor);

static gdouble      pika_stroke_real_get_length      (PikaStroke       *stroke,
                                                      gdouble           precision);
static gdouble      pika_stroke_real_get_distance    (PikaStroke       *stroke,
                                                      const PikaCoords *coord);
static GArray *     pika_stroke_real_interpolate     (PikaStroke       *stroke,
                                                      gdouble           precision,
                                                      gboolean         *closed);
static PikaStroke * pika_stroke_real_duplicate       (PikaStroke       *stroke);
static PikaBezierDesc * pika_stroke_real_make_bezier (PikaStroke       *stroke);

static void         pika_stroke_real_translate       (PikaStroke       *stroke,
                                                      gdouble           offset_x,
                                                      gdouble           offset_y);
static void         pika_stroke_real_scale           (PikaStroke       *stroke,
                                                      gdouble           scale_x,
                                                      gdouble           scale_y);
static void         pika_stroke_real_rotate          (PikaStroke       *stroke,
                                                      gdouble           center_x,
                                                      gdouble           center_y,
                                                      gdouble           angle);
static void         pika_stroke_real_flip            (PikaStroke       *stroke,
                                                      PikaOrientationType flip_type,
                                                      gdouble           axis);
static void         pika_stroke_real_flip_free       (PikaStroke       *stroke,
                                                      gdouble           x1,
                                                      gdouble           y1,
                                                      gdouble           x2,
                                                      gdouble           y2);
static void         pika_stroke_real_transform       (PikaStroke        *stroke,
                                                      const PikaMatrix3 *matrix,
                                                      GQueue            *ret_strokes);

static GList    * pika_stroke_real_get_draw_anchors  (PikaStroke       *stroke);
static GList    * pika_stroke_real_get_draw_controls (PikaStroke       *stroke);
static GArray   * pika_stroke_real_get_draw_lines    (PikaStroke       *stroke);
static GArray *  pika_stroke_real_control_points_get (PikaStroke       *stroke,
                                                      gboolean         *ret_closed);
static gboolean   pika_stroke_real_get_point_at_dist (PikaStroke       *stroke,
                                                      gdouble           dist,
                                                      gdouble           precision,
                                                      PikaCoords       *position,
                                                      gdouble          *slope);


G_DEFINE_TYPE (PikaStroke, pika_stroke, PIKA_TYPE_OBJECT)

#define parent_class pika_stroke_parent_class


static void
pika_stroke_class_init (PikaStrokeClass *klass)
{
  GObjectClass    *object_class      = G_OBJECT_CLASS (klass);
  PikaObjectClass *pika_object_class = PIKA_OBJECT_CLASS (klass);
  GParamSpec      *param_spec;

  object_class->finalize          = pika_stroke_finalize;
  object_class->get_property      = pika_stroke_get_property;
  object_class->set_property      = pika_stroke_set_property;

  pika_object_class->get_memsize  = pika_stroke_get_memsize;

  klass->changed                  = NULL;
  klass->removed                  = NULL;

  klass->anchor_get               = pika_stroke_real_anchor_get;
  klass->anchor_get_next          = pika_stroke_real_anchor_get_next;
  klass->anchor_select            = pika_stroke_real_anchor_select;
  klass->anchor_move_relative     = pika_stroke_real_anchor_move_relative;
  klass->anchor_move_absolute     = pika_stroke_real_anchor_move_absolute;
  klass->anchor_convert           = pika_stroke_real_anchor_convert;
  klass->anchor_delete            = pika_stroke_real_anchor_delete;

  klass->point_is_movable         = pika_stroke_real_point_is_movable;
  klass->point_move_relative      = pika_stroke_real_point_move_relative;
  klass->point_move_absolute      = pika_stroke_real_point_move_absolute;

  klass->nearest_point_get        = NULL;
  klass->nearest_tangent_get      = NULL;
  klass->nearest_intersection_get = NULL;
  klass->close                    = pika_stroke_real_close;
  klass->open                     = pika_stroke_real_open;
  klass->anchor_is_insertable     = pika_stroke_real_anchor_is_insertable;
  klass->anchor_insert            = pika_stroke_real_anchor_insert;
  klass->is_extendable            = pika_stroke_real_is_extendable;
  klass->extend                   = pika_stroke_real_extend;
  klass->connect_stroke           = pika_stroke_real_connect_stroke;

  klass->is_empty                 = pika_stroke_real_is_empty;
  klass->reverse                  = pika_stroke_real_reverse;
  klass->shift_start              = pika_stroke_real_shift_start;
  klass->get_length               = pika_stroke_real_get_length;
  klass->get_distance             = pika_stroke_real_get_distance;
  klass->get_point_at_dist        = pika_stroke_real_get_point_at_dist;
  klass->interpolate              = pika_stroke_real_interpolate;

  klass->duplicate                = pika_stroke_real_duplicate;
  klass->make_bezier              = pika_stroke_real_make_bezier;

  klass->translate                = pika_stroke_real_translate;
  klass->scale                    = pika_stroke_real_scale;
  klass->rotate                   = pika_stroke_real_rotate;
  klass->flip                     = pika_stroke_real_flip;
  klass->flip_free                = pika_stroke_real_flip_free;
  klass->transform                = pika_stroke_real_transform;


  klass->get_draw_anchors         = pika_stroke_real_get_draw_anchors;
  klass->get_draw_controls        = pika_stroke_real_get_draw_controls;
  klass->get_draw_lines           = pika_stroke_real_get_draw_lines;
  klass->control_points_get       = pika_stroke_real_control_points_get;

  param_spec = g_param_spec_boxed ("pika-anchor",
                                   "Pika Anchor",
                                   "The control points of a Stroke",
                                   PIKA_TYPE_ANCHOR,
                                   PIKA_PARAM_WRITABLE |
                                   G_PARAM_CONSTRUCT_ONLY);
  g_object_class_install_property (object_class, PROP_CONTROL_POINTS,
                                   pika_param_spec_value_array ("control-points",
                                                                "Control Points",
                                                                "This is an ValueArray "
                                                                "with the initial "
                                                                "control points of "
                                                                "the new Stroke",
                                                                param_spec,
                                                                PIKA_PARAM_WRITABLE |
                                                                G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_CLOSED,
                                   g_param_spec_boolean ("closed",
                                                         "Close Flag",
                                                         "this flag indicates "
                                                         "whether the stroke "
                                                         "is closed or not",
                                                         FALSE,
                                                         PIKA_PARAM_READWRITE |
                                                         G_PARAM_CONSTRUCT_ONLY));
}

static void
pika_stroke_init (PikaStroke *stroke)
{
  stroke->anchors = g_queue_new ();
}

static void
pika_stroke_set_property (GObject      *object,
                          guint         property_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  PikaStroke     *stroke = PIKA_STROKE (object);
  PikaValueArray *val_array;
  gint            length;
  gint            i;

  switch (property_id)
    {
    case PROP_CLOSED:
      stroke->closed = g_value_get_boolean (value);
      break;

    case PROP_CONTROL_POINTS:
      g_return_if_fail (g_queue_is_empty (stroke->anchors));
      g_return_if_fail (value != NULL);

      val_array = g_value_get_boxed (value);

      if (val_array == NULL)
        return;

      length = pika_value_array_length (val_array);

      for (i = 0; i < length; i++)
        {
          GValue *item = pika_value_array_index (val_array, i);

          g_return_if_fail (G_VALUE_HOLDS (item, PIKA_TYPE_ANCHOR));
          g_queue_push_tail (stroke->anchors, g_value_dup_boxed (item));
        }

      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
pika_stroke_get_property (GObject    *object,
                          guint       property_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  PikaStroke *stroke = PIKA_STROKE (object);

  switch (property_id)
    {
    case PROP_CLOSED:
      g_value_set_boolean (value, stroke->closed);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
pika_stroke_finalize (GObject *object)
{
  PikaStroke *stroke = PIKA_STROKE (object);

  g_queue_free_full (stroke->anchors, (GDestroyNotify) pika_anchor_free);
  stroke->anchors = NULL;

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

static gint64
pika_stroke_get_memsize (PikaObject *object,
                         gint64     *gui_size)
{
  PikaStroke *stroke  = PIKA_STROKE (object);
  gint64      memsize = 0;

  memsize += pika_g_queue_get_memsize (stroke->anchors, sizeof (PikaAnchor));

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

void
pika_stroke_set_id (PikaStroke *stroke,
                    gint        id)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));
  g_return_if_fail (stroke->id == 0 /* we don't want changing IDs... */);

  stroke->id = id;
}

gint
pika_stroke_get_id (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), -1);

  return stroke->id;
}


PikaAnchor *
pika_stroke_anchor_get (PikaStroke       *stroke,
                        const PikaCoords *coord)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->anchor_get (stroke, coord);
}


gdouble
pika_stroke_nearest_point_get (PikaStroke       *stroke,
                               const PikaCoords *coord,
                               const gdouble     precision,
                               PikaCoords       *ret_point,
                               PikaAnchor      **ret_segment_start,
                               PikaAnchor      **ret_segment_end,
                               gdouble          *ret_pos)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);
  g_return_val_if_fail (coord != NULL, FALSE);

  if (PIKA_STROKE_GET_CLASS (stroke)->nearest_point_get)
    return PIKA_STROKE_GET_CLASS (stroke)->nearest_point_get (stroke,
                                                              coord,
                                                              precision,
                                                              ret_point,
                                                              ret_segment_start,
                                                              ret_segment_end,
                                                              ret_pos);
  return -1;
}

gdouble
pika_stroke_nearest_tangent_get (PikaStroke            *stroke,
                                 const PikaCoords      *coords1,
                                 const PikaCoords      *coords2,
                                 gdouble                precision,
                                 PikaCoords            *nearest,
                                 PikaAnchor           **ret_segment_start,
                                 PikaAnchor           **ret_segment_end,
                                 gdouble               *ret_pos)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);
  g_return_val_if_fail (coords1 != NULL, FALSE);
  g_return_val_if_fail (coords2 != NULL, FALSE);

  if (PIKA_STROKE_GET_CLASS (stroke)->nearest_tangent_get)
    return PIKA_STROKE_GET_CLASS (stroke)->nearest_tangent_get (stroke,
                                                                coords1,
                                                                coords2,
                                                                precision,
                                                                nearest,
                                                                ret_segment_start,
                                                                ret_segment_end,
                                                                ret_pos);
  return -1;
}

gdouble
pika_stroke_nearest_intersection_get (PikaStroke        *stroke,
                                      const PikaCoords  *coords1,
                                      const PikaCoords  *direction,
                                      gdouble            precision,
                                      PikaCoords        *nearest,
                                      PikaAnchor       **ret_segment_start,
                                      PikaAnchor       **ret_segment_end,
                                      gdouble           *ret_pos)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);
  g_return_val_if_fail (coords1 != NULL, FALSE);
  g_return_val_if_fail (direction != NULL, FALSE);

  if (PIKA_STROKE_GET_CLASS (stroke)->nearest_intersection_get)
    return PIKA_STROKE_GET_CLASS (stroke)->nearest_intersection_get (stroke,
                                                                     coords1,
                                                                     direction,
                                                                     precision,
                                                                     nearest,
                                                                     ret_segment_start,
                                                                     ret_segment_end,
                                                                     ret_pos);
  return -1;
}

static PikaAnchor *
pika_stroke_real_anchor_get (PikaStroke       *stroke,
                             const PikaCoords *coord)
{
  gdouble     dx, dy;
  gdouble     mindist = -1;
  GList      *anchors;
  GList      *list;
  PikaAnchor *anchor = NULL;

  anchors = pika_stroke_get_draw_controls (stroke);

  for (list = anchors; list; list = g_list_next (list))
    {
      dx = coord->x - PIKA_ANCHOR (list->data)->position.x;
      dy = coord->y - PIKA_ANCHOR (list->data)->position.y;

      if (mindist < 0 || mindist > dx * dx + dy * dy)
        {
          mindist = dx * dx + dy * dy;
          anchor = PIKA_ANCHOR (list->data);
        }
    }

  g_list_free (anchors);

  anchors = pika_stroke_get_draw_anchors (stroke);

  for (list = anchors; list; list = g_list_next (list))
    {
      dx = coord->x - PIKA_ANCHOR (list->data)->position.x;
      dy = coord->y - PIKA_ANCHOR (list->data)->position.y;

      if (mindist < 0 || mindist > dx * dx + dy * dy)
        {
          mindist = dx * dx + dy * dy;
          anchor = PIKA_ANCHOR (list->data);
        }
    }

  g_list_free (anchors);

  return anchor;
}


PikaAnchor *
pika_stroke_anchor_get_next (PikaStroke       *stroke,
                             const PikaAnchor *prev)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->anchor_get_next (stroke, prev);
}

static PikaAnchor *
pika_stroke_real_anchor_get_next (PikaStroke       *stroke,
                                  const PikaAnchor *prev)
{
  GList *list;

  if (prev)
    {
      list = g_queue_find (stroke->anchors, prev);
      if (list)
        list = g_list_next (list);
    }
  else
    {
      list = stroke->anchors->head;
    }

  if (list)
    return PIKA_ANCHOR (list->data);

  return NULL;
}


void
pika_stroke_anchor_select (PikaStroke *stroke,
                           PikaAnchor *anchor,
                           gboolean    selected,
                           gboolean    exclusive)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->anchor_select (stroke, anchor,
                                                 selected, exclusive);
}

static void
pika_stroke_real_anchor_select (PikaStroke *stroke,
                                PikaAnchor *anchor,
                                gboolean    selected,
                                gboolean    exclusive)
{
  GList *list = stroke->anchors->head;

  if (exclusive)
    {
      while (list)
        {
          PIKA_ANCHOR (list->data)->selected = FALSE;
          list = g_list_next (list);
        }
    }

  list = g_queue_find (stroke->anchors, anchor);

  if (list)
    PIKA_ANCHOR (list->data)->selected = selected;
}


void
pika_stroke_anchor_move_relative (PikaStroke            *stroke,
                                  PikaAnchor            *anchor,
                                  const PikaCoords      *delta,
                                  PikaAnchorFeatureType  feature)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));
  g_return_if_fail (anchor != NULL);
  g_return_if_fail (g_queue_find (stroke->anchors, anchor));

  PIKA_STROKE_GET_CLASS (stroke)->anchor_move_relative (stroke, anchor,
                                                        delta, feature);
}

static void
pika_stroke_real_anchor_move_relative (PikaStroke            *stroke,
                                       PikaAnchor            *anchor,
                                       const PikaCoords      *delta,
                                       PikaAnchorFeatureType  feature)
{
  anchor->position.x += delta->x;
  anchor->position.y += delta->y;
}


void
pika_stroke_anchor_move_absolute (PikaStroke            *stroke,
                                  PikaAnchor            *anchor,
                                  const PikaCoords      *coord,
                                  PikaAnchorFeatureType  feature)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));
  g_return_if_fail (anchor != NULL);
  g_return_if_fail (g_queue_find (stroke->anchors, anchor));

  PIKA_STROKE_GET_CLASS (stroke)->anchor_move_absolute (stroke, anchor,
                                                        coord, feature);
}

static void
pika_stroke_real_anchor_move_absolute (PikaStroke            *stroke,
                                       PikaAnchor            *anchor,
                                       const PikaCoords      *coord,
                                       PikaAnchorFeatureType  feature)
{
  anchor->position.x = coord->x;
  anchor->position.y = coord->y;
}

gboolean
pika_stroke_point_is_movable (PikaStroke *stroke,
                              PikaAnchor *predec,
                              gdouble     position)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->point_is_movable (stroke, predec,
                                                           position);
}


static gboolean
pika_stroke_real_point_is_movable (PikaStroke *stroke,
                                   PikaAnchor *predec,
                                   gdouble     position)
{
  return FALSE;
}


void
pika_stroke_point_move_relative (PikaStroke            *stroke,
                                 PikaAnchor            *predec,
                                 gdouble                position,
                                 const PikaCoords      *deltacoord,
                                 PikaAnchorFeatureType  feature)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->point_move_relative (stroke, predec,
                                                       position, deltacoord,
                                                       feature);
}


static void
pika_stroke_real_point_move_relative (PikaStroke           *stroke,
                                      PikaAnchor           *predec,
                                      gdouble               position,
                                      const PikaCoords     *deltacoord,
                                      PikaAnchorFeatureType feature)
{
  g_printerr ("pika_stroke_point_move_relative: default implementation\n");
}


void
pika_stroke_point_move_absolute (PikaStroke            *stroke,
                                 PikaAnchor            *predec,
                                 gdouble                position,
                                 const PikaCoords      *coord,
                                 PikaAnchorFeatureType  feature)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->point_move_absolute (stroke, predec,
                                                       position, coord,
                                                       feature);
}

static void
pika_stroke_real_point_move_absolute (PikaStroke           *stroke,
                                      PikaAnchor           *predec,
                                      gdouble               position,
                                      const PikaCoords     *coord,
                                      PikaAnchorFeatureType feature)
{
  g_printerr ("pika_stroke_point_move_absolute: default implementation\n");
}


void
pika_stroke_close (PikaStroke *stroke)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));
  g_return_if_fail (g_queue_is_empty (stroke->anchors) == FALSE);

  PIKA_STROKE_GET_CLASS (stroke)->close (stroke);
}

static void
pika_stroke_real_close (PikaStroke *stroke)
{
  stroke->closed = TRUE;
  g_object_notify (G_OBJECT (stroke), "closed");
}


void
pika_stroke_anchor_convert (PikaStroke            *stroke,
                            PikaAnchor            *anchor,
                            PikaAnchorFeatureType  feature)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->anchor_convert (stroke, anchor, feature);
}

static void
pika_stroke_real_anchor_convert (PikaStroke            *stroke,
                                 PikaAnchor            *anchor,
                                 PikaAnchorFeatureType  feature)
{
  g_printerr ("pika_stroke_anchor_convert: default implementation\n");
}


void
pika_stroke_anchor_delete (PikaStroke *stroke,
                           PikaAnchor *anchor)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));
  g_return_if_fail (anchor && anchor->type == PIKA_ANCHOR_ANCHOR);

  PIKA_STROKE_GET_CLASS (stroke)->anchor_delete (stroke, anchor);
}

static void
pika_stroke_real_anchor_delete (PikaStroke *stroke,
                                PikaAnchor *anchor)
{
  g_printerr ("pika_stroke_anchor_delete: default implementation\n");
}

PikaStroke *
pika_stroke_open (PikaStroke *stroke,
                  PikaAnchor *end_anchor)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);
  g_return_val_if_fail (end_anchor &&
                        end_anchor->type == PIKA_ANCHOR_ANCHOR, NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->open (stroke, end_anchor);
}

static PikaStroke *
pika_stroke_real_open (PikaStroke *stroke,
                       PikaAnchor *end_anchor)
{
  g_printerr ("pika_stroke_open: default implementation\n");
  return NULL;
}

gboolean
pika_stroke_anchor_is_insertable (PikaStroke *stroke,
                                  PikaAnchor *predec,
                                  gdouble     position)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->anchor_is_insertable (stroke,
                                                               predec,
                                                               position);
}

static gboolean
pika_stroke_real_anchor_is_insertable (PikaStroke *stroke,
                                       PikaAnchor *predec,
                                       gdouble     position)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);

  return FALSE;
}

PikaAnchor *
pika_stroke_anchor_insert (PikaStroke *stroke,
                           PikaAnchor *predec,
                           gdouble     position)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);
  g_return_val_if_fail (predec->type == PIKA_ANCHOR_ANCHOR, NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->anchor_insert (stroke,
                                                        predec, position);
}

static PikaAnchor *
pika_stroke_real_anchor_insert (PikaStroke *stroke,
                                PikaAnchor *predec,
                                gdouble     position)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return NULL;
}


gboolean
pika_stroke_is_extendable (PikaStroke *stroke,
                           PikaAnchor *neighbor)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->is_extendable (stroke, neighbor);
}

static gboolean
pika_stroke_real_is_extendable (PikaStroke *stroke,
                                PikaAnchor *neighbor)
{
  return FALSE;
}


PikaAnchor *
pika_stroke_extend (PikaStroke           *stroke,
                    const PikaCoords     *coords,
                    PikaAnchor           *neighbor,
                    PikaVectorExtendMode  extend_mode)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);
  g_return_val_if_fail (!stroke->closed, NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->extend (stroke, coords,
                                                 neighbor, extend_mode);
}

static PikaAnchor *
pika_stroke_real_extend (PikaStroke           *stroke,
                         const PikaCoords     *coords,
                         PikaAnchor           *neighbor,
                         PikaVectorExtendMode  extend_mode)
{
  g_printerr ("pika_stroke_extend: default implementation\n");
  return NULL;
}

gboolean
pika_stroke_connect_stroke (PikaStroke *stroke,
                            PikaAnchor *anchor,
                            PikaStroke *extension,
                            PikaAnchor *neighbor)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);
  g_return_val_if_fail (PIKA_IS_STROKE (extension), FALSE);
  g_return_val_if_fail (stroke->closed == FALSE &&
                        extension->closed == FALSE, FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->connect_stroke (stroke, anchor,
                                                         extension, neighbor);
}

gboolean
pika_stroke_real_connect_stroke (PikaStroke *stroke,
                                 PikaAnchor *anchor,
                                 PikaStroke *extension,
                                 PikaAnchor *neighbor)
{
  g_printerr ("pika_stroke_connect_stroke: default implementation\n");
  return FALSE;
}

gboolean
pika_stroke_is_empty (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->is_empty (stroke);
}

static gboolean
pika_stroke_real_is_empty (PikaStroke *stroke)
{
  return g_queue_is_empty (stroke->anchors);
}


gboolean
pika_stroke_reverse (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->reverse (stroke);
}


static gboolean
pika_stroke_real_reverse (PikaStroke *stroke)
{
  g_queue_reverse (stroke->anchors);

  /* keep the first node the same for closed strokes */
  if (stroke->closed && stroke->anchors->length > 0)
    g_queue_push_head_link (stroke->anchors,
                            g_queue_pop_tail_link (stroke->anchors));

  return TRUE;
}


gboolean
pika_stroke_shift_start (PikaStroke *stroke,
                         PikaAnchor *new_start)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);
  g_return_val_if_fail (new_start != NULL, FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->shift_start (stroke, new_start);
}

static gboolean
pika_stroke_real_shift_start (PikaStroke *stroke,
                              PikaAnchor *new_start)
{
  GList *link;

  link = g_queue_find (stroke->anchors, new_start);
  if (!link)
    return FALSE;

  if (link == stroke->anchors->head)
    return TRUE;

  stroke->anchors->tail->next = stroke->anchors->head;
  stroke->anchors->head->prev = stroke->anchors->tail;
  stroke->anchors->tail = link->prev;
  stroke->anchors->head = link;
  stroke->anchors->tail->next = NULL;
  stroke->anchors->head->prev = NULL;

  return TRUE;
}


gdouble
pika_stroke_get_length (PikaStroke *stroke,
                        gdouble     precision)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), 0.0);

  return PIKA_STROKE_GET_CLASS (stroke)->get_length (stroke, precision);
}

static gdouble
pika_stroke_real_get_length (PikaStroke *stroke,
                             gdouble     precision)
{
  GArray     *points;
  gint        i;
  gdouble     length;
  PikaCoords  difference;

  if (g_queue_is_empty (stroke->anchors))
    return -1;

  points = pika_stroke_interpolate (stroke, precision, NULL);
  if (points == NULL)
    return -1;

  length = 0;

  for (i = 0; i < points->len - 1; i++ )
    {
       pika_coords_difference (&(g_array_index (points, PikaCoords, i)),
                               &(g_array_index (points, PikaCoords, i+1)),
                               &difference);
       length += pika_coords_length (&difference);
    }

  g_array_free(points, TRUE);

  return length;
}


gdouble
pika_stroke_get_distance (PikaStroke       *stroke,
                          const PikaCoords *coord)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), 0.0);

  return PIKA_STROKE_GET_CLASS (stroke)->get_distance (stroke, coord);
}

static gdouble
pika_stroke_real_get_distance (PikaStroke       *stroke,
                               const PikaCoords *coord)
{
  g_printerr ("pika_stroke_get_distance: default implementation\n");

  return 0.0;
}


GArray *
pika_stroke_interpolate (PikaStroke *stroke,
                         gdouble     precision,
                         gboolean   *ret_closed)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->interpolate (stroke, precision,
                                                      ret_closed);
}

static GArray *
pika_stroke_real_interpolate (PikaStroke *stroke,
                              gdouble     precision,
                              gboolean   *ret_closed)
{
  g_printerr ("pika_stroke_interpolate: default implementation\n");

  return NULL;
}

PikaStroke *
pika_stroke_duplicate (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->duplicate (stroke);
}

static PikaStroke *
pika_stroke_real_duplicate (PikaStroke *stroke)
{
  PikaStroke *new_stroke;
  GList      *list;

  new_stroke = g_object_new (G_TYPE_FROM_INSTANCE (stroke),
                             "name", pika_object_get_name (stroke),
                             NULL);

  g_queue_free_full (new_stroke->anchors, (GDestroyNotify) pika_anchor_free);
  new_stroke->anchors = g_queue_copy (stroke->anchors);

  for (list = new_stroke->anchors->head; list; list = g_list_next (list))
    {
      list->data = pika_anchor_copy (PIKA_ANCHOR (list->data));
    }

  new_stroke->closed = stroke->closed;
  /* we do *not* copy the ID! */

  return new_stroke;
}


PikaBezierDesc *
pika_stroke_make_bezier (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->make_bezier (stroke);
}

static PikaBezierDesc *
pika_stroke_real_make_bezier (PikaStroke *stroke)
{
  g_printerr ("pika_stroke_make_bezier: default implementation\n");

  return NULL;
}


void
pika_stroke_translate (PikaStroke *stroke,
                       gdouble     offset_x,
                       gdouble     offset_y)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->translate (stroke, offset_x, offset_y);
}

static void
pika_stroke_real_translate (PikaStroke *stroke,
                            gdouble     offset_x,
                            gdouble     offset_y)
{
  GList *list;

  for (list = stroke->anchors->head; list; list = g_list_next (list))
    {
      PikaAnchor *anchor = list->data;

      anchor->position.x += offset_x;
      anchor->position.y += offset_y;
    }
}


void
pika_stroke_scale (PikaStroke *stroke,
                   gdouble     scale_x,
                   gdouble     scale_y)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->scale (stroke, scale_x, scale_y);
}

static void
pika_stroke_real_scale (PikaStroke *stroke,
                        gdouble     scale_x,
                        gdouble     scale_y)
{
  GList *list;

  for (list = stroke->anchors->head; list; list = g_list_next (list))
    {
      PikaAnchor *anchor = list->data;

      anchor->position.x *= scale_x;
      anchor->position.y *= scale_y;
    }
}

void
pika_stroke_rotate (PikaStroke *stroke,
                    gdouble     center_x,
                    gdouble     center_y,
                    gdouble     angle)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->rotate (stroke, center_x, center_y, angle);
}

static void
pika_stroke_real_rotate (PikaStroke *stroke,
                         gdouble     center_x,
                         gdouble     center_y,
                         gdouble     angle)
{
  PikaMatrix3  matrix;

  angle = angle / 180.0 * G_PI;
  pika_matrix3_identity (&matrix);
  pika_transform_matrix_rotate_center (&matrix, center_x, center_y, angle);

  pika_stroke_transform (stroke, &matrix, NULL);
}

void
pika_stroke_flip (PikaStroke          *stroke,
                  PikaOrientationType  flip_type,
                  gdouble              axis)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->flip (stroke, flip_type, axis);
}

static void
pika_stroke_real_flip (PikaStroke          *stroke,
                       PikaOrientationType  flip_type,
                       gdouble              axis)
{
  PikaMatrix3  matrix;

  pika_matrix3_identity (&matrix);
  pika_transform_matrix_flip (&matrix, flip_type, axis);
  pika_stroke_transform (stroke, &matrix, NULL);
}

void
pika_stroke_flip_free (PikaStroke *stroke,
                       gdouble     x1,
                       gdouble     y1,
                       gdouble     x2,
                       gdouble     y2)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->flip_free (stroke, x1, y1, x2, y2);
}

static void
pika_stroke_real_flip_free (PikaStroke *stroke,
                            gdouble     x1,
                            gdouble     y1,
                            gdouble     x2,
                            gdouble     y2)
{
  /* x, y, width and height parameter in pika_transform_matrix_flip_free are unused */
  PikaMatrix3  matrix;

  pika_matrix3_identity (&matrix);
  pika_transform_matrix_flip_free (&matrix, x1, y1, x2, y2);

  pika_stroke_transform (stroke, &matrix, NULL);
}

/* transforms 'stroke' by 'matrix'.  due to clipping, the transformation may
 * result in multiple strokes.
 *
 * if 'ret_strokes' is not NULL, the transformed strokes are appended to the
 * queue, and 'stroke' is left in an unspecified state.  one of the resulting
 * strokes may alias 'stroke'.
 *
 * if 'ret_strokes' is NULL, the transformation is performed in-place.  if the
 * transformation results in multiple strokes (which, atm, can only happen for
 * non-affine transformation), the result is undefined.
 */
void
pika_stroke_transform (PikaStroke        *stroke,
                       const PikaMatrix3 *matrix,
                       GQueue            *ret_strokes)
{
  g_return_if_fail (PIKA_IS_STROKE (stroke));

  PIKA_STROKE_GET_CLASS (stroke)->transform (stroke, matrix, ret_strokes);
}

static void
pika_stroke_real_transform (PikaStroke        *stroke,
                            const PikaMatrix3 *matrix,
                            GQueue            *ret_strokes)
{
  GList *list;

  for (list = stroke->anchors->head; list; list = g_list_next (list))
    {
      PikaAnchor *anchor = list->data;

      pika_matrix3_transform_point (matrix,
                                    anchor->position.x,
                                    anchor->position.y,
                                    &anchor->position.x,
                                    &anchor->position.y);
    }

  if (ret_strokes)
    {
      stroke->id = 0;

      g_queue_push_tail (ret_strokes, g_object_ref (stroke));
    }
}


GList *
pika_stroke_get_draw_anchors (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->get_draw_anchors (stroke);
}

static GList *
pika_stroke_real_get_draw_anchors (PikaStroke *stroke)
{
  GList *list;
  GList *ret_list = NULL;

  for (list = stroke->anchors->head; list; list = g_list_next (list))
    {
      if (PIKA_ANCHOR (list->data)->type == PIKA_ANCHOR_ANCHOR)
        ret_list = g_list_prepend (ret_list, list->data);
    }

  return g_list_reverse (ret_list);
}


GList *
pika_stroke_get_draw_controls (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->get_draw_controls (stroke);
}

static GList *
pika_stroke_real_get_draw_controls (PikaStroke *stroke)
{
  GList *list;
  GList *ret_list = NULL;

  for (list = stroke->anchors->head; list; list = g_list_next (list))
    {
      PikaAnchor *anchor = list->data;

      if (anchor->type == PIKA_ANCHOR_CONTROL)
        {
          PikaAnchor *next = list->next ? list->next->data : NULL;
          PikaAnchor *prev = list->prev ? list->prev->data : NULL;

          if (next && next->type == PIKA_ANCHOR_ANCHOR && next->selected)
            {
              /* Ok, this is a hack.
               * The idea is to give control points at the end of a
               * stroke a higher priority for the interactive tool.
               */
              if (prev)
                ret_list = g_list_prepend (ret_list, anchor);
              else
                ret_list = g_list_append (ret_list, anchor);
            }
          else if (prev && prev->type == PIKA_ANCHOR_ANCHOR && prev->selected)
            {
              /* same here... */
              if (next)
                ret_list = g_list_prepend (ret_list, anchor);
              else
                ret_list = g_list_append (ret_list, anchor);
            }
        }
    }

  return g_list_reverse (ret_list);
}


GArray *
pika_stroke_get_draw_lines (PikaStroke *stroke)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->get_draw_lines (stroke);
}

static GArray *
pika_stroke_real_get_draw_lines (PikaStroke *stroke)
{
  GList  *list;
  GArray *ret_lines = NULL;
  gint    count = 0;

  for (list = stroke->anchors->head; list; list = g_list_next (list))
    {
      PikaAnchor *anchor = list->data;

      if (anchor->type == PIKA_ANCHOR_ANCHOR && anchor->selected)
        {
          if (list->next)
            {
              PikaAnchor *next = list->next->data;

              if (count == 0)
                ret_lines = g_array_new (FALSE, FALSE, sizeof (PikaCoords));

              ret_lines = g_array_append_val (ret_lines, anchor->position);
              ret_lines = g_array_append_val (ret_lines, next->position);
              count += 1;
            }

          if (list->prev)
            {
              PikaAnchor *prev = list->prev->data;

              if (count == 0)
                ret_lines = g_array_new (FALSE, FALSE, sizeof (PikaCoords));

              ret_lines = g_array_append_val (ret_lines, anchor->position);
              ret_lines = g_array_append_val (ret_lines, prev->position);
              count += 1;
            }
        }
    }

  return ret_lines;
}

GArray *
pika_stroke_control_points_get (PikaStroke *stroke,
                                gboolean   *ret_closed)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), NULL);

  return PIKA_STROKE_GET_CLASS (stroke)->control_points_get (stroke,
                                                             ret_closed);
}

static GArray *
pika_stroke_real_control_points_get (PikaStroke *stroke,
                                     gboolean   *ret_closed)
{
  guint   num_anchors;
  GArray *ret_array;
  GList  *list;

  num_anchors = g_queue_get_length (stroke->anchors);
  ret_array = g_array_sized_new (FALSE, FALSE,
                                 sizeof (PikaAnchor), num_anchors);

  for (list = stroke->anchors->head; list; list = g_list_next (list))
    {
      g_array_append_vals (ret_array, list->data, 1);
    }

  if (ret_closed)
    *ret_closed = stroke->closed;

  return ret_array;
}

gboolean
pika_stroke_get_point_at_dist (PikaStroke *stroke,
                               gdouble     dist,
                               gdouble     precision,
                               PikaCoords *position,
                               gdouble    *slope)
{
  g_return_val_if_fail (PIKA_IS_STROKE (stroke), FALSE);

  return PIKA_STROKE_GET_CLASS (stroke)->get_point_at_dist (stroke,
                                                            dist,
                                                            precision,
                                                            position,
                                                            slope);
}


static gboolean
pika_stroke_real_get_point_at_dist (PikaStroke *stroke,
                                    gdouble     dist,
                                    gdouble     precision,
                                    PikaCoords *position,
                                    gdouble    *slope)
{
  GArray     *points;
  gint        i;
  gdouble     length;
  gdouble     segment_length;
  gboolean    ret = FALSE;
  PikaCoords  difference;

  points = pika_stroke_interpolate (stroke, precision, NULL);
  if (points == NULL)
    return ret;

  length = 0;
  for (i=0; i < points->len - 1; i++)
    {
      pika_coords_difference (&(g_array_index (points, PikaCoords , i)),
                              &(g_array_index (points, PikaCoords , i+1)),
                              &difference);
      segment_length = pika_coords_length (&difference);

      if (segment_length == 0 || length + segment_length < dist )
        {
          length += segment_length;
        }
      else
        {
          /* x = x1 + (x2 - x1 ) u  */
          /* x   = x1 (1-u) + u x2  */

          gdouble u = (dist - length) / segment_length;

          pika_coords_mix (1 - u, &(g_array_index (points, PikaCoords , i)),
                               u, &(g_array_index (points, PikaCoords , i+1)),
                           position);

          if (difference.x == 0)
            *slope = G_MAXDOUBLE;
          else
            *slope = difference.y / difference.x;

          ret = TRUE;
          break;
        }
    }

  g_array_free (points, TRUE);

  return ret;
}
