/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * Copyright (C) 2017 Sébastien Fourey & David Tchumperlé
 * Copyright (C) 2018 Jehan
 *
 * 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 <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

#include "libgimpmath/gimpmath.h"

#include "core-types.h"

#include "gegl/gimp-gegl-loops.h"
#include "gegl/gimp-gegl-utils.h"

#include "gimp-parallel.h"
#include "gimp-priorities.h"
#include "gimp-utils.h" /* GIMP_TIMER */
#include "gimpasync.h"
#include "gimpcancelable.h"
#include "gimpdrawable.h"
#include "gimpimage.h"
#include "gimplineart.h"
#include "gimpmarshal.h"
#include "gimppickable.h"
#include "gimpprojection.h"
#include "gimpviewable.h"
#include "gimpwaitable.h"

#include "gimp-intl.h"

enum
{
  COMPUTING_START,
  COMPUTING_END,
  LAST_SIGNAL,
};

enum
{
  PROP_0,
  PROP_SELECT_TRANSPARENT,
  PROP_MAX_GROW,
  PROP_THRESHOLD,
  PROP_SPLINE_MAX_LEN,
  PROP_SEGMENT_MAX_LEN,
};

typedef struct _GimpLineArtPrivate GimpLineArtPrivate;

struct _GimpLineArtPrivate
{
  gboolean      frozen;
  gboolean      compute_after_thaw;

  GimpAsync    *async;

  gint          idle_id;

  GimpPickable *input;
  GeglBuffer   *closed;
  gfloat       *distmap;

  /* Used in the closing step. */
  gboolean      select_transparent;
  gdouble       threshold;
  gint          spline_max_len;
  gint          segment_max_len;
  gboolean      max_len_bound;

  /* Used in the grow step. */
  gint          max_grow;
};

typedef struct
{
  GeglBuffer  *buffer;

  gboolean     select_transparent;
  gdouble      threshold;
  gint         spline_max_len;
  gint         segment_max_len;
} LineArtData;

typedef struct
{
  GeglBuffer *closed;
  gfloat     *distmap;
} LineArtResult;

static int DeltaX[4] = {+1, -1, 0, 0};
static int DeltaY[4] = {0, 0, +1, -1};

static const GimpVector2 Direction2Normal[4] =
{
    {  1.0f,  0.0f },
    { -1.0f,  0.0f },
    {  0.0f,  1.0f },
    {  0.0f, -1.0f }
};

typedef enum _Direction
{
  XPlusDirection  = 0,
  XMinusDirection = 1,
  YPlusDirection  = 2,
  YMinusDirection = 3
} Direction;

typedef GimpVector2 Pixel;

typedef struct _SplineCandidate
{
  Pixel p1;
  Pixel p2;
  float quality;
} SplineCandidate;

typedef struct _Edgel
{
  gint      x, y;
  Direction direction;

  gfloat    x_normal;
  gfloat    y_normal;
  gfloat    curvature;
  guint     next, previous;
} Edgel;


static void            gimp_line_art_finalize                  (GObject               *object);
static void            gimp_line_art_set_property              (GObject                *object,
                                                                guint                   property_id,
                                                                const GValue           *value,
                                                                GParamSpec             *pspec);
static void            gimp_line_art_get_property              (GObject                *object,
                                                                guint                   property_id,
                                                                GValue                 *value,
                                                                GParamSpec             *pspec);

/* Functions for asynchronous computation. */

static void            gimp_line_art_compute                   (GimpLineArt            *line_art);
static void            gimp_line_art_compute_cb                (GimpAsync              *async,
                                                                GimpLineArt            *line_art);

static GimpAsync     * gimp_line_art_prepare_async             (GimpLineArt            *line_art,
                                                                gint                    priority);
static void            gimp_line_art_prepare_async_func        (GimpAsync              *async,
                                                                LineArtData            *data);
static LineArtData   * line_art_data_new                       (GeglBuffer             *buffer,
                                                                GimpLineArt            *line_art);
static void            line_art_data_free                      (LineArtData            *data);
static LineArtResult * line_art_result_new                     (GeglBuffer             *line_art,
                                                                gfloat                 *distmap);
static void            line_art_result_free                    (LineArtResult          *result);

static gboolean        gimp_line_art_idle                      (GimpLineArt            *line_art);
static void            gimp_line_art_input_invalidate_preview  (GimpViewable           *viewable,
                                                                GimpLineArt            *line_art);


/* All actual computation functions. */

static GeglBuffer    * gimp_line_art_close                     (GeglBuffer             *buffer,
                                                                gboolean                select_transparent,
                                                                gdouble                 stroke_threshold,
                                                                gint                    spline_max_length,
                                                                gint                    segment_max_length,
                                                                gint                    minimal_lineart_area,
                                                                gint                    normal_estimate_mask_size,
                                                                gfloat                  end_point_rate,
                                                                gfloat                  spline_max_angle,
                                                                gint                    end_point_connectivity,
                                                                gfloat                  spline_roundness,
                                                                gboolean                allow_self_intersections,
                                                                gint                    created_regions_significant_area,
                                                                gint                    created_regions_minimum_area,
                                                                gboolean                small_segments_from_spline_sources,
                                                                gfloat                **lineart_distmap,
                                                                GimpAsync              *async);

static void            gimp_lineart_denoise                    (GeglBuffer             *buffer,
                                                                int                     size,
                                                                GimpAsync              *async);
static void            gimp_lineart_compute_normals_curvatures (GeglBuffer             *mask,
                                                                gfloat                 *normals,
                                                                gfloat                 *curvatures,
                                                                gfloat                 *smoothed_curvatures,
                                                                int                     normal_estimate_mask_size,
                                                                GimpAsync              *async);
static gfloat        * gimp_lineart_get_smooth_curvatures      (GArray                 *edgelset,
                                                                GimpAsync              *async);
static GArray        * gimp_lineart_curvature_extremums        (gfloat                 *curvatures,
                                                                gfloat                 *smoothed_curvatures,
                                                                gint                    curvatures_width,
                                                                gint                    curvatures_height,
                                                                GimpAsync              *async);
static gint            gimp_spline_candidate_cmp               (const SplineCandidate  *a,
                                                                const SplineCandidate  *b,
                                                                gpointer                user_data);
static GList         * gimp_lineart_find_spline_candidates     (GArray                 *max_positions,
                                                                gfloat                 *normals,
                                                                gint                    width,
                                                                gint                    distance_threshold,
                                                                gfloat                  max_angle_deg,
                                                                GimpAsync              *async);

static GArray        * gimp_lineart_discrete_spline            (Pixel                   p0,
                                                                GimpVector2             n0,
                                                                Pixel                   p1,
                                                                GimpVector2             n1);

static gint            gimp_number_of_transitions               (GArray                 *pixels,
                                                                 GeglBuffer             *buffer);
static gboolean        gimp_line_art_allow_closure              (GeglBuffer             *mask,
                                                                 GArray                 *pixels,
                                                                 GList                 **fill_pixels,
                                                                 int                     significant_size,
                                                                 int                     minimum_size);
static GArray        * gimp_lineart_line_segment_until_hit      (const GeglBuffer       *buffer,
                                                                 Pixel                   start,
                                                                 GimpVector2             direction,
                                                                 int                     size);
static gfloat        * gimp_lineart_estimate_strokes_radii      (GeglBuffer             *mask,
                                                                 GimpAsync              *async);
static void            gimp_line_art_simple_fill                (GeglBuffer             *buffer,
                                                                 gint                    x,
                                                                 gint                    y,
                                                                 gint                   *counter);

/* Some callback-type functions. */

static guint           visited_hash_fun                         (Pixel                  *key);
static gboolean        visited_equal_fun                        (Pixel                  *e1,
                                                                 Pixel                  *e2);

static inline gboolean border_in_direction                      (GeglBuffer             *mask,
                                                                 Pixel                   p,
                                                                 int                     direction);
static inline GimpVector2 pair2normal                           (Pixel                   p,
                                                                 gfloat                 *normals,
                                                                 gint                    width);

/* Edgel */

static Edgel    * gimp_edgel_new                  (int                x,
                                                   int                y,
                                                   Direction          direction);
static void       gimp_edgel_init                 (Edgel             *edgel);
static void       gimp_edgel_clear                (Edgel            **edgel);
static int        gimp_edgel_cmp                  (const Edgel       *e1,
                                                   const Edgel       *e2);
static guint      edgel2index_hash_fun            (Edgel              *key);
static gboolean   edgel2index_equal_fun           (Edgel              *e1,
                                                   Edgel              *e2);

static glong      gimp_edgel_track_mark           (GeglBuffer         *mask,
                                                   Edgel               edgel,
                                                   long                size_limit);
static glong      gimp_edgel_region_area          (const GeglBuffer   *mask,
                                                   Edgel               start_edgel);

/* Edgel set */

static GArray   * gimp_edgelset_new               (GeglBuffer         *buffer,
                                                   GimpAsync          *async);
static void       gimp_edgelset_add               (GArray             *set,
                                                   int                 x,
                                                   int                 y,
                                                   Direction           direction,
                                                   GHashTable         *edgel2index);
static void       gimp_edgelset_init_normals      (GArray             *set);
static void       gimp_edgelset_smooth_normals    (GArray             *set,
                                                   int                 mask_size,
                                                   GimpAsync          *async);
static void       gimp_edgelset_compute_curvature (GArray             *set,
                                                   GimpAsync          *async);

static void       gimp_edgelset_build_graph       (GArray            *set,
                                                   GeglBuffer        *buffer,
                                                   GHashTable        *edgel2index,
                                                   GimpAsync         *async);
static void       gimp_edgelset_next8             (const GeglBuffer  *buffer,
                                                   Edgel             *it,
                                                   Edgel             *n);

G_DEFINE_TYPE_WITH_CODE (GimpLineArt, gimp_line_art, GIMP_TYPE_OBJECT,
                         G_ADD_PRIVATE (GimpLineArt))

static guint gimp_line_art_signals[LAST_SIGNAL] = { 0 };

