/* GIMP - The GNU Image Manipulation Program
 * 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/>.
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>

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

#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"

#include "core-types.h"

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

#include "gimp.h"
#include "gimp-memsize.h"
#include "gimpchunkiterator.h"
#include "gimpimage.h"
#include "gimpmarshal.h"
#include "gimppickable.h"
#include "gimpprojectable.h"
#include "gimpprojection.h"
#include "gimptilehandlerprojectable.h"

#include "gimp-log.h"
#include "gimp-priorities.h"


/*  chunk size for area updates  */
#define GIMP_PROJECTION_UPDATE_CHUNK_WIDTH  32
#define GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT 32


enum
{
  UPDATE,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_BUFFER
};


struct _GimpProjectionPrivate
{
  GimpProjectable           *projectable;

  GeglBuffer                *buffer;
  GimpTileHandlerValidate   *validate_handler;

  gint                       priority;

  cairo_region_t            *update_region;
  GeglRectangle              priority_rect;
  GimpChunkIterator         *iter;
  guint                      idle_id;

  gboolean                   invalidate_preview;
};


/*  local function prototypes  */

static void   gimp_projection_pickable_iface_init (GimpPickableInterface  *iface);

static void        gimp_projection_finalize              (GObject         *object);
static void        gimp_projection_set_property          (GObject         *object,
                                                          guint            property_id,
                                                          const GValue    *value,
                                                          GParamSpec      *pspec);
static void        gimp_projection_get_property          (GObject         *object,
                                                          guint            property_id,
                                                          GValue          *value,
                                                          GParamSpec      *pspec);

static gint64      gimp_projection_get_memsize           (GimpObject      *object,
                                                          gint64          *gui_size);

static void        gimp_projection_pickable_flush        (GimpPickable    *pickable);
static GimpImage * gimp_projection_get_image             (GimpPickable    *pickable);
static const Babl * gimp_projection_get_format           (GimpPickable    *pickable);
static GeglBuffer * gimp_projection_get_buffer           (GimpPickable    *pickable);
static gboolean    gimp_projection_get_pixel_at          (GimpPickable    *pickable,
                                                          gint             x,
                                                          gint             y,
                                                          const Babl      *format,
                                                          gpointer         pixel);
static gdouble     gimp_projection_get_opacity_at        (GimpPickable    *pickable,
                                                          gint             x,
                                                          gint             y);
static void        gimp_projection_get_pixel_average     (GimpPickable    *pickable,
                                                          const GeglRectangle *rect,
                                                          const Babl      *format,
                                                          gpointer         pixel);
static void        gimp_projection_pixel_to_srgb         (GimpPickable    *pickable,
                                                          const Babl      *format,
                                                          gpointer         pixel,
                                                          GimpRGB         *color);
static void        gimp_projection_srgb_to_pixel         (GimpPickable    *pickable,
                                                          const GimpRGB   *color,
                                                          const Babl      *format,
                                                          gpointer         pixel);

static void        gimp_projection_allocate_buffer       (GimpProjection  *proj);
static void        gimp_projection_free_buffer           (GimpProjection  *proj);
static void        gimp_projection_add_update_area       (GimpProjection  *proj,
                                                          gint             x,
                                                          gint             y,
                                                          gint             w,
                                                          gint             h);
static void        gimp_projection_flush_whenever        (GimpProjection  *proj,
                                                          gboolean         now,
                                                          gboolean         direct);
static void        gimp_projection_update_priority_rect  (GimpProjection  *proj);
static void        gimp_projection_chunk_render_start    (GimpProjection  *proj);
static void        gimp_projection_chunk_render_stop     (GimpProjection  *proj,
                                                          gboolean         merge);
static gboolean    gimp_projection_chunk_render_callback (GimpProjection  *proj);
static gboolean    gimp_projection_chunk_render_iteration(GimpProjection  *proj);
static void        gimp_projection_paint_area            (GimpProjection  *proj,
                                                          gboolean         now,
                                                          gint             x,
                                                          gint             y,
                                                          gint             w,
                                                          gint             h);

static void        gimp_projection_projectable_invalidate(GimpProjectable *projectable,
                                                          gint             x,
                                                          gint             y,
                                                          gint             w,
                                                          gint             h,
                                                          GimpProjection  *proj);
