/* 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
 *
 * 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/>.
 */

/* This file contains the code necessary for generating on canvas
 * previews, by connecting a specified GEGL operation to do the
 * processing. It uses drawable filters that allow for non-destructive
 * manipulation of drawable data, with live preview on screen.
 *
 * To create a tool that uses this, see app/tools/pikafiltertool.c for
 * the interface and e.g. app/tools/pikacolorbalancetool.c for an
 * example of using that interface.
 */

#include "config.h"

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

#include "libpikabase/pikabase.h"
#include "libpikacolor/pikacolor.h"

#include "core-types.h"

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

#include "pikachannel.h"
#include "pikadrawable-filters.h"
#include "pikadrawablefilter.h"
#include "pikaimage.h"
#include "pikalayer.h"
#include "pikaprogress.h"


enum
{
  FLUSH,
  LAST_SIGNAL
};


struct _PikaDrawableFilter
{
  PikaFilter              parent_instance;

  PikaDrawable           *drawable;
  GeglNode               *operation;

  gboolean                has_input;

  gboolean                clip;
  PikaFilterRegion        region;
  gboolean                crop_enabled;
  GeglRectangle           crop_rect;
  gboolean                preview_enabled;
  gboolean                preview_split_enabled;
  PikaAlignmentType       preview_split_alignment;
  gint                    preview_split_position;
  gdouble                 opacity;
  PikaLayerMode           paint_mode;
  PikaLayerColorSpace     blend_space;
  PikaLayerColorSpace     composite_space;
  PikaLayerCompositeMode  composite_mode;
  gboolean                add_alpha;
  gboolean                color_managed;
  gboolean                gamma_hack;

  gboolean                override_constraints;

  GeglRectangle           filter_area;
  gboolean                filter_clip;

  GeglNode               *translate;
  GeglNode               *crop_before;
  GeglNode               *cast_before;
  GeglNode               *cast_after;
  GeglNode               *crop_after;
  PikaApplicator         *applicator;
};


static void       pika_drawable_filter_dispose               (GObject             *object);
static void       pika_drawable_filter_finalize              (GObject             *object);

static void       pika_drawable_filter_sync_active           (PikaDrawableFilter  *filter);
static void       pika_drawable_filter_sync_clip             (PikaDrawableFilter  *filter,
                                                              gboolean             sync_region);
static void       pika_drawable_filter_sync_region           (PikaDrawableFilter  *filter);
static void       pika_drawable_filter_sync_crop             (PikaDrawableFilter  *filter,
                                                              gboolean             old_crop_enabled,
                                                              const GeglRectangle *old_crop_rect,
                                                              gboolean             old_preview_split_enabled,
                                                              PikaAlignmentType    old_preview_split_alignment,
                                                              gint                 old_preview_split_position,
                                                              gboolean             update);
static void       pika_drawable_filter_sync_opacity          (PikaDrawableFilter  *filter);
static void       pika_drawable_filter_sync_mode             (PikaDrawableFilter  *filter);
static void       pika_drawable_filter_sync_affect           (PikaDrawableFilter  *filter);
static void       pika_drawable_filter_sync_format           (PikaDrawableFilter  *filter);
static void       pika_drawable_filter_sync_mask             (PikaDrawableFilter  *filter);
static void       pika_drawable_filter_sync_gamma_hack       (PikaDrawableFilter  *filter);

static gboolean   pika_drawable_filter_is_added              (PikaDrawableFilter  *filter);
static gboolean   pika_drawable_filter_is_active             (PikaDrawableFilter  *filter);
static gboolean   pika_drawable_filter_add_filter            (PikaDrawableFilter  *filter);
static gboolean   pika_drawable_filter_remove_filter         (PikaDrawableFilter  *filter);

static void       pika_drawable_filter_update_drawable       (PikaDrawableFilter  *filter,
                                                              const GeglRectangle *area);

static void       pika_drawable_filter_affect_changed        (PikaImage           *image,
                                                              PikaChannelType      channel,
                                                              PikaDrawableFilter  *filter);
static void       pika_drawable_filter_mask_changed          (PikaImage           *image,
                                                              PikaDrawableFilter  *filter);
static void       pika_drawable_filter_lock_position_changed (PikaDrawable        *drawable,
                                                              PikaDrawableFilter  *filter);
static void       pika_drawable_filter_format_changed        (PikaDrawable        *drawable,
                                                              PikaDrawableFilter  *filter);
static void       pika_drawable_filter_drawable_removed      (PikaDrawable        *drawable,
                                                              PikaDrawableFilter  *filter);
