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

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

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

#include "core-types.h"

#include "pika-memsize.h"
#include "pikadrawable.h"
#include "pikagrid.h"
#include "pikaimage.h"
#include "pikaimage-color-profile.h"
#include "pikaimage-colormap.h"
#include "pikaimage-grid.h"
#include "pikaimage-metadata.h"
#include "pikaimage-private.h"
#include "pikaimageundo.h"


enum
{
  PROP_0,
  PROP_PREVIOUS_ORIGIN_X,
  PROP_PREVIOUS_ORIGIN_Y,
  PROP_PREVIOUS_WIDTH,
  PROP_PREVIOUS_HEIGHT,
  PROP_GRID,
  PROP_PARASITE_NAME
};


static void     pika_image_undo_constructed  (GObject             *object);
static void     pika_image_undo_set_property (GObject             *object,
                                              guint                property_id,
                                              const GValue        *value,
                                              GParamSpec          *pspec);
static void     pika_image_undo_get_property (GObject             *object,
                                              guint                property_id,
                                              GValue              *value,
                                              GParamSpec          *pspec);

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

static void     pika_image_undo_pop          (PikaUndo            *undo,
                                              PikaUndoMode         undo_mode,
                                              PikaUndoAccumulator *accum);
static void     pika_image_undo_free         (PikaUndo            *undo,
                                              PikaUndoMode         undo_mode);


G_DEFINE_TYPE (PikaImageUndo, pika_image_undo, PIKA_TYPE_UNDO)

#define parent_class pika_image_undo_parent_class


