/* 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 <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

#include "core-types.h"

#include "config/pikacoreconfig.h"

#include "pika.h"
#include "pika-utils.h"
#include "pikaimage.h"
#include "pikaimage-private.h"
#include "pikaimage-undo.h"
#include "pikaitem.h"
#include "pikalist.h"
#include "pikaundostack.h"


/*  local function prototypes  */

static void          pika_image_undo_pop_stack       (PikaImage     *image,
                                                      PikaUndoStack *undo_stack,
                                                      PikaUndoStack *redo_stack,
                                                      PikaUndoMode   undo_mode);
static void          pika_image_undo_free_space      (PikaImage     *image);
static void          pika_image_undo_free_redo       (PikaImage     *image);

static PikaDirtyMask pika_image_undo_dirty_from_type (PikaUndoType   undo_type);


/*  public functions  */

gboolean
pika_image_undo_is_enabled (PikaImage *image)
{
  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  return (PIKA_IMAGE_GET_PRIVATE (image)->undo_freeze_count == 0);
}

gboolean
pika_image_undo_enable (PikaImage *image)
{
  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  /*  Free all undo steps as they are now invalidated  */
  pika_image_undo_free (image);

  return pika_image_undo_thaw (image);
}

gboolean
pika_image_undo_disable (PikaImage *image)
{
  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  return pika_image_undo_freeze (image);
}

gboolean
pika_image_undo_freeze (PikaImage *image)
{
  PikaImagePrivate *private;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  private->undo_freeze_count++;

  if (private->undo_freeze_count == 1)
    pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_FREEZE, NULL);

  return TRUE;
}

gboolean
pika_image_undo_thaw (PikaImage *image)
{
  PikaImagePrivate *private;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  g_return_val_if_fail (private->undo_freeze_count > 0, FALSE);

  private->undo_freeze_count--;

  if (private->undo_freeze_count == 0)
    pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_THAW, NULL);

  return TRUE;
}

gboolean
pika_image_undo (PikaImage *image)
{
  PikaImagePrivate *private;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE,
                        FALSE);

  pika_image_undo_pop_stack (image,
                             private->undo_stack,
                             private->redo_stack,
                             PIKA_UNDO_MODE_UNDO);

  return TRUE;
}

gboolean
pika_image_redo (PikaImage *image)
{
  PikaImagePrivate *private;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE,
                        FALSE);

  pika_image_undo_pop_stack (image,
                             private->redo_stack,
                             private->undo_stack,
                             PIKA_UNDO_MODE_REDO);

  return TRUE;
}

/*
 * this function continues to undo as long as it only sees certain
 * undo types, in particular visibility changes.
 */
gboolean
pika_image_strong_undo (PikaImage *image)
{
  PikaImagePrivate *private;
  PikaUndo         *undo;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE,
                        FALSE);

  undo = pika_undo_stack_peek (private->undo_stack);

  pika_image_undo (image);

  while (pika_undo_is_weak (undo))
    {
      undo = pika_undo_stack_peek (private->undo_stack);
      if (pika_undo_is_weak (undo))
        pika_image_undo (image);
    }

  return TRUE;
}

/*
 * this function continues to redo as long as it only sees certain
 * undo types, in particular visibility changes.  Note that the
 * order of events is set up to make it exactly reverse
 * pika_image_strong_undo().
 */
gboolean
pika_image_strong_redo (PikaImage *image)
{
  PikaImagePrivate *private;
  PikaUndo         *undo;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  g_return_val_if_fail (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE,
                        FALSE);

  undo = pika_undo_stack_peek (private->redo_stack);

  pika_image_redo (image);

  while (pika_undo_is_weak (undo))
    {
      undo = pika_undo_stack_peek (private->redo_stack);
      if (pika_undo_is_weak (undo))
        pika_image_redo (image);
    }

  return TRUE;
}

PikaUndoStack *
pika_image_get_undo_stack (PikaImage *image)
{
  g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL);

  return PIKA_IMAGE_GET_PRIVATE (image)->undo_stack;
}