static void       pika_drawable_filter_lock_alpha_changed    (PikaLayer           *layer,
                                                              PikaDrawableFilter  *filter);


G_DEFINE_TYPE (PikaDrawableFilter, pika_drawable_filter, PIKA_TYPE_FILTER)

#define parent_class pika_drawable_filter_parent_class

static guint drawable_filter_signals[LAST_SIGNAL] = { 0, };


static void
pika_drawable_filter_class_init (PikaDrawableFilterClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  drawable_filter_signals[FLUSH] =
    g_signal_new ("flush",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaDrawableFilterClass, flush),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  object_class->dispose  = pika_drawable_filter_dispose;
  object_class->finalize = pika_drawable_filter_finalize;
}

static void
pika_drawable_filter_init (PikaDrawableFilter *drawable_filter)
{
  drawable_filter->clip                    = TRUE;
  drawable_filter->region                  = PIKA_FILTER_REGION_SELECTION;
  drawable_filter->preview_enabled         = TRUE;
  drawable_filter->preview_split_enabled   = FALSE;
  drawable_filter->preview_split_alignment = PIKA_ALIGN_LEFT;
  drawable_filter->preview_split_position  = 0;
  drawable_filter->opacity                 = PIKA_OPACITY_OPAQUE;
  drawable_filter->paint_mode              = PIKA_LAYER_MODE_REPLACE;
  drawable_filter->blend_space             = PIKA_LAYER_COLOR_SPACE_AUTO;
  drawable_filter->composite_space         = PIKA_LAYER_COLOR_SPACE_AUTO;
  drawable_filter->composite_mode          = PIKA_LAYER_COMPOSITE_AUTO;
}

static void
pika_drawable_filter_dispose (GObject *object)
{
  PikaDrawableFilter *drawable_filter = PIKA_DRAWABLE_FILTER (object);

  if (drawable_filter->drawable)
    pika_drawable_filter_remove_filter (drawable_filter);

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

static void
pika_drawable_filter_finalize (GObject *object)
{
  PikaDrawableFilter *drawable_filter = PIKA_DRAWABLE_FILTER (object);

  g_clear_object (&drawable_filter->operation);
  g_clear_object (&drawable_filter->applicator);
  g_clear_object (&drawable_filter->drawable);

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

PikaDrawableFilter *
pika_drawable_filter_new (PikaDrawable *drawable,
                          const gchar  *undo_desc,
                          GeglNode     *operation,
                          const gchar  *icon_name)
{
  PikaDrawableFilter *filter;
  GeglNode           *node;

  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 (GEGL_IS_NODE (operation), NULL);
  g_return_val_if_fail (gegl_node_has_pad (operation, "output"), NULL);

  filter = g_object_new (PIKA_TYPE_DRAWABLE_FILTER,
                         "name",      undo_desc,
                         "icon-name", icon_name,
                         NULL);

  filter->drawable  = g_object_ref (drawable);
  filter->operation = g_object_ref (operation);

  node = pika_filter_get_node (PIKA_FILTER (filter));

  gegl_node_add_child (node, operation);
  pika_gegl_node_set_underlying_operation (node, operation);

  filter->applicator = pika_applicator_new (node);

  pika_filter_set_applicator (PIKA_FILTER (filter), filter->applicator);

  pika_applicator_set_cache (filter->applicator, TRUE);

  filter->has_input = gegl_node_has_pad (filter->operation, "input");

  if (filter->has_input)
    {
      GeglNode *input;

      input = gegl_node_get_input_proxy (node, "input");

      filter->translate = gegl_node_new_child (node,
                                               "operation", "gegl:translate",
                                               NULL);

      filter->crop_before = gegl_node_new_child (node,
                                                 "operation", "gegl:crop",
                                                 NULL);

      filter->cast_before = gegl_node_new_child (node,
                                                 "operation", "gegl:nop",
                                                 NULL);

      gegl_node_link_many (input,
                           filter->translate,
                           filter->crop_before,
                           filter->cast_before,
                           filter->operation,
                           NULL);
    }

  filter->cast_after = gegl_node_new_child (node,
                                            "operation", "gegl:nop",
                                            NULL);

  filter->crop_after = gegl_node_new_child (node,
                                            "operation", "gegl:crop",
                                            NULL);

  gegl_node_link_many (filter->operation,
                       filter->cast_after,
                       filter->crop_after,
                       NULL);

  gegl_node_connect (filter->crop_after, "output", node, "aux");

  return filter;
}

PikaDrawable *
pika_drawable_filter_get_drawable (PikaDrawableFilter *filter)
{
  g_return_val_if_fail (PIKA_IS_DRAWABLE_FILTER (filter), NULL);

  return filter->drawable;
}

GeglNode *
pika_drawable_filter_get_operation (PikaDrawableFilter *filter)
{
  g_return_val_if_fail (PIKA_IS_DRAWABLE_FILTER (filter), NULL);

  return filter->operation;
}

void
pika_drawable_filter_set_clip (PikaDrawableFilter *filter,
                               gboolean            clip)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (clip != filter->clip)
    {
      filter->clip = clip;

      pika_drawable_filter_sync_clip (filter, TRUE);
    }
}

void
pika_drawable_filter_set_region (PikaDrawableFilter *filter,
                                 PikaFilterRegion    region)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (region != filter->region)
    {
      filter->region = region;

      pika_drawable_filter_sync_region (filter);

      if (pika_drawable_filter_is_active (filter))
        pika_drawable_filter_update_drawable (filter, NULL);
    }
}