static void        gimp_projection_projectable_flush     (GimpProjectable *projectable,
                                                          gboolean         invalidate_preview,
                                                          GimpProjection  *proj);
static void
           gimp_projection_projectable_structure_changed (GimpProjectable *projectable,
                                                          GimpProjection  *proj);
static void   gimp_projection_projectable_bounds_changed (GimpProjectable *projectable,
                                                          gint             old_x,
                                                          gint             old_y,
                                                          gint             old_w,
                                                          gint             old_h,
                                                          GimpProjection  *proj);


G_DEFINE_TYPE_WITH_CODE (GimpProjection, gimp_projection, GIMP_TYPE_OBJECT,
                         G_ADD_PRIVATE (GimpProjection)
                         G_IMPLEMENT_INTERFACE (GIMP_TYPE_PICKABLE,
                                                gimp_projection_pickable_iface_init))

#define parent_class gimp_projection_parent_class

static guint projection_signals[LAST_SIGNAL] = { 0 };


static void
gimp_projection_class_init (GimpProjectionClass *klass)
{
  GObjectClass    *object_class      = G_OBJECT_CLASS (klass);
  GimpObjectClass *gimp_object_class = GIMP_OBJECT_CLASS (klass);

  projection_signals[UPDATE] =
    g_signal_new ("update",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpProjectionClass, update),
                  NULL, NULL,
                  gimp_marshal_VOID__BOOLEAN_INT_INT_INT_INT,
                  G_TYPE_NONE, 5,
                  G_TYPE_BOOLEAN,
                  G_TYPE_INT,
                  G_TYPE_INT,
                  G_TYPE_INT,
                  G_TYPE_INT);

  object_class->finalize         = gimp_projection_finalize;
  object_class->set_property     = gimp_projection_set_property;
  object_class->get_property     = gimp_projection_get_property;

  gimp_object_class->get_memsize = gimp_projection_get_memsize;

  g_object_class_override_property (object_class, PROP_BUFFER, "buffer");
}

static void
gimp_projection_init (GimpProjection *proj)
{
  proj->priv = gimp_projection_get_instance_private (proj);
}

static void
gimp_projection_pickable_iface_init (GimpPickableInterface *iface)
{
  iface->flush                 = gimp_projection_pickable_flush;
  iface->get_image             = gimp_projection_get_image;
  iface->get_format            = gimp_projection_get_format;
  iface->get_format_with_alpha = gimp_projection_get_format; /* sic */
  iface->get_buffer            = gimp_projection_get_buffer;
  iface->get_pixel_at          = gimp_projection_get_pixel_at;
  iface->get_opacity_at        = gimp_projection_get_opacity_at;
  iface->get_pixel_average     = gimp_projection_get_pixel_average;
  iface->pixel_to_srgb         = gimp_projection_pixel_to_srgb;
  iface->srgb_to_pixel         = gimp_projection_srgb_to_pixel;
}