PikaUndoStack *
pika_image_get_redo_stack (PikaImage *image)
{
  g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL);

  return PIKA_IMAGE_GET_PRIVATE (image)->redo_stack;
}

void
pika_image_undo_free (PikaImage *image)
{
  PikaImagePrivate *private;

  g_return_if_fail (PIKA_IS_IMAGE (image));

  private = PIKA_IMAGE_GET_PRIVATE (image);

  /*  Emit the UNDO_FREE event before actually freeing everything
   *  so the views can properly detach from the undo items
   */
  pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_FREE, NULL);

  pika_undo_free (PIKA_UNDO (private->undo_stack), PIKA_UNDO_MODE_UNDO);
  pika_undo_free (PIKA_UNDO (private->redo_stack), PIKA_UNDO_MODE_REDO);

  /* If the image was dirty, but could become clean by redo-ing
   * some actions, then it should now become 'infinitely' dirty.
   * This is because we've just nuked the actions that would allow
   * the image to become clean again.
   */
  if (private->dirty < 0)
    private->dirty = 100000;

  /* The same applies to the case where the image would become clean
   * due to undo actions, but since user can't undo without an undo
   * stack, that's not so much a problem.
   */
}

gint
pika_image_get_undo_group_count (PikaImage *image)
{
  g_return_val_if_fail (PIKA_IS_IMAGE (image), 0);

  return PIKA_IMAGE_GET_PRIVATE (image)->group_count;
}

gboolean
pika_image_undo_group_start (PikaImage    *image,
                             PikaUndoType  undo_type,
                             const gchar  *name)
{
  PikaImagePrivate *private;
  PikaUndoStack    *undo_group;
  PikaDirtyMask     dirty_mask;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);
  g_return_val_if_fail (undo_type >  PIKA_UNDO_GROUP_FIRST &&
                        undo_type <= PIKA_UNDO_GROUP_LAST, FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  if (! name)
    name = pika_undo_type_to_name (undo_type);

  dirty_mask = pika_image_undo_dirty_from_type (undo_type);

  /* Notify listeners that the image will be modified */
  if (private->group_count == 0 && dirty_mask != PIKA_DIRTY_NONE)
    pika_image_dirty (image, dirty_mask);

  if (private->undo_freeze_count > 0)
    return FALSE;

  private->group_count++;

  /*  If we're already in a group...ignore  */
  if (private->group_count > 1)
    return TRUE;

  /*  nuke the redo stack  */
  pika_image_undo_free_redo (image);

  undo_group = pika_undo_stack_new (image);

  pika_object_set_name (PIKA_OBJECT (undo_group), name);
  PIKA_UNDO (undo_group)->undo_type  = undo_type;
  PIKA_UNDO (undo_group)->dirty_mask = dirty_mask;

  pika_undo_stack_push_undo (private->undo_stack, PIKA_UNDO (undo_group));

  private->pushing_undo_group = undo_type;

  return TRUE;
}

gboolean
pika_image_undo_group_end (PikaImage *image)
{
  PikaImagePrivate *private;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), FALSE);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  if (private->undo_freeze_count > 0)
    return FALSE;

  g_return_val_if_fail (private->group_count > 0, FALSE);

  private->group_count--;

  if (private->group_count == 0)
    {
      private->pushing_undo_group = PIKA_UNDO_GROUP_NONE;

      /* Do it here, since undo_push doesn't emit this event while in
       * the middle of a group
       */
      pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_PUSHED,
                             pika_undo_stack_peek (private->undo_stack));

      pika_image_undo_free_space (image);
    }

  return TRUE;
}