void
pika_drawable_filter_set_crop (PikaDrawableFilter  *filter,
                               const GeglRectangle *rect,
                               gboolean             update)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if ((rect != NULL) != filter->crop_enabled ||
      (rect && ! gegl_rectangle_equal (rect, &filter->crop_rect)))
    {
      gboolean      old_enabled = filter->crop_enabled;
      GeglRectangle old_rect    = filter->crop_rect;

      if (rect)
        {
          filter->crop_enabled = TRUE;
          filter->crop_rect    = *rect;
        }
      else
        {
          filter->crop_enabled = FALSE;
        }

      pika_drawable_filter_sync_crop (filter,
                                      old_enabled,
                                      &old_rect,
                                      filter->preview_split_enabled,
                                      filter->preview_split_alignment,
                                      filter->preview_split_position,
                                      update);
    }
}

void
pika_drawable_filter_set_preview (PikaDrawableFilter *filter,
                                  gboolean            enabled)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (enabled != filter->preview_enabled)
    {
      filter->preview_enabled = enabled;

      pika_drawable_filter_sync_active (filter);

      if (pika_drawable_filter_is_added (filter))
        {
          pika_drawable_update_bounding_box (filter->drawable);

          pika_drawable_filter_update_drawable (filter, NULL);
        }
    }
}

void
pika_drawable_filter_set_preview_split (PikaDrawableFilter  *filter,
                                        gboolean             enabled,
                                        PikaAlignmentType    alignment,
                                        gint                 position)
{
  PikaItem *item;

  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));
  g_return_if_fail (alignment == PIKA_ALIGN_LEFT  ||
                    alignment == PIKA_ALIGN_RIGHT ||
                    alignment == PIKA_ALIGN_TOP   ||
                    alignment == PIKA_ALIGN_BOTTOM);

  item = PIKA_ITEM (filter->drawable);

  switch (alignment)
    {
    case PIKA_ALIGN_LEFT:
    case PIKA_ALIGN_RIGHT:
      position = CLAMP (position, 0, pika_item_get_width (item));
      break;

    case PIKA_ALIGN_TOP:
    case PIKA_ALIGN_BOTTOM:
      position = CLAMP (position, 0, pika_item_get_height (item));
      break;

    default:
      g_return_if_reached ();
    }

  if (enabled   != filter->preview_split_enabled   ||
      alignment != filter->preview_split_alignment ||
      position  != filter->preview_split_position)
    {
      gboolean          old_enabled   = filter->preview_split_enabled;
      PikaAlignmentType old_alignment = filter->preview_split_alignment;
      gint              old_position  = filter->preview_split_position;

      filter->preview_split_enabled   = enabled;
      filter->preview_split_alignment = alignment;
      filter->preview_split_position  = position;

      pika_drawable_filter_sync_crop (filter,
                                      filter->crop_enabled,
                                      &filter->crop_rect,
                                      old_enabled,
                                      old_alignment,
                                      old_position,
                                      TRUE);
    }
}

void
pika_drawable_filter_set_opacity (PikaDrawableFilter *filter,
                                  gdouble             opacity)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (opacity != filter->opacity)
    {
      filter->opacity = opacity;

      pika_drawable_filter_sync_opacity (filter);

      if (pika_drawable_filter_is_active (filter))
        pika_drawable_filter_update_drawable (filter, NULL);
    }
}

