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

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

#include "libpikabase/pikabase.h"

#include "core-types.h"

#include "config/pikacoreconfig.h"

#include "pika.h"
#include "pikacontext.h"
#include "pikaimage.h"
#include "pikaimage-undo.h"
#include "pikamarshal.h"
#include "pikatempbuf.h"
#include "pikaundo.h"
#include "pikaundostack.h"

#include "pika-priorities.h"

#include "pika-intl.h"


enum
{
  POP,
  FREE,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_IMAGE,
  PROP_TIME,
  PROP_UNDO_TYPE,
  PROP_DIRTY_MASK
};


static void          pika_undo_constructed         (GObject             *object);
static void          pika_undo_finalize            (GObject             *object);
static void          pika_undo_set_property        (GObject             *object,
                                                    guint                property_id,
                                                    const GValue        *value,
                                                    GParamSpec          *pspec);
static void          pika_undo_get_property        (GObject             *object,
                                                    guint                property_id,
                                                    GValue              *value,
                                                    GParamSpec          *pspec);

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

static gboolean      pika_undo_get_popup_size      (PikaViewable        *viewable,
                                                    gint                 width,
                                                    gint                 height,
                                                    gboolean             dot_for_dot,
                                                    gint                *popup_width,
                                                    gint                *popup_height);
static PikaTempBuf * pika_undo_get_new_preview     (PikaViewable        *viewable,
                                                    PikaContext         *context,
                                                    gint                 width,
                                                    gint                 height);

static void          pika_undo_real_pop            (PikaUndo            *undo,
                                                    PikaUndoMode         undo_mode,
                                                    PikaUndoAccumulator *accum);
static void          pika_undo_real_free           (PikaUndo            *undo,
                                                    PikaUndoMode         undo_mode);

static gboolean      pika_undo_create_preview_idle (gpointer             data);
static void       pika_undo_create_preview_private (PikaUndo            *undo,
                                                    PikaContext         *context);


G_DEFINE_TYPE (PikaUndo, pika_undo, PIKA_TYPE_VIEWABLE)

#define parent_class pika_undo_parent_class

static guint undo_signals[LAST_SIGNAL] = { 0 };


