/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 * Copyright (C) 2013 Daniel Sabo
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

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

#include "paint-types.h"

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

#include "gegl/pika-babl.h"
#include "gegl/pika-gegl-loops.h"
#include "gegl/pika-gegl-nodes.h"
#include "gegl/pika-gegl-utils.h"
#include "gegl/pikaapplicator.h"

#include "core/pika.h"
#include "core/pika-utils.h"
#include "core/pikachannel.h"
#include "core/pikaimage.h"
#include "core/pikaimage-guides.h"
#include "core/pikaimage-symmetry.h"
#include "core/pikaimage-undo.h"
#include "core/pikapickable.h"
#include "core/pikaprojection.h"
#include "core/pikasymmetry.h"
#include "core/pikatempbuf.h"

#include "pikapaintcore.h"
#include "pikapaintcoreundo.h"
#include "pikapaintcore-loops.h"
#include "pikapaintoptions.h"

#include "pikaairbrush.h"

#include "pika-intl.h"


#define STROKE_BUFFER_INIT_SIZE 2000

enum
{
  PROP_0,
  PROP_UNDO_DESC
};


/*  local function prototypes  */

static void      pika_paint_core_finalize            (GObject          *object);
static void      pika_paint_core_set_property        (GObject          *object,
                                                      guint             property_id,
                                                      const GValue     *value,
                                                      GParamSpec       *pspec);
static void      pika_paint_core_get_property        (GObject          *object,
                                                      guint             property_id,
                                                      GValue           *value,
                                                      GParamSpec       *pspec);

static gboolean  pika_paint_core_real_start          (PikaPaintCore    *core,
                                                      GList            *drawables,
                                                      PikaPaintOptions *paint_options,
                                                      const PikaCoords *coords,
                                                      GError          **error);
static gboolean  pika_paint_core_real_pre_paint      (PikaPaintCore    *core,
                                                      GList            *drawables,
                                                      PikaPaintOptions *options,
                                                      PikaPaintState    paint_state,
                                                      guint32           time);
static void      pika_paint_core_real_paint          (PikaPaintCore    *core,
                                                      GList            *drawables,
                                                      PikaPaintOptions *options,
                                                      PikaSymmetry     *sym,
                                                      PikaPaintState    paint_state,
                                                      guint32           time);
static void      pika_paint_core_real_post_paint     (PikaPaintCore    *core,
                                                      GList            *drawables,
                                                      PikaPaintOptions *options,
                                                      PikaPaintState    paint_state,
                                                      guint32           time);
static void      pika_paint_core_real_interpolate    (PikaPaintCore    *core,
                                                      GList            *drawables,
                                                      PikaPaintOptions *options,
                                                      guint32           time);
static GeglBuffer *
               pika_paint_core_real_get_paint_buffer (PikaPaintCore    *core,
                                                      PikaDrawable     *drawable,
                                                      PikaPaintOptions *options,
                                                      PikaLayerMode     paint_mode,
                                                      const PikaCoords *coords,
                                                      gint             *paint_buffer_x,
                                                      gint             *paint_buffer_y,
                                                      gint             *paint_width,
                                                      gint             *paint_height);
static PikaUndo* pika_paint_core_real_push_undo      (PikaPaintCore    *core,
                                                      PikaImage        *image,
                                                      const gchar      *undo_desc);


G_DEFINE_TYPE (PikaPaintCore, pika_paint_core, PIKA_TYPE_OBJECT)

#define parent_class pika_paint_core_parent_class

static gint global_core_ID = 1;


static void
pika_paint_core_class_init (PikaPaintCoreClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize     = pika_paint_core_finalize;
  object_class->set_property = pika_paint_core_set_property;
  object_class->get_property = pika_paint_core_get_property;

  klass->start               = pika_paint_core_real_start;
  klass->pre_paint           = pika_paint_core_real_pre_paint;
  klass->paint               = pika_paint_core_real_paint;
  klass->post_paint          = pika_paint_core_real_post_paint;
  klass->interpolate         = pika_paint_core_real_interpolate;
  klass->get_paint_buffer    = pika_paint_core_real_get_paint_buffer;
  klass->push_undo           = pika_paint_core_real_push_undo;

  g_object_class_install_property (object_class, PROP_UNDO_DESC,
                                   g_param_spec_string ("undo-desc", NULL, NULL,
                                                        _("Paint"),
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));
}

static void
pika_paint_core_init (PikaPaintCore *core)
{
  core->ID = global_core_ID++;
  core->undo_buffers = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref);
}