static void
gimp_line_art_class_init (GimpLineArtClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  gimp_line_art_signals[COMPUTING_START] =
    g_signal_new ("computing-start",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpLineArtClass, computing_start),
                  NULL, NULL,
                  gimp_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);
  gimp_line_art_signals[COMPUTING_END] =
    g_signal_new ("computing-end",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpLineArtClass, computing_end),
                  NULL, NULL,
                  gimp_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

  object_class->finalize     = gimp_line_art_finalize;
  object_class->set_property = gimp_line_art_set_property;
  object_class->get_property = gimp_line_art_get_property;

  g_object_class_install_property (object_class, PROP_SELECT_TRANSPARENT,
                                   g_param_spec_boolean ("select-transparent",
                                                         _("Select transparent pixels instead of gray ones"),
                                                         _("Select transparent pixels instead of gray ones"),
                                                         TRUE,
                                                         G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_THRESHOLD,
                                   g_param_spec_double ("threshold",
                                                        _("Line art detection threshold"),
                                                        _("Threshold to detect contour (higher values will include more pixels)"),
                                                        0.0, 1.0, 0.92,
                                                        G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_MAX_GROW,
                                   g_param_spec_int ("max-grow",
                                                     _("Maximum growing size"),
                                                     _("Maximum number of pixels grown under the line art"),
                                                     1, 100, 3,
                                                     G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_SPLINE_MAX_LEN,
                                   g_param_spec_int ("spline-max-length",
                                                     _("Maximum curved closing length"),
                                                     _("Maximum curved length (in pixels) to close the line art"),
                                                     0, 1000, 100,
                                                     G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_SEGMENT_MAX_LEN,
                                   g_param_spec_int ("segment-max-length",
                                                     _("Maximum straight closing length"),
                                                     _("Maximum straight length (in pixels) to close the line art"),
                                                     0, 1000, 100,
                                                     G_PARAM_CONSTRUCT | GIMP_PARAM_READWRITE));
}

static void
gimp_line_art_init (GimpLineArt *line_art)
{
  line_art->priv = gimp_line_art_get_instance_private (line_art);
}

static void
gimp_line_art_finalize (GObject *object)
{
  GimpLineArt *line_art = GIMP_LINE_ART (object);

  line_art->priv->frozen = FALSE;

  gimp_line_art_set_input (line_art, NULL);
}

static void
gimp_line_art_set_property (GObject      *object,
                            guint         property_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  GimpLineArt *line_art = GIMP_LINE_ART (object);

  switch (property_id)
    {
    case PROP_SELECT_TRANSPARENT:
      if (line_art->priv->select_transparent != g_value_get_boolean (value))
        {
          line_art->priv->select_transparent = g_value_get_boolean (value);
          gimp_line_art_compute (line_art);
        }
      break;
    case PROP_MAX_GROW:
      line_art->priv->max_grow = g_value_get_int (value);
      break;
    case PROP_THRESHOLD:
      if (line_art->priv->threshold != g_value_get_double (value))
        {
          line_art->priv->threshold = g_value_get_double (value);
          gimp_line_art_compute (line_art);
        }
      break;
    case PROP_SPLINE_MAX_LEN:
      if (line_art->priv->spline_max_len != g_value_get_int (value))
        {
          line_art->priv->spline_max_len = g_value_get_int (value);
          if (line_art->priv->max_len_bound)
            line_art->priv->segment_max_len = line_art->priv->spline_max_len;
          gimp_line_art_compute (line_art);
        }
      break;
    case PROP_SEGMENT_MAX_LEN:
      if (line_art->priv->segment_max_len != g_value_get_int (value))
        {
          line_art->priv->segment_max_len = g_value_get_int (value);
          if (line_art->priv->max_len_bound)
            line_art->priv->spline_max_len = line_art->priv->segment_max_len;
          gimp_line_art_compute (line_art);
        }
      break;

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

static void
gimp_line_art_get_property (GObject    *object,
                            guint       property_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  GimpLineArt *line_art = GIMP_LINE_ART (object);

  switch (property_id)
    {
    case PROP_SELECT_TRANSPARENT:
      g_value_set_boolean (value, line_art->priv->select_transparent);
      break;
    case PROP_MAX_GROW:
      g_value_set_int (value, line_art->priv->max_grow);
      break;
    case PROP_THRESHOLD:
      g_value_set_double (value, line_art->priv->threshold);
      break;
    case PROP_SPLINE_MAX_LEN:
      g_value_set_int (value, line_art->priv->spline_max_len);
      break;
    case PROP_SEGMENT_MAX_LEN:
      g_value_set_int (value, line_art->priv->segment_max_len);
      break;

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

/* Public functions */

GimpLineArt *
gimp_line_art_new (void)
{
  return g_object_new (GIMP_TYPE_LINE_ART,
                       NULL);
}

void
gimp_line_art_bind_gap_length (GimpLineArt *line_art,
                               gboolean     bound)
{
  line_art->priv->max_len_bound = bound;
}

void
gimp_line_art_set_input (GimpLineArt  *line_art,
                         GimpPickable *pickable)
{
  g_return_if_fail (pickable == NULL || GIMP_IS_VIEWABLE (pickable));

  if (pickable != line_art->priv->input)
    {
      if (line_art->priv->input)
        g_signal_handlers_disconnect_by_data (line_art->priv->input, line_art);

      g_set_object (&line_art->priv->input, pickable);

      gimp_line_art_compute (line_art);

      if (pickable)
        {
          g_signal_connect (pickable, "invalidate-preview",
                            G_CALLBACK (gimp_line_art_input_invalidate_preview),
                            line_art);
        }
    }
}

GimpPickable *
gimp_line_art_get_input (GimpLineArt *line_art)
{
  return line_art->priv->input;
}

void
gimp_line_art_freeze (GimpLineArt *line_art)
{
  g_return_if_fail (! line_art->priv->frozen);

  line_art->priv->frozen             = TRUE;
  line_art->priv->compute_after_thaw = FALSE;
}

void
gimp_line_art_thaw (GimpLineArt *line_art)
{
  g_return_if_fail (line_art->priv->frozen);

  line_art->priv->frozen = FALSE;
  if (line_art->priv->compute_after_thaw)
    {
      gimp_line_art_compute (line_art);
      line_art->priv->compute_after_thaw = FALSE;
    }
}

gboolean
gimp_line_art_is_frozen (GimpLineArt *line_art)
{
  return line_art->priv->frozen;
}

GeglBuffer *
gimp_line_art_get (GimpLineArt  *line_art,
                   gfloat      **distmap)
{
  g_return_val_if_fail (line_art->priv->input, NULL);

  if (line_art->priv->async)
    {
      gimp_waitable_wait (GIMP_WAITABLE (line_art->priv->async));
    }
  else if (! line_art->priv->closed)
    {
      gimp_line_art_compute (line_art);
      if (line_art->priv->async)
        gimp_waitable_wait (GIMP_WAITABLE (line_art->priv->async));
    }

  g_return_val_if_fail (line_art->priv->closed, NULL);

  if (distmap)
    *distmap = line_art->priv->distmap;

  return line_art->priv->closed;
}

/* Functions for asynchronous computation. */

static void
gimp_line_art_compute (GimpLineArt *line_art)
{
  if (line_art->priv->frozen)
    {
      line_art->priv->compute_after_thaw = TRUE;
      return;
    }

  if (line_art->priv->async)
    {
      /* we cancel the async, but don't wait for it to finish, since
       * it might take a while to respond.  instead gimp_line_art_compute_cb()
       * bails if the async has been canceled, to avoid accessing the line art.
       */
      g_signal_emit (line_art, gimp_line_art_signals[COMPUTING_END], 0);
      gimp_cancelable_cancel (GIMP_CANCELABLE (line_art->priv->async));
      g_clear_object (&line_art->priv->async);
    }

  if (line_art->priv->idle_id)
    {
      g_source_remove (line_art->priv->idle_id);
      line_art->priv->idle_id = 0;
    }

  g_clear_object (&line_art->priv->closed);
  g_clear_pointer (&line_art->priv->distmap, g_free);

  if (line_art->priv->input)
    {
      /* gimp_line_art_prepare_async() will flush the pickable, which
       * may trigger this signal handler, and will leak a line art (as
       * line_art->priv->async has not been set yet).
       */
      g_signal_handlers_block_by_func (
        line_art->priv->input,
        G_CALLBACK (gimp_line_art_input_invalidate_preview),
        line_art);
      line_art->priv->async = gimp_line_art_prepare_async (line_art, +1);
      g_signal_emit (line_art, gimp_line_art_signals[COMPUTING_START], 0);
      g_signal_handlers_unblock_by_func (
        line_art->priv->input,
        G_CALLBACK (gimp_line_art_input_invalidate_preview),
        line_art);

      gimp_async_add_callback_for_object (line_art->priv->async,
                                          (GimpAsyncCallback) gimp_line_art_compute_cb,
                                          line_art, line_art);
    }
}

static void
gimp_line_art_compute_cb (GimpAsync   *async,
                          GimpLineArt *line_art)
{
  if (gimp_async_is_canceled (async))
    return;

  if (gimp_async_is_finished (async))
    {
      LineArtResult *result;

      result = gimp_async_get_result (async);

      line_art->priv->closed  = g_object_ref (result->closed);
      line_art->priv->distmap = result->distmap;
      result->distmap  = NULL;
      g_signal_emit (line_art, gimp_line_art_signals[COMPUTING_END], 0);
    }

  g_clear_object (&line_art->priv->async);
}

static GimpAsync *
gimp_line_art_prepare_async (GimpLineArt *line_art,
                             gint         priority)
{
  GeglBuffer  *buffer;
  GimpAsync   *async;
  LineArtData *data;

  g_return_val_if_fail (GIMP_IS_PICKABLE (line_art->priv->input), NULL);

  gimp_pickable_flush (line_art->priv->input);

  buffer = gimp_gegl_buffer_dup (
    gimp_pickable_get_buffer (line_art->priv->input));

  data  = line_art_data_new (buffer, line_art);

  g_object_unref (buffer);

  async = gimp_parallel_run_async_full (
    priority,
    (GimpRunAsyncFunc) gimp_line_art_prepare_async_func,
    data, (GDestroyNotify) line_art_data_free);

  return async;
}

static void
gimp_line_art_prepare_async_func (GimpAsync   *async,
                                  LineArtData *data)
{
  GeglBuffer *buffer;
  GeglBuffer *closed  = NULL;
  gfloat     *distmap = NULL;
  gint        buffer_x;
  gint        buffer_y;
  gboolean    has_alpha;
  gboolean    select_transparent = FALSE;

  has_alpha = babl_format_has_alpha (gegl_buffer_get_format (data->buffer));

  if (has_alpha)
    {
      if (data->select_transparent)
        {
          /*  don't select transparent regions if there are no fully
           *  transparent pixels.
           */
          GeglBufferIterator *gi;

          gi = gegl_buffer_iterator_new (data->buffer, NULL, 0,
                                         babl_format ("A u8"),
                                         GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 3);
          while (gegl_buffer_iterator_next (gi))
            {
              guint8 *p = (guint8*) gi->items[0].data;
              gint    k;

              if (gimp_async_is_canceled (async))
                {
                  gegl_buffer_iterator_stop (gi);

                  gimp_async_abort (async);

                  line_art_data_free (data);

                  return;
                }

              for (k = 0; k < gi->length; k++)
                {
                  if (! *p)
                    {
                      select_transparent = TRUE;
                      break;
                    }
                  p++;
                }
              if (select_transparent)
                break;
            }
          if (select_transparent)
            gegl_buffer_iterator_stop (gi);
        }
    }

  buffer   = data->buffer;
  buffer_x = gegl_buffer_get_x (data->buffer);
  buffer_y = gegl_buffer_get_y (data->buffer);

  if (buffer_x != 0 || buffer_y != 0)
    {
      buffer = g_object_new (GEGL_TYPE_BUFFER,
                             "source",  buffer,
                             "shift-x", buffer_x,
                             "shift-y", buffer_y,
                             NULL);
    }

  /* For smart selection, we generate a binarized image with close
   * regions, then run a composite selection with no threshold on
   * this intermediate buffer.
   */
  GIMP_TIMER_START();

  closed = gimp_line_art_close (buffer,
                                select_transparent,
                                data->threshold,
                                data->spline_max_len,
                                data->segment_max_len,
                                /*minimal_lineart_area,*/
                                5,
                                /*normal_estimate_mask_size,*/
                                5,
                                /*end_point_rate,*/
                                0.85,
                                /*spline_max_angle,*/
                                90.0,
                                /*end_point_connectivity,*/
                                2,
                                /*spline_roundness,*/
                                1.0,
                                /*allow_self_intersections,*/
                                TRUE,
                                /*created_regions_significant_area,*/
                                4,
                                /*created_regions_minimum_area,*/
                                100,
                                /*small_segments_from_spline_sources,*/
                                TRUE,
                                &distmap,
                                async);

  GIMP_TIMER_END("close line-art");

  if (buffer != data->buffer)
    g_object_unref (buffer);

  if (! gimp_async_is_stopped (async))
    {
      if (buffer_x != 0 || buffer_y != 0)
        {
          buffer = g_object_new (GEGL_TYPE_BUFFER,
                                 "source",  closed,
                                 "shift-x", -buffer_x,
                                 "shift-y", -buffer_y,
                                 NULL);

          g_object_unref (closed);

          closed = buffer;
        }

      gimp_async_finish_full (async,
                              line_art_result_new (closed, distmap),
                              (GDestroyNotify) line_art_result_free);
    }

  line_art_data_free (data);
}

static LineArtData *
line_art_data_new (GeglBuffer  *buffer,
                   GimpLineArt *line_art)
{
  LineArtData *data = g_slice_new (LineArtData);

  data->buffer             = g_object_ref (buffer);
  data->select_transparent = line_art->priv->select_transparent;
  data->threshold          = line_art->priv->threshold;
  data->spline_max_len     = line_art->priv->spline_max_len;
  data->segment_max_len    = line_art->priv->segment_max_len;

  return data;
}

static void
line_art_data_free (LineArtData *data)
{
  g_object_unref (data->buffer);

  g_slice_free (LineArtData, data);
}

static LineArtResult *
line_art_result_new (GeglBuffer *closed,
                     gfloat     *distmap)
{
  LineArtResult *data;

  data = g_slice_new (LineArtResult);
  data->closed  = closed;
  data->distmap = distmap;

  return data;
}

static void
line_art_result_free (LineArtResult *data)
{
  g_object_unref (data->closed);
  g_clear_pointer (&data->distmap, g_free);

  g_slice_free (LineArtResult, data);
}

static gboolean
gimp_line_art_idle (GimpLineArt *line_art)
{
  line_art->priv->idle_id = 0;

  gimp_line_art_compute (line_art);

  return G_SOURCE_REMOVE;
}

static void
gimp_line_art_input_invalidate_preview (GimpViewable *viewable,
                                        GimpLineArt  *line_art)
{
  if (! line_art->priv->idle_id)
    {
      line_art->priv->idle_id = g_idle_add_full (
        GIMP_PRIORITY_VIEWABLE_IDLE,
        (GSourceFunc) gimp_line_art_idle,
        line_art, NULL);
    }
}

/* All actual computation functions. */

/**
 * gimp_line_art_close:
 * @buffer: the input #GeglBuffer.
 * @select_transparent: whether we binarize the alpha channel or the
 *                      luminosity.
 * @stroke_threshold: [0-1] threshold value for detecting stroke pixels
 *                    (higher values will detect more stroke pixels).
 * @spline_max_length: the maximum length for creating splines between
 *                     end points.
 * @segment_max_length: the maximum length for creating segments
 *                      between end points. Unlike splines, segments
 *                      are straight lines.
 * @minimal_lineart_area: the minimum size in number pixels for area to
 *                        be considered as line art.
 * @normal_estimate_mask_size:
 * @end_point_rate: threshold to estimate if a curvature is an end-point
 *                  in [0-1] range value.
 * @spline_max_angle: the maximum angle between end point normals for
 *                    creating splines between them.
 * @end_point_connectivity:
 * @spline_roundness:
 * @allow_self_intersections: whether to allow created splines and
 *                            segments to intersect.
 * @created_regions_significant_area:
 * @created_regions_minimum_area:
 * @small_segments_from_spline_sources:
 * @closed_distmap: a distance map of the closed line art pixels.
 * @async: the #GimpAsync associated with the computation
 *
 * Creates a binarized version of the strokes of @buffer, detected either
 * with luminosity (light means background) or alpha values depending on
 * @select_transparent. This binary version of the strokes will have closed
 * regions allowing adequate selection of "nearly closed regions".
 * This algorithm is meant for digital painting (and in particular on the
 * sketch-only step), and therefore will likely produce unexpected results on
 * other types of input.
 *
 * The algorithm is the first step from the research paper "A Fast and
 * Efficient Semi-guided Algorithm for Flat Coloring Line-arts", by Sébastian
 * Fourey, David Tschumperlé, David Revoy.
 * https://hal.archives-ouvertes.fr/hal-01891876
 *
 * Returns: a new #GeglBuffer of format "Y u8" representing the
 *          binarized @line_art. If @lineart_distmap is not #NULL, a
 *          newly allocated float buffer is returned, which can be used
 *          for overflowing created masks later.
 */
static GeglBuffer *
gimp_line_art_close (GeglBuffer  *buffer,
                     gboolean     select_transparent,
                     gdouble      stroke_threshold,
                     gint         spline_max_length,
                     gint         segment_max_length,
                     gint         minimal_lineart_area,
                     gint         normal_estimate_mask_size,
                     gfloat       end_point_rate,
                     gfloat       spline_max_angle,
                     gint         end_point_connectivity,
                     gfloat       spline_roundness,
                     gboolean     allow_self_intersections,
                     gint         created_regions_significant_area,
                     gint         created_regions_minimum_area,
                     gboolean     small_segments_from_spline_sources,
                     gfloat     **closed_distmap,
                     GimpAsync   *async)
{
  const Babl         *gray_format;
  GeglBufferIterator *gi;
  GeglBuffer         *closed  = NULL;
  GeglBuffer         *strokes = NULL;
  guchar              max_value = 0;
  gint                width  = gegl_buffer_get_width (buffer);
  gint                height = gegl_buffer_get_height (buffer);
  gint                i;

  if (select_transparent)
    /* Keep alpha channel as gray levels */
    gray_format = babl_format ("A u8");
  else
    /* Keep luminance */
    gray_format = babl_format ("Y' u8");

  /* Transform the line art from any format to gray. */
  strokes = gegl_buffer_new (gegl_buffer_get_extent (buffer),
                             gray_format);
  gimp_gegl_buffer_copy (buffer, NULL, GEGL_ABYSS_NONE, strokes, NULL);
  gegl_buffer_set_format (strokes, babl_format ("Y' u8"));

  if (! select_transparent)
    {
      /* Compute the biggest value */
      gi = gegl_buffer_iterator_new (strokes, NULL, 0, NULL,
                                     GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1);
      while (gegl_buffer_iterator_next (gi))
        {
          guchar *data = (guchar*) gi->items[0].data;
          gint    k;

          if (gimp_async_is_canceled (async))
            {
              gegl_buffer_iterator_stop (gi);

              gimp_async_abort (async);

              goto end1;
            }

          for (k = 0; k < gi->length; k++)
            {
              if (*data > max_value)
                max_value = *data;
              data++;
            }
        }
    }

  /* Make the image binary: 1 is stroke, 0 background */
  gi = gegl_buffer_iterator_new (strokes, NULL, 0, NULL,
                                 GEGL_ACCESS_READWRITE, GEGL_ABYSS_NONE, 1);
  while (gegl_buffer_iterator_next (gi))
    {
      guchar *data = (guchar*) gi->items[0].data;
      gint    k;

      if (gimp_async_is_canceled (async))
        {
          gegl_buffer_iterator_stop (gi);

          gimp_async_abort (async);

          goto end1;
        }

      for (k = 0; k < gi->length; k++)
        {
          if (! select_transparent)
            /* Negate the value. */
            *data = max_value - *data;
          /* Apply a threshold. */
          if (*data > (guchar) (255.0f * (1.0f - stroke_threshold)))
            *data = 1;
          else
            *data = 0;
          data++;
        }
    }

  /* Denoise (remove small connected components) */
  gimp_lineart_denoise (strokes, minimal_lineart_area, async);
  if (gimp_async_is_stopped (async))
    goto end1;

  closed = g_object_ref (strokes);

  if (spline_max_length > 0 || segment_max_length > 0)
    {
      GArray     *keypoints           = NULL;
      GHashTable *visited             = NULL;
      gfloat     *radii               = NULL;
      gfloat     *normals             = NULL;
      gfloat     *curvatures          = NULL;
      gfloat     *smoothed_curvatures = NULL;
      gfloat      threshold;
      gfloat      clamped_threshold;
      GList      *fill_pixels         = NULL;
      GList      *iter;

      normals             = g_new0 (gfloat, width * height * 2);
      curvatures          = g_new0 (gfloat, width * height);
      smoothed_curvatures = g_new0 (gfloat, width * height);

      /* Estimate normals & curvature */
      gimp_lineart_compute_normals_curvatures (strokes, normals, curvatures,
                                               smoothed_curvatures,
                                               normal_estimate_mask_size,
                                               async);
      if (gimp_async_is_stopped (async))
        goto end2;

      radii = gimp_lineart_estimate_strokes_radii (strokes, async);
      if (gimp_async_is_stopped (async))
        goto end2;
      threshold = 1.0f - end_point_rate;
      clamped_threshold = MAX (0.25f, threshold);
      for (i = 0; i < width; i++)
        {
          gint j;

          if (gimp_async_is_canceled (async))
            {
              gimp_async_abort (async);

              goto end2;
            }

          for (j = 0; j < height; j++)
            {
              if (smoothed_curvatures[i + j * width] >= (threshold / MAX (1.0f, radii[i + j * width])) ||
                  curvatures[i + j * width] >= clamped_threshold)
                curvatures[i + j * width] = 1.0;
              else
                curvatures[i + j * width] = 0.0;
            }
        }
      g_clear_pointer (&radii, g_free);

      keypoints = gimp_lineart_curvature_extremums (curvatures, smoothed_curvatures,
                                                    width, height, async);
      if (gimp_async_is_stopped (async))
        goto end2;

      visited = g_hash_table_new_full ((GHashFunc) visited_hash_fun,
                                       (GEqualFunc) visited_equal_fun,
                                       (GDestroyNotify) g_free, NULL);

      if (spline_max_length > 0)
        {
          GList           *candidates;
          SplineCandidate *candidate;

          candidates = gimp_lineart_find_spline_candidates (keypoints, normals, width,
                                                            spline_max_length,
                                                            spline_max_angle,
                                                            async);
          if (gimp_async_is_stopped (async))
            goto end3;

          g_object_unref (closed);
          closed = gimp_gegl_buffer_dup (strokes);

          /* Draw splines */
          while (candidates)
            {
              Pixel    *p1;
              Pixel    *p2;
              gboolean  inserted = FALSE;

              if (gimp_async_is_canceled (async))
                {
                  gimp_async_abort (async);

                  goto end3;
                }

              p1 = g_new (Pixel, 1);
              p2 = g_new (Pixel, 1);

              candidate = (SplineCandidate *) candidates->data;
              p1->x = candidate->p1.x;
              p1->y = candidate->p1.y;
              p2->x = candidate->p2.x;
              p2->y = candidate->p2.y;

              g_free (candidate);
              candidates = g_list_delete_link (candidates, candidates);

              if ((! g_hash_table_contains (visited, p1) ||
                   GPOINTER_TO_INT (g_hash_table_lookup (visited, p1)) < end_point_connectivity) &&
                  (! g_hash_table_contains (visited, p2) ||
                   GPOINTER_TO_INT (g_hash_table_lookup (visited, p2)) < end_point_connectivity))
                {
                  GArray      *discrete_curve;
                  GimpVector2  vect1 = pair2normal (*p1, normals, width);
                  GimpVector2  vect2 = pair2normal (*p2, normals, width);
                  gfloat       distance = gimp_vector2_length_val (gimp_vector2_sub_val (*p1, *p2));
                  gint         transitions;

                  gimp_vector2_mul (&vect1, distance);
                  gimp_vector2_mul (&vect1, spline_roundness);
                  gimp_vector2_mul (&vect2, distance);
                  gimp_vector2_mul (&vect2, spline_roundness);

                  discrete_curve = gimp_lineart_discrete_spline (*p1, vect1, *p2, vect2);

                  transitions = allow_self_intersections ?
                    gimp_number_of_transitions (discrete_curve, strokes) :
                    gimp_number_of_transitions (discrete_curve, closed);

                  if (transitions == 2 &&
                      gimp_line_art_allow_closure (closed, discrete_curve,
                                                   &fill_pixels,
                                                   created_regions_significant_area,
                                                   created_regions_minimum_area))
                    {
                      for (i = 0; i < discrete_curve->len; i++)
                        {
                          Pixel p = g_array_index (discrete_curve, Pixel, i);

                          if (p.x >= 0 && p.x < gegl_buffer_get_width (closed) &&
                              p.y >= 0 && p.y < gegl_buffer_get_height (closed))
                            {
                              guchar val = 2;

                              gegl_buffer_set (closed, GEGL_RECTANGLE ((gint) p.x, (gint) p.y, 1, 1), 0,
                                               NULL, &val, GEGL_AUTO_ROWSTRIDE);
                            }
                        }
                      g_hash_table_replace (visited, p1,
                                            GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p1)) + 1));
                      g_hash_table_replace (visited, p2,
                                            GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p2)) + 1));
                      inserted = TRUE;
                    }
                  g_array_free (discrete_curve, TRUE);
                }
              if (! inserted)
                {
                  g_free (p1);
                  g_free (p2);
                }
            }

 end3:
          g_list_free_full (candidates, g_free);

          if (gimp_async_is_stopped (async))
            goto end2;
        }

      g_clear_object (&strokes);

      /* Draw straight line segments */
      if (segment_max_length > 0)
        {
          Pixel *point;

          point = (Pixel *) keypoints->data;
          for (i = 0; i < keypoints->len; i++)
            {
              Pixel    *p;
              gboolean  inserted = FALSE;

              if (gimp_async_is_canceled (async))
                {
                  gimp_async_abort (async);

                  goto end2;
                }

              p  = g_new (Pixel, 1);
              *p = *point;

              if (! g_hash_table_contains (visited, p) ||
                  (small_segments_from_spline_sources &&
                   GPOINTER_TO_INT (g_hash_table_lookup (visited, p)) < end_point_connectivity))
                {
                  GArray *segment = gimp_lineart_line_segment_until_hit (closed, *point,
                                                                         pair2normal (*point, normals, width),
                                                                         segment_max_length);

                  if (segment->len &&
                      gimp_line_art_allow_closure (closed, segment, &fill_pixels,
                                                   created_regions_significant_area,
                                                   created_regions_minimum_area))
                    {
                      gint j;

                      for (j = 0; j < segment->len; j++)
                        {
                          Pixel  p2 = g_array_index (segment, Pixel, j);
                          guchar val = 2;

                          gegl_buffer_set (closed, GEGL_RECTANGLE ((gint) p2.x, (gint) p2.y, 1, 1), 0,
                                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
                        }
                      g_hash_table_replace (visited, p,
                                            GINT_TO_POINTER (GPOINTER_TO_INT (g_hash_table_lookup (visited, p)) + 1));
                      inserted = TRUE;
                    }
                  g_array_free (segment, TRUE);
                }
              if (! inserted)
                g_free (p);
              point++;
            }
        }

      for (iter = fill_pixels; iter; iter = iter->next)
        {
          Pixel *p        = iter->data;
          gint   fill_max = created_regions_significant_area - 1;

          if (gimp_async_is_canceled (async))
            {
              gimp_async_abort (async);

              goto end2;
            }

          /* XXX A best approach would be to generalize
           * gimp_drawable_bucket_fill() to work on any buffer (the code
           * is already mostly there) rather than reimplementing a naive
           * bucket fill.
           * This is mostly a quick'n dirty first implementation which I
           * will improve later.
           */
          gimp_line_art_simple_fill (closed, (gint) p->x, (gint) p->y, &fill_max);
        }

 end2:
      g_list_free_full (fill_pixels, g_free);
      g_free (normals);
      g_free (curvatures);
      g_free (smoothed_curvatures);
      g_clear_pointer (&radii, g_free);
      if (keypoints)
        g_array_free (keypoints, TRUE);
      g_clear_pointer (&visited, g_hash_table_destroy);

      if (gimp_async_is_stopped (async))
        goto end1;
    }
  else
    {
      g_clear_object (&strokes);
    }

  if (closed_distmap)
    {
      GeglNode   *graph;
      GeglNode   *input;
      GeglNode   *op;

      /* Flooding needs a distance map for closed line art. */
      *closed_distmap = g_new (gfloat, width * height);

      graph = gegl_node_new ();
      input = gegl_node_new_child (graph,
                                   "operation", "gegl:buffer-source",
                                   "buffer", closed,
                                   NULL);
      op  = gegl_node_new_child (graph,
                                 "operation", "gegl:distance-transform",
                                 "metric",    GEGL_DISTANCE_METRIC_EUCLIDEAN,
                                 "normalize", FALSE,
                                 NULL);
      gegl_node_connect_to (input, "output",
                            op, "input");
      gegl_node_blit (op, 1.0, gegl_buffer_get_extent (closed),
                      NULL, *closed_distmap,
                      GEGL_AUTO_ROWSTRIDE, GEGL_BLIT_DEFAULT);
      g_object_unref (graph);
    }

 end1:
  g_clear_object (&strokes);

  if (gimp_async_is_stopped (async))
    g_clear_object (&closed);

  return closed;
}

static void
gimp_lineart_denoise (GeglBuffer *buffer,
                      int         minimum_area,
                      GimpAsync  *async)
{
  /* Keep connected regions with significant area. */
  GArray   *region;
  GQueue   *q       = g_queue_new ();
  gint      width   = gegl_buffer_get_width (buffer);
  gint      height  = gegl_buffer_get_height (buffer);
  gboolean *visited = g_new0 (gboolean, width * height);
  gint      x, y;

  region = g_array_sized_new (TRUE, TRUE, sizeof (Pixel *), minimum_area);

  for (y = 0; y < height; ++y)
    for (x = 0; x < width; ++x)
      {
        guchar has_stroke;

        if (gimp_async_is_canceled (async))
          {
            gimp_async_abort (async);

            goto end;
          }

        gegl_buffer_sample (buffer, x, y, NULL, &has_stroke, NULL,
                            GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
        if (has_stroke && ! visited[x + y * width])
          {
            Pixel *p = g_new (Pixel, 1);
            gint   regionSize = 0;

            p->x = x;
            p->y = y;

            g_queue_push_tail (q, p);
            visited[x + y * width] = TRUE;

            while (! g_queue_is_empty (q))
              {
                Pixel *p;
                gint   p2x;
                gint   p2y;

                if (gimp_async_is_canceled (async))
                  {
                    gimp_async_abort (async);

                    goto end;
                  }

                p = (Pixel *) g_queue_pop_head (q);

                p2x = p->x + 1;
                p2y = p->y;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x +p2y * width] = TRUE;
                      }
                  }
                p2x = p->x - 1;
                p2y = p->y;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x;
                p2y = p->y - 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x;
                p2y = p->y + 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x + 1;
                p2y = p->y + 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x - 1;
                p2y = p->y - 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x - 1;
                p2y = p->y + 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }
                p2x = p->x + 1;
                p2y = p->y - 1;
                if (p2x >= 0 && p2x < width && p2y >= 0 && p2y < height)
                  {
                    gegl_buffer_sample (buffer, p2x, p2y, NULL, &has_stroke, NULL,
                                        GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                    if (has_stroke && ! visited[p2x + p2y * width])
                      {
                        Pixel *p2 = g_new (Pixel, 1);

                        p2->x = p2x;
                        p2->y = p2y;
                        g_queue_push_tail (q, p2);
                        visited[p2x + p2y * width] = TRUE;
                      }
                  }

                ++regionSize;
                if (regionSize < minimum_area)
                  g_array_append_val (region, *p);
                g_free (p);
              }
            if (regionSize < minimum_area)
              {
                Pixel *pixel = (Pixel *) region->data;
                gint   i = 0;

                for (; i < region->len; i++)
                  {
                    guchar val = 0;
                    gegl_buffer_set (buffer, GEGL_RECTANGLE (pixel->x, pixel->y, 1, 1), 0,
                                     NULL, &val, GEGL_AUTO_ROWSTRIDE);
                    pixel++;
                  }
              }
            g_array_remove_range (region, 0, region->len);
          }
      }

 end:
  g_array_free (region, TRUE);
  g_queue_free_full (q, g_free);
  g_free (visited);
}

