/* The GIMP -- an 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "config.h"

#include <glib-object.h>

#include "core-types.h"

#include "base/pixel-region.h"
#include "base/tile-manager.h"
#include "base/tile.h"

#include "paint-funcs/paint-funcs.h"

#include "gimpdrawable.h"
#include "gimpimage.h"
#include "gimpimagemap.h"
#include "gimpmarshal.h"
#include "gimppickable.h"


enum
{
  FLUSH,
  LAST_SIGNAL
};


struct _GimpImageMap
{
  GimpObject             parent_instance;

  GimpDrawable          *drawable;
  gchar                 *undo_desc;

  TileManager           *undo_tiles;
  gint                   undo_offset_x;
  gint                   undo_offset_y;
  GimpImageMapApplyFunc  apply_func;
  gpointer               user_data;
  PixelRegion            srcPR;
  PixelRegion            destPR;
  PixelRegionIterator   *PRI;
  guint                  idle_id;
};


static void   gimp_image_map_class_init          (GimpImageMapClass *klass);
static void   gimp_image_map_init                (GimpImageMap      *image_map);
static void   gimp_image_map_pickable_iface_init (GimpPickableInterface *pickable_iface);

static void            gimp_image_map_finalize       (GObject      *object);

static GimpImage     * gimp_image_map_get_image      (GimpPickable *pickable);
static GimpImageType   gimp_image_map_get_image_type (GimpPickable *pickable);
static TileManager   * gimp_image_map_get_tiles      (GimpPickable *pickable);
static guchar        * gimp_image_map_get_color_at   (GimpPickable *pickable,
                                                      gint          x,
                                                      gint          y);

static gboolean        gimp_image_map_do             (GimpImageMap *image_map);


static guint  image_map_signals[LAST_SIGNAL] = { 0 };

static GimpObjectClass *parent_class = NULL;


GType
gimp_image_map_get_type (void)
{
  static GType image_map_type = 0;

  if (! image_map_type)
    {
      static const GTypeInfo image_map_info =
      {
        sizeof (GimpImageMapClass),
        (GBaseInitFunc) NULL,
        (GBaseFinalizeFunc) NULL,
        (GClassInitFunc) gimp_image_map_class_init,
        NULL,           /* class_finalize */
        NULL,           /* class_data     */
        sizeof (GimpImageMap),
        0,              /* n_preallocs    */
        (GInstanceInitFunc) gimp_image_map_init,
      };

      static const GInterfaceInfo pickable_iface_info =
      {
        (GInterfaceInitFunc) gimp_image_map_pickable_iface_init,
        NULL,           /* iface_finalize */
        NULL            /* iface_data     */
      };

      image_map_type = g_type_register_static (GIMP_TYPE_OBJECT,
                                               "GimpImageMap",
                                               &image_map_info, 0);

      g_type_add_interface_static (image_map_type, GIMP_TYPE_PICKABLE,
                                   &pickable_iface_info);
    }

  return image_map_type;
}

static void
gimp_image_map_class_init (GimpImageMapClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  parent_class = g_type_class_peek_parent (klass);

  image_map_signals[FLUSH] =
    g_signal_new ("flush",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpImageMapClass, flush),
                  NULL, NULL,
                  gimp_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

  object_class->finalize = gimp_image_map_finalize;
}

static void
gimp_image_map_init (GimpImageMap *image_map)
{
  image_map->drawable      = NULL;
  image_map->undo_tiles    = NULL;
  image_map->undo_offset_x = 0;
  image_map->undo_offset_y = 0;
  image_map->apply_func    = NULL;
  image_map->user_data     = NULL;
  image_map->PRI           = NULL;
  image_map->idle_id       = 0;
}

static void
gimp_image_map_pickable_iface_init (GimpPickableInterface *pickable_iface)
{
  pickable_iface->get_image      = gimp_image_map_get_image;
  pickable_iface->get_image_type = gimp_image_map_get_image_type;
  pickable_iface->get_tiles      = gimp_image_map_get_tiles;
  pickable_iface->get_color_at   = gimp_image_map_get_color_at;
}