static void
pika_paint_core_finalize (GObject *object)
{
  PikaPaintCore *core = PIKA_PAINT_CORE (object);

  pika_paint_core_cleanup (core);

  g_clear_pointer (&core->undo_desc, g_free);
  g_hash_table_unref (core->undo_buffers);
  if (core->applicators)
    g_hash_table_unref (core->applicators);

  if (core->stroke_buffer)
    {
      g_array_free (core->stroke_buffer, TRUE);
      core->stroke_buffer = NULL;
    }

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

static void
pika_paint_core_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  PikaPaintCore *core = PIKA_PAINT_CORE (object);

  switch (property_id)
    {
    case PROP_UNDO_DESC:
      g_free (core->undo_desc);
      core->undo_desc = g_value_dup_string (value);
      break;

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

static void
pika_paint_core_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  PikaPaintCore *core = PIKA_PAINT_CORE (object);

  switch (property_id)
    {
    case PROP_UNDO_DESC:
      g_value_set_string (value, core->undo_desc);
      break;

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

static gboolean
pika_paint_core_real_start (PikaPaintCore    *core,
                            GList            *drawables,
                            PikaPaintOptions *paint_options,
                            const PikaCoords *coords,
                            GError          **error)
{
  return TRUE;
}

static gboolean
pika_paint_core_real_pre_paint (PikaPaintCore    *core,
                                GList            *drawables,
                                PikaPaintOptions *paint_options,
                                PikaPaintState    paint_state,
                                guint32           time)
{
  return TRUE;
}

static void
pika_paint_core_real_paint (PikaPaintCore    *core,
                            GList            *drawables,
                            PikaPaintOptions *paint_options,
                            PikaSymmetry     *sym,
                            PikaPaintState    paint_state,
                            guint32           time)
{
}

static void
pika_paint_core_real_post_paint (PikaPaintCore    *core,
                                 GList            *drawables,
                                 PikaPaintOptions *paint_options,
                                 PikaPaintState    paint_state,
                                 guint32           time)
{
}

static void
pika_paint_core_real_interpolate (PikaPaintCore    *core,
                                  GList            *drawables,
                                  PikaPaintOptions *paint_options,
                                  guint32           time)
{
  pika_paint_core_paint (core, drawables, paint_options,
                         PIKA_PAINT_STATE_MOTION, time);

  core->last_coords = core->cur_coords;
}

static GeglBuffer *
pika_paint_core_real_get_paint_buffer (PikaPaintCore    *core,
                                       PikaDrawable     *drawable,
                                       PikaPaintOptions *paint_options,
                                       PikaLayerMode     paint_mode,
                                       const PikaCoords *coords,
                                       gint             *paint_buffer_x,
                                       gint             *paint_buffer_y,
                                       gint             *paint_width,
                                       gint             *paint_height)
{
  return NULL;
}

static PikaUndo *
pika_paint_core_real_push_undo (PikaPaintCore *core,
                                PikaImage     *image,
                                const gchar   *undo_desc)
{
  return pika_image_undo_push (image, PIKA_TYPE_PAINT_CORE_UNDO,
                               PIKA_UNDO_PAINT, undo_desc,
                               0,
                               "paint-core", core,
                               NULL);
}


/*  public functions  */

void
pika_paint_core_paint (PikaPaintCore    *core,
                       GList            *drawables,
                       PikaPaintOptions *paint_options,
                       PikaPaintState    paint_state,
                       guint32           time)
{
  PikaPaintCoreClass *core_class;

  g_return_if_fail (PIKA_IS_PAINT_CORE (core));
  g_return_if_fail (drawables != NULL);
  g_return_if_fail (PIKA_IS_PAINT_OPTIONS (paint_options));

  core_class = PIKA_PAINT_CORE_GET_CLASS (core);

  if (core_class->pre_paint (core, drawables,
                             paint_options,
                             paint_state, time))
    {
      PikaSymmetry *sym;
      PikaImage    *image;

      image = pika_item_get_image (PIKA_ITEM (drawables->data));

      if (paint_state == PIKA_PAINT_STATE_MOTION)
        {
          /* Save coordinates for pika_paint_core_interpolate() */
          core->last_paint.x = core->cur_coords.x;
          core->last_paint.y = core->cur_coords.y;
        }

      sym = g_object_ref (pika_image_get_active_symmetry (image));
      pika_symmetry_set_origin (sym, drawables->data, &core->cur_coords);

      core_class->paint (core, drawables,
                         paint_options,
                         sym, paint_state, time);

      pika_symmetry_clear_origin (sym);
      g_object_unref (sym);

      core_class->post_paint (core, drawables,
                              paint_options,
                              paint_state, time);
    }
}

gboolean
pika_paint_core_start (PikaPaintCore     *core,
                       GList             *drawables,
                       PikaPaintOptions  *paint_options,
                       const PikaCoords  *coords,
                       GError           **error)
{
  PikaImage   *image;
  PikaChannel *mask;
  gint         max_width  = 0;
  gint         max_height = 0;

  g_return_val_if_fail (PIKA_IS_PAINT_CORE (core), FALSE);
  g_return_val_if_fail (g_list_length (drawables) > 0, FALSE);
  g_return_val_if_fail (PIKA_IS_PAINT_OPTIONS (paint_options), FALSE);
  g_return_val_if_fail (coords != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  for (GList *iter = drawables; iter; iter = iter->next)
    g_return_val_if_fail (pika_item_is_attached (iter->data), FALSE);

  image = pika_item_get_image (PIKA_ITEM (drawables->data));

  if (core->stroke_buffer)
    {
      g_array_free (core->stroke_buffer, TRUE);
      core->stroke_buffer = NULL;
    }

  core->stroke_buffer = g_array_sized_new (TRUE, TRUE,
                                           sizeof (PikaCoords),
                                           STROKE_BUFFER_INIT_SIZE);

  /* remember the last stroke's endpoint for later undo */
  core->start_coords = core->last_coords;
  core->cur_coords   = *coords;

  if (paint_options->use_applicator)
    core->applicators = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref);
  else
    core->applicators  = NULL;

  if (! PIKA_PAINT_CORE_GET_CLASS (core)->start (core, drawables,
                                                 paint_options,
                                                 coords, error))
    {
      return FALSE;
    }

  /*  Set the image pickable  */
  if (! core->show_all)
    core->image_pickable = PIKA_PICKABLE (image);
  else
    core->image_pickable = PIKA_PICKABLE (pika_image_get_projection (image));

  /*  Allocate the saved proj structure  */
  g_clear_object (&core->saved_proj_buffer);

  if (core->use_saved_proj)
    {
      GeglBuffer *buffer = pika_pickable_get_buffer (core->image_pickable);

      core->saved_proj_buffer = pika_gegl_buffer_dup (buffer);
    }

  for (GList *iter = drawables; iter; iter = iter->next)
    {
      /*  Allocate the undo structures  */
      g_hash_table_insert (core->undo_buffers, iter->data,
                           pika_gegl_buffer_dup (pika_drawable_get_buffer (iter->data)));
      max_width  = MAX (max_width, pika_item_get_width (iter->data));
      max_height = MAX (max_height, pika_item_get_height (iter->data));
    }

  /*  Allocate the canvas blocks structure  */
  if (core->canvas_buffer)
    g_object_unref (core->canvas_buffer);

  core->canvas_buffer =
    gegl_buffer_new (GEGL_RECTANGLE (0, 0, max_width, max_height),
                     babl_format ("Y float"));

  /*  Get the initial undo extents  */

  core->x1 = core->x2 = core->cur_coords.x;
  core->y1 = core->y2 = core->cur_coords.y;

  core->last_paint.x = -1e6;
  core->last_paint.y = -1e6;

  mask = pika_image_get_mask (image);

  /*  don't apply the mask to itself and don't apply an empty mask  */
  if (! pika_channel_is_empty (mask) &&
      (g_list_length (drawables) > 1 || PIKA_DRAWABLE (mask) != drawables->data))
    {
      GeglBuffer *mask_buffer;

      mask_buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (mask));

      core->mask_buffer   = g_object_ref (mask_buffer);
    }
  else
    {
      core->mask_buffer = NULL;
    }

  if (paint_options->use_applicator)
    {
      for (GList *iter = drawables; iter; iter = iter->next)
        {
          PikaApplicator *applicator;

          applicator = pika_applicator_new (NULL);
          g_hash_table_insert (core->applicators, iter->data, applicator);

          if (core->mask_buffer)
            {
              gint offset_x;
              gint offset_y;

              pika_applicator_set_mask_buffer (applicator,
                                               core->mask_buffer);
              pika_item_get_offset (iter->data, &offset_x, &offset_y);
              pika_applicator_set_mask_offset (applicator, -offset_x, -offset_y);
            }

          pika_applicator_set_affect (applicator,
                                      pika_drawable_get_active_mask (iter->data));
          pika_applicator_set_dest_buffer (applicator,
                                           pika_drawable_get_buffer (iter->data));
        }
    }

  /*  Freeze the drawable preview so that it isn't constantly updated.  */
  for (GList *iter = drawables; iter; iter = iter->next)
    pika_viewable_preview_freeze (PIKA_VIEWABLE (iter->data));

  return TRUE;
}

void
pika_paint_core_finish (PikaPaintCore *core,
                        GList         *drawables,
                        gboolean       push_undo)
{
  PikaImage *image;
  gboolean   undo_group_started = FALSE;

  g_return_if_fail (PIKA_IS_PAINT_CORE (core));

  if (core->applicators)
    {
      g_hash_table_unref (core->applicators);
      core->applicators = NULL;
    }

  if (core->stroke_buffer)
    {
      g_array_free (core->stroke_buffer, TRUE);
      core->stroke_buffer = NULL;
    }

  g_clear_object (&core->mask_buffer);

  image = pika_item_get_image (PIKA_ITEM (drawables->data));

  for (GList *iter = drawables; iter; iter = iter->next)
    {
      /*  Determine if any part of the image has been altered--
       *  if nothing has, then just go to the next drawable...
       */
      if ((core->x2 == core->x1) || (core->y2 == core->y1))
        {
          pika_viewable_preview_thaw (PIKA_VIEWABLE (iter->data));
          continue;
        }

      if (push_undo)
        {
          GeglBuffer    *undo_buffer;
          GeglBuffer    *buffer;
          GeglRectangle  rect;

          if (! g_hash_table_steal_extended (core->undo_buffers, iter->data,
                                             NULL, (gpointer*) &undo_buffer))
            {
              g_critical ("%s: missing undo buffer for '%s'.",
                          G_STRFUNC, pika_object_get_name (iter->data));
              continue;
            }

          pika_rectangle_intersect (core->x1, core->y1,
                                    core->x2 - core->x1, core->y2 - core->y1,
                                    0, 0,
                                    pika_item_get_width  (PIKA_ITEM (iter->data)),
                                    pika_item_get_height (PIKA_ITEM (iter->data)),
                                    &rect.x, &rect.y, &rect.width, &rect.height);

          gegl_rectangle_align_to_buffer (&rect, &rect, undo_buffer,
                                          GEGL_RECTANGLE_ALIGNMENT_SUPERSET);

          if (! undo_group_started)
            {
              pika_image_undo_group_start (image, PIKA_UNDO_GROUP_PAINT,
                                           core->undo_desc);
              undo_group_started = TRUE;
            }

          PIKA_PAINT_CORE_GET_CLASS (core)->push_undo (core, image, NULL);

          buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, rect.width, rect.height),
                                    pika_drawable_get_format (iter->data));

          pika_gegl_buffer_copy (undo_buffer,
                                 &rect,
                                 GEGL_ABYSS_NONE,
                                 buffer,
                                 GEGL_RECTANGLE (0, 0, 0, 0));

          pika_drawable_push_undo (iter->data, NULL,
                                   buffer, rect.x, rect.y, rect.width, rect.height);

          g_object_unref (buffer);
          g_object_unref (undo_buffer);
        }

      pika_viewable_preview_thaw (PIKA_VIEWABLE (iter->data));
    }

  core->image_pickable = NULL;

  g_clear_object (&core->saved_proj_buffer);

  if (undo_group_started)
    pika_image_undo_group_end (image);
}