void
pika_drawable_filter_set_mode (PikaDrawableFilter     *filter,
                               PikaLayerMode           paint_mode,
                               PikaLayerColorSpace     blend_space,
                               PikaLayerColorSpace     composite_space,
                               PikaLayerCompositeMode  composite_mode)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (paint_mode      != filter->paint_mode      ||
      blend_space     != filter->blend_space     ||
      composite_space != filter->composite_space ||
      composite_mode  != filter->composite_mode)
    {
      filter->paint_mode      = paint_mode;
      filter->blend_space     = blend_space;
      filter->composite_space = composite_space;
      filter->composite_mode  = composite_mode;

      pika_drawable_filter_sync_mode (filter);

      if (pika_drawable_filter_is_active (filter))
        pika_drawable_filter_update_drawable (filter, NULL);
    }
}

void
pika_drawable_filter_set_add_alpha (PikaDrawableFilter *filter,
                                    gboolean            add_alpha)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (add_alpha != filter->add_alpha)
    {
      filter->add_alpha = add_alpha;

      pika_drawable_filter_sync_format (filter);

      if (pika_drawable_filter_is_active (filter))
        pika_drawable_filter_update_drawable (filter, NULL);
    }
}

void
pika_drawable_filter_set_gamma_hack (PikaDrawableFilter *filter,
                                     gboolean            gamma_hack)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (gamma_hack != filter->gamma_hack)
    {
      filter->gamma_hack = gamma_hack;

      pika_drawable_filter_sync_gamma_hack (filter);

      if (pika_drawable_filter_is_active (filter))
        pika_drawable_filter_update_drawable (filter, NULL);
    }
}

void
pika_drawable_filter_set_override_constraints (PikaDrawableFilter *filter,
                                               gboolean            override_constraints)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (override_constraints != filter->override_constraints)
    {
      filter->override_constraints = override_constraints;

      pika_drawable_filter_sync_affect (filter);
      pika_drawable_filter_sync_format (filter);
      pika_drawable_filter_sync_clip   (filter, TRUE);

      if (pika_drawable_filter_is_active (filter))
        pika_drawable_filter_update_drawable (filter, NULL);
    }
}

const Babl *
pika_drawable_filter_get_format (PikaDrawableFilter *filter)
{
  const Babl *format;

  g_return_val_if_fail (PIKA_IS_DRAWABLE_FILTER (filter), NULL);

  format = pika_applicator_get_output_format (filter->applicator);

  if (! format)
    format = pika_drawable_get_format (filter->drawable);

  return format;
}

void
pika_drawable_filter_apply (PikaDrawableFilter  *filter,
                            const GeglRectangle *area)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));
  g_return_if_fail (pika_item_is_attached (PIKA_ITEM (filter->drawable)));

  pika_drawable_filter_add_filter (filter);

  pika_drawable_filter_sync_clip (filter, TRUE);

  if (pika_drawable_filter_is_active (filter))
    {
      pika_drawable_update_bounding_box (filter->drawable);

      pika_drawable_filter_update_drawable (filter, area);
    }
}

gboolean
pika_drawable_filter_commit (PikaDrawableFilter *filter,
                             PikaProgress       *progress,
                             gboolean            cancellable)
{
  gboolean success = TRUE;

  g_return_val_if_fail (PIKA_IS_DRAWABLE_FILTER (filter), FALSE);
  g_return_val_if_fail (pika_item_is_attached (PIKA_ITEM (filter->drawable)),
                        FALSE);
  g_return_val_if_fail (progress == NULL || PIKA_IS_PROGRESS (progress), FALSE);

  if (pika_drawable_filter_is_added (filter))
    {
      const Babl *format;

      format = pika_drawable_filter_get_format (filter);

      pika_drawable_filter_set_preview_split (filter, FALSE,
                                              filter->preview_split_alignment,
                                              filter->preview_split_position);
      pika_drawable_filter_set_preview (filter, TRUE);

      success = pika_drawable_merge_filter (filter->drawable,
                                            PIKA_FILTER (filter),
                                            progress,
                                            pika_object_get_name (filter),
                                            format,
                                            filter->filter_clip,
                                            cancellable,
                                            FALSE);

      pika_drawable_filter_remove_filter (filter);

      if (! success)
        pika_drawable_filter_update_drawable (filter, NULL);

      g_signal_emit (filter, drawable_filter_signals[FLUSH], 0);
    }

  return success;
}

void
pika_drawable_filter_abort (PikaDrawableFilter *filter)
{
  g_return_if_fail (PIKA_IS_DRAWABLE_FILTER (filter));

  if (pika_drawable_filter_remove_filter (filter))
    {
      pika_drawable_filter_update_drawable (filter, NULL);
    }
}