static void
gimp_image_map_finalize (GObject *object)
{
  GimpImageMap *image_map = GIMP_IMAGE_MAP (object);

  if (image_map->undo_desc)
    {
      g_free (image_map->undo_desc);
      image_map->undo_desc = NULL;
    }

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

static GimpImage *
gimp_image_map_get_image (GimpPickable *pickable)
{
  GimpImageMap *image_map = GIMP_IMAGE_MAP (pickable);

  return gimp_pickable_get_image (GIMP_PICKABLE (image_map->drawable));
}

static GimpImageType
gimp_image_map_get_image_type (GimpPickable *pickable)
{
  GimpImageMap *image_map = GIMP_IMAGE_MAP (pickable);

  return gimp_pickable_get_image_type (GIMP_PICKABLE (image_map->drawable));
}

static TileManager *
gimp_image_map_get_tiles (GimpPickable *pickable)
{
  GimpImageMap *image_map = GIMP_IMAGE_MAP (pickable);

  if (image_map->undo_tiles)
    return image_map->undo_tiles;
  else
    return gimp_pickable_get_tiles (GIMP_PICKABLE (image_map->drawable));
}

static guchar *
gimp_image_map_get_color_at (GimpPickable *pickable,
                             gint          x,
                             gint          y)
{
  GimpImageMap *image_map = GIMP_IMAGE_MAP (pickable);
  guchar        src[5];
  guchar       *dest;

  if (x >= 0 && x < gimp_item_width  (GIMP_ITEM (image_map->drawable)) &&
      y >= 0 && y < gimp_item_height (GIMP_ITEM (image_map->drawable)))
    {
      /* Check if done damage to original image */
      if (! image_map->undo_tiles)
        return gimp_pickable_get_color_at (GIMP_PICKABLE (image_map->drawable),
                                           x, y);

      if ((! gimp_item_get_image (GIMP_ITEM (image_map->drawable)) &&
           gimp_drawable_is_indexed (image_map->drawable)) ||
          x < 0 || y < 0                                   ||
          x >= tile_manager_width (image_map->undo_tiles)  ||
          y >= tile_manager_height (image_map->undo_tiles))
        {
          return NULL;
        }

      dest = g_new (guchar, 5);

      read_pixel_data_1 (image_map->undo_tiles, x, y, src);

      gimp_image_get_color (gimp_item_get_image  (GIMP_ITEM (image_map->drawable)),
                            gimp_drawable_type (image_map->drawable),
                            src, dest);

      if (gimp_drawable_is_indexed (image_map->drawable))
        dest[4] = src[0];
      else
        dest[4] = 0;

      return dest;
    }
  else /* out of bounds error */
    {
      return NULL;
    }
}

GimpImageMap *
gimp_image_map_new (GimpDrawable *drawable,
                    const gchar  *undo_desc)
{
  GimpImageMap *image_map;
  GimpImage    *gimage;

  g_return_val_if_fail (GIMP_IS_DRAWABLE (drawable), NULL);

  gimage = gimp_item_get_image (GIMP_ITEM (drawable));

  g_return_val_if_fail (GIMP_IS_IMAGE (gimage), NULL);

  image_map = g_object_new (GIMP_TYPE_IMAGE_MAP, NULL);

  image_map->drawable  = drawable;
  image_map->undo_desc = g_strdup (undo_desc);

  return image_map;
}

void
gimp_image_map_apply (GimpImageMap          *image_map,
                      GimpImageMapApplyFunc  apply_func,
                      gpointer               apply_data)
{
  gint x, y;
  gint width, height;
  gint undo_offset_x, undo_offset_y;
  gint undo_width, undo_height;

  g_return_if_fail (GIMP_IS_IMAGE_MAP (image_map));
  g_return_if_fail (apply_func != NULL);

  image_map->apply_func = apply_func;
  image_map->user_data  = apply_data;

  /*  If we're still working, remove the timer  */
  if (image_map->idle_id)
    {
      g_source_remove (image_map->idle_id);
      image_map->idle_id = 0;

      pixel_regions_process_stop (image_map->PRI);
      image_map->PRI = NULL;
    }

  /*  Make sure the drawable is still valid  */
  if (! gimp_item_get_image (GIMP_ITEM (image_map->drawable)))
    return;

  /*  The application should occur only within selection bounds  */
  if (! gimp_drawable_mask_intersect (image_map->drawable,
                                      &x, &y, &width, &height))
    return;

  /*  If undo tiles don't exist, or change size, (re)allocate  */
  if (image_map->undo_tiles)
    {
      undo_offset_x = image_map->undo_offset_x;
      undo_offset_y = image_map->undo_offset_y;
      undo_width    = tile_manager_width (image_map->undo_tiles);
      undo_height   = tile_manager_height (image_map->undo_tiles);
    }
  else
    {
      undo_offset_x = 0;
      undo_offset_y = 0;
      undo_width    = 0;
      undo_height   = 0;
    }

  if (! image_map->undo_tiles ||
      undo_offset_x != x      ||
      undo_offset_y != y      ||
      undo_width    != width  ||
      undo_height   != height)
    {
      /* If either the extents changed or the tiles don't exist,
       * allocate new
       */
      if (! image_map->undo_tiles ||
          undo_width  != width    ||
          undo_height != height)
        {
          /*  Destroy old tiles  */
          if (image_map->undo_tiles)
            tile_manager_unref (image_map->undo_tiles);

          /*  Allocate new tiles  */
          image_map->undo_tiles =
            tile_manager_new (width, height,
                              gimp_drawable_bytes (image_map->drawable));
        }

      /*  Copy from the image to the new tiles  */
      pixel_region_init (&image_map->srcPR,
                         gimp_drawable_data (image_map->drawable),
                         x, y, width, height, FALSE);
      pixel_region_init (&image_map->destPR, image_map->undo_tiles,
                         0, 0, width, height, TRUE);

      copy_region (&image_map->srcPR, &image_map->destPR);

      /*  Set the offsets  */
      image_map->undo_offset_x = x;
      image_map->undo_offset_y = y;
    }
  else /* image_map->undo_tiles exist AND drawable dimensions have not changed... */
    {
      /* Reset to initial drawable conditions.            */
      /* Copy from the backup undo tiles to the drawable  */
      pixel_region_init (&image_map->srcPR, image_map->undo_tiles,
                         0, 0, undo_width, undo_height, FALSE);
      pixel_region_init (&image_map->destPR,
                         gimp_drawable_data (image_map->drawable),
                         undo_offset_x, undo_offset_y,
                         undo_width, undo_height, TRUE);

      copy_region (&image_map->srcPR, &image_map->destPR);
    }

  /*  Configure the src from the drawable data  */
  pixel_region_init (&image_map->srcPR, image_map->undo_tiles,
                     0, 0, width, height, FALSE);

  /*  Configure the dest as the shadow buffer  */
  pixel_region_init (&image_map->destPR,
                     gimp_drawable_shadow (image_map->drawable),
                     x, y, width, height, TRUE);

  /*  Apply the image transformation to the pixels  */
  image_map->PRI = pixel_regions_register (2,
                                           &image_map->srcPR,
                                           &image_map->destPR);

  /*  Start the intermittant work procedure  */
  image_map->idle_id = g_idle_add ((GSourceFunc) gimp_image_map_do,
                                   image_map);
}

void
gimp_image_map_commit (GimpImageMap *image_map)
{
  GimpImage *gimage;
  gint       x1, y1, x2, y2;

  g_return_if_fail (GIMP_IS_IMAGE_MAP (image_map));

  if (image_map->idle_id)
    {
      g_source_remove (image_map->idle_id);
      image_map->idle_id = 0;

      /*  Finish the changes  */
      while (gimp_image_map_do (image_map));
    }

  /*  Make sure the drawable is still valid  */
  gimage = gimp_item_get_image (GIMP_ITEM (image_map->drawable));

  if (! gimage)
    return;

  /*  Register an undo step  */
  if (image_map->undo_tiles)
    {
      x1 = image_map->undo_offset_x;
      y1 = image_map->undo_offset_y;
      x2 = x1 + tile_manager_width (image_map->undo_tiles);
      y2 = y1 + tile_manager_height (image_map->undo_tiles);

      gimp_drawable_push_undo (image_map->drawable,
                               image_map->undo_desc,
                               x1, y1, x2, y2,
                               image_map->undo_tiles, FALSE);

      tile_manager_unref (image_map->undo_tiles);
      image_map->undo_tiles = NULL;
    }

  g_object_unref (image_map);
}

void
gimp_image_map_clear (GimpImageMap *image_map)
{
  GimpImage   *gimage;
  PixelRegion  srcPR, destPR;

  g_return_if_fail (GIMP_IS_IMAGE_MAP (image_map));

  if (image_map->idle_id)
    {
      g_source_remove (image_map->idle_id);
      image_map->idle_id = 0;

      pixel_regions_process_stop (image_map->PRI);
      image_map->PRI = NULL;
    }

  /*  Make sure the drawable is still valid  */
  gimage =  gimp_item_get_image (GIMP_ITEM (image_map->drawable));

  if (! gimage)
    return;

  /*  restore the original image  */
  if (image_map->undo_tiles)
    {
      gint offset_x;
      gint offset_y;
      gint width;
      gint height;

      offset_x = image_map->undo_offset_x;
      offset_y = image_map->undo_offset_y;
      width    = tile_manager_width (image_map->undo_tiles);
      height   = tile_manager_height (image_map->undo_tiles),

      /*  Copy from the drawable to the tiles  */
      pixel_region_init (&srcPR, image_map->undo_tiles,
                         0, 0, width, height, FALSE);
      pixel_region_init (&destPR, gimp_drawable_data (image_map->drawable),
                         offset_x, offset_y, width, height, TRUE);

      /* if the user has changed the image depth get out quickly */
      if (destPR.bytes != srcPR.bytes)
        {
          g_message ("image depth change, unable to restore original image");

          tile_manager_unref (image_map->undo_tiles);
          image_map->undo_tiles = NULL;

          g_object_unref (image_map);
          return;
        }

      copy_region (&srcPR, &destPR);

      /*  Update the area  */
      gimp_drawable_update (image_map->drawable,
                            offset_x, offset_y,
                            width, height);

      /*  Free the undo_tiles tile manager  */
      tile_manager_unref (image_map->undo_tiles);
      image_map->undo_tiles = NULL;
    }
}

void
gimp_image_map_abort (GimpImageMap *image_map)
{
  GimpImage *gimage;

  g_return_if_fail (GIMP_IS_IMAGE_MAP (image_map));

  gimage = gimp_item_get_image (GIMP_ITEM (image_map->drawable));

  if (! gimage)
    return;

  gimp_image_map_clear (image_map);

  g_object_unref (image_map);
}


/*  private functions  */

static gboolean
gimp_image_map_do (GimpImageMap *image_map)
{
  GimpImage   *gimage;
  PixelRegion  shadowPR;
  gint         x, y, w, h;

  gimage = gimp_item_get_image (GIMP_ITEM (image_map->drawable));

  if (! gimage)
    {
      image_map->idle_id = 0;

      return FALSE;
    }

  /*  Process the pixel regions and apply the image mapping  */
  (* image_map->apply_func) (&image_map->srcPR,
                             &image_map->destPR,
                             image_map->user_data);

  x = image_map->destPR.x;
  y = image_map->destPR.y;
  w = image_map->destPR.w;
  h = image_map->destPR.h;

  /*  apply the results  */
  pixel_region_init (&shadowPR, gimage->shadow, x, y, w, h, FALSE);
  gimp_drawable_apply_region (image_map->drawable, &shadowPR,
                              FALSE, NULL,
                              GIMP_OPACITY_OPAQUE, GIMP_REPLACE_MODE,
                              NULL,
                              x, y);

  /*  display the results  */
  gimp_drawable_update (image_map->drawable, x, y, w, h);

  g_signal_emit (image_map, image_map_signals[FLUSH], 0);

  image_map->PRI = pixel_regions_process (image_map->PRI);

  if (image_map->PRI == NULL)
    {
      image_map->idle_id = 0;

      gimp_image_flush (gimage);

      return FALSE;
    }

  return TRUE;
}