void
pika_paint_core_cancel (PikaPaintCore *core,
                        GList         *drawables)
{
  gint x, y;
  gint width, height;

  g_return_if_fail (PIKA_IS_PAINT_CORE (core));

  /*  Determine if any part of the image has been altered--
   *  if nothing has, then just return...
   */
  if ((core->x2 == core->x1) || (core->y2 == core->y1))
    return;

  for (GList *iter = drawables; iter; iter = iter->next)
    {
      if (pika_rectangle_intersect (core->x1, core->y1,
                                    core->x2 - core->x1,
                                    core->y2 - core->y1,
                                    0, 0,
                                    pika_item_get_width  (PIKA_ITEM (iter->data)),
                                    pika_item_get_height (PIKA_ITEM (iter->data)),
                                    &x, &y, &width, &height))
        {
          GeglBuffer    *undo_buffer;
          GeglRectangle  rect;

          if (! g_hash_table_steal_extended (core->undo_buffers, iter->data,
                                             NULL, (gpointer*) &undo_buffer))
            {
              g_critical ("%s: missing undo buffer for '%s'.",
                          G_STRFUNC, pika_object_get_name (iter->data));
              continue;
            }

          gegl_rectangle_align_to_buffer (&rect,
                                          GEGL_RECTANGLE (x, y, width, height),
                                          pika_drawable_get_buffer (iter->data),
                                          GEGL_RECTANGLE_ALIGNMENT_SUPERSET);

          pika_gegl_buffer_copy (undo_buffer,
                                 &rect,
                                 GEGL_ABYSS_NONE,
                                 pika_drawable_get_buffer (iter->data),
                                 &rect);
          g_object_unref (undo_buffer);
        }

      pika_drawable_update (iter->data, x, y, width, height);

      pika_viewable_preview_thaw (PIKA_VIEWABLE (iter->data));
    }

  g_clear_object (&core->saved_proj_buffer);
}