static void
gimp_lineart_compute_normals_curvatures (GeglBuffer *mask,
                                         gfloat     *normals,
                                         gfloat     *curvatures,
                                         gfloat     *smoothed_curvatures,
                                         int         normal_estimate_mask_size,
                                         GimpAsync  *async)
{
  gfloat  *edgels_curvatures  = NULL;
  gfloat  *smoothed_curvature;
  GArray  *es                 = NULL;
  Edgel  **e;
  gint     width              = gegl_buffer_get_width (mask);

  es = gimp_edgelset_new (mask, async);
  if (gimp_async_is_stopped (async))
    goto end;

  e = (Edgel **) es->data;

  gimp_edgelset_smooth_normals (es, normal_estimate_mask_size, async);
  if (gimp_async_is_stopped (async))
    goto end;

  gimp_edgelset_compute_curvature (es, async);
  if (gimp_async_is_stopped (async))
    goto end;

  while (*e)
    {
      const float curvature = ((*e)->curvature > 0.0f) ? (*e)->curvature : 0.0f;
      const float w         = MAX (1e-8f, curvature * curvature);

      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          goto end;
        }

      normals[((*e)->x + (*e)->y * width) * 2] += w * (*e)->x_normal;
      normals[((*e)->x + (*e)->y * width) * 2 + 1] += w * (*e)->y_normal;
      curvatures[(*e)->x + (*e)->y * width] = MAX (curvature,
                                                   curvatures[(*e)->x + (*e)->y * width]);
      e++;
    }
  for (int y = 0; y < gegl_buffer_get_height (mask); ++y)
    {
      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          goto end;
        }

      for (int x = 0; x < gegl_buffer_get_width (mask); ++x)
        {
          const float _angle = atan2f (normals[(x + y * width) * 2 + 1],
                                       normals[(x + y * width) * 2]);
          normals[(x + y * width) * 2] = cosf (_angle);
          normals[(x + y * width) * 2 + 1] = sinf (_angle);
        }
    }

  /* Smooth curvatures on edgels, then take maximum on each pixel. */
  edgels_curvatures = gimp_lineart_get_smooth_curvatures (es, async);
  if (gimp_async_is_stopped (async))
    goto end;

  smoothed_curvature = edgels_curvatures;

  e = (Edgel **) es->data;
  while (*e)
    {
      gfloat *pixel_curvature = &smoothed_curvatures[(*e)->x + (*e)->y * width];

      if (*pixel_curvature < *smoothed_curvature)
        *pixel_curvature = *smoothed_curvature;

      ++smoothed_curvature;
      e++;
    }

 end:
  g_free (edgels_curvatures);

  if (es)
    g_array_free (es, TRUE);
}