PikaUndo *
pika_image_undo_push (PikaImage     *image,
                      GType          object_type,
                      PikaUndoType   undo_type,
                      const gchar   *name,
                      PikaDirtyMask  dirty_mask,
                      ...)
{
  PikaImagePrivate  *private;
  gint               n_properties = 0;
  gchar            **names        = NULL;
  GValue            *values       = NULL;
  va_list            args;
  PikaUndo          *undo;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL);
  g_return_val_if_fail (g_type_is_a (object_type, PIKA_TYPE_UNDO), NULL);
  g_return_val_if_fail (undo_type > PIKA_UNDO_GROUP_LAST, NULL);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  /* Does this undo dirty the image?  If so, we always want to mark
   * image dirty, even if we can't actually push the undo.
   */
  if (dirty_mask != PIKA_DIRTY_NONE)
    pika_image_dirty (image, dirty_mask);

  if (private->undo_freeze_count > 0)
    return NULL;

  if (! name)
    name = pika_undo_type_to_name (undo_type);

  names = pika_properties_append (object_type,
                                  &n_properties, names, &values,
                                  "name",       name,
                                  "image",      image,
                                  "undo-type",  undo_type,
                                  "dirty-mask", dirty_mask,
                                  NULL);

  va_start (args, dirty_mask);
  names = pika_properties_append_valist (object_type,
                                         &n_properties, names, &values,
                                         args);
  va_end (args);

  undo = (PikaUndo *) g_object_new_with_properties (object_type,
                                                    n_properties,
                                                    (const gchar **) names,
                                                    (const GValue *) values);

  pika_properties_free (n_properties, names, values);

  /*  nuke the redo stack  */
  pika_image_undo_free_redo (image);

  if (private->pushing_undo_group == PIKA_UNDO_GROUP_NONE)
    {
      pika_undo_stack_push_undo (private->undo_stack, undo);

      pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_PUSHED, undo);

      pika_image_undo_free_space (image);

      /*  freeing undo space may have freed the newly pushed undo  */
      if (pika_undo_stack_peek (private->undo_stack) == undo)
        return undo;
    }
  else
    {
      PikaUndoStack *undo_group;

      undo_group = PIKA_UNDO_STACK (pika_undo_stack_peek (private->undo_stack));

      pika_undo_stack_push_undo (undo_group, undo);

      return undo;
    }

  return NULL;
}

PikaUndo *
pika_image_undo_can_compress (PikaImage    *image,
                              GType         object_type,
                              PikaUndoType  undo_type)
{
  PikaImagePrivate *private;

  g_return_val_if_fail (PIKA_IS_IMAGE (image), NULL);

  private = PIKA_IMAGE_GET_PRIVATE (image);

  if (pika_image_is_dirty (image) &&
      ! pika_undo_stack_peek (private->redo_stack))
    {
      PikaUndo *undo = pika_undo_stack_peek (private->undo_stack);

      if (undo && undo->undo_type == undo_type &&
          g_type_is_a (G_TYPE_FROM_INSTANCE (undo), object_type))
        {
          return undo;
        }
    }

  return NULL;
}


/*  private functions  */

static void
pika_image_undo_pop_stack (PikaImage     *image,
                           PikaUndoStack *undo_stack,
                           PikaUndoStack *redo_stack,
                           PikaUndoMode   undo_mode)
{
  PikaUndo            *undo;
  PikaUndoAccumulator  accum = { 0, };

  g_object_freeze_notify (G_OBJECT (image));

  undo = pika_undo_stack_pop_undo (undo_stack, undo_mode, &accum);

  if (undo)
    {
      if (PIKA_IS_UNDO_STACK (undo))
        pika_list_reverse (PIKA_LIST (PIKA_UNDO_STACK (undo)->undos));

      pika_undo_stack_push_undo (redo_stack, undo);

      if (accum.mode_changed)
        pika_image_mode_changed (image);

      if (accum.precision_changed)
        pika_image_precision_changed (image);

      if (accum.size_changed)
        pika_image_size_changed_detailed (image,
                                          accum.previous_origin_x,
                                          accum.previous_origin_y,
                                          accum.previous_width,
                                          accum.previous_height);

      if (accum.resolution_changed)
        pika_image_resolution_changed (image);

      if (accum.unit_changed)
        pika_image_unit_changed (image);

      /* let others know that we just popped an action */
      pika_image_undo_event (image,
                             (undo_mode == PIKA_UNDO_MODE_UNDO) ?
                             PIKA_UNDO_EVENT_UNDO : PIKA_UNDO_EVENT_REDO,
                             undo);
    }

  g_object_thaw_notify (G_OBJECT (image));
}