void
pika_paint_core_cleanup (PikaPaintCore *core)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));

  g_hash_table_remove_all (core->undo_buffers);

  g_clear_object (&core->saved_proj_buffer);
  g_clear_object (&core->canvas_buffer);
  g_clear_object (&core->paint_buffer);
}

void
pika_paint_core_interpolate (PikaPaintCore    *core,
                             GList            *drawables,
                             PikaPaintOptions *paint_options,
                             const PikaCoords *coords,
                             guint32           time)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));
  g_return_if_fail (drawables != NULL);
  g_return_if_fail (PIKA_IS_PAINT_OPTIONS (paint_options));
  g_return_if_fail (coords != NULL);

  core->cur_coords = *coords;

  PIKA_PAINT_CORE_GET_CLASS (core)->interpolate (core, drawables,
                                                 paint_options, time);
}

void
pika_paint_core_set_show_all (PikaPaintCore *core,
                              gboolean       show_all)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));

  core->show_all = show_all;
}

gboolean
pika_paint_core_get_show_all (PikaPaintCore *core)
{
  g_return_val_if_fail (PIKA_IS_PAINT_CORE (core), FALSE);

  return core->show_all;
}

void
pika_paint_core_set_current_coords (PikaPaintCore    *core,
                                    const PikaCoords *coords)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));
  g_return_if_fail (coords != NULL);

  core->cur_coords = *coords;
}

void
pika_paint_core_get_current_coords (PikaPaintCore    *core,
                                    PikaCoords       *coords)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));
  g_return_if_fail (coords != NULL);

  *coords = core->cur_coords;
}

void
pika_paint_core_set_last_coords (PikaPaintCore    *core,
                                 const PikaCoords *coords)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));
  g_return_if_fail (coords != NULL);

  core->last_coords = *coords;
}

void
pika_paint_core_get_last_coords (PikaPaintCore *core,
                                 PikaCoords    *coords)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));
  g_return_if_fail (coords != NULL);

  *coords = core->last_coords;
}