/*  private functions  */

static void
pika_drawable_filter_sync_active (PikaDrawableFilter *filter)
{
  pika_applicator_set_active (filter->applicator, filter->preview_enabled);
}

static void
pika_drawable_filter_sync_clip (PikaDrawableFilter *filter,
                                gboolean            sync_region)
{
  gboolean clip;

  if (filter->override_constraints)
    clip = filter->clip;
  else
    clip = pika_item_get_clip (PIKA_ITEM (filter->drawable), filter->clip);

  if (! clip)
    {
      PikaImage   *image = pika_item_get_image (PIKA_ITEM (filter->drawable));
      PikaChannel *mask  = pika_image_get_mask (image);

      if (! pika_channel_is_empty (mask))
        clip = TRUE;
    }

  if (! clip)
    {
      GeglRectangle bounding_box;

      bounding_box = gegl_node_get_bounding_box (filter->operation);

      if (gegl_rectangle_is_infinite_plane (&bounding_box))
        clip = TRUE;
    }

  if (clip != filter->filter_clip)
    {
      filter->filter_clip = clip;

      if (sync_region)
        pika_drawable_filter_sync_region (filter);
    }
}

static void
pika_drawable_filter_sync_region (PikaDrawableFilter *filter)
{
  if (filter->region == PIKA_FILTER_REGION_SELECTION)
    {
      if (filter->has_input)
        {
          gegl_node_set (filter->translate,
                         "x", (gdouble) -filter->filter_area.x,
                         "y", (gdouble) -filter->filter_area.y,
                         NULL);

          gegl_node_set (filter->crop_before,
                         "width",  (gdouble) filter->filter_area.width,
                         "height", (gdouble) filter->filter_area.height,
                         NULL);
        }

      if (filter->filter_clip)
        {
          gegl_node_set (filter->crop_after,
                         "operation", "gegl:crop",
                         "x",         0.0,
                         "y",         0.0,
                         "width",     (gdouble) filter->filter_area.width,
                         "height",    (gdouble) filter->filter_area.height,
                         NULL);
        }
      else
        {
          gegl_node_set (filter->crop_after,
                         "operation", "gegl:nop",
                         NULL);
        }

      pika_applicator_set_apply_offset (filter->applicator,
                                        filter->filter_area.x,
                                        filter->filter_area.y);
    }
  else
    {
      PikaItem *item   = PIKA_ITEM (filter->drawable);
      gdouble   width  = pika_item_get_width (item);
      gdouble   height = pika_item_get_height (item);

      if (filter->has_input)
        {
          gegl_node_set (filter->translate,
                         "x", (gdouble) 0.0,
                         "y", (gdouble) 0.0,
                         NULL);

          gegl_node_set (filter->crop_before,
                         "width",  width,
                         "height", height,
                         NULL);
        }

      if (filter->filter_clip)
        {
          gegl_node_set (filter->crop_after,
                         "operation", "gegl:crop",
                         "x",         (gdouble) filter->filter_area.x,
                         "y",         (gdouble) filter->filter_area.y,
                         "width",     (gdouble) filter->filter_area.width,
                         "height",    (gdouble) filter->filter_area.height,
                         NULL);
        }
      else
        {
          gegl_node_set (filter->crop_after,
                         "operation", "gegl:nop",
                         NULL);
        }

      pika_applicator_set_apply_offset (filter->applicator, 0, 0);
    }

  if (pika_drawable_filter_is_active (filter))
    {
      if (pika_drawable_update_bounding_box (filter->drawable))
        g_signal_emit (filter, drawable_filter_signals[FLUSH], 0);
    }
}

static gboolean
pika_drawable_filter_get_crop_rect (PikaDrawableFilter  *filter,
                                    gboolean             crop_enabled,
                                    const GeglRectangle *crop_rect,
                                    gboolean             preview_split_enabled,
                                    PikaAlignmentType    preview_split_alignment,
                                    gint                 preview_split_position,
                                    GeglRectangle       *rect)
{
  GeglRectangle bounds;
  gint          x1, x2;
  gint          y1, y2;

  bounds = gegl_rectangle_infinite_plane ();

  x1 = bounds.x;
  x2 = bounds.x + bounds.width;

  y1 = bounds.y;
  y2 = bounds.y + bounds.height;

  if (preview_split_enabled)
    {
      switch (preview_split_alignment)
        {
        case PIKA_ALIGN_LEFT:
          x2 = preview_split_position;
          break;

        case PIKA_ALIGN_RIGHT:
          x1 = preview_split_position;
          break;

        case PIKA_ALIGN_TOP:
          y2 = preview_split_position;
          break;

        case PIKA_ALIGN_BOTTOM:
          y1 = preview_split_position;
          break;

        default:
          g_return_val_if_reached (FALSE);
        }
    }

  gegl_rectangle_set (rect, x1, y1, x2 - x1, y2 - y1);

  if (crop_enabled)
    gegl_rectangle_intersect (rect, rect, crop_rect);

  return ! gegl_rectangle_equal (rect, &bounds);
}