static void
pika_image_undo_free_space (PikaImage *image)
{
  PikaImagePrivate *private = PIKA_IMAGE_GET_PRIVATE (image);
  PikaContainer    *container;
  gint              min_undo_levels;
  gint              max_undo_levels;
  gint64            undo_size;

  container = private->undo_stack->undos;

  min_undo_levels = image->pika->config->levels_of_undo;
  max_undo_levels = 1024; /* FIXME */
  undo_size       = image->pika->config->undo_size;

#ifdef DEBUG_IMAGE_UNDO
  g_printerr ("undo_steps: %d    undo_bytes: %ld\n",
              pika_container_get_n_children (container),
              (glong) pika_object_get_memsize (PIKA_OBJECT (container), NULL));
#endif

  /*  keep at least min_undo_levels undo steps  */
  if (pika_container_get_n_children (container) <= min_undo_levels)
    return;

  while ((pika_object_get_memsize (PIKA_OBJECT (container), NULL) > undo_size) ||
         (pika_container_get_n_children (container) > max_undo_levels))
    {
      PikaUndo *freed = pika_undo_stack_free_bottom (private->undo_stack,
                                                     PIKA_UNDO_MODE_UNDO);

#ifdef DEBUG_IMAGE_UNDO
      g_printerr ("freed one step: undo_steps: %d    undo_bytes: %ld\n",
                  pika_container_get_n_children (container),
                  (glong) pika_object_get_memsize (PIKA_OBJECT (container),
                                                   NULL));
#endif

      pika_image_undo_event (image, PIKA_UNDO_EVENT_UNDO_EXPIRED, freed);

      g_object_unref (freed);

      if (pika_container_get_n_children (container) <= min_undo_levels)
        return;
    }
}

static void
pika_image_undo_free_redo (PikaImage *image)
{
  PikaImagePrivate *private   = PIKA_IMAGE_GET_PRIVATE (image);
  PikaContainer    *container = private->redo_stack->undos;

#ifdef DEBUG_IMAGE_UNDO
  g_printerr ("redo_steps: %d    redo_bytes: %ld\n",
              pika_container_get_n_children (container),
              (glong) pika_object_get_memsize (PIKA_OBJECT (container), NULL));
#endif

  if (pika_container_is_empty (container))
    return;

  while (pika_container_get_n_children (container) > 0)
    {
      PikaUndo *freed = pika_undo_stack_free_bottom (private->redo_stack,
                                                     PIKA_UNDO_MODE_REDO);

#ifdef DEBUG_IMAGE_UNDO
      g_printerr ("freed one step: redo_steps: %d    redo_bytes: %ld\n",
                  pika_container_get_n_children (container),
                  (glong )pika_object_get_memsize (PIKA_OBJECT (container),
                                                   NULL));
#endif

      pika_image_undo_event (image, PIKA_UNDO_EVENT_REDO_EXPIRED, freed);

      g_object_unref (freed);
    }

  /* We need to use <= here because the undo counter has already been
   * incremented at this point.
   */
  if (private->dirty <= 0)
    {
      /* If the image was dirty, but could become clean by redo-ing
       * some actions, then it should now become 'infinitely' dirty.
       * This is because we've just nuked the actions that would allow
       * the image to become clean again.
       */
      private->dirty = 100000;
    }
}