/**
 * pika_paint_core_round_line:
 * @core:                   the #PikaPaintCore
 * @options:                the #PikaPaintOptions to use
 * @constrain_15_degrees:   the modifier state
 * @constrain_offset_angle: the angle by which to offset the lines, in degrees
 * @constrain_xres:         the horizontal resolution
 * @constrain_yres:         the vertical resolution
 *
 * Adjusts core->last_coords and core_cur_coords in preparation to
 * drawing a straight line. If @center_pixels is TRUE the endpoints
 * get pushed to the center of the pixels. This avoids artifacts
 * for e.g. the hard mode. The rounding of the slope to 15 degree
 * steps if ctrl is pressed happens, as does rounding the start and
 * end coordinates (which may be fractional in high zoom modes) to
 * the center of pixels.
 **/
void
pika_paint_core_round_line (PikaPaintCore    *core,
                            PikaPaintOptions *paint_options,
                            gboolean          constrain_15_degrees,
                            gdouble           constrain_offset_angle,
                            gdouble           constrain_xres,
                            gdouble           constrain_yres)
{
  g_return_if_fail (PIKA_IS_PAINT_CORE (core));
  g_return_if_fail (PIKA_IS_PAINT_OPTIONS (paint_options));

  if (pika_paint_options_get_brush_mode (paint_options) == PIKA_BRUSH_HARD)
    {
      core->last_coords.x = floor (core->last_coords.x) + 0.5;
      core->last_coords.y = floor (core->last_coords.y) + 0.5;
      core->cur_coords.x  = floor (core->cur_coords.x ) + 0.5;
      core->cur_coords.y  = floor (core->cur_coords.y ) + 0.5;
    }

  if (constrain_15_degrees)
    pika_constrain_line (core->last_coords.x, core->last_coords.y,
                         &core->cur_coords.x, &core->cur_coords.y,
                         PIKA_CONSTRAIN_LINE_15_DEGREES,
                         constrain_offset_angle,
                         constrain_xres, constrain_yres);
}


/*  protected functions  */

GeglBuffer *
pika_paint_core_get_paint_buffer (PikaPaintCore    *core,
                                  PikaDrawable     *drawable,
                                  PikaPaintOptions *paint_options,
                                  PikaLayerMode     paint_mode,
                                  const PikaCoords *coords,
                                  gint             *paint_buffer_x,
                                  gint             *paint_buffer_y,
                                  gint             *paint_width,
                                  gint             *paint_height)
{
  GeglBuffer *paint_buffer;

  g_return_val_if_fail (PIKA_IS_PAINT_CORE (core), NULL);
  g_return_val_if_fail (PIKA_IS_DRAWABLE (drawable), NULL);
  g_return_val_if_fail (pika_item_is_attached (PIKA_ITEM (drawable)), NULL);
  g_return_val_if_fail (PIKA_IS_PAINT_OPTIONS (paint_options), NULL);
  g_return_val_if_fail (coords != NULL, NULL);
  g_return_val_if_fail (paint_buffer_x != NULL, NULL);
  g_return_val_if_fail (paint_buffer_y != NULL, NULL);

  paint_buffer =
    PIKA_PAINT_CORE_GET_CLASS (core)->get_paint_buffer (core, drawable,
                                                        paint_options,
                                                        paint_mode,
                                                        coords,
                                                        paint_buffer_x,
                                                        paint_buffer_y,
                                                        paint_width,
                                                        paint_height);

  core->paint_buffer_x = *paint_buffer_x;
  core->paint_buffer_y = *paint_buffer_y;

  return paint_buffer;
}

PikaPickable *
pika_paint_core_get_image_pickable (PikaPaintCore *core)
{
  g_return_val_if_fail (PIKA_IS_PAINT_CORE (core), NULL);
  g_return_val_if_fail (core->image_pickable != NULL, NULL);

  return core->image_pickable;
}

GeglBuffer *
pika_paint_core_get_orig_image (PikaPaintCore *core,
                                PikaDrawable  *drawable)
{
  GeglBuffer *undo_buffer;

  g_return_val_if_fail (PIKA_IS_PAINT_CORE (core), NULL);

  undo_buffer = g_hash_table_lookup (core->undo_buffers, drawable);
  g_return_val_if_fail (undo_buffer != NULL, NULL);

  return undo_buffer;
}

GeglBuffer *
pika_paint_core_get_orig_proj (PikaPaintCore *core)
{
  g_return_val_if_fail (PIKA_IS_PAINT_CORE (core), NULL);
  g_return_val_if_fail (core->saved_proj_buffer != NULL, NULL);

  return core->saved_proj_buffer;
}