static gfloat *
gimp_lineart_get_smooth_curvatures (GArray    *edgelset,
                                    GimpAsync *async)
{
  Edgel  **e;
  gfloat  *smoothed_curvatures = g_new0 (gfloat, edgelset->len);
  gfloat   weights[9];
  gfloat   smoothed_curvature;
  gfloat   weights_sum;
  gint     idx = 0;

  weights[0] = 1.0f;
  for (int i = 1; i <= 8; ++i)
    weights[i] = expf (-(i * i) / 30.0f);

  e = (Edgel **) edgelset->data;
  while (*e)
    {
      Edgel *edgel_before = g_array_index (edgelset, Edgel*, (*e)->previous);
      Edgel *edgel_after  = g_array_index (edgelset, Edgel*, (*e)->next);
      int    n = 5;
      int    i = 1;

      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          g_free (smoothed_curvatures);

          return NULL;
        }

      smoothed_curvature = (*e)->curvature;
      weights_sum = weights[0];
      while (n-- && (edgel_after != edgel_before))
        {
          smoothed_curvature += weights[i] * edgel_before->curvature;
          smoothed_curvature += weights[i] * edgel_after->curvature;
          edgel_before = g_array_index (edgelset, Edgel*, edgel_before->previous);
          edgel_after  = g_array_index (edgelset, Edgel*, edgel_after->next);
          weights_sum += 2 * weights[i];
          i++;
        }
      smoothed_curvature /= weights_sum;
      smoothed_curvatures[idx++] = smoothed_curvature;

      e++;
    }

  return smoothed_curvatures;
}