static void
pika_drawable_filter_sync_crop (PikaDrawableFilter  *filter,
                                gboolean             old_crop_enabled,
                                const GeglRectangle *old_crop_rect,
                                gboolean             old_preview_split_enabled,
                                PikaAlignmentType    old_preview_split_alignment,
                                gint                 old_preview_split_position,
                                gboolean             update)
{
  GeglRectangle old_rect;
  GeglRectangle new_rect;
  gboolean      enabled;

  pika_drawable_filter_get_crop_rect (filter,
                                      old_crop_enabled,
                                      old_crop_rect,
                                      old_preview_split_enabled,
                                      old_preview_split_alignment,
                                      old_preview_split_position,
                                      &old_rect);

  enabled = pika_drawable_filter_get_crop_rect (filter,
                                                filter->crop_enabled,
                                                &filter->crop_rect,
                                                filter->preview_split_enabled,
                                                filter->preview_split_alignment,
                                                filter->preview_split_position,
                                                &new_rect);

  pika_applicator_set_crop (filter->applicator, enabled ? &new_rect : NULL);

  if (update                                     &&
      pika_drawable_filter_is_active (filter) &&
      ! gegl_rectangle_equal (&old_rect, &new_rect))
    {
      GeglRectangle diff_rects[4];
      gint          n_diff_rects;
      gint          i;

      pika_drawable_update_bounding_box (filter->drawable);

      n_diff_rects = gegl_rectangle_xor (diff_rects, &old_rect, &new_rect);

      for (i = 0; i < n_diff_rects; i++)
        pika_drawable_filter_update_drawable (filter, &diff_rects[i]);
    }
}

static void
pika_drawable_filter_sync_opacity (PikaDrawableFilter *filter)
{
  pika_applicator_set_opacity (filter->applicator,
                               filter->opacity);
}

static void
pika_drawable_filter_sync_mode (PikaDrawableFilter *filter)
{
  PikaLayerMode paint_mode = filter->paint_mode;

  if (! filter->has_input && paint_mode == PIKA_LAYER_MODE_REPLACE)
    {
      /* if the filter's op has no input, use NORMAL instead of REPLACE, so
       * that we composite the op's output on top of the input, instead of
       * completely replacing it.
       */
      paint_mode = PIKA_LAYER_MODE_NORMAL;
    }

  pika_applicator_set_mode (filter->applicator,
                            paint_mode,
                            filter->blend_space,
                            filter->composite_space,
                            filter->composite_mode);
}

static void
pika_drawable_filter_sync_affect (PikaDrawableFilter *filter)
{
  pika_applicator_set_affect (
    filter->applicator,
    filter->override_constraints ?

      PIKA_COMPONENT_MASK_RED   |
      PIKA_COMPONENT_MASK_GREEN |
      PIKA_COMPONENT_MASK_BLUE  |
      PIKA_COMPONENT_MASK_ALPHA :

      pika_drawable_get_active_mask (filter->drawable));
}

static void
pika_drawable_filter_sync_format (PikaDrawableFilter *filter)
{
  const Babl *format;

  if (filter->add_alpha                                &&
      (pika_drawable_supports_alpha (filter->drawable) ||
       filter->override_constraints))
    {
      format = pika_drawable_get_format_with_alpha (filter->drawable);
    }
  else
    {
      format = pika_drawable_get_format (filter->drawable);
    }

  pika_applicator_set_output_format (filter->applicator, format);
}

