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

#include "config.h"

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

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

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

#include "core-types.h"

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

#include "pika.h"
#include "pika-memsize.h"
#include "pikachunkiterator.h"
#include "pikaimage.h"
#include "pikamarshal.h"
#include "pikapickable.h"
#include "pikaprojectable.h"
#include "pikaprojection.h"
#include "pikatilehandlerprojectable.h"

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


/*  chunk size for area updates  */
#define PIKA_PROJECTION_UPDATE_CHUNK_WIDTH  32
#define PIKA_PROJECTION_UPDATE_CHUNK_HEIGHT 32


enum
{
  UPDATE,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_BUFFER
};


struct _PikaProjectionPrivate
{
  PikaProjectable           *projectable;

  GeglBuffer                *buffer;
  PikaTileHandlerValidate   *validate_handler;

  gint                       priority;

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

  gboolean                   invalidate_preview;
};


/*  local function prototypes  */

static void   pika_projection_pickable_iface_init (PikaPickableInterface  *iface);

static void        pika_projection_finalize              (GObject         *object);
static void        pika_projection_set_property          (GObject         *object,
                                                          guint            property_id,
                                                          const GValue    *value,
                                                          GParamSpec      *pspec);
static void        pika_projection_get_property          (GObject         *object,
                                                          guint            property_id,
                                                          GValue          *value,
                                                          GParamSpec      *pspec);

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

static void        pika_projection_pickable_flush        (PikaPickable    *pickable);
static PikaImage * pika_projection_get_image             (PikaPickable    *pickable);
static const Babl * pika_projection_get_format           (PikaPickable    *pickable);
static GeglBuffer * pika_projection_get_buffer           (PikaPickable    *pickable);
static gboolean    pika_projection_get_pixel_at          (PikaPickable    *pickable,
                                                          gint             x,
                                                          gint             y,
                                                          const Babl      *format,
                                                          gpointer         pixel);
static gdouble     pika_projection_get_opacity_at        (PikaPickable    *pickable,
                                                          gint             x,
                                                          gint             y);
static void        pika_projection_get_pixel_average     (PikaPickable    *pickable,
                                                          const GeglRectangle *rect,
                                                          const Babl      *format,
                                                          gpointer         pixel);
static void        pika_projection_pixel_to_rgb          (PikaPickable    *pickable,
                                                          const Babl      *format,
                                                          gpointer         pixel,
                                                          PikaRGB         *color);
static void        pika_projection_rgb_to_pixel          (PikaPickable    *pickable,
                                                          const PikaRGB   *color,
                                                          const Babl      *format,
                                                          gpointer         pixel);

static void        pika_projection_allocate_buffer       (PikaProjection  *proj);
static void        pika_projection_free_buffer           (PikaProjection  *proj);
static void        pika_projection_add_update_area       (PikaProjection  *proj,
                                                          gint             x,
                                                          gint             y,
                                                          gint             w,
                                                          gint             h);
static void        pika_projection_flush_whenever        (PikaProjection  *proj,
                                                          gboolean         now,
                                                          gboolean         direct);
static void        pika_projection_update_priority_rect  (PikaProjection  *proj);
static void        pika_projection_chunk_render_start    (PikaProjection  *proj);
static void        pika_projection_chunk_render_stop     (PikaProjection  *proj,
                                                          gboolean         merge);
static gboolean    pika_projection_chunk_render_callback (PikaProjection  *proj);
static gboolean    pika_projection_chunk_render_iteration(PikaProjection  *proj);
static void        pika_projection_paint_area            (PikaProjection  *proj,
                                                          gboolean         now,
                                                          gint             x,
                                                          gint             y,
                                                          gint             w,
                                                          gint             h);

static void        pika_projection_projectable_invalidate(PikaProjectable *projectable,
                                                          gint             x,
                                                          gint             y,
                                                          gint             w,
                                                          gint             h,
                                                          PikaProjection  *proj);
static void        pika_projection_projectable_flush     (PikaProjectable *projectable,
                                                          gboolean         invalidate_preview,
                                                          PikaProjection  *proj);
static void
           pika_projection_projectable_structure_changed (PikaProjectable *projectable,
                                                          PikaProjection  *proj);
static void   pika_projection_projectable_bounds_changed (PikaProjectable *projectable,
                                                          gint             old_x,
                                                          gint             old_y,
                                                          PikaProjection  *proj);


G_DEFINE_TYPE_WITH_CODE (PikaProjection, pika_projection, PIKA_TYPE_OBJECT,
                         G_ADD_PRIVATE (PikaProjection)
                         G_IMPLEMENT_INTERFACE (PIKA_TYPE_PICKABLE,
                                                pika_projection_pickable_iface_init))