/**
 * Keep one pixel per connected component of curvature extremums.
 */
static GArray *
gimp_lineart_curvature_extremums (gfloat    *curvatures,
                                  gfloat    *smoothed_curvatures,
                                  gint       width,
                                  gint       height,
                                  GimpAsync *async)
{
  gboolean *visited = g_new0 (gboolean, width * height);
  GQueue   *q       = g_queue_new ();
  GArray   *max_positions;

  max_positions = g_array_new (FALSE, TRUE, sizeof (Pixel));

  for (int y = 0; y < height; ++y)
    {
      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          goto end;
        }

      for (int x = 0; x < width; ++x)
        {
          if ((curvatures[x + y * width] > 0.0) && ! visited[x + y * width])
            {
              Pixel  *p = g_new (Pixel, 1);
              Pixel   max_smoothed_curvature_pixel;
              Pixel   max_raw_curvature_pixel;
              gfloat  max_smoothed_curvature;
              gfloat  max_raw_curvature;

              max_smoothed_curvature_pixel = gimp_vector2_new (-1.0, -1.0);
              max_smoothed_curvature       = 0.0f;

              max_raw_curvature_pixel = gimp_vector2_new (x, y);
              max_raw_curvature       = curvatures[x + y * width];

              p->x = x;
              p->y = y;
              g_queue_push_tail (q, p);
              visited[x + y * width] = TRUE;

              while (! g_queue_is_empty (q))
                {
                  gfloat sc;
                  gfloat c;
                  gint   p2x;
                  gint   p2y;

                  if (gimp_async_is_canceled (async))
                    {
                      gimp_async_abort (async);

                      goto end;
                    }

                  p  = (Pixel *) g_queue_pop_head (q);
                  sc = smoothed_curvatures[(gint) p->x + (gint) p->y * width];
                  c  = curvatures[(gint) p->x + (gint) p->y * width];

                  curvatures[(gint) p->x + (gint) p->y * width] = 0.0f;

                  p2x = (gint) p->x + 1;
                  p2y = (gint) p->y;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  p2x = p->x - 1;
                  p2y = p->y;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  p2x = p->x;
                  p2y = p->y - 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  p2x = p->x;
                  p2y = p->y + 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  p2x = p->x + 1;
                  p2y = p->y + 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  p2x = p->x - 1;
                  p2y = p->y - 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  p2x = p->x - 1;
                  p2y = p->y + 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  p2x = p->x + 1;
                  p2y = p->y - 1;
                  if (p2x >= 0 && p2x < width    &&
                      p2y >= 0 && p2y < height   &&
                      curvatures[p2x + p2y * width] > 0.0 &&
                      ! visited[p2x + p2y * width])
                    {
                      Pixel *p2 = g_new (Pixel, 1);

                      p2->x = p2x;
                      p2->y = p2y;
                      g_queue_push_tail (q, p2);
                      visited[p2x + p2y * width] = TRUE;
                    }

                  if (sc > max_smoothed_curvature)
                    {
                      max_smoothed_curvature_pixel = *p;
                      max_smoothed_curvature = sc;
                    }
                  if (c > max_raw_curvature)
                    {
                      max_raw_curvature_pixel = *p;
                      max_raw_curvature = c;
                    }
                  g_free (p);
                }
              if (max_smoothed_curvature > 0.0f)
                {
                  curvatures[(gint) max_smoothed_curvature_pixel.x + (gint) max_smoothed_curvature_pixel.y * width] = max_smoothed_curvature;
                  g_array_append_val (max_positions, max_smoothed_curvature_pixel);
                }
              else
                {
                  curvatures[(gint) max_raw_curvature_pixel.x + (gint) max_raw_curvature_pixel.y * width] = max_raw_curvature;
                  g_array_append_val (max_positions, max_raw_curvature_pixel);
                }
            }
        }
    }

 end:
  g_queue_free_full (q, g_free);
  g_free (visited);

  if (gimp_async_is_stopped (async))
    {
      g_array_free (max_positions, TRUE);
      max_positions = NULL;
    }

  return max_positions;
}

static gint
gimp_spline_candidate_cmp (const SplineCandidate *a,
                           const SplineCandidate *b,
                           gpointer               user_data)
{
  /* This comparison actually returns the opposite of common comparison
   * functions on purpose, as we want the first element on the list to
   * be the "bigger".
   */
  if (a->quality < b->quality)
    return 1;
  else if (a->quality > b->quality)
    return -1;
  else
    return 0;
}