static void
pika_drawable_filter_sync_mask (PikaDrawableFilter *filter)
{
  PikaImage   *image = pika_item_get_image (PIKA_ITEM (filter->drawable));
  PikaChannel *mask  = pika_image_get_mask (image);

  if (pika_channel_is_empty (mask))
    {
      pika_applicator_set_mask_buffer (filter->applicator, NULL);
    }
  else
    {
      GeglBuffer *mask_buffer;
      gint        offset_x, offset_y;

      mask_buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (mask));
      pika_item_get_offset (PIKA_ITEM (filter->drawable),
                            &offset_x, &offset_y);

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

  pika_item_mask_intersect (PIKA_ITEM (filter->drawable),
                            &filter->filter_area.x,
                            &filter->filter_area.y,
                            &filter->filter_area.width,
                            &filter->filter_area.height);
}

static void
pika_drawable_filter_sync_gamma_hack (PikaDrawableFilter *filter)
{
  if (filter->gamma_hack)
    {
      const Babl  *drawable_format;
      const Babl  *cast_format;
      PikaTRCType  trc = PIKA_TRC_LINEAR;

      switch (pika_drawable_get_trc (filter->drawable))
        {
        case PIKA_TRC_LINEAR:     trc = PIKA_TRC_NON_LINEAR; break;
        case PIKA_TRC_NON_LINEAR: trc = PIKA_TRC_LINEAR;     break;
        case PIKA_TRC_PERCEPTUAL: trc = PIKA_TRC_LINEAR;     break;
        }

      drawable_format =
        pika_drawable_get_format_with_alpha (filter->drawable);

      cast_format =
        pika_babl_format (pika_babl_format_get_base_type (drawable_format),
                          pika_babl_precision (pika_babl_format_get_component_type (drawable_format),
                                               trc),
                          TRUE,
                          babl_format_get_space (drawable_format));

      if (filter->has_input)
        {
          gegl_node_set (filter->cast_before,
                         "operation",     "gegl:cast-format",
                         "input-format",  drawable_format,
                         "output-format", cast_format,
                         NULL);
        }

      gegl_node_set (filter->cast_after,
                     "operation",     "gegl:cast-format",
                     "input-format",  cast_format,
                     "output-format", drawable_format,
                     NULL);
    }
  else
    {
      if (filter->has_input)
        {
          gegl_node_set (filter->cast_before,
                         "operation", "gegl:nop",
                         NULL);
        }

      gegl_node_set (filter->cast_after,
                     "operation", "gegl:nop",
                     NULL);
    }
}

static gboolean
pika_drawable_filter_is_added (PikaDrawableFilter *filter)
{
  return pika_drawable_has_filter (filter->drawable,
                                   PIKA_FILTER (filter));
}

static gboolean
pika_drawable_filter_is_active (PikaDrawableFilter *filter)
{
  return pika_drawable_filter_is_added (filter) &&
         filter->preview_enabled;
}

static gboolean
pika_drawable_filter_add_filter (PikaDrawableFilter *filter)
{
  if (! pika_drawable_filter_is_added (filter))
    {
      PikaImage *image = pika_item_get_image (PIKA_ITEM (filter->drawable));

      pika_viewable_preview_freeze (PIKA_VIEWABLE (filter->drawable));

      pika_drawable_filter_sync_active (filter);
      pika_drawable_filter_sync_mask (filter);
      pika_drawable_filter_sync_clip (filter, FALSE);
      pika_drawable_filter_sync_region (filter);
      pika_drawable_filter_sync_crop (filter,
                                      filter->crop_enabled,
                                      &filter->crop_rect,
                                      filter->preview_split_enabled,
                                      filter->preview_split_alignment,
                                      filter->preview_split_position,
                                      TRUE);
      pika_drawable_filter_sync_opacity (filter);
      pika_drawable_filter_sync_mode (filter);
      pika_drawable_filter_sync_affect (filter);
      pika_drawable_filter_sync_format (filter);
      pika_drawable_filter_sync_gamma_hack (filter);

      pika_drawable_add_filter (filter->drawable,
                                PIKA_FILTER (filter));

      pika_drawable_update_bounding_box (filter->drawable);

      g_signal_connect (image, "component-active-changed",
                        G_CALLBACK (pika_drawable_filter_affect_changed),
                        filter);
      g_signal_connect (image, "mask-changed",
                        G_CALLBACK (pika_drawable_filter_mask_changed),
                        filter);
      g_signal_connect (filter->drawable, "lock-position-changed",
                        G_CALLBACK (pika_drawable_filter_lock_position_changed),
                        filter);
      g_signal_connect (filter->drawable, "format-changed",
                        G_CALLBACK (pika_drawable_filter_format_changed),
                        filter);
      g_signal_connect (filter->drawable, "removed",
                        G_CALLBACK (pika_drawable_filter_drawable_removed),
                        filter);

      if (PIKA_IS_LAYER (filter->drawable))
        {
          g_signal_connect (filter->drawable, "lock-alpha-changed",
                            G_CALLBACK (pika_drawable_filter_lock_alpha_changed),
                            filter);
        }

      return TRUE;
    }

  return FALSE;
}