#define parent_class pika_projection_parent_class

static guint projection_signals[LAST_SIGNAL] = { 0 };


static void
pika_projection_class_init (PikaProjectionClass *klass)
{
  GObjectClass    *object_class      = G_OBJECT_CLASS (klass);
  PikaObjectClass *pika_object_class = PIKA_OBJECT_CLASS (klass);

  projection_signals[UPDATE] =
    g_signal_new ("update",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaProjectionClass, update),
                  NULL, NULL,
                  pika_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         = pika_projection_finalize;
  object_class->set_property     = pika_projection_set_property;
  object_class->get_property     = pika_projection_get_property;

  pika_object_class->get_memsize = pika_projection_get_memsize;

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

static void
pika_projection_init (PikaProjection *proj)
{
  proj->priv = pika_projection_get_instance_private (proj);
}

static void
pika_projection_pickable_iface_init (PikaPickableInterface *iface)
{
  iface->flush                 = pika_projection_pickable_flush;
  iface->get_image             = pika_projection_get_image;
  iface->get_format            = pika_projection_get_format;
  iface->get_format_with_alpha = pika_projection_get_format; /* sic */
  iface->get_buffer            = pika_projection_get_buffer;
  iface->get_pixel_at          = pika_projection_get_pixel_at;
  iface->get_opacity_at        = pika_projection_get_opacity_at;
  iface->get_pixel_average     = pika_projection_get_pixel_average;
  iface->pixel_to_rgb          = pika_projection_pixel_to_rgb;
  iface->rgb_to_pixel          = pika_projection_rgb_to_pixel;
}

static void
pika_projection_finalize (GObject *object)
{
  PikaProjection *proj = PIKA_PROJECTION (object);

  pika_projection_free_buffer (proj);

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

static void
pika_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
pika_projection_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  PikaProjection *projection = PIKA_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
pika_projection_get_memsize (PikaObject *object,
                             gint64     *gui_size)
{
  PikaProjection *projection = PIKA_PROJECTION (object);
  gint64          memsize    = 0;

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

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

/**
 * pika_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.
 *
 * Returns: a rough estimate of the memory requirements.
 **/
gint64
pika_projection_estimate_memsize (PikaImageBaseType type,
                                  PikaComponentType component_type,
                                  gint              width,
                                  gint              height)
{
  const Babl *format;
  gint64      bytes;

  if (type == PIKA_INDEXED)
    type = PIKA_RGB;

  format = pika_babl_format (type,
                             pika_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
pika_projection_pickable_flush (PikaPickable *pickable)
{
  PikaProjection *proj = PIKA_PROJECTION (pickable);

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

  pika_projection_finish_draw (proj);
  pika_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;

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

static PikaImage *
pika_projection_get_image (PikaPickable *pickable)
{
  PikaProjection *proj = PIKA_PROJECTION (pickable);

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

static const Babl *
pika_projection_get_format (PikaPickable *pickable)
{
  PikaProjection *proj = PIKA_PROJECTION (pickable);

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

static GeglBuffer *
pika_projection_get_buffer (PikaPickable *pickable)
{
  PikaProjection *proj = PIKA_PROJECTION (pickable);

  if (! proj->priv->buffer)
    {
      GeglRectangle bounding_box;

      bounding_box =
        pika_projectable_get_bounding_box (proj->priv->projectable);

      pika_projection_allocate_buffer (proj);

      /*  This used to call pika_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.
       */
      pika_projection_add_update_area (proj,
                                       bounding_box.x,     bounding_box.y,
                                       bounding_box.width, bounding_box.height);
      proj->priv->invalidate_preview = TRUE;
      pika_projection_flush (proj);
    }

  return proj->priv->buffer;
}

static gboolean
pika_projection_get_pixel_at (PikaPickable *pickable,
                              gint          x,
                              gint          y,
                              const Babl   *format,
                              gpointer      pixel)
{
  PikaProjection *proj   = PIKA_PROJECTION (pickable);
  GeglBuffer     *buffer = pika_projection_get_buffer (pickable);
  GeglRectangle   bounding_box;

  bounding_box = pika_projectable_get_bounding_box (proj->priv->projectable);

  if (x <  bounding_box.x                      ||
      y <  bounding_box.y                      ||
      x >= bounding_box.x + bounding_box.width ||
      y >= bounding_box.y + bounding_box.height)
    {
      return FALSE;
    }

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

  return TRUE;
}

static gdouble
pika_projection_get_opacity_at (PikaPickable *pickable,
                                gint          x,
                                gint          y)
{
  return PIKA_OPACITY_OPAQUE;
}

static void
pika_projection_get_pixel_average (PikaPickable        *pickable,
                                   const GeglRectangle *rect,
                                   const Babl          *format,
                                   gpointer             pixel)
{
  GeglBuffer *buffer = pika_projection_get_buffer (pickable);

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

static void
pika_projection_pixel_to_rgb (PikaPickable *pickable,
                              const Babl   *format,
                              gpointer      pixel,
                              PikaRGB      *color)
{
  PikaProjection *proj  = PIKA_PROJECTION (pickable);
  PikaImage      *image = pika_projectable_get_image (proj->priv->projectable);

  pika_pickable_pixel_to_rgb (PIKA_PICKABLE (image), format, pixel, color);
}

static void
pika_projection_rgb_to_pixel (PikaPickable  *pickable,
                              const PikaRGB *color,
                              const Babl    *format,
                              gpointer       pixel)
{
  PikaProjection *proj  = PIKA_PROJECTION (pickable);
  PikaImage      *image = pika_projectable_get_image (proj->priv->projectable);

  pika_pickable_rgb_to_pixel (PIKA_PICKABLE (image), color, format, pixel);
}


/*  public functions  */

PikaProjection *
pika_projection_new (PikaProjectable *projectable)
{
  PikaProjection *proj;

  g_return_val_if_fail (PIKA_IS_PROJECTABLE (projectable), NULL);

  proj = g_object_new (PIKA_TYPE_PROJECTION, NULL);

  proj->priv->projectable = projectable;

  g_signal_connect_object (projectable, "invalidate",
                           G_CALLBACK (pika_projection_projectable_invalidate),
                           proj, 0);
  g_signal_connect_object (projectable, "flush",
                           G_CALLBACK (pika_projection_projectable_flush),
                           proj, 0);
  g_signal_connect_object (projectable, "structure-changed",
                           G_CALLBACK (pika_projection_projectable_structure_changed),
                           proj, 0);
  g_signal_connect_object (projectable, "bounds-changed",
                           G_CALLBACK (pika_projection_projectable_bounds_changed),
                           proj, 0);

  return proj;
}

void
pika_projection_set_priority (PikaProjection *proj,
                              gint            priority)
{
  g_return_if_fail (PIKA_IS_PROJECTION (proj));

  proj->priv->priority = priority;
}

gint
pika_projection_get_priority (PikaProjection *proj)
{
  g_return_val_if_fail (PIKA_IS_PROJECTION (proj), 0);

  return proj->priv->priority;
}

void
pika_projection_set_priority_rect (PikaProjection *proj,
                                   gint            x,
                                   gint            y,
                                   gint            w,
                                   gint            h)
{
  g_return_if_fail (PIKA_IS_PROJECTION (proj));

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

  pika_projection_update_priority_rect (proj);
}

void
pika_projection_stop_rendering (PikaProjection *proj)
{
  g_return_if_fail (PIKA_IS_PROJECTION (proj));

  pika_projection_chunk_render_stop (proj, TRUE);
}

void
pika_projection_flush (PikaProjection *proj)
{
  g_return_if_fail (PIKA_IS_PROJECTION (proj));

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

void
pika_projection_flush_now (PikaProjection *proj,
                           gboolean        direct)
{
  g_return_if_fail (PIKA_IS_PROJECTION (proj));

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

void
pika_projection_finish_draw (PikaProjection *proj)
{
  g_return_if_fail (PIKA_IS_PROJECTION (proj));

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

      pika_tile_handler_validate_begin_validate (proj->priv->validate_handler);

      while (pika_projection_chunk_render_iteration (proj));

      pika_tile_handler_validate_end_validate (proj->priv->validate_handler);

      pika_projection_chunk_render_stop (proj, FALSE);
    }
}


/*  private functions  */

static void
pika_projection_allocate_buffer (PikaProjection *proj)
{
  const Babl    *format;
  GeglRectangle  bounding_box;

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

  format       = pika_projection_get_format (PIKA_PICKABLE (proj));
  bounding_box =
    pika_projectable_get_bounding_box (proj->priv->projectable);

  proj->priv->buffer = gegl_buffer_new (&bounding_box, format);

  proj->priv->validate_handler =
    PIKA_TILE_HANDLER_VALIDATE (
      pika_tile_handler_projectable_new (proj->priv->projectable));

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

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

static void
pika_projection_free_buffer (PikaProjection  *proj)
{
  pika_projection_chunk_render_stop (proj, FALSE);

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

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

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

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

static void
pika_projection_add_update_area (PikaProjection *proj,
                                 gint            x,
                                 gint            y,
                                 gint            w,
                                 gint            h)
{
  cairo_rectangle_int_t rect;
  GeglRectangle         bounding_box;

  bounding_box = pika_projectable_get_bounding_box (proj->priv->projectable);

  /*  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) / PIKA_PROJECTION_UPDATE_CHUNK_WIDTH ) * PIKA_PROJECTION_UPDATE_CHUNK_WIDTH;
  h = ceil  ((gdouble) (y + h) / PIKA_PROJECTION_UPDATE_CHUNK_HEIGHT) * PIKA_PROJECTION_UPDATE_CHUNK_HEIGHT;
  x = floor ((gdouble) x       / PIKA_PROJECTION_UPDATE_CHUNK_WIDTH ) * PIKA_PROJECTION_UPDATE_CHUNK_WIDTH;
  y = floor ((gdouble) y       / PIKA_PROJECTION_UPDATE_CHUNK_HEIGHT) * PIKA_PROJECTION_UPDATE_CHUNK_HEIGHT;

  w -= x;
  h -= y;

  if (gegl_rectangle_intersect ((GeglRectangle *) &rect,
                                GEGL_RECTANGLE (x, y, w, h), &bounding_box))
    {
      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
pika_projection_flush_whenever (PikaProjection *proj,
                                gboolean        now,
                                gboolean        direct)
{
  if (proj->priv->update_region)
    {
      /* Make sure we have a buffer */
      pika_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);

              pika_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  */
          pika_projection_chunk_render_start (proj);
        }
    }
  else if (! now && ! proj->priv->iter && proj->priv->invalidate_preview)
    {
      /* invalidate the preview here since it is constructed from
       * the projection
       */
      proj->priv->invalidate_preview = FALSE;

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

static void
pika_projection_update_priority_rect (PikaProjection *proj)
{
  if (proj->priv->iter)
    {
      GeglRectangle rect;
      GeglRectangle bounding_box;
      gint          off_x, off_y;

      rect = proj->priv->priority_rect;

      pika_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);
      bounding_box = pika_projectable_get_bounding_box (proj->priv->projectable);

      /*  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, &bounding_box);

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

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

  if (proj->priv->iter)
    {
      region = pika_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 = pika_chunk_iterator_new (region);

      pika_projection_update_priority_rect (proj);

      if (! proj->priv->idle_id)
        {
          proj->priv->idle_id = g_idle_add_full (
            PIKA_PRIORITY_PROJECTION_IDLE + proj->priv->priority,
            (GSourceFunc) pika_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;

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

static void
pika_projection_chunk_render_stop (PikaProjection *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 = pika_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
        {
          pika_chunk_iterator_stop (proj->priv->iter, TRUE);
        }

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

static gboolean
pika_projection_chunk_render_callback (PikaProjection *proj)
{
  if (pika_projection_chunk_render_iteration (proj))
    {
      return G_SOURCE_CONTINUE;
    }
  else
    {
      proj->priv->idle_id = 0;

      return G_SOURCE_REMOVE;
    }
}

static gboolean
pika_projection_chunk_render_iteration (PikaProjection *proj)
{
  if (pika_chunk_iterator_next (proj->priv->iter))
    {
      GeglRectangle rect;

      pika_tile_handler_validate_begin_validate (proj->priv->validate_handler);

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

      pika_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;

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

      /* FINISHED */
      return FALSE;
    }
}

static void
pika_projection_paint_area (PikaProjection *proj,
                            gboolean        now,
                            gint            x,
                            gint            y,
                            gint            w,
                            gint            h)
{
  gint          off_x, off_y;
  GeglRectangle bounding_box;
  GeglRectangle rect;

  pika_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);
  bounding_box = pika_projectable_get_bounding_box (proj->priv->projectable);

  if (gegl_rectangle_intersect (&rect,
                                GEGL_RECTANGLE (x, y, w, h), &bounding_box))
    {
      if (now)
        {
          pika_tile_handler_validate_validate (
            proj->priv->validate_handler,
            proj->priv->buffer,
            &rect,
            FALSE, FALSE);
        }
      else
        {
          pika_tile_handler_validate_invalidate (
            proj->priv->validate_handler,
            &rect);
        }

      /*  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,
                     rect.x + off_x,
                     rect.y + off_y,
                     rect.width,
                     rect.height);
    }
}


/*  image callbacks  */

static void
pika_projection_projectable_invalidate (PikaProjectable *projectable,
                                        gint             x,
                                        gint             y,
                                        gint             w,
                                        gint             h,
                                        PikaProjection  *proj)
{
  gint off_x, off_y;

  pika_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;

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

static void
pika_projection_projectable_flush (PikaProjectable *projectable,
                                   gboolean         invalidate_preview,
                                   PikaProjection  *proj)
{
  if (invalidate_preview)
    proj->priv->invalidate_preview = TRUE;

  pika_projection_flush (proj);
}

static void
pika_projection_projectable_structure_changed (PikaProjectable *projectable,
                                               PikaProjection  *proj)
{
  GeglRectangle bounding_box;

  pika_projection_free_buffer (proj);

  bounding_box = pika_projectable_get_bounding_box (projectable);

  pika_projection_add_update_area (proj,
                                   bounding_box.x,     bounding_box.y,
                                   bounding_box.width, bounding_box.height);
}

static void
pika_projection_projectable_bounds_changed (PikaProjectable *projectable,
                                            gint             old_x,
                                            gint             old_y,
                                            PikaProjection  *proj)
{
  GeglBuffer              *old_buffer = proj->priv->buffer;
  PikaTileHandlerValidate *old_validate_handler;
  GeglRectangle            old_bounding_box;
  GeglRectangle            bounding_box;
  GeglRectangle            old_bounds;
  GeglRectangle            bounds;
  GeglRectangle            int_bounds;
  gint                     x, y;
  gint                     dx, dy;

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

      return;
    }

  old_bounding_box = *gegl_buffer_get_extent (old_buffer);

  pika_projectable_get_offset (projectable, &x, &y);
  bounding_box = pika_projectable_get_bounding_box (projectable);

  if (x == old_x && y == old_y &&
      gegl_rectangle_equal (&bounding_box, &old_bounding_box))
    {
      return;
    }

  old_bounds    = old_bounding_box;
  old_bounds.x += old_x;
  old_bounds.y += old_y;

  bounds        = bounding_box;
  bounds.x     += x;
  bounds.y     += y;

  if (! gegl_rectangle_intersect (&int_bounds, &bounds, &old_bounds))
    {
      pika_projection_projectable_structure_changed (projectable, proj);

      return;
    }

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

#if 1
  /* FIXME:  when there's an offset between the new bounds and the old bounds,
   * use pika_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 pika_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)
    {
      pika_projection_projectable_structure_changed (projectable, proj);

      return;
    }
#endif

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

  pika_projection_chunk_render_stop (proj, TRUE);

  if (dx == 0 && dy == 0)
    {
      pika_tile_handler_validate_buffer_set_extent (old_buffer, &bounding_box);
    }
  else
    {
      old_validate_handler = proj->priv->validate_handler;

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

      pika_projection_allocate_buffer (proj);

      pika_tile_handler_validate_buffer_copy (
        old_buffer,
        GEGL_RECTANGLE (int_bounds.x - old_x,
                        int_bounds.y - old_y,
                        int_bounds.width,
                        int_bounds.height),
        proj->priv->buffer,
        GEGL_RECTANGLE (int_bounds.x - x,
                        int_bounds.y - y,
                        int_bounds.width,
                        int_bounds.height));

      pika_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)
    {
      cairo_region_translate (proj->priv->update_region, dx, dy);
      cairo_region_intersect_rectangle (
        proj->priv->update_region,
        (const cairo_rectangle_int_t *) &bounding_box);
    }

  int_bounds.x -= x;
  int_bounds.y -= y;

  if (int_bounds.x > bounding_box.x)
    {
      pika_projection_add_update_area (proj,
                                       bounding_box.x,
                                       bounding_box.y,
                                       int_bounds.x - bounding_box.x,
                                       bounding_box.height);
    }
  if (int_bounds.y > bounding_box.y)
    {
      pika_projection_add_update_area (proj,
                                       bounding_box.x,
                                       bounding_box.y,
                                       bounding_box.width,
                                       int_bounds.y - bounding_box.y);
    }
  if (int_bounds.x + int_bounds.width < bounding_box.x + bounding_box.width)
    {
      pika_projection_add_update_area (proj,
                                       int_bounds.x + int_bounds.width,
                                       bounding_box.y,
                                       bounding_box.x + bounding_box.width -
                                       (int_bounds.x  + int_bounds.width),
                                       bounding_box.height);
    }
  if (int_bounds.y + int_bounds.height < bounding_box.y + bounding_box.height)
    {
      pika_projection_add_update_area (proj,
                                       bounding_box.x,
                                       int_bounds.y + int_bounds.height,
                                       bounding_box.width,
                                       bounding_box.y + bounding_box.height -
                                       (int_bounds.y  + int_bounds.height));
    }

  proj->priv->invalidate_preview = TRUE;
}