static GList *
gimp_lineart_find_spline_candidates (GArray    *max_positions,
                                     gfloat    *normals,
                                     gint       width,
                                     gint       distance_threshold,
                                     gfloat     max_angle_deg,
                                     GimpAsync *async)
{
  GList       *candidates = NULL;
  const float  CosMin     = cosf (M_PI * (max_angle_deg / 180.0));
  gint         i;

  for (i = 0; i < max_positions->len; i++)
    {
      Pixel p1 = g_array_index (max_positions, Pixel, i);
      gint  j;

      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          g_list_free_full (candidates, g_free);

          return NULL;
        }

      for (j = i + 1; j < max_positions->len; j++)
        {
          Pixel       p2 = g_array_index (max_positions, Pixel, j);
          const float distance = gimp_vector2_length_val (gimp_vector2_sub_val (p1, p2));

          if (distance <= distance_threshold)
            {
              GimpVector2 normalP1;
              GimpVector2 normalP2;
              GimpVector2 p1f;
              GimpVector2 p2f;
              GimpVector2 p1p2;
              float       cosN;
              float       qualityA;
              float       qualityB;
              float       qualityC;
              float       quality;

              normalP1 = gimp_vector2_new (normals[((gint) p1.x + (gint) p1.y * width) * 2],
                                           normals[((gint) p1.x + (gint) p1.y * width) * 2 + 1]);
              normalP2 = gimp_vector2_new (normals[((gint) p2.x + (gint) p2.y * width) * 2],
                                           normals[((gint) p2.x + (gint) p2.y * width) * 2 + 1]);
              p1f = gimp_vector2_new (p1.x, p1.y);
              p2f = gimp_vector2_new (p2.x, p2.y);
              p1p2 = gimp_vector2_sub_val (p2f, p1f);

              cosN = gimp_vector2_inner_product_val (normalP1, (gimp_vector2_neg_val (normalP2)));
              qualityA = MAX (0.0f, 1 - distance / distance_threshold);
              qualityB = MAX (0.0f,
                              (float) (gimp_vector2_inner_product_val (normalP1, p1p2) - gimp_vector2_inner_product_val (normalP2, p1p2)) /
                              distance);
              qualityC = MAX (0.0f, cosN - CosMin);
              quality = qualityA * qualityB * qualityC;
              if (quality > 0)
                {
                  SplineCandidate *candidate = g_new (SplineCandidate, 1);

                  candidate->p1      = p1;
                  candidate->p2      = p2;
                  candidate->quality = quality;

                  candidates = g_list_insert_sorted_with_data (candidates, candidate,
                                                               (GCompareDataFunc) gimp_spline_candidate_cmp,
                                                               NULL);
                }
            }
        }
    }
  return candidates;
}

static GArray *
gimp_lineart_discrete_spline (Pixel       p0,
                              GimpVector2 n0,
                              Pixel       p1,
                              GimpVector2 n1)
{
  GArray       *points = g_array_new (FALSE, TRUE, sizeof (Pixel));
  const double  a0 = 2 * p0.x - 2 * p1.x + n0.x - n1.x;
  const double  b0 = -3 * p0.x + 3 * p1.x - 2 * n0.x + n1.x;
  const double  c0 = n0.x;
  const double  d0 = p0.x;
  const double  a1 = 2 * p0.y - 2 * p1.y + n0.y - n1.y;
  const double  b1 = -3 * p0.y + 3 * p1.y - 2 * n0.y + n1.y;
  const double  c1 = n0.y;
  const double  d1 = p0.y;

  double        t = 0.0;
  const double  dtMin = 1.0 / MAX (fabs (p0.x - p1.x), fabs (p0.y - p1.y));
  Pixel         point = gimp_vector2_new ((gint) round (d0), (gint) round (d1));

  g_array_append_val (points, point);

  while (t <= 1.0)
    {
      const double t2 = t * t;
      const double t3 = t * t2;
      double       dx;
      double       dy;
      Pixel        p = gimp_vector2_new ((gint) round (a0 * t3 + b0 * t2 + c0 * t + d0),
                                         (gint) round (a1 * t3 + b1 * t2 + c1 * t + d1));

      /* create gimp_vector2_neq () ? */
      if (g_array_index (points, Pixel, points->len - 1).x != p.x ||
          g_array_index (points, Pixel, points->len - 1).y != p.y)
        {
          g_array_append_val (points, p);
        }
      dx = fabs (3 * a0 * t * t + 2 * b0 * t + c0) + 1e-8;
      dy = fabs (3 * a1 * t * t + 2 * b1 * t + c1) + 1e-8;
      t += MIN (dtMin, 0.75 / MAX (dx, dy));
    }
  if (g_array_index (points, Pixel, points->len - 1).x != p1.x ||
      g_array_index (points, Pixel, points->len - 1).y != p1.y)
    {
      g_array_append_val (points, p1);
    }
  return points;
}