static void
pika_image_undo_class_init (PikaImageUndoClass *klass)
{
  GObjectClass    *object_class      = G_OBJECT_CLASS (klass);
  PikaObjectClass *pika_object_class = PIKA_OBJECT_CLASS (klass);
  PikaUndoClass   *undo_class        = PIKA_UNDO_CLASS (klass);

  object_class->constructed      = pika_image_undo_constructed;
  object_class->set_property     = pika_image_undo_set_property;
  object_class->get_property     = pika_image_undo_get_property;

  pika_object_class->get_memsize = pika_image_undo_get_memsize;

  undo_class->pop                = pika_image_undo_pop;
  undo_class->free               = pika_image_undo_free;

  g_object_class_install_property (object_class, PROP_PREVIOUS_ORIGIN_X,
                                   g_param_spec_int ("previous-origin-x",
                                                     NULL, NULL,
                                                     -PIKA_MAX_IMAGE_SIZE,
                                                     PIKA_MAX_IMAGE_SIZE,
                                                     0,
                                                     PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_PREVIOUS_ORIGIN_Y,
                                   g_param_spec_int ("previous-origin-y",
                                                     NULL, NULL,
                                                     -PIKA_MAX_IMAGE_SIZE,
                                                     PIKA_MAX_IMAGE_SIZE,
                                                     0,
                                                     PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_PREVIOUS_WIDTH,
                                   g_param_spec_int ("previous-width",
                                                     NULL, NULL,
                                                     -PIKA_MAX_IMAGE_SIZE,
                                                     PIKA_MAX_IMAGE_SIZE,
                                                     0,
                                                     PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_PREVIOUS_HEIGHT,
                                   g_param_spec_int ("previous-height",
                                                     NULL, NULL,
                                                     -PIKA_MAX_IMAGE_SIZE,
                                                     PIKA_MAX_IMAGE_SIZE,
                                                     0,
                                                     PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_GRID,
                                   g_param_spec_object ("grid", NULL, NULL,
                                                        PIKA_TYPE_GRID,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_PARASITE_NAME,
                                   g_param_spec_string ("parasite-name",
                                                        NULL, NULL,
                                                        NULL,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));
}

static void
pika_image_undo_init (PikaImageUndo *undo)
{
}

static void
pika_image_undo_constructed (GObject *object)
{
  PikaImageUndo *image_undo = PIKA_IMAGE_UNDO (object);
  PikaImage     *image;

  G_OBJECT_CLASS (parent_class)->constructed (object);

  image = PIKA_UNDO (object)->image;

  switch (PIKA_UNDO (object)->undo_type)
    {
    case PIKA_UNDO_IMAGE_TYPE:
      image_undo->base_type = pika_image_get_base_type (image);
      break;

    case PIKA_UNDO_IMAGE_PRECISION:
      image_undo->precision = pika_image_get_precision (image);
      break;

    case PIKA_UNDO_IMAGE_SIZE:
      image_undo->width  = pika_image_get_width  (image);
      image_undo->height = pika_image_get_height (image);
      break;

    case PIKA_UNDO_IMAGE_RESOLUTION:
      pika_image_get_resolution (image,
                                 &image_undo->xresolution,
                                 &image_undo->yresolution);
      image_undo->resolution_unit = pika_image_get_unit (image);
      break;

    case PIKA_UNDO_IMAGE_GRID:
      pika_assert (PIKA_IS_GRID (image_undo->grid));
      break;

    case PIKA_UNDO_IMAGE_COLORMAP:
      image_undo->num_colors = pika_image_get_colormap_size (image);
      image_undo->colormap   = pika_image_get_colormap (image);
      break;

    case PIKA_UNDO_IMAGE_HIDDEN_PROFILE:
      g_set_object (&image_undo->hidden_profile,
                    _pika_image_get_hidden_profile (image));
      break;

    case PIKA_UNDO_IMAGE_METADATA:
      image_undo->metadata =
        pika_metadata_duplicate (pika_image_get_metadata (image));
      break;

    case PIKA_UNDO_PARASITE_ATTACH:
    case PIKA_UNDO_PARASITE_REMOVE:
      pika_assert (image_undo->parasite_name != NULL);

      image_undo->parasite = pika_parasite_copy
        (pika_image_parasite_find (image, image_undo->parasite_name));
      break;

    default:
      g_return_if_reached ();
    }
}

static void
pika_image_undo_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  PikaImageUndo *image_undo = PIKA_IMAGE_UNDO (object);

  switch (property_id)
    {
    case PROP_PREVIOUS_ORIGIN_X:
      image_undo->previous_origin_x = g_value_get_int (value);
      break;
    case PROP_PREVIOUS_ORIGIN_Y:
      image_undo->previous_origin_y = g_value_get_int (value);
      break;
    case PROP_PREVIOUS_WIDTH:
      image_undo->previous_width = g_value_get_int (value);
      break;
    case PROP_PREVIOUS_HEIGHT:
      image_undo->previous_height = g_value_get_int (value);
      break;
    case PROP_GRID:
      {
        PikaGrid *grid = g_value_get_object (value);

        if (grid)
          image_undo->grid = pika_config_duplicate (PIKA_CONFIG (grid));
      }
      break;
    case PROP_PARASITE_NAME:
      image_undo->parasite_name = g_value_dup_string (value);
      break;

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

static void
pika_image_undo_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  PikaImageUndo *image_undo = PIKA_IMAGE_UNDO (object);

  switch (property_id)
    {
    case PROP_PREVIOUS_ORIGIN_X:
      g_value_set_int (value, image_undo->previous_origin_x);
      break;
    case PROP_PREVIOUS_ORIGIN_Y:
      g_value_set_int (value, image_undo->previous_origin_y);
      break;
    case PROP_PREVIOUS_WIDTH:
      g_value_set_int (value, image_undo->previous_width);
      break;
    case PROP_PREVIOUS_HEIGHT:
      g_value_set_int (value, image_undo->previous_height);
      break;
    case PROP_GRID:
      g_value_set_object (value, image_undo->grid);
       break;
    case PROP_PARASITE_NAME:
      g_value_set_string (value, image_undo->parasite_name);
      break;

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

static gint64
pika_image_undo_get_memsize (PikaObject *object,
                             gint64     *gui_size)
{
  PikaImageUndo *image_undo = PIKA_IMAGE_UNDO (object);
  gint64         memsize    = 0;

  if (image_undo->colormap)
    memsize += PIKA_IMAGE_COLORMAP_SIZE;

  if (image_undo->metadata)
    memsize += pika_g_object_get_memsize (G_OBJECT (image_undo->metadata));

  memsize += pika_object_get_memsize (PIKA_OBJECT (image_undo->grid),
                                      gui_size);
  memsize += pika_string_get_memsize (image_undo->parasite_name);
  memsize += pika_parasite_get_memsize (image_undo->parasite, gui_size);

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

static void
pika_image_undo_pop (PikaUndo            *undo,
                     PikaUndoMode         undo_mode,
                     PikaUndoAccumulator *accum)
{
  PikaImageUndo    *image_undo = PIKA_IMAGE_UNDO (undo);
  PikaImage        *image      = undo->image;
  PikaImagePrivate *private    = PIKA_IMAGE_GET_PRIVATE (image);

  PIKA_UNDO_CLASS (parent_class)->pop (undo, undo_mode, accum);

  switch (undo->undo_type)
    {
    case PIKA_UNDO_IMAGE_TYPE:
      {
        PikaImageBaseType base_type;

        base_type = image_undo->base_type;
        image_undo->base_type = pika_image_get_base_type (image);
        g_object_set (image, "base-type", base_type, NULL);

        pika_image_colormap_changed (image, -1);

        if (image_undo->base_type != pika_image_get_base_type (image))
          {
            if ((image_undo->base_type            == PIKA_GRAY) ||
                (pika_image_get_base_type (image) == PIKA_GRAY))
              {
                /* in case there was no profile undo, we need to emit
                 * profile-changed anyway
                 */
                pika_color_managed_profile_changed (PIKA_COLOR_MANAGED (image));
              }

            accum->mode_changed = TRUE;
          }
      }
      break;

    case PIKA_UNDO_IMAGE_PRECISION:
      {
        PikaPrecision precision;

        precision = image_undo->precision;
        image_undo->precision = pika_image_get_precision (image);
        g_object_set (image, "precision", precision, NULL);

        if (image_undo->precision != pika_image_get_precision (image))
          accum->precision_changed = TRUE;
      }
      break;

    case PIKA_UNDO_IMAGE_SIZE:
      {
        gint width;
        gint height;
        gint previous_origin_x;
        gint previous_origin_y;
        gint previous_width;
        gint previous_height;

        width             = image_undo->width;
        height            = image_undo->height;
        previous_origin_x = image_undo->previous_origin_x;
        previous_origin_y = image_undo->previous_origin_y;
        previous_width    = image_undo->previous_width;
        previous_height   = image_undo->previous_height;

        /* Transform to a redo */
        image_undo->width             = pika_image_get_width  (image);
        image_undo->height            = pika_image_get_height (image);
        image_undo->previous_origin_x = -previous_origin_x;
        image_undo->previous_origin_y = -previous_origin_y;
        image_undo->previous_width    = width;
        image_undo->previous_height   = height;

        g_object_set (image,
                      "width",  width,
                      "height", height,
                      NULL);

        pika_drawable_invalidate_boundary
          (PIKA_DRAWABLE (pika_image_get_mask (image)));

        if (pika_image_get_width  (image) != image_undo->width ||
            pika_image_get_height (image) != image_undo->height)
          {
            accum->size_changed      = TRUE;
            accum->previous_origin_x = previous_origin_x;
            accum->previous_origin_y = previous_origin_y;
            accum->previous_width    = previous_width;
            accum->previous_height   = previous_height;
          }
      }
      break;

    case PIKA_UNDO_IMAGE_RESOLUTION:
      {
        gdouble xres;
        gdouble yres;

        pika_image_get_resolution (image, &xres, &yres);

        if (ABS (image_undo->xresolution - xres) >= 1e-5 ||
            ABS (image_undo->yresolution - yres) >= 1e-5)
          {
            private->xresolution = image_undo->xresolution;
            private->yresolution = image_undo->yresolution;

            image_undo->xresolution = xres;
            image_undo->yresolution = yres;

            accum->resolution_changed = TRUE;
          }
      }

      if (image_undo->resolution_unit != pika_image_get_unit (image))
        {
          PikaUnit unit;

          unit = pika_image_get_unit (image);
          private->resolution_unit = image_undo->resolution_unit;
          image_undo->resolution_unit = unit;

          accum->unit_changed = TRUE;
        }
      break;

    case PIKA_UNDO_IMAGE_GRID:
      {
        PikaGrid *grid;

        grid = pika_config_duplicate (PIKA_CONFIG (pika_image_get_grid (image)));

        pika_image_set_grid (image, image_undo->grid, FALSE);

        g_object_unref (image_undo->grid);
        image_undo->grid = grid;
      }
      break;

    case PIKA_UNDO_IMAGE_COLORMAP:
      {
        guchar *colormap;
        gint    num_colors;

        num_colors = pika_image_get_colormap_size (image);
        colormap   = pika_image_get_colormap (image);

        if (image_undo->colormap)
          pika_image_set_colormap (image,
                                   image_undo->colormap, image_undo->num_colors,
                                   FALSE);
        else
          pika_image_unset_colormap (image, FALSE);

        if (image_undo->colormap)
          g_free (image_undo->colormap);

        image_undo->num_colors = num_colors;
        image_undo->colormap   = colormap;
      }
      break;

    case PIKA_UNDO_IMAGE_HIDDEN_PROFILE:
      {
        PikaColorProfile *hidden_profile = NULL;

        g_set_object (&hidden_profile, _pika_image_get_hidden_profile (image));
        _pika_image_set_hidden_profile (image, image_undo->hidden_profile,
                                        FALSE);
        image_undo->hidden_profile = hidden_profile;
      }
      break;

    case PIKA_UNDO_IMAGE_METADATA:
      {
        PikaMetadata *metadata;

        metadata = pika_metadata_duplicate (pika_image_get_metadata (image));

        pika_image_set_metadata (image, image_undo->metadata, FALSE);

        if (image_undo->metadata)
          g_object_unref (image_undo->metadata);
        image_undo->metadata = metadata;
      }
      break;

    case PIKA_UNDO_PARASITE_ATTACH:
    case PIKA_UNDO_PARASITE_REMOVE:
      {
        PikaParasite *parasite = image_undo->parasite;

        image_undo->parasite = pika_parasite_copy
          (pika_image_parasite_find (image, image_undo->parasite_name));

        if (parasite)
          pika_image_parasite_attach (image, parasite, FALSE);
        else
          pika_image_parasite_detach (image, image_undo->parasite_name, FALSE);

        if (parasite)
          pika_parasite_free (parasite);
      }
      break;

    default:
      g_return_if_reached ();
    }
}

static void
pika_image_undo_free (PikaUndo     *undo,
                      PikaUndoMode  undo_mode)
{
  PikaImageUndo *image_undo = PIKA_IMAGE_UNDO (undo);

  g_clear_object (&image_undo->grid);
  g_clear_pointer (&image_undo->colormap, g_free);
  g_clear_object (&image_undo->hidden_profile);
  g_clear_object (&image_undo->metadata);
  g_clear_pointer (&image_undo->parasite_name, g_free);
  g_clear_pointer (&image_undo->parasite, pika_parasite_free);

  PIKA_UNDO_CLASS (parent_class)->free (undo, undo_mode);
}