static gboolean
pika_drawable_filter_remove_filter (PikaDrawableFilter *filter)
{
  if (pika_drawable_filter_is_added (filter))
    {
      PikaImage *image = pika_item_get_image (PIKA_ITEM (filter->drawable));

      if (PIKA_IS_LAYER (filter->drawable))
        {
          g_signal_handlers_disconnect_by_func (filter->drawable,
                                                pika_drawable_filter_lock_alpha_changed,
                                                filter);
        }

      g_signal_handlers_disconnect_by_func (filter->drawable,
                                            pika_drawable_filter_drawable_removed,
                                            filter);
      g_signal_handlers_disconnect_by_func (filter->drawable,
                                            pika_drawable_filter_format_changed,
                                            filter);
      g_signal_handlers_disconnect_by_func (filter->drawable,
                                            pika_drawable_filter_lock_position_changed,
                                            filter);
      g_signal_handlers_disconnect_by_func (image,
                                            pika_drawable_filter_mask_changed,
                                            filter);
      g_signal_handlers_disconnect_by_func (image,
                                            pika_drawable_filter_affect_changed,
                                            filter);

      pika_drawable_remove_filter (filter->drawable,
                                   PIKA_FILTER (filter));

      pika_drawable_update_bounding_box (filter->drawable);

      pika_viewable_preview_thaw (PIKA_VIEWABLE (filter->drawable));

      return TRUE;
    }

  return FALSE;
}

static void
pika_drawable_filter_update_drawable (PikaDrawableFilter  *filter,
                                      const GeglRectangle *area)
{
  GeglRectangle bounding_box;
  GeglRectangle update_area;

  bounding_box = pika_drawable_get_bounding_box (filter->drawable);

  if (area)
    {
      if (! gegl_rectangle_intersect (&update_area,
                                      area, &bounding_box))
        {
          return;
        }
    }
  else
    {
      pika_drawable_filter_get_crop_rect (filter,
                                          filter->crop_enabled,
                                          &filter->crop_rect,
                                          filter->preview_split_enabled,
                                          filter->preview_split_alignment,
                                          filter->preview_split_position,
                                          &update_area);

      if (! gegl_rectangle_intersect (&update_area,
                                      &update_area, &bounding_box))
        {
          return;
        }
    }

  if (update_area.width  > 0 &&
      update_area.height > 0)
    {
      pika_drawable_update (filter->drawable,
                            update_area.x,
                            update_area.y,
                            update_area.width,
                            update_area.height);

      g_signal_emit (filter, drawable_filter_signals[FLUSH], 0);
    }
}

static void
pika_drawable_filter_affect_changed (PikaImage          *image,
                                     PikaChannelType     channel,
                                     PikaDrawableFilter *filter)
{
  pika_drawable_filter_sync_affect (filter);
  pika_drawable_filter_update_drawable (filter, NULL);
}

static void
pika_drawable_filter_mask_changed (PikaImage          *image,
                                   PikaDrawableFilter *filter)
{
  pika_drawable_filter_update_drawable (filter, NULL);

  pika_drawable_filter_sync_mask (filter);
  pika_drawable_filter_sync_clip (filter, FALSE);
  pika_drawable_filter_sync_region (filter);

  pika_drawable_filter_update_drawable (filter, NULL);
}

static void
pika_drawable_filter_lock_position_changed (PikaDrawable       *drawable,
                                            PikaDrawableFilter *filter)
{
  pika_drawable_filter_sync_clip (filter, TRUE);
  pika_drawable_filter_update_drawable (filter, NULL);
}

static void
pika_drawable_filter_format_changed (PikaDrawable       *drawable,
                                     PikaDrawableFilter *filter)
{
  pika_drawable_filter_sync_format (filter);
  pika_drawable_filter_update_drawable (filter, NULL);
}

static void
pika_drawable_filter_drawable_removed (PikaDrawable       *drawable,
                                       PikaDrawableFilter *filter)
{
  pika_drawable_filter_remove_filter (filter);
}

static void
pika_drawable_filter_lock_alpha_changed (PikaLayer          *layer,
                                         PikaDrawableFilter *filter)
{
  pika_drawable_filter_sync_affect (filter);
  pika_drawable_filter_update_drawable (filter, NULL);
}