void
pika_paint_core_paste (PikaPaintCore            *core,
                       const PikaTempBuf        *paint_mask,
                       gint                      paint_mask_offset_x,
                       gint                      paint_mask_offset_y,
                       PikaDrawable             *drawable,
                       gdouble                   paint_opacity,
                       gdouble                   image_opacity,
                       PikaLayerMode             paint_mode,
                       PikaPaintApplicationMode  mode)
{
  gint               width  = gegl_buffer_get_width  (core->paint_buffer);
  gint               height = gegl_buffer_get_height (core->paint_buffer);
  PikaComponentMask  affect = pika_drawable_get_active_mask (drawable);
  GeglBuffer        *undo_buffer;

  undo_buffer = g_hash_table_lookup (core->undo_buffers, drawable);

  if (! affect)
    return;

  if (core->applicators)
    {
      PikaApplicator *applicator;

      applicator = g_hash_table_lookup (core->applicators, drawable);

      /*  If the mode is CONSTANT:
       *   combine the canvas buffer and the paint mask to the paint buffer
       */
      if (mode == PIKA_PAINT_CONSTANT)
        {
          /* Some tools (ink) paint the mask to paint_core->canvas_buffer
           * directly. Don't need to copy it in this case.
           */
          if (paint_mask != NULL)
            {
              GeglBuffer *paint_mask_buffer =
                pika_temp_buf_create_buffer ((PikaTempBuf *) paint_mask);

              pika_gegl_combine_mask_weird (paint_mask_buffer,
                                            GEGL_RECTANGLE (paint_mask_offset_x,
                                                            paint_mask_offset_y,
                                                            width, height),
                                            core->canvas_buffer,
                                            GEGL_RECTANGLE (core->paint_buffer_x,
                                                            core->paint_buffer_y,
                                                            width, height),
                                            paint_opacity,
                                            PIKA_IS_AIRBRUSH (core));

              g_object_unref (paint_mask_buffer);
            }

          pika_gegl_apply_mask (core->canvas_buffer,
                                GEGL_RECTANGLE (core->paint_buffer_x,
                                                core->paint_buffer_y,
                                                width, height),
                                core->paint_buffer,
                                GEGL_RECTANGLE (0, 0, width, height),
                                1.0);

          pika_applicator_set_src_buffer (applicator, undo_buffer);
        }
      /*  Otherwise:
       *   combine the paint mask to the paint buffer directly
       */
      else
        {
          GeglBuffer *paint_mask_buffer =
            pika_temp_buf_create_buffer ((PikaTempBuf *) paint_mask);

          pika_gegl_apply_mask (paint_mask_buffer,
                                GEGL_RECTANGLE (paint_mask_offset_x,
                                                paint_mask_offset_y,
                                                width, height),
                                core->paint_buffer,
                                GEGL_RECTANGLE (0, 0, width, height),
                                paint_opacity);

          g_object_unref (paint_mask_buffer);

          pika_applicator_set_src_buffer (applicator,
                                          pika_drawable_get_buffer (drawable));
        }

      pika_applicator_set_apply_buffer (applicator,
                                        core->paint_buffer);
      pika_applicator_set_apply_offset (applicator,
                                        core->paint_buffer_x,
                                        core->paint_buffer_y);

      pika_applicator_set_opacity (applicator, image_opacity);
      pika_applicator_set_mode (applicator, paint_mode,
                                PIKA_LAYER_COLOR_SPACE_AUTO,
                                PIKA_LAYER_COLOR_SPACE_AUTO,
                                pika_layer_mode_get_paint_composite_mode (paint_mode));

      /*  apply the paint area to the image  */
      pika_applicator_blit (applicator,
                            GEGL_RECTANGLE (core->paint_buffer_x,
                                            core->paint_buffer_y,
                                            width, height));
    }
  else
    {
      PikaPaintCoreLoopsParams    params = {};
      PikaPaintCoreLoopsAlgorithm algorithms = PIKA_PAINT_CORE_LOOPS_ALGORITHM_NONE;

      params.paint_buf          = pika_gegl_buffer_get_temp_buf (core->paint_buffer);
      params.paint_buf_offset_x = core->paint_buffer_x;
      params.paint_buf_offset_y = core->paint_buffer_y;

      if (! params.paint_buf)
        return;

      params.dest_buffer = pika_drawable_get_buffer (drawable);

      if (mode == PIKA_PAINT_CONSTANT)
        {
          params.canvas_buffer = core->canvas_buffer;

          /* This step is skipped by the ink tool, which writes
           * directly to canvas_buffer
           */
          if (paint_mask != NULL)
            {
              /* Mix paint mask and canvas_buffer */
              params.paint_mask          = paint_mask;
              params.paint_mask_offset_x = paint_mask_offset_x;
              params.paint_mask_offset_y = paint_mask_offset_y;
              params.stipple             = PIKA_IS_AIRBRUSH (core);
              params.paint_opacity       = paint_opacity;

              algorithms |= PIKA_PAINT_CORE_LOOPS_ALGORITHM_COMBINE_PAINT_MASK_TO_CANVAS_BUFFER;
            }

          /* Write canvas_buffer to paint_buf */
          algorithms |= PIKA_PAINT_CORE_LOOPS_ALGORITHM_CANVAS_BUFFER_TO_COMP_MASK;

          /* undo buf -> paint_buf -> dest_buffer */
          params.src_buffer = undo_buffer;
        }
      else
        {
          g_return_if_fail (paint_mask);

          /* Write paint_mask to paint_buf, does not modify canvas_buffer */
          params.paint_mask          = paint_mask;
          params.paint_mask_offset_x = paint_mask_offset_x;
          params.paint_mask_offset_y = paint_mask_offset_y;
          params.paint_opacity       = paint_opacity;

          algorithms |= PIKA_PAINT_CORE_LOOPS_ALGORITHM_PAINT_MASK_TO_COMP_MASK;

          /* dest_buffer -> paint_buf -> dest_buffer */
          params.src_buffer = params.dest_buffer;
        }

      pika_item_get_offset (PIKA_ITEM (drawable),
                            &params.mask_offset_x, &params.mask_offset_y);
      params.mask_offset_x = -params.mask_offset_x;
      params.mask_offset_y = -params.mask_offset_y;
      params.mask_buffer   = core->mask_buffer;
      params.image_opacity = image_opacity;
      params.paint_mode    = paint_mode;

      algorithms |= PIKA_PAINT_CORE_LOOPS_ALGORITHM_DO_LAYER_BLEND;

      if (affect != PIKA_COMPONENT_MASK_ALL)
        {
          params.affect = affect;

          algorithms |= PIKA_PAINT_CORE_LOOPS_ALGORITHM_MASK_COMPONENTS;
        }

      pika_paint_core_loops_process (&params, algorithms);
    }

  /*  Update the undo extents  */
  core->x1 = MIN (core->x1, core->paint_buffer_x);
  core->y1 = MIN (core->y1, core->paint_buffer_y);
  core->x2 = MAX (core->x2, core->paint_buffer_x + width);
  core->y2 = MAX (core->y2, core->paint_buffer_y + height);

  /*  Update the drawable  */
  pika_drawable_update (drawable,
                        core->paint_buffer_x,
                        core->paint_buffer_y,
                        width, height);
}