static void
gimp_projection_finalize (GObject *object)
{
  GimpProjection *proj = GIMP_PROJECTION (object);

  gimp_projection_free_buffer (proj);

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

static void
gimp_projection_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  switch (property_id)
    {
    case PROP_BUFFER:
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_projection_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  GimpProjection *projection = GIMP_PROJECTION (object);

  switch (property_id)
    {
    case PROP_BUFFER:
      g_value_set_object (value, projection->priv->buffer);
      break;

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

static gint64
gimp_projection_get_memsize (GimpObject *object,
                             gint64     *gui_size)
{
  GimpProjection *projection = GIMP_PROJECTION (object);
  gint64          memsize    = 0;

  memsize += gimp_gegl_pyramid_get_memsize (projection->priv->buffer);

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

/**
 * gimp_projection_estimate_memsize:
 * @type:           the projectable's base type
 * @component_type: the projectable's component type
 * @width:          projection width
 * @height:         projection height
 *
 * Calculates a rough estimate of the memory that is required for the
 * projection of an image with the given @width and @height.
 *
 * Return value: a rough estimate of the memory requirements.
 **/
gint64
gimp_projection_estimate_memsize (GimpImageBaseType type,
                                  GimpComponentType component_type,
                                  gint              width,
                                  gint              height)
{
  const Babl *format;
  gint64      bytes;

  if (type == GIMP_INDEXED)
    type = GIMP_RGB;

  format = gimp_babl_format (type,
                             gimp_babl_precision (component_type, FALSE),
                             TRUE, NULL);
  bytes  = babl_format_get_bytes_per_pixel (format);

  /* The pyramid levels constitute a geometric sum with a ratio of 1/4. */
  return bytes * (gint64) width * (gint64) height * 1.33;
}


static void
gimp_projection_pickable_flush (GimpPickable *pickable)
{
  GimpProjection *proj = GIMP_PROJECTION (pickable);

  /* create the buffer if it doesn't exist */
  gimp_projection_get_buffer (pickable);

  gimp_projection_finish_draw (proj);
  gimp_projection_flush_now (proj, FALSE);

  if (proj->priv->invalidate_preview)
    {
      /* invalidate the preview here since it is constructed from
       * the projection
       */
      proj->priv->invalidate_preview = FALSE;

      gimp_projectable_invalidate_preview (proj->priv->projectable);
    }
}

static GimpImage *
gimp_projection_get_image (GimpPickable *pickable)
{
  GimpProjection *proj = GIMP_PROJECTION (pickable);

  return gimp_projectable_get_image (proj->priv->projectable);
}

static const Babl *
gimp_projection_get_format (GimpPickable *pickable)
{
  GimpProjection *proj = GIMP_PROJECTION (pickable);

  return gimp_projectable_get_format (proj->priv->projectable);
}

static GeglBuffer *
gimp_projection_get_buffer (GimpPickable *pickable)
{
  GimpProjection *proj = GIMP_PROJECTION (pickable);

  if (! proj->priv->buffer)
    {
      gint width;
      gint height;

      gimp_projectable_get_size (proj->priv->projectable, &width, &height);

      gimp_projection_allocate_buffer (proj);

      /*  This used to call gimp_tile_handler_validate_invalidate()
       *  which forced the entire projection to be constructed in one
       *  go for new images, causing a potentially huge delay. Now we
       *  initially validate stuff the normal way, which makes the
       *  image appear incrementally, but it keeps everything
       *  responsive.
       */
      gimp_projection_add_update_area (proj, 0, 0, width, height);
      proj->priv->invalidate_preview = TRUE;
      gimp_projection_flush (proj);
    }

  return proj->priv->buffer;
}

static gboolean
gimp_projection_get_pixel_at (GimpPickable *pickable,
                              gint          x,
                              gint          y,
                              const Babl   *format,
                              gpointer      pixel)
{
  GeglBuffer *buffer = gimp_projection_get_buffer (pickable);

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

  gegl_buffer_sample (buffer, x, y, NULL, pixel, format,
                      GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);

  return TRUE;
}

static gdouble
gimp_projection_get_opacity_at (GimpPickable *pickable,
                                gint          x,
                                gint          y)
{
  return GIMP_OPACITY_OPAQUE;
}

static void
gimp_projection_get_pixel_average (GimpPickable        *pickable,
                                   const GeglRectangle *rect,
                                   const Babl          *format,
                                   gpointer             pixel)
{
  GeglBuffer *buffer = gimp_projection_get_buffer (pickable);

  return gimp_gegl_average_color (buffer, rect, TRUE, GEGL_ABYSS_NONE, format,
                                  pixel);
}

static void
gimp_projection_pixel_to_srgb (GimpPickable *pickable,
                               const Babl   *format,
                               gpointer      pixel,
                               GimpRGB      *color)
{
  GimpProjection *proj  = GIMP_PROJECTION (pickable);
  GimpImage      *image = gimp_projectable_get_image (proj->priv->projectable);

  gimp_pickable_pixel_to_srgb (GIMP_PICKABLE (image), format, pixel, color);
}

static void
gimp_projection_srgb_to_pixel (GimpPickable  *pickable,
                               const GimpRGB *color,
                               const Babl    *format,
                               gpointer       pixel)
{
  GimpProjection *proj  = GIMP_PROJECTION (pickable);
  GimpImage      *image = gimp_projectable_get_image (proj->priv->projectable);

  gimp_pickable_srgb_to_pixel (GIMP_PICKABLE (image), color, format, pixel);
}


/*  public functions  */

GimpProjection *
gimp_projection_new (GimpProjectable *projectable)
{
  GimpProjection *proj;

  g_return_val_if_fail (GIMP_IS_PROJECTABLE (projectable), NULL);

  proj = g_object_new (GIMP_TYPE_PROJECTION, NULL);

  proj->priv->projectable = projectable;

  g_signal_connect_object (projectable, "invalidate",
                           G_CALLBACK (gimp_projection_projectable_invalidate),
                           proj, 0);
  g_signal_connect_object (projectable, "flush",
                           G_CALLBACK (gimp_projection_projectable_flush),
                           proj, 0);
  g_signal_connect_object (projectable, "structure-changed",
                           G_CALLBACK (gimp_projection_projectable_structure_changed),
                           proj, 0);
  g_signal_connect_object (projectable, "bounds-changed",
                           G_CALLBACK (gimp_projection_projectable_bounds_changed),
                           proj, 0);

  return proj;
}

void
gimp_projection_set_priority (GimpProjection *proj,
                              gint            priority)
{
  g_return_if_fail (GIMP_IS_PROJECTION (proj));

  proj->priv->priority = priority;
}

gint
gimp_projection_get_priority (GimpProjection *proj)
{
  g_return_val_if_fail (GIMP_IS_PROJECTION (proj), 0);

  return proj->priv->priority;
}

void
gimp_projection_set_priority_rect (GimpProjection *proj,
                                   gint            x,
                                   gint            y,
                                   gint            w,
                                   gint            h)
{
  g_return_if_fail (GIMP_IS_PROJECTION (proj));

  proj->priv->priority_rect = *GEGL_RECTANGLE (x, y, w, h);

  gimp_projection_update_priority_rect (proj);
}

void
gimp_projection_stop_rendering (GimpProjection *proj)
{
  g_return_if_fail (GIMP_IS_PROJECTION (proj));

  gimp_projection_chunk_render_stop (proj, TRUE);
}

void
gimp_projection_flush (GimpProjection *proj)
{
  g_return_if_fail (GIMP_IS_PROJECTION (proj));

  /* Construct in chunks */
  gimp_projection_flush_whenever (proj, FALSE, FALSE);
}

void
gimp_projection_flush_now (GimpProjection *proj,
                           gboolean        direct)
{
  g_return_if_fail (GIMP_IS_PROJECTION (proj));

  /* Construct NOW */
  gimp_projection_flush_whenever (proj, TRUE, direct);
}

void
gimp_projection_finish_draw (GimpProjection *proj)
{
  g_return_if_fail (GIMP_IS_PROJECTION (proj));

  if (proj->priv->iter)
    {
      gimp_chunk_iterator_set_priority_rect (proj->priv->iter, NULL);

      gimp_tile_handler_validate_begin_validate (proj->priv->validate_handler);

      while (gimp_projection_chunk_render_iteration (proj));

      gimp_tile_handler_validate_end_validate (proj->priv->validate_handler);

      gimp_projection_chunk_render_stop (proj, FALSE);
    }
}


/*  private functions  */

static void
gimp_projection_allocate_buffer (GimpProjection *proj)
{
  const Babl *format;
  gint        width;
  gint        height;

  if (proj->priv->buffer)
    return;

  format = gimp_projection_get_format (GIMP_PICKABLE (proj));
  gimp_projectable_get_size (proj->priv->projectable, &width, &height);

  proj->priv->buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, width, height),
                                        format);

  proj->priv->validate_handler =
    GIMP_TILE_HANDLER_VALIDATE (
      gimp_tile_handler_projectable_new (proj->priv->projectable));

  gimp_tile_handler_validate_assign (proj->priv->validate_handler,
                                     proj->priv->buffer);

  g_object_notify (G_OBJECT (proj), "buffer");
}

static void
gimp_projection_free_buffer (GimpProjection  *proj)
{
  gimp_projection_chunk_render_stop (proj, FALSE);

  g_clear_pointer (&proj->priv->update_region, cairo_region_destroy);

  if (proj->priv->buffer)
    {
      gimp_tile_handler_validate_unassign (proj->priv->validate_handler,
                                           proj->priv->buffer);

      g_clear_object (&proj->priv->buffer);
      g_clear_object (&proj->priv->validate_handler);
    }
}

static void
gimp_projection_add_update_area (GimpProjection *proj,
                                 gint            x,
                                 gint            y,
                                 gint            w,
                                 gint            h)
{
  cairo_rectangle_int_t rect;
  gint                  width, height;

  gimp_projectable_get_size   (proj->priv->projectable, &width, &height);

  /*  align the rectangle to the UPDATE_CHUNK_WIDTH x UPDATE_CHUNK_HEIGHT grid,
   *  to decrease the complexity of the update area.
   */
  w = ceil  ((gdouble) (x + w) / GIMP_PROJECTION_UPDATE_CHUNK_WIDTH ) * GIMP_PROJECTION_UPDATE_CHUNK_WIDTH;
  h = ceil  ((gdouble) (y + h) / GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT) * GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT;
  x = floor ((gdouble) x       / GIMP_PROJECTION_UPDATE_CHUNK_WIDTH ) * GIMP_PROJECTION_UPDATE_CHUNK_WIDTH;
  y = floor ((gdouble) y       / GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT) * GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT;

  w -= x;
  h -= y;

  if (gimp_rectangle_intersect (x, y, w, h,
                                0, 0, width, height,
                                &rect.x, &rect.y, &rect.width, &rect.height))
    {
      if (proj->priv->update_region)
        cairo_region_union_rectangle (proj->priv->update_region, &rect);
      else
        proj->priv->update_region = cairo_region_create_rectangle (&rect);
    }
}

static void
gimp_projection_flush_whenever (GimpProjection *proj,
                                gboolean        now,
                                gboolean        direct)
{
  if (proj->priv->update_region)
    {
      /* Make sure we have a buffer */
      gimp_projection_allocate_buffer (proj);

      if (now)  /* Synchronous */
        {
          gint n_rects = cairo_region_num_rectangles (proj->priv->update_region);
          gint i;

          for (i = 0; i < n_rects; i++)
            {
              cairo_rectangle_int_t rect;

              cairo_region_get_rectangle (proj->priv->update_region,
                                          i, &rect);

              gimp_projection_paint_area (proj,
                                          direct,
                                          rect.x,
                                          rect.y,
                                          rect.width,
                                          rect.height);
            }

          /*  Free the update region  */
          g_clear_pointer (&proj->priv->update_region, cairo_region_destroy);
        }
      else  /* Asynchronous */
        {
          /*  Consumes the update region  */
          gimp_projection_chunk_render_start (proj);
        }
    }
  else if (! now && proj->priv->invalidate_preview)
    {
      /* invalidate the preview here since it is constructed from
       * the projection
       */
      proj->priv->invalidate_preview = FALSE;

      gimp_projectable_invalidate_preview (proj->priv->projectable);
    }
}

static void
gimp_projection_update_priority_rect (GimpProjection *proj)
{
  if (proj->priv->iter)
    {
      GeglRectangle rect;
      gint          off_x, off_y;
      gint          width, height;

      rect = proj->priv->priority_rect;

      gimp_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);
      gimp_projectable_get_size   (proj->priv->projectable, &width, &height);

      /*  subtract the projectable's offsets because the list of update
       *  areas is in tile-pyramid coordinates, but our external API is
       *  always in terms of image coordinates.
       */
      rect.x -= off_x;
      rect.y -= off_y;

      gegl_rectangle_intersect (&rect,
                                &rect,
                                GEGL_RECTANGLE (0, 0, width, height));

      gimp_chunk_iterator_set_priority_rect (proj->priv->iter, &rect);
    }
}

static void
gimp_projection_chunk_render_start (GimpProjection *proj)
{
  cairo_region_t *region             = proj->priv->update_region;
  gboolean        invalidate_preview = FALSE;

  if (proj->priv->iter)
    {
      region = gimp_chunk_iterator_stop (proj->priv->iter, FALSE);

      proj->priv->iter = NULL;

      if (cairo_region_is_empty (region))
        invalidate_preview = proj->priv->invalidate_preview;

      if (proj->priv->update_region)
        {
          cairo_region_union (region, proj->priv->update_region);

          cairo_region_destroy (proj->priv->update_region);
        }
    }

  proj->priv->update_region = NULL;

  if (region && ! cairo_region_is_empty (region))
    {
      proj->priv->iter = gimp_chunk_iterator_new (region);

      gimp_projection_update_priority_rect (proj);

      if (! proj->priv->idle_id)
        {
          proj->priv->idle_id = g_idle_add_full (
            GIMP_PRIORITY_PROJECTION_IDLE + proj->priv->priority,
            (GSourceFunc) gimp_projection_chunk_render_callback,
            proj, NULL);
        }
    }
  else
    {
      if (region)
        cairo_region_destroy (region);

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

      if (invalidate_preview)
        {
          /* invalidate the preview here since it is constructed from
           * the projection
           */
          proj->priv->invalidate_preview = FALSE;

          gimp_projectable_invalidate_preview (proj->priv->projectable);
        }
    }
}

static void
gimp_projection_chunk_render_stop (GimpProjection *proj,
                                   gboolean        merge)
{
  if (proj->priv->idle_id)
    {
      g_source_remove (proj->priv->idle_id);
      proj->priv->idle_id = 0;
    }

  if (proj->priv->iter)
    {
      if (merge)
        {
          cairo_region_t *region;

          region = gimp_chunk_iterator_stop (proj->priv->iter, FALSE);

          if (proj->priv->update_region)
            {
              cairo_region_union (proj->priv->update_region, region);

              cairo_region_destroy (region);
            }
          else
            {
              proj->priv->update_region = region;
            }
        }
      else
        {
          gimp_chunk_iterator_stop (proj->priv->iter, TRUE);
        }

      proj->priv->iter = NULL;
    }
}

static gboolean
gimp_projection_chunk_render_callback (GimpProjection *proj)
{
  if (gimp_projection_chunk_render_iteration (proj))
    {
      return G_SOURCE_CONTINUE;
    }
  else
    {
      proj->priv->idle_id = 0;

      return G_SOURCE_REMOVE;
    }
}

static gboolean
gimp_projection_chunk_render_iteration (GimpProjection *proj)
{
  if (gimp_chunk_iterator_next (proj->priv->iter))
    {
      GeglRectangle rect;

      gimp_tile_handler_validate_begin_validate (proj->priv->validate_handler);

      while (gimp_chunk_iterator_get_rect (proj->priv->iter, &rect))
        {
          gimp_projection_paint_area (proj, TRUE,
                                      rect.x, rect.y, rect.width, rect.height);
        }

      gimp_tile_handler_validate_end_validate (proj->priv->validate_handler);

      /* Still work to do. */
      return TRUE;
    }
  else
    {
      proj->priv->iter = NULL;

      if (proj->priv->invalidate_preview)
        {
          /* invalidate the preview here since it is constructed from
           * the projection
           */
          proj->priv->invalidate_preview = FALSE;

          gimp_projectable_invalidate_preview (proj->priv->projectable);
        }

      /* FINISHED */
      return FALSE;
    }
}

static void
gimp_projection_paint_area (GimpProjection *proj,
                            gboolean        now,
                            gint            x,
                            gint            y,
                            gint            w,
                            gint            h)
{
  gint off_x, off_y;
  gint width, height;

  gimp_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);
  gimp_projectable_get_size   (proj->priv->projectable, &width, &height);

  if (gimp_rectangle_intersect (x, y, w, h,
                                0, 0, width, height,
                                &x, &y, &w, &h))
    {
      if (now)
        {
          gimp_tile_handler_validate_validate (
            proj->priv->validate_handler,
            proj->priv->buffer,
            GEGL_RECTANGLE (x, y, w, h),
            FALSE);
        }
      else
        {
          gimp_tile_handler_validate_invalidate (
            proj->priv->validate_handler,
            GEGL_RECTANGLE (x, y, w, h));
        }

      /*  add the projectable's offsets because the list of update areas
       *  is in tile-pyramid coordinates, but our external API is always
       *  in terms of image coordinates.
       */
      g_signal_emit (proj, projection_signals[UPDATE], 0,
                     now,
                     x + off_x,
                     y + off_y,
                     w,
                     h);
    }
}