static PikaDirtyMask
pika_image_undo_dirty_from_type (PikaUndoType undo_type)
{
  switch (undo_type)
    {
    case PIKA_UNDO_GROUP_IMAGE_SCALE:
    case PIKA_UNDO_GROUP_IMAGE_RESIZE:
    case PIKA_UNDO_GROUP_IMAGE_FLIP:
    case PIKA_UNDO_GROUP_IMAGE_ROTATE:
    case PIKA_UNDO_GROUP_IMAGE_TRANSFORM:
    case PIKA_UNDO_GROUP_IMAGE_CROP:
      return PIKA_DIRTY_IMAGE | PIKA_DIRTY_IMAGE_SIZE;

    case PIKA_UNDO_GROUP_IMAGE_CONVERT:
      return PIKA_DIRTY_IMAGE | PIKA_DIRTY_DRAWABLE;

    case PIKA_UNDO_GROUP_IMAGE_COLORMAP_REMAP:
      return PIKA_DIRTY_IMAGE | PIKA_DIRTY_DRAWABLE;

    case PIKA_UNDO_GROUP_IMAGE_LAYERS_MERGE:
      return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_DRAWABLE;

    case PIKA_UNDO_GROUP_IMAGE_VECTORS_MERGE:
      return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_VECTORS;

    case PIKA_UNDO_GROUP_IMAGE_QUICK_MASK: /* FIXME */
      return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_SELECTION;

    case PIKA_UNDO_GROUP_IMAGE_GRID:
    case PIKA_UNDO_GROUP_GUIDE:
      return PIKA_DIRTY_IMAGE_META;

    case PIKA_UNDO_GROUP_DRAWABLE:
    case PIKA_UNDO_GROUP_DRAWABLE_MOD:
      return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE;

    case PIKA_UNDO_GROUP_MASK: /* FIXME */
      return PIKA_DIRTY_SELECTION;

    case PIKA_UNDO_GROUP_ITEM_VISIBILITY:
    case PIKA_UNDO_GROUP_ITEM_PROPERTIES:
      return PIKA_DIRTY_ITEM_META;

    case PIKA_UNDO_GROUP_ITEM_DISPLACE: /* FIXME */
      return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE | PIKA_DIRTY_VECTORS;

    case PIKA_UNDO_GROUP_ITEM_SCALE: /* FIXME */
    case PIKA_UNDO_GROUP_ITEM_RESIZE: /* FIXME */
      return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE | PIKA_DIRTY_VECTORS;

    case PIKA_UNDO_GROUP_LAYER_ADD_MASK:
    case PIKA_UNDO_GROUP_LAYER_APPLY_MASK:
      return PIKA_DIRTY_IMAGE_STRUCTURE;

    case PIKA_UNDO_GROUP_FS_TO_LAYER:
    case PIKA_UNDO_GROUP_FS_FLOAT:
    case PIKA_UNDO_GROUP_FS_ANCHOR:
      return PIKA_DIRTY_IMAGE_STRUCTURE;

    case PIKA_UNDO_GROUP_EDIT_PASTE:
      return PIKA_DIRTY_IMAGE_STRUCTURE;

    case PIKA_UNDO_GROUP_EDIT_CUT:
      return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE;

    case PIKA_UNDO_GROUP_TEXT:
      return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE;

    case PIKA_UNDO_GROUP_TRANSFORM: /* FIXME */
      return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE | PIKA_DIRTY_VECTORS;

    case PIKA_UNDO_GROUP_PAINT:
      return PIKA_DIRTY_ITEM | PIKA_DIRTY_DRAWABLE;

    case PIKA_UNDO_GROUP_PARASITE_ATTACH:
    case PIKA_UNDO_GROUP_PARASITE_REMOVE:
      return PIKA_DIRTY_IMAGE_META | PIKA_DIRTY_ITEM_META;

    case PIKA_UNDO_GROUP_VECTORS_IMPORT:
      return PIKA_DIRTY_IMAGE_STRUCTURE | PIKA_DIRTY_VECTORS;

    case PIKA_UNDO_GROUP_MISC:
      return PIKA_DIRTY_ALL;

    default:
      break;
    }

  return PIKA_DIRTY_ALL;
}