/* This works similarly to pika_paint_core_paste. However, instead of
 * combining the canvas to the paint core drawable using one of the
 * combination modes, it uses a "replace" mode (i.e. transparent
 * pixels in the canvas erase the paint core drawable).

 * When not drawing on alpha-enabled images, it just paints using
 * NORMAL mode.
 */
void
pika_paint_core_replace (PikaPaintCore            *core,
                         const PikaTempBuf        *paint_mask,
                         gint                      paint_mask_offset_x,
                         gint                      paint_mask_offset_y,
                         PikaDrawable             *drawable,
                         gdouble                   paint_opacity,
                         gdouble                   image_opacity,
                         PikaPaintApplicationMode  mode)
{
  GeglBuffer        *undo_buffer;
  gint               width, height;
  PikaComponentMask  affect;

  if (! pika_drawable_has_alpha (drawable))
    {
      pika_paint_core_paste (core, paint_mask,
                             paint_mask_offset_x,
                             paint_mask_offset_y,
                             drawable,
                             paint_opacity,
                             image_opacity,
                             PIKA_LAYER_MODE_NORMAL,
                             mode);
      return;
    }

  width  = gegl_buffer_get_width  (core->paint_buffer);
  height = gegl_buffer_get_height (core->paint_buffer);

  affect = pika_drawable_get_active_mask (drawable);

  if (! affect)
    return;

  undo_buffer = g_hash_table_lookup (core->undo_buffers, drawable);

  if (core->applicators)
    {
      PikaApplicator *applicator;
      GeglRectangle   mask_rect;
      GeglBuffer     *mask_buffer;
      gint            offset_x;
      gint            offset_y;

      applicator = g_hash_table_lookup (core->applicators, drawable);

      /*  If the mode is CONSTANT:
       *   combine the paint mask to the canvas buffer, and use it as the mask
       *   buffer
       */
      if (mode == PIKA_PAINT_CONSTANT)
        {
          /* Some tools (ink) paint the mask to paint_core->canvas_buffer
           * directly. Don't need to copy it in this case.
           */
          if (paint_mask != NULL)
            {
              GeglBuffer *paint_mask_buffer =
                pika_temp_buf_create_buffer ((PikaTempBuf *) paint_mask);

              pika_gegl_combine_mask_weird (paint_mask_buffer,
                                            GEGL_RECTANGLE (paint_mask_offset_x,
                                                            paint_mask_offset_y,
                                                            width, height),
                                            core->canvas_buffer,
                                            GEGL_RECTANGLE (core->paint_buffer_x,
                                                            core->paint_buffer_y,
                                                            width, height),
                                            paint_opacity,
                                            PIKA_IS_AIRBRUSH (core));

              g_object_unref (paint_mask_buffer);
            }

          mask_buffer = g_object_ref (core->canvas_buffer);
          mask_rect   = *GEGL_RECTANGLE (core->paint_buffer_x,
                                         core->paint_buffer_y,
                                         width, height);

          pika_applicator_set_src_buffer (applicator, undo_buffer);
        }
      /*  Otherwise:
       *   use the paint mask as the mask buffer directly
       */
      else
        {
          mask_buffer =
            pika_temp_buf_create_buffer ((PikaTempBuf *) paint_mask);
          mask_rect   = *GEGL_RECTANGLE (paint_mask_offset_x,
                                         paint_mask_offset_y,
                                         width, height);

          pika_applicator_set_src_buffer (applicator,
                                          pika_drawable_get_buffer (drawable));
        }

      pika_item_get_offset (PIKA_ITEM (drawable), &offset_x, &offset_y);
      if (core->mask_buffer)
        {
          GeglBuffer    *combined_mask_buffer;
          GeglRectangle  combined_mask_rect;
          GeglRectangle  aligned_combined_mask_rect;

          combined_mask_rect = *GEGL_RECTANGLE (core->paint_buffer_x,
                                                core->paint_buffer_y,
                                                width, height);

          gegl_rectangle_align_to_buffer (
            &aligned_combined_mask_rect, &combined_mask_rect,
            pika_drawable_get_buffer (drawable),
            GEGL_RECTANGLE_ALIGNMENT_SUPERSET);

          combined_mask_buffer = gegl_buffer_new (&aligned_combined_mask_rect,
                                                  babl_format ("Y float"));

          pika_gegl_buffer_copy (
            core->mask_buffer,
            GEGL_RECTANGLE (aligned_combined_mask_rect.x + offset_x,
                            aligned_combined_mask_rect.y + offset_y,
                            aligned_combined_mask_rect.width,
                            aligned_combined_mask_rect.height),
            GEGL_ABYSS_NONE,
            combined_mask_buffer,
            &aligned_combined_mask_rect);

          pika_gegl_combine_mask (mask_buffer,          &mask_rect,
                                  combined_mask_buffer, &combined_mask_rect,
                                  1.0);

          g_object_unref (mask_buffer);

          mask_buffer = combined_mask_buffer;
          mask_rect   = combined_mask_rect;
        }

      pika_applicator_set_mask_buffer (applicator, mask_buffer);
      pika_applicator_set_mask_offset (applicator,
                                       core->paint_buffer_x - mask_rect.x,
                                       core->paint_buffer_y - mask_rect.y);

      pika_applicator_set_apply_buffer (applicator,
                                        core->paint_buffer);
      pika_applicator_set_apply_offset (applicator,
                                        core->paint_buffer_x,
                                        core->paint_buffer_y);

      pika_applicator_set_opacity (applicator, image_opacity);
      pika_applicator_set_mode (applicator, PIKA_LAYER_MODE_REPLACE,
                                PIKA_LAYER_COLOR_SPACE_AUTO,
                                PIKA_LAYER_COLOR_SPACE_AUTO,
                                pika_layer_mode_get_paint_composite_mode (
                                  PIKA_LAYER_MODE_REPLACE));

      /*  apply the paint area to the image  */
      pika_applicator_blit (applicator,
                            GEGL_RECTANGLE (core->paint_buffer_x,
                                            core->paint_buffer_y,
                                            width, height));

      pika_applicator_set_mask_buffer (applicator, core->mask_buffer);
      pika_applicator_set_mask_offset (applicator, -offset_x, -offset_y);

      g_object_unref (mask_buffer);
    }
  else
    {
      pika_paint_core_paste (core, paint_mask,
                             paint_mask_offset_x,
                             paint_mask_offset_y,
                             drawable,
                             paint_opacity,
                             image_opacity,
                             PIKA_LAYER_MODE_REPLACE,
                             mode);
      return;
    }

  /*  Update the undo extents  */
  core->x1 = MIN (core->x1, core->paint_buffer_x);
  core->y1 = MIN (core->y1, core->paint_buffer_y);
  core->x2 = MAX (core->x2, core->paint_buffer_x + width);
  core->y2 = MAX (core->y2, core->paint_buffer_y + height);

  /*  Update the drawable  */
  pika_drawable_update (drawable,
                        core->paint_buffer_x,
                        core->paint_buffer_y,
                        width, height);
}