static gint
gimp_number_of_transitions (GArray     *pixels,
                            GeglBuffer *buffer)
{
  int result = 0;

  if (pixels->len > 0)
    {
      Pixel    it = g_array_index (pixels, Pixel, 0);
      guchar   value;
      gboolean previous;
      gint     i;

      gegl_buffer_sample (buffer, (gint) it.x, (gint) it.y, NULL, &value, NULL,
                          GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
      previous = (gboolean) value;

      /* Starts at the second element. */
      for (i = 1; i < pixels->len; i++)
        {
          it = g_array_index (pixels, Pixel, i);

          gegl_buffer_sample (buffer, (gint) it.x, (gint) it.y, NULL, &value, NULL,
                              GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          result += ((gboolean) value != previous);
          previous = (gboolean) value;
        }
    }

  return result;
}

/**
 * gimp_line_art_allow_closure:
 * @mask: the current state of line art closure.
 * @pixels: the pixels of a candidate closure (spline or segment).
 * @fill_pixels: #GList of unsignificant pixels to bucket fill.
 * @significant_size: number of pixels for area to be considered
 *                    "significant".
 * @minimum_size: number of pixels for area to be allowed.
 *
 * Checks whether adding the set of points @pixels to @mask will create
 * 4-connected background regions whose size (i.e. number of pixels)
 * will be below @minimum_size. If it creates such small areas, the
 * function will refuse this candidate spline/segment, with the
 * exception of very small areas under @significant_size. These
 * micro-area are considered "unsignificant" and accepted (because they
 * can be created in some conditions, for instance when created curves
 * cross or start from a same endpoint), and one pixel for each
 * micro-area will be added to @fill_pixels to be later filled along
 * with the candidate pixels.
 *
 * Returns: #TRUE if @pixels should be added to @mask, #FALSE otherwise.
 */
static gboolean
gimp_line_art_allow_closure (GeglBuffer *mask,
                             GArray     *pixels,
                             GList     **fill_pixels,
                             int         significant_size,
                             int         minimum_size)
{
  /* A theorem from the paper is that a zone with more than
   * `2 * (@minimum_size - 1)` edgels (border pixels) will have more
   * than @minimum_size pixels.
   * Since we are following the edges of the area, we can therefore stop
   * earlier if we reach this number of edgels.
   */
  const glong max_edgel_count = 2 * minimum_size;

  Pixel      *p = (Pixel*) pixels->data;
  GList      *fp = NULL;
  gint        i;

  /* Mark pixels */
  for (i = 0; i < pixels->len; i++)
    {
      if (p->x >= 0 && p->x < gegl_buffer_get_width (mask) &&
          p->y >= 0 && p->y < gegl_buffer_get_height (mask))
        {
          guchar val;

          gegl_buffer_sample (mask, (gint) p->x, (gint) p->y, NULL, &val,
                              NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          val = val ? 3 : 2;
          gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p->x, (gint) p->y, 1, 1), 0,
                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
        }
      p++;
    }

  for (i = 0; i < pixels->len; i++)
    {
      Pixel p = g_array_index (pixels, Pixel, i);

      for (int direction = 0; direction < 4; ++direction)
        {
          if (p.x >= 0 && p.x < gegl_buffer_get_width (mask) &&
              p.y >= 0 && p.y < gegl_buffer_get_height (mask) &&
              border_in_direction (mask, p, direction))
            {
              Edgel  e;
              guchar val;
              glong  count;
              glong  area;

              gegl_buffer_sample (mask, (gint) p.x, (gint) p.y, NULL, &val,
                                  NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
              if ((gboolean) (val & (4 << direction)))
                continue;

              gimp_edgel_init (&e);
              e.x = p.x;
              e.y = p.y;
              e.direction = direction;

              count = gimp_edgel_track_mark (mask, e, max_edgel_count);
              if ((count != -1) && (count <= max_edgel_count))
                {
                  area = gimp_edgel_region_area (mask, e);

                  if (area >= significant_size && area < minimum_size)
                    {
                      gint j;

                      /* Remove marks */
                      for (j = 0; j < pixels->len; j++)
                        {
                          Pixel p2 = g_array_index (pixels, Pixel, j);

                          if (p2.x >= 0 && p2.x < gegl_buffer_get_width (mask) &&
                              p2.y >= 0 && p2.y < gegl_buffer_get_height (mask))
                            {
                              guchar val;

                              gegl_buffer_sample (mask, (gint) p2.x, (gint) p2.y, NULL, &val,
                                                  NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
                              val &= 1;
                              gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p2.x, (gint) p2.y, 1, 1), 0,
                                               NULL, &val, GEGL_AUTO_ROWSTRIDE);
                            }
                        }
                      g_list_free_full (fp, g_free);

                      return FALSE;
                    }
                  else if (area > 0 && area < significant_size)
                    {
                      Pixel *np = g_new (Pixel, 1);

                      np->x = direction == XPlusDirection ? p.x + 1 : (direction == XMinusDirection ? p.x - 1 : p.x);
                      np->y = direction == YPlusDirection ? p.y + 1 : (direction == YMinusDirection ? p.y - 1 : p.y);

                      if (np->x >= 0 && np->x < gegl_buffer_get_width (mask) &&
                          np->y >= 0 && np->y < gegl_buffer_get_height (mask))
                        fp = g_list_prepend (fp, np);
                      else
                        g_free (np);
                    }
                }
            }
        }
    }

  *fill_pixels = g_list_concat (*fill_pixels, fp);
  /* Remove marks */
  for (i = 0; i < pixels->len; i++)
    {
      Pixel p = g_array_index (pixels, Pixel, i);

      if (p.x >= 0 && p.x < gegl_buffer_get_width (mask) &&
          p.y >= 0 && p.y < gegl_buffer_get_height (mask))
        {
          guchar val;

          gegl_buffer_sample (mask, (gint) p.x, (gint) p.y, NULL, &val,
                              NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          val &= 1;
          gegl_buffer_set (mask, GEGL_RECTANGLE ((gint) p.x, (gint) p.y, 1, 1), 0,
                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
        }
    }
  return TRUE;
}

static GArray *
gimp_lineart_line_segment_until_hit (const GeglBuffer *mask,
                                     Pixel             start,
                                     GimpVector2       direction,
                                     int               size)
{
  GeglBuffer  *buffer = (GeglBuffer *) mask;
  gboolean     out = FALSE;
  GArray      *points = g_array_new (FALSE, TRUE, sizeof (Pixel));
  int          tmax;
  GimpVector2  p0 = gimp_vector2_new (start.x, start.y);

  gimp_vector2_mul (&direction, (gdouble) size);
  direction.x = round (direction.x);
  direction.y = round (direction.y);

  tmax = MAX (abs ((int) direction.x), abs ((int) direction.y));

  for (int t = 0; t <= tmax; ++t)
    {
      GimpVector2 v = gimp_vector2_add_val (p0, gimp_vector2_mul_val (direction, (float)t / tmax));
      Pixel       p;

      p.x = (gint) round (v.x);
      p.y = (gint) round (v.y);
      if (p.x >= 0 && p.x < gegl_buffer_get_width (buffer) &&
          p.y >= 0 && p.y < gegl_buffer_get_height (buffer))
        {
          guchar val;
          gegl_buffer_sample (buffer, p.x, p.y, NULL, &val,
                              NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
          if (out && val)
            {
              return points;
            }
          out = ! val;
        }
      else if (out)
        {
          return points;
        }
      else
        {
          g_array_free (points, TRUE);
          return g_array_new (FALSE, TRUE, sizeof (Pixel));
        }
      g_array_append_val (points, p);
    }

  g_array_free (points, TRUE);
  return g_array_new (FALSE, TRUE, sizeof (Pixel));
}

static gfloat *
gimp_lineart_estimate_strokes_radii (GeglBuffer *mask,
                                     GimpAsync  *async)
{
  GeglBufferIterator *gi;
  gfloat             *dist;
  gfloat             *thickness;
  GeglNode           *graph;
  GeglNode           *input;
  GeglNode           *op;
  gint                width  = gegl_buffer_get_width (mask);
  gint                height = gegl_buffer_get_height (mask);

  /* Compute a distance map for the line art. */
  dist = g_new (gfloat, width * height);

  graph = gegl_node_new ();
  input = gegl_node_new_child (graph,
                               "operation", "gegl:buffer-source",
                               "buffer", mask,
                               NULL);
  op  = gegl_node_new_child (graph,
                             "operation", "gegl:distance-transform",
                             "metric",    GEGL_DISTANCE_METRIC_EUCLIDEAN,
                             "normalize", FALSE,
                             NULL);
  gegl_node_connect_to (input, "output", op, "input");
  gegl_node_blit (op, 1.0, gegl_buffer_get_extent (mask),
                  NULL, dist, GEGL_AUTO_ROWSTRIDE, GEGL_BLIT_DEFAULT);
  g_object_unref (graph);

  thickness = g_new0 (gfloat, width * height);
  gi = gegl_buffer_iterator_new (mask, NULL, 0, NULL,
                                 GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 1);
  while (gegl_buffer_iterator_next (gi))
    {
      guint8 *m = (guint8*) gi->items[0].data;
      gint    startx = gi->items[0].roi.x;
      gint    starty = gi->items[0].roi.y;
      gint    endy   = starty + gi->items[0].roi.height;
      gint    endx   = startx + gi->items[0].roi.width;
      gint    x;
      gint    y;

      if (gimp_async_is_canceled (async))
        {
          gegl_buffer_iterator_stop (gi);

          gimp_async_abort (async);

          goto end;
        }

      for (y = starty; y < endy; y++)
        for (x = startx; x < endx; x++)
          {
            if (*m && dist[x + y * width] == 1.0)
              {
                gint     dx = x;
                gint     dy = y;
                gfloat   d  = 1.0;
                gfloat   nd;
                gboolean neighbour_thicker = TRUE;

                while (neighbour_thicker)
                  {
                    gint px = dx - 1;
                    gint py = dy - 1;
                    gint nx = dx + 1;
                    gint ny = dy + 1;

                    neighbour_thicker = FALSE;
                    if (px >= 0)
                      {
                        if ((nd = dist[px + dy * width]) > d)
                          {
                            d = nd;
                            dx = px;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (py >= 0 && (nd = dist[px + py * width]) > d)
                          {
                            d = nd;
                            dx = px;
                            dy = py;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (ny < height && (nd = dist[px + ny * width]) > d)
                          {
                            d = nd;
                            dx = px;
                            dy = ny;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                      }
                    if (nx < width)
                      {
                        if ((nd = dist[nx + dy * width]) > d)
                          {
                            d = nd;
                            dx = nx;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (py >= 0 && (nd = dist[nx + py * width]) > d)
                          {
                            d = nd;
                            dx = nx;
                            dy = py;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                        if (ny < height && (nd = dist[nx + ny * width]) > d)
                          {
                            d = nd;
                            dx = nx;
                            dy = ny;
                            neighbour_thicker = TRUE;
                            continue;
                          }
                      }
                    if (py > 0 && (nd = dist[dx + py * width]) > d)
                      {
                        d = nd;
                        dy = py;
                        neighbour_thicker = TRUE;
                        continue;
                      }
                    if (ny < height && (nd = dist[dx + ny * width]) > d)
                      {
                        d = nd;
                        dy = ny;
                        neighbour_thicker = TRUE;
                        continue;
                      }
                  }
                thickness[(gint) x + (gint) y * width] = d;
              }
            m++;
          }
    }

 end:
  g_free (dist);

  if (gimp_async_is_stopped (async))
    g_clear_pointer (&thickness, g_free);

  return thickness;
}

static void
gimp_line_art_simple_fill (GeglBuffer *buffer,
                           gint        x,
                           gint        y,
                           gint       *counter)
{
  guchar val;

  if (x < 0 || x >= gegl_buffer_get_width (buffer)  ||
      y < 0 || y >= gegl_buffer_get_height (buffer) ||
      *counter <= 0)
    return;

  gegl_buffer_sample (buffer, x, y, NULL, &val,
                      NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);

  if (! val)
    {
      val = 1;
      gegl_buffer_set (buffer, GEGL_RECTANGLE (x, y, 1, 1), 0,
                       NULL, &val, GEGL_AUTO_ROWSTRIDE);
      (*counter)--;
      gimp_line_art_simple_fill (buffer, x + 1, y, counter);
      gimp_line_art_simple_fill (buffer, x - 1, y, counter);
      gimp_line_art_simple_fill (buffer, x, y + 1, counter);
      gimp_line_art_simple_fill (buffer, x, y - 1, counter);
    }
}

static guint
visited_hash_fun (Pixel *key)
{
  /* Cantor pairing function. */
  return (key->x + key->y) * (key->x + key->y + 1) / 2 + key->y;
}

static gboolean
visited_equal_fun (Pixel *e1,
                   Pixel *e2)
{
  return (e1->x == e2->x && e1->y == e2->y);
}

static inline gboolean
border_in_direction (GeglBuffer *mask,
                     Pixel       p,
                     int         direction)
{
  gint px = (gint) p.x + DeltaX[direction];
  gint py = (gint) p.y + DeltaY[direction];

  if (px >= 0 && px < gegl_buffer_get_width (mask) &&
      py >= 0 && py < gegl_buffer_get_height (mask))
    {
      guchar val;

      gegl_buffer_sample (mask, px, py, NULL, &val,
                          NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
      return ! ((gboolean) val);
    }
  return TRUE;
}

static inline GimpVector2
pair2normal (Pixel   p,
             gfloat *normals,
             gint    width)
{
  return gimp_vector2_new (normals[((gint) p.x + (gint) p.y * width) * 2],
                           normals[((gint) p.x + (gint) p.y * width) * 2 + 1]);
}
/* Edgel functions */

static Edgel *
gimp_edgel_new (int       x,
                int       y,
                Direction direction)
{
  Edgel *edgel = g_new (Edgel, 1);

  edgel->x         = x;
  edgel->y         = y;
  edgel->direction = direction;

  gimp_edgel_init (edgel);

  return edgel;
}

static void
gimp_edgel_init (Edgel *edgel)
{
  edgel->x_normal  = 0;
  edgel->y_normal  = 0;
  edgel->curvature = 0;
  edgel->next      = edgel->previous = G_MAXUINT;
}

static void
gimp_edgel_clear (Edgel **edgel)
{
  g_clear_pointer (edgel, g_free);
}

static int
gimp_edgel_cmp (const Edgel* e1,
                const Edgel* e2)
{
  gimp_assert (e1 && e2);

  if ((e1->x == e2->x) && (e1->y == e2->y) &&
      (e1->direction == e2->direction))
    return 0;
  else if ((e1->y < e2->y) || (e1->y == e2->y && e1->x < e2->x) ||
           (e1->y == e2->y && e1->x == e2->x && e1->direction < e2->direction))
    return -1;
  else
    return 1;
}

static guint
edgel2index_hash_fun (Edgel *key)
{
  /* Cantor pairing function.
   * Was not sure how to use the direction though. :-/
   */
  return (key->x + key->y) * (key->x + key->y + 1) / 2 + key->y * key->direction;
}

static gboolean
edgel2index_equal_fun (Edgel *e1,
                       Edgel *e2)
{
  return (e1->x == e2->x && e1->y == e2->y &&
          e1->direction == e2->direction);
}

/**
 * @mask;
 * @edgel:
 * @size_limit:
 *
 * Track a border, marking inner pixels with a bit corresponding to the
 * edgel traversed (4 << direction) for direction in {0,1,2,3}.
 * Stop tracking after @size_limit edgels have been visited.
 *
 * Returns: Number of visited edgels, or -1 if an already visited edgel
 *          has been encountered.
 */
static glong
gimp_edgel_track_mark (GeglBuffer *mask,
                       Edgel       edgel,
                       long        size_limit)
{
  Edgel start = edgel;
  long  count = 1;

  do
    {
      guchar val;

      gimp_edgelset_next8 (mask, &edgel, &edgel);
      gegl_buffer_sample (mask, edgel.x, edgel.y, NULL, &val,
                          NULL, GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
      if (val & 2)
        {
          /* Only mark pixels of the spline/segment */
          if (val & (4 << edgel.direction))
            return -1;

          /* Mark edgel in pixel (1 == In Mask, 2 == Spline/Segment) */
          val |= (4 << edgel.direction);
          gegl_buffer_set (mask, GEGL_RECTANGLE (edgel.x, edgel.y, 1, 1), 0,
                           NULL, &val, GEGL_AUTO_ROWSTRIDE);
        }
      if (gimp_edgel_cmp (&edgel, &start) != 0)
        ++count;
    }
  while (gimp_edgel_cmp (&edgel, &start) != 0 && count <= size_limit);

  return count;
}

/**
 * gimp_edgel_region_area:
 * @mask: current state of closed line art buffer.
 * @start_edgel: edgel to follow.
 *
 * Follows a line border, starting from @start_edgel to compute the area
 * enclosed by this border.
 * Unfortunately this may return a negative area when the line does not
 * close a zone. In this case, there is an uncertaincy on the size of
 * the created zone, and we should consider it a big size.
 *
 * Returns: the area enclosed by the followed line, or a negative value
 * if the zone is not closed (hence actual area unknown).
 */
static glong
gimp_edgel_region_area (const GeglBuffer *mask,
                        Edgel             start_edgel)
{
  Edgel edgel = start_edgel;
  glong area = 0;

  do
    {
      if (edgel.direction == XPlusDirection)
        area -= edgel.x;
      else if (edgel.direction == XMinusDirection)
        area += edgel.x - 1;

      gimp_edgelset_next8 (mask, &edgel, &edgel);
    }
  while (gimp_edgel_cmp (&edgel, &start_edgel) != 0);

  return area;
}

/* Edgel sets */

static GArray *
gimp_edgelset_new (GeglBuffer *buffer,
                   GimpAsync  *async)
{
  GeglBufferIterator *gi;
  GArray             *set;
  GHashTable         *edgel2index;
  gint                width  = gegl_buffer_get_width (buffer);
  gint                height = gegl_buffer_get_height (buffer);

  set = g_array_new (TRUE, TRUE, sizeof (Edgel *));
  g_array_set_clear_func (set, (GDestroyNotify) gimp_edgel_clear);

  if (width <= 1 || height <= 1)
    return set;

  edgel2index = g_hash_table_new ((GHashFunc) edgel2index_hash_fun,
                                  (GEqualFunc) edgel2index_equal_fun);

  gi = gegl_buffer_iterator_new (buffer, GEGL_RECTANGLE (0, 0, width, height),
                                 0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE, 5);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (0, -1, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (0, 1, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (-1, 0, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  gegl_buffer_iterator_add (gi, buffer, GEGL_RECTANGLE (1, 0, width, height),
                            0, NULL, GEGL_ACCESS_READ, GEGL_ABYSS_NONE);
  while (gegl_buffer_iterator_next (gi))
    {
      guint8 *p      = (guint8*) gi->items[0].data;
      guint8 *prevy  = (guint8*) gi->items[1].data;
      guint8 *nexty  = (guint8*) gi->items[2].data;
      guint8 *prevx  = (guint8*) gi->items[3].data;
      guint8 *nextx  = (guint8*) gi->items[4].data;
      gint    startx = gi->items[0].roi.x;
      gint    starty = gi->items[0].roi.y;
      gint    endy   = starty + gi->items[0].roi.height;
      gint    endx   = startx + gi->items[0].roi.width;
      gint    x;
      gint    y;

      if (gimp_async_is_canceled (async))
        {
          gegl_buffer_iterator_stop (gi);

          gimp_async_abort (async);

          goto end;
        }

      for (y = starty; y < endy; y++)
        for (x = startx; x < endx; x++)
          {
            if (*(p++))
              {
                if (! *prevy)
                  gimp_edgelset_add (set, x, y, YMinusDirection, edgel2index);
                if (! *nexty)
                  gimp_edgelset_add (set, x, y, YPlusDirection, edgel2index);
                if (! *prevx)
                  gimp_edgelset_add (set, x, y, XMinusDirection, edgel2index);
                if (! *nextx)
                  gimp_edgelset_add (set, x, y, XPlusDirection, edgel2index);
              }
            prevy++;
            nexty++;
            prevx++;
            nextx++;
          }
    }

  gimp_edgelset_build_graph (set, buffer, edgel2index, async);
  if (gimp_async_is_stopped (async))
    goto end;

  gimp_edgelset_init_normals (set);

 end:
  g_hash_table_destroy (edgel2index);

  if (gimp_async_is_stopped (async))
    {
      g_array_free (set, TRUE);
      set = NULL;
    }

  return set;
}

static void
gimp_edgelset_add (GArray     *set,
                   int         x,
                   int         y,
                   Direction   direction,
                   GHashTable *edgel2index)
{
  Edgel         *edgel    = gimp_edgel_new (x, y, direction);
  unsigned long  position = set->len;

  g_array_append_val (set, edgel);
  g_hash_table_insert (edgel2index, edgel, GUINT_TO_POINTER (position));
}

static void
gimp_edgelset_init_normals (GArray *set)
{
  Edgel **e = (Edgel**) set->data;

  while (*e)
    {
      GimpVector2 n = Direction2Normal[(*e)->direction];

      (*e)->x_normal = n.x;
      (*e)->y_normal = n.y;
      e++;
    }
}

static void
gimp_edgelset_smooth_normals (GArray    *set,
                              int        mask_size,
                              GimpAsync *async)
{
  const gfloat sigma = mask_size * 0.775;
  const gfloat den   = 2 * sigma * sigma;
  gfloat       weights[65];
  GimpVector2  smoothed_normal;
  gint         i;

  gimp_assert (mask_size <= 65);

  weights[0] = 1.0f;
  for (int i = 1; i <= mask_size; ++i)
    weights[i] = expf (-(i * i) / den);

  for (i = 0; i < set->len; i++)
    {
      Edgel *it           = g_array_index (set, Edgel*, i);
      Edgel *edgel_before = g_array_index (set, Edgel*, it->previous);
      Edgel *edgel_after  = g_array_index (set, Edgel*, it->next);
      int    n = mask_size;
      int    i = 1;

      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          return;
        }

      smoothed_normal = Direction2Normal[it->direction];
      while (n-- && (edgel_after != edgel_before))
        {
          smoothed_normal = gimp_vector2_add_val (smoothed_normal,
                                                  gimp_vector2_mul_val (Direction2Normal[edgel_before->direction], weights[i]));
          smoothed_normal = gimp_vector2_add_val (smoothed_normal,
                                                  gimp_vector2_mul_val (Direction2Normal[edgel_after->direction], weights[i]));
          edgel_before = g_array_index (set, Edgel *, edgel_before->previous);
          edgel_after  = g_array_index (set, Edgel *, edgel_after->next);
          ++i;
        }
      gimp_vector2_normalize (&smoothed_normal);
      it->x_normal = smoothed_normal.x;
      it->y_normal = smoothed_normal.y;
    }
}

static void
gimp_edgelset_compute_curvature (GArray    *set,
                                 GimpAsync *async)
{
  gint i;

  for (i = 0; i < set->len; i++)
    {
      Edgel       *it       = g_array_index (set, Edgel*, i);
      Edgel       *previous = g_array_index (set, Edgel *, it->previous);
      Edgel       *next     = g_array_index (set, Edgel *, it->next);
      GimpVector2  n_prev   = gimp_vector2_new (previous->x_normal, previous->y_normal);
      GimpVector2  n_next   = gimp_vector2_new (next->x_normal, next->y_normal);
      GimpVector2  diff     = gimp_vector2_mul_val (gimp_vector2_sub_val (n_next, n_prev),
                                                    0.5);
      const float  c        = gimp_vector2_length_val (diff);
      const float  crossp   = n_prev.x * n_next.y - n_prev.y * n_next.x;

      it->curvature = (crossp > 0.0f) ? c : -c;

      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          return;
        }
    }
}

static void
gimp_edgelset_build_graph (GArray     *set,
                           GeglBuffer *buffer,
                           GHashTable *edgel2index,
                           GimpAsync  *async)
{
  Edgel edgel;
  gint  i;

  for (i = 0; i < set->len; i++)
    {
      Edgel *neighbor;
      Edgel *it = g_array_index (set, Edgel *, i);
      guint  neighbor_pos;

      if (gimp_async_is_canceled (async))
        {
          gimp_async_abort (async);

          return;
        }

      gimp_edgelset_next8 (buffer, it, &edgel);

      gimp_assert (g_hash_table_contains (edgel2index, &edgel));
      neighbor_pos = GPOINTER_TO_UINT (g_hash_table_lookup (edgel2index, &edgel));
      it->next = neighbor_pos;
      neighbor = g_array_index (set, Edgel *, neighbor_pos);
      neighbor->previous = i;
    }
}

static void
gimp_edgelset_next8 (const GeglBuffer *buffer,
                     Edgel            *it,
                     Edgel            *n)
{
  guint8 pixels[9];

  n->x         = it->x;
  n->y         = it->y;
  n->direction = it->direction;

  gegl_buffer_get ((GeglBuffer *) buffer,
                   GEGL_RECTANGLE (n->x - 1, n->y - 1, 3, 3),
                   1.0, NULL, pixels, GEGL_AUTO_ROWSTRIDE,
                   GEGL_ABYSS_NONE);
  switch (n->direction)
    {
    case XPlusDirection:
      if (pixels[8])
        {
          ++(n->y);
          ++(n->x);
          n->direction = YMinusDirection;
        }
      else if (pixels[7])
        {
          ++(n->y);
        }
      else
        {
          n->direction = YPlusDirection;
        }
      break;
    case YMinusDirection:
      if (pixels[2])
        {
          ++(n->x);
          --(n->y);
          n->direction = XMinusDirection;
        }
      else if (pixels[5])
        {
          ++(n->x);
        }
      else
        {
          n->direction = XPlusDirection;
        }
      break;
    case XMinusDirection:
      if (pixels[0])
        {
          --(n->x);
          --(n->y);
          n->direction = YPlusDirection;
        }
      else if (pixels[1])
        {
          --(n->y);
        }
      else
        {
          n->direction = YMinusDirection;
        }
      break;
    case YPlusDirection:
      if (pixels[6])
        {
          --(n->x);
          ++(n->y);
          n->direction = XPlusDirection;
        }
      else if (pixels[3])
        {
          --(n->x);
        }
      else
        {
          n->direction = XMinusDirection;
        }
      break;
    default:
      g_return_if_reached ();
      break;
    }
}