static void
pika_undo_class_init (PikaUndoClass *klass)
{
  GObjectClass      *object_class      = G_OBJECT_CLASS (klass);
  PikaObjectClass   *pika_object_class = PIKA_OBJECT_CLASS (klass);
  PikaViewableClass *viewable_class    = PIKA_VIEWABLE_CLASS (klass);

  undo_signals[POP] =
    g_signal_new ("pop",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaUndoClass, pop),
                  NULL, NULL,
                  pika_marshal_VOID__ENUM_POINTER,
                  G_TYPE_NONE, 2,
                  PIKA_TYPE_UNDO_MODE,
                  G_TYPE_POINTER);

  undo_signals[FREE] =
    g_signal_new ("free",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaUndoClass, free),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 1,
                  PIKA_TYPE_UNDO_MODE);

  object_class->constructed         = pika_undo_constructed;
  object_class->finalize            = pika_undo_finalize;
  object_class->set_property        = pika_undo_set_property;
  object_class->get_property        = pika_undo_get_property;

  pika_object_class->get_memsize    = pika_undo_get_memsize;

  viewable_class->default_icon_name = "edit-undo";
  viewable_class->get_popup_size    = pika_undo_get_popup_size;
  viewable_class->get_new_preview   = pika_undo_get_new_preview;

  klass->pop                        = pika_undo_real_pop;
  klass->free                       = pika_undo_real_free;

  g_object_class_install_property (object_class, PROP_IMAGE,
                                   g_param_spec_object ("image", NULL, NULL,
                                                        PIKA_TYPE_IMAGE,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_TIME,
                                   g_param_spec_uint ("time", NULL, NULL,
                                                      0, G_MAXUINT, 0,
                                                      PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_UNDO_TYPE,
                                   g_param_spec_enum ("undo-type", NULL, NULL,
                                                      PIKA_TYPE_UNDO_TYPE,
                                                      PIKA_UNDO_GROUP_NONE,
                                                      PIKA_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_DIRTY_MASK,
                                   g_param_spec_flags ("dirty-mask",
                                                       NULL, NULL,
                                                       PIKA_TYPE_DIRTY_MASK,
                                                       PIKA_DIRTY_NONE,
                                                       PIKA_PARAM_READWRITE |
                                                       G_PARAM_CONSTRUCT_ONLY));
}

static void
pika_undo_init (PikaUndo *undo)
{
  undo->time = time (NULL);
}

static void
pika_undo_constructed (GObject *object)
{
  PikaUndo *undo = PIKA_UNDO (object);

  G_OBJECT_CLASS (parent_class)->constructed (object);

  pika_assert (PIKA_IS_IMAGE (undo->image));
}

static void
pika_undo_finalize (GObject *object)
{
  PikaUndo *undo = PIKA_UNDO (object);

  if (undo->preview_idle_id)
    {
      g_source_remove (undo->preview_idle_id);
      undo->preview_idle_id = 0;
    }

  g_clear_pointer (&undo->preview, pika_temp_buf_unref);

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

static void
pika_undo_set_property (GObject      *object,
                        guint         property_id,
                        const GValue *value,
                        GParamSpec   *pspec)
{
  PikaUndo *undo = PIKA_UNDO (object);

  switch (property_id)
    {
    case PROP_IMAGE:
      /* don't ref */
      undo->image = g_value_get_object (value);
      break;
    case PROP_TIME:
      undo->time = g_value_get_uint (value);
      break;
    case PROP_UNDO_TYPE:
      undo->undo_type = g_value_get_enum (value);
      break;
    case PROP_DIRTY_MASK:
      undo->dirty_mask = g_value_get_flags (value);
      break;

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

static void
pika_undo_get_property (GObject    *object,
                        guint       property_id,
                        GValue     *value,
                        GParamSpec *pspec)
{
  PikaUndo *undo = PIKA_UNDO (object);

  switch (property_id)
    {
    case PROP_IMAGE:
      g_value_set_object (value, undo->image);
      break;
    case PROP_TIME:
      g_value_set_uint (value, undo->time);
      break;
    case PROP_UNDO_TYPE:
      g_value_set_enum (value, undo->undo_type);
      break;
    case PROP_DIRTY_MASK:
      g_value_set_flags (value, undo->dirty_mask);
      break;

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

static gint64
pika_undo_get_memsize (PikaObject *object,
                       gint64     *gui_size)
{
  PikaUndo *undo    = PIKA_UNDO (object);
  gint64    memsize = 0;

  *gui_size += pika_temp_buf_get_memsize (undo->preview);

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

static gboolean
pika_undo_get_popup_size (PikaViewable *viewable,
                          gint          width,
                          gint          height,
                          gboolean      dot_for_dot,
                          gint         *popup_width,
                          gint         *popup_height)
{
  PikaUndo *undo = PIKA_UNDO (viewable);

  if (undo->preview &&
      (pika_temp_buf_get_width  (undo->preview) > width ||
       pika_temp_buf_get_height (undo->preview) > height))
    {
      *popup_width  = pika_temp_buf_get_width  (undo->preview);
      *popup_height = pika_temp_buf_get_height (undo->preview);

      return TRUE;
    }

  return FALSE;
}

static PikaTempBuf *
pika_undo_get_new_preview (PikaViewable *viewable,
                           PikaContext  *context,
                           gint          width,
                           gint          height)
{
  PikaUndo *undo = PIKA_UNDO (viewable);

  if (undo->preview)
    {
      gint preview_width;
      gint preview_height;

      pika_viewable_calc_preview_size (pika_temp_buf_get_width  (undo->preview),
                                       pika_temp_buf_get_height (undo->preview),
                                       width,
                                       height,
                                       TRUE, 1.0, 1.0,
                                       &preview_width,
                                       &preview_height,
                                       NULL);

      if (preview_width  < pika_temp_buf_get_width  (undo->preview) &&
          preview_height < pika_temp_buf_get_height (undo->preview))
        {
          return pika_temp_buf_scale (undo->preview,
                                      preview_width, preview_height);
        }

      return pika_temp_buf_copy (undo->preview);
    }

  return NULL;
}

static void
pika_undo_real_pop (PikaUndo            *undo,
                    PikaUndoMode         undo_mode,
                    PikaUndoAccumulator *accum)
{
}

static void
pika_undo_real_free (PikaUndo     *undo,
                     PikaUndoMode  undo_mode)
{
}

void
pika_undo_pop (PikaUndo            *undo,
               PikaUndoMode         undo_mode,
               PikaUndoAccumulator *accum)
{
  g_return_if_fail (PIKA_IS_UNDO (undo));
  g_return_if_fail (accum != NULL);

  if (undo->dirty_mask != PIKA_DIRTY_NONE)
    {
      switch (undo_mode)
        {
        case PIKA_UNDO_MODE_UNDO:
          pika_image_clean (undo->image, undo->dirty_mask);
          break;

        case PIKA_UNDO_MODE_REDO:
          pika_image_dirty (undo->image, undo->dirty_mask);
          break;
        }
    }

  g_signal_emit (undo, undo_signals[POP], 0, undo_mode, accum);
}

void
pika_undo_free (PikaUndo     *undo,
                PikaUndoMode  undo_mode)
{
  g_return_if_fail (PIKA_IS_UNDO (undo));

  g_signal_emit (undo, undo_signals[FREE], 0, undo_mode);
}

typedef struct _PikaUndoIdle PikaUndoIdle;

struct _PikaUndoIdle
{
  PikaUndo    *undo;
  PikaContext *context;
};

static void
pika_undo_idle_free (PikaUndoIdle *idle)
{
  if (idle->context)
    g_object_unref (idle->context);

  g_slice_free (PikaUndoIdle, idle);
}

void
pika_undo_create_preview (PikaUndo    *undo,
                          PikaContext *context,
                          gboolean     create_now)
{
  g_return_if_fail (PIKA_IS_UNDO (undo));
  g_return_if_fail (context == NULL || PIKA_IS_CONTEXT (context));

  if (undo->preview || undo->preview_idle_id)
    return;

  if (create_now)
    {
      pika_undo_create_preview_private (undo, context);
    }
  else
    {
      PikaUndoIdle *idle = g_slice_new0 (PikaUndoIdle);

      idle->undo = undo;

      if (context)
        idle->context = g_object_ref (context);

      undo->preview_idle_id =
        g_idle_add_full (PIKA_PRIORITY_VIEWABLE_IDLE,
                         pika_undo_create_preview_idle, idle,
                         (GDestroyNotify) pika_undo_idle_free);
    }
}

static gboolean
pika_undo_create_preview_idle (gpointer data)
{
  PikaUndoIdle *idle   = data;
  PikaUndoStack *stack = pika_image_get_undo_stack (idle->undo->image);

  if (idle->undo == pika_undo_stack_peek (stack))
    {
      pika_undo_create_preview_private (idle->undo, idle->context);
    }

  idle->undo->preview_idle_id = 0;

  return FALSE;
}

static void
pika_undo_create_preview_private (PikaUndo    *undo,
                                  PikaContext *context)
{
  PikaImage    *image = undo->image;
  PikaViewable *preview_viewable;
  PikaViewSize  preview_size;
  gint          width;
  gint          height;

  switch (undo->undo_type)
    {
    case PIKA_UNDO_GROUP_IMAGE_QUICK_MASK:
    case PIKA_UNDO_GROUP_MASK:
    case PIKA_UNDO_MASK:
      preview_viewable = PIKA_VIEWABLE (pika_image_get_mask (image));
      break;

    default:
      preview_viewable = PIKA_VIEWABLE (image);
      break;
    }

  preview_size = image->pika->config->undo_preview_size;

  if (pika_image_get_width  (image) <= preview_size &&
      pika_image_get_height (image) <= preview_size)
    {
      width  = pika_image_get_width  (image);
      height = pika_image_get_height (image);
    }
  else
    {
      if (pika_image_get_width (image) > pika_image_get_height (image))
        {
          width  = preview_size;
          height = MAX (1, (pika_image_get_height (image) * preview_size /
                            pika_image_get_width (image)));
        }
      else
        {
          height = preview_size;
          width  = MAX (1, (pika_image_get_width (image) * preview_size /
                            pika_image_get_height (image)));
        }
    }

  undo->preview = pika_viewable_get_new_preview (preview_viewable, context,
                                                 width, height);

  pika_viewable_invalidate_preview (PIKA_VIEWABLE (undo));
}

void
pika_undo_refresh_preview (PikaUndo    *undo,
                           PikaContext *context)
{
  g_return_if_fail (PIKA_IS_UNDO (undo));
  g_return_if_fail (context == NULL || PIKA_IS_CONTEXT (context));

  if (undo->preview_idle_id)
    return;

  if (undo->preview)
    {
      g_clear_pointer (&undo->preview, pika_temp_buf_unref);
      pika_undo_create_preview (undo, context, FALSE);
    }
}

const gchar *
pika_undo_type_to_name (PikaUndoType type)
{
  const gchar *desc;

  if (pika_enum_get_value (PIKA_TYPE_UNDO_TYPE, type, NULL, NULL, &desc, NULL))
    return desc;
  else
    return "";
}

gboolean
pika_undo_is_weak (PikaUndo *undo)
{
  if (! undo)
    return FALSE;

  switch (undo->undo_type)
    {
    case PIKA_UNDO_GROUP_ITEM_VISIBILITY:
    case PIKA_UNDO_GROUP_ITEM_PROPERTIES:
    case PIKA_UNDO_GROUP_LAYER_APPLY_MASK:
    case PIKA_UNDO_ITEM_VISIBILITY:
    case PIKA_UNDO_LAYER_MODE:
    case PIKA_UNDO_LAYER_OPACITY:
    case PIKA_UNDO_LAYER_MASK_APPLY:
    case PIKA_UNDO_LAYER_MASK_SHOW:
      return TRUE;
      break;

    default:
      break;
    }

  return FALSE;
}

/**
 * pika_undo_get_age:
 * @undo:
 *
 * Returns: the time in seconds since this undo item was created
 */
gint
pika_undo_get_age (PikaUndo *undo)
{
  guint now = time (NULL);

  g_return_val_if_fail (PIKA_IS_UNDO (undo), 0);
  g_return_val_if_fail (now >= undo->time, 0);

  return now - undo->time;
}

/**
 * pika_undo_reset_age:
 * @undo:
 *
 * Changes the creation time of this undo item to the current time.
 */
void
pika_undo_reset_age (PikaUndo *undo)
{
  g_return_if_fail (PIKA_IS_UNDO (undo));

  undo->time = time (NULL);

  g_object_notify (G_OBJECT (undo), "time");
}