/**
 * Smooth and store coords in the stroke buffer
 */

void
pika_paint_core_smooth_coords (PikaPaintCore    *core,
                               PikaPaintOptions *paint_options,
                               PikaCoords       *coords)
{
  PikaSmoothingOptions *smoothing_options = paint_options->smoothing_options;
  GArray               *history           = core->stroke_buffer;

  if (core->stroke_buffer == NULL)
    return; /* Paint core has not initialized yet */

  if (smoothing_options->use_smoothing &&
      smoothing_options->smoothing_quality > 0)
    {
      gint       i;
      guint      length;
      gint       min_index;
      gdouble    gaussian_weight  = 0.0;
      gdouble    gaussian_weight2 = SQR (smoothing_options->smoothing_factor);
      gdouble    velocity_sum     = 0.0;
      gdouble    scale_sum        = 0.0;

      g_array_append_val (history, *coords);

      if (history->len < 2)
        return; /* Just don't bother, nothing to do */

      coords->x = coords->y = 0.0;

      length = MIN (smoothing_options->smoothing_quality, history->len);

      min_index = history->len - length;

      if (gaussian_weight2 != 0.0)
        gaussian_weight = 1 / (sqrt (2 * G_PI) * smoothing_options->smoothing_factor);

      for (i = history->len - 1; i >= min_index; i--)
        {
          gdouble     rate        = 0.0;
          PikaCoords *next_coords = &g_array_index (history,
                                                    PikaCoords, i);

          if (gaussian_weight2 != 0.0)
            {
              /* We use gaussian function with velocity as a window function */
              velocity_sum += next_coords->velocity * 100;
              rate = gaussian_weight * exp (-velocity_sum * velocity_sum /
                                            (2 * gaussian_weight2));
            }

          scale_sum += rate;
          coords->x += rate * next_coords->x;
          coords->y += rate * next_coords->y;
        }

      if (scale_sum != 0.0)
        {
          coords->x /= scale_sum;
          coords->y /= scale_sum;
        }
    }
}