/*  image callbacks  */

static void
gimp_projection_projectable_invalidate (GimpProjectable *projectable,
                                        gint             x,
                                        gint             y,
                                        gint             w,
                                        gint             h,
                                        GimpProjection  *proj)
{
  gint off_x, off_y;

  gimp_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);

  /*  subtract the projectable's offsets because the list of update
   *  areas is in tile-pyramid coordinates, but our external API is
   *  always in terms of image coordinates.
   */
  x -= off_x;
  y -= off_y;

  gimp_projection_add_update_area (proj, x, y, w, h);
}

static void
gimp_projection_projectable_flush (GimpProjectable *projectable,
                                   gboolean         invalidate_preview,
                                   GimpProjection  *proj)
{
  if (invalidate_preview)
    proj->priv->invalidate_preview = TRUE;

  gimp_projection_flush (proj);
}

static void
gimp_projection_projectable_structure_changed (GimpProjectable *projectable,
                                               GimpProjection  *proj)
{
  gint width, height;

  gimp_projection_free_buffer (proj);

  gimp_projectable_get_size (projectable, &width, &height);

  gimp_projection_add_update_area (proj, 0, 0, width, height);
}

static void
gimp_projection_projectable_bounds_changed (GimpProjectable *projectable,
                                            gint             old_x,
                                            gint             old_y,
                                            gint             old_w,
                                            gint             old_h,
                                            GimpProjection  *proj)
{
  GeglBuffer              *old_buffer = proj->priv->buffer;
  GimpTileHandlerValidate *old_validate_handler;
  gint                     x, y, w, h;
  gint                     dx, dy;

  if (! old_buffer)
    {
      gimp_projection_projectable_structure_changed (projectable, proj);

      return;
    }

  gimp_projectable_get_offset (projectable, &x, &y);
  gimp_projectable_get_size   (projectable, &w, &h);

  if (x == old_x && y == old_y && w == old_w && h == old_h)
    return;

  if (! gimp_rectangle_intersect (x,     y,     w,     h,
                                  old_x, old_y, old_w, old_h,
                                  NULL,  NULL,  NULL,  NULL))
    {
      gimp_projection_projectable_structure_changed (projectable, proj);

      return;
    }

  dx = old_x - x;
  dy = old_y - y;

#if 1
  /* FIXME:  when there's an offset between the new bounds and the old bounds,
   * use gimp_projection_projectable_structure_changed(), instead of copying a
   * shifted version of the old buffer, since the synchronous copy can take a
   * notable amount of time for big buffers, when the offset is such that tiles
   * are not COW-ed.  while gimp_projection_projectable_structure_changed()
   * causes the projection to be re-rendered, which is overall slower, it's
   * done asynchronously.
   *
   * this needs to be improved.
   */
  if (dx || dy)
    {
      gimp_projection_projectable_structure_changed (projectable, proj);

      return;
    }
#endif

  /* reallocate the buffer, and copy the old buffer to the corresponding
   * region of the new buffer.
   */

  gimp_projection_chunk_render_stop (proj, TRUE);

  old_validate_handler = proj->priv->validate_handler;

  proj->priv->buffer           = NULL;
  proj->priv->validate_handler = NULL;

  gimp_projection_allocate_buffer (proj);

  gimp_tile_handler_validate_buffer_copy (old_buffer,
                                          GEGL_RECTANGLE (0, 0, old_w, old_h),
                                          proj->priv->buffer,
                                          GEGL_RECTANGLE (dx, dy, old_w, old_h));

  if (old_validate_handler)
    {
      gimp_tile_handler_validate_unassign (old_validate_handler, old_buffer);

      g_object_unref (old_validate_handler);
    }

  g_object_unref (old_buffer);

  if (proj->priv->update_region)
    {
      const cairo_rectangle_int_t bounds = {0, 0, w, h};

      cairo_region_translate           (proj->priv->update_region, dx, dy);
      cairo_region_intersect_rectangle (proj->priv->update_region, &bounds);
    }

  if (dx > 0)
    gimp_projection_add_update_area (proj, 0, 0, dx, h);
  if (dy > 0)
    gimp_projection_add_update_area (proj, 0, 0, w, dy);
  if (dx + old_w < w)
    gimp_projection_add_update_area (proj, dx + old_w, 0, w - (dx + old_w), h);
  if (dy + old_h < h)
    gimp_projection_add_update_area (proj, 0, dy + old_h, w, h - (dy + old_h));

  proj->priv->invalidate_preview = TRUE;
}
