/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * gimpsymmetry.c
 * Copyright (C) 2015 Jehan <jehan@gimp.org>
 *
 * 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 <gegl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include "libgimpbase/gimpbase.h"
#include "libgimpconfig/gimpconfig.h"

#include "core-types.h"

#include "gimpdrawable.h"
#include "gimpimage.h"
#include "gimpimage-symmetry.h"
#include "gimpitem.h"
#include "gimpsymmetry.h"

#include "gimp-intl.h"


enum
{
  STROKES_UPDATED,
  GUI_PARAM_CHANGED,
  ACTIVE_CHANGED,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_IMAGE,
  PROP_ACTIVE,
  PROP_VERSION,
};


/* Local function prototypes */

static void       gimp_symmetry_finalize            (GObject      *object);
static void       gimp_symmetry_set_property        (GObject      *object,
                                                     guint         property_id,
                                                     const GValue *value,
                                                     GParamSpec   *pspec);
static void       gimp_symmetry_get_property        (GObject      *object,
                                                     guint         property_id,
                                                     GValue       *value,
                                                     GParamSpec   *pspec);

static void       gimp_symmetry_real_update_strokes (GimpSymmetry *sym,
                                                     GimpDrawable *drawable,
                                                     GimpCoords   *origin);
static GeglNode * gimp_symmetry_real_get_op         (GimpSymmetry *sym,
                                                     gint          stroke,
                                                     gint          paint_width,
                                                     gint          paint_height);
static gboolean   gimp_symmetry_real_update_version (GimpSymmetry *sym);


G_DEFINE_TYPE_WITH_CODE (GimpSymmetry, gimp_symmetry, GIMP_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (GIMP_TYPE_CONFIG, NULL))

#define parent_class gimp_symmetry_parent_class

static guint gimp_symmetry_signals[LAST_SIGNAL] = { 0 };


static void
gimp_symmetry_class_init (GimpSymmetryClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  /* This signal should likely be emitted at the end of
   * update_strokes() if stroke coordinates were changed.
   */
  gimp_symmetry_signals[STROKES_UPDATED] =
    g_signal_new ("strokes-updated",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  0,
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE,
                  1, GIMP_TYPE_IMAGE);

  /* This signal should be emitted when you request a change in the
   * settings UI. For instance adding some settings (therefore having
   * a dynamic UI), or changing scale min/max extremes, etc.
   */
  gimp_symmetry_signals[GUI_PARAM_CHANGED] =
    g_signal_new ("gui-param-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  0,
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE,
                  1, GIMP_TYPE_IMAGE);

  gimp_symmetry_signals[ACTIVE_CHANGED] =
    g_signal_new ("active-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpSymmetryClass, active_changed),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 0);

  object_class->finalize     = gimp_symmetry_finalize;
  object_class->set_property = gimp_symmetry_set_property;
  object_class->get_property = gimp_symmetry_get_property;

  klass->label               = _("None");
  klass->update_strokes      = gimp_symmetry_real_update_strokes;
  klass->get_operation       = gimp_symmetry_real_get_op;
  klass->active_changed      = NULL;
  klass->update_version      = gimp_symmetry_real_update_version;

  g_object_class_install_property (object_class, PROP_IMAGE,
                                   g_param_spec_object ("image",
                                                        NULL, NULL,
                                                        GIMP_TYPE_IMAGE,
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));
  GIMP_CONFIG_PROP_BOOLEAN (object_class, PROP_ACTIVE,
                            "active",
                            _("Active"),
                            _("Activate symmetry painting"),
                            FALSE,
                            GIMP_PARAM_STATIC_STRINGS);

  GIMP_CONFIG_PROP_INT (object_class, PROP_VERSION,
                        "version",
                        "Symmetry version",
                        "Version of the symmetry object",
                        -1, G_MAXINT, 0,
                        GIMP_PARAM_STATIC_STRINGS);
}

static void
gimp_symmetry_init (GimpSymmetry *sym)
{
}

static void
gimp_symmetry_finalize (GObject *object)
{
  GimpSymmetry *sym = GIMP_SYMMETRY (object);

  gimp_symmetry_clear_origin (sym);

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

static void
gimp_symmetry_set_property (GObject      *object,
                            guint         property_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  GimpSymmetry *sym = GIMP_SYMMETRY (object);

  switch (property_id)
    {
    case PROP_IMAGE:
      sym->image = g_value_get_object (value);
      break;
    case PROP_ACTIVE:
      sym->active = g_value_get_boolean (value);
      g_signal_emit (sym, gimp_symmetry_signals[ACTIVE_CHANGED], 0,
                     sym->active);
      break;
    case PROP_VERSION:
      sym->version = g_value_get_int (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_symmetry_get_property (GObject    *object,
                            guint       property_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  GimpSymmetry *sym = GIMP_SYMMETRY (object);

  switch (property_id)
    {
    case PROP_IMAGE:
      g_value_set_object (value, sym->image);
      break;
    case PROP_ACTIVE:
      g_value_set_boolean (value, sym->active);
      break;
    case PROP_VERSION:
      g_value_set_int (value, sym->version);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_symmetry_real_update_strokes (GimpSymmetry *sym,
                                   GimpDrawable *drawable,
                                   GimpCoords   *origin)
{
  /* The basic symmetry just uses the origin as is. */
  sym->strokes = g_list_prepend (sym->strokes,
                                 g_memdup (origin, sizeof (GimpCoords)));
}

static GeglNode *
gimp_symmetry_real_get_op (GimpSymmetry *sym,
                           gint          stroke,
                           gint          paint_width,
                           gint          paint_height)
{
  /* The basic symmetry just returns NULL, since no transformation of the
   * brush painting happen. */
  return NULL;
}

static gboolean
gimp_symmetry_real_update_version (GimpSymmetry *symmetry)
{
  /* Currently all symmetries are at version 0. So all this check has to
   * do is verify that we are at version 0.
   * If one of the child symmetry bumps its version, it will have to
   * override the update_version() virtual function and do any necessary
   * update there (for instance new properties, modified properties, or
   * whatnot).
   */
  gint version;

  g_object_get (symmetry,
                "version", &version,
                NULL);

  return (version == 0);
}

/***** Public Functions *****/

/**
 * gimp_symmetry_set_stateful:
 * @sym:      the #GimpSymmetry
 * @stateful: whether the symmetry should be stateful or stateless.
 *
 * By default, symmetry is made stateless, which means in particular
 * that the size of points can change from one stroke to the next, and
 * in particular you cannot map the coordinates from a stroke to the
 * next. I.e. stroke N at time T+1 is not necessarily the continuation
 * of stroke N at time T.
 * To obtain corresponding strokes, stateful tools, such as MyPaint
 * brushes or the ink tool, need to run this function. They should reset
 * to stateless behavior once finished painting.
 *
 * One of the first consequence of being stateful is that the number of
 * strokes cannot be changed, so more strokes than possible on canvas
 * may be computed, and oppositely it will be possible to end up in
 * cases with missing strokes (e.g. a tiling, theoretically infinite,
 * won't be for the ink tool if one draws too far out of canvas).
 **/
void
gimp_symmetry_set_stateful (GimpSymmetry *symmetry,
                            gboolean      stateful)
{
  symmetry->stateful = stateful;
}

/**
 * gimp_symmetry_set_origin:
 * @sym:      the #GimpSymmetry
 * @drawable: the #GimpDrawable where painting will happen
 * @origin:   new base coordinates.
 *
 * Set the symmetry to new origin coordinates and drawable.
 **/
void
gimp_symmetry_set_origin (GimpSymmetry *sym,
                          GimpDrawable *drawable,
                          GimpCoords   *origin)
{
  g_return_if_fail (GIMP_IS_SYMMETRY (sym));
  g_return_if_fail (GIMP_IS_DRAWABLE (drawable));
  g_return_if_fail (gimp_item_get_image (GIMP_ITEM (drawable)) == sym->image);

  if (drawable != sym->drawable)
    {
      if (sym->drawable)
        g_object_unref (sym->drawable);
      sym->drawable = g_object_ref (drawable);
    }

  if (origin != sym->origin)
    {
      g_free (sym->origin);
      sym->origin = g_memdup (origin, sizeof (GimpCoords));
    }

  g_list_free_full (sym->strokes, g_free);
  sym->strokes = NULL;

  GIMP_SYMMETRY_GET_CLASS (sym)->update_strokes (sym, drawable, origin);
}

/**
 * gimp_symmetry_clear_origin:
 * @sym: the #GimpSymmetry
 *
 * Clear the symmetry's origin coordinates and drawable.
 **/
void
gimp_symmetry_clear_origin (GimpSymmetry *sym)
{
  g_return_if_fail (GIMP_IS_SYMMETRY (sym));

  g_clear_object (&sym->drawable);

  g_clear_pointer (&sym->origin, g_free);

  g_list_free_full (sym->strokes, g_free);
  sym->strokes = NULL;
}

/**
 * gimp_symmetry_get_origin:
 * @sym: the #GimpSymmetry
 *
 * Returns: the origin stroke coordinates.
 * The returned value is owned by the #GimpSymmetry and must not be freed.
 **/
GimpCoords *
gimp_symmetry_get_origin (GimpSymmetry *sym)
{
  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);

  return sym->origin;
}

/**
 * gimp_symmetry_get_size:
 * @sym: the #GimpSymmetry
 *
 * Returns: the total number of strokes.
 **/
gint
gimp_symmetry_get_size (GimpSymmetry *sym)
{
  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), 0);

  return g_list_length (sym->strokes);
}

/**
 * gimp_symmetry_get_coords:
 * @sym:    the #GimpSymmetry
 * @stroke: the stroke number
 *
 * Returns: the coordinates of the stroke number @stroke.
 * The returned value is owned by the #GimpSymmetry and must not be freed.
 **/
GimpCoords *
gimp_symmetry_get_coords (GimpSymmetry *sym,
                          gint          stroke)
{
  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);

  return g_list_nth_data (sym->strokes, stroke);
}

/**
 * gimp_symmetry_get_operation:
 * @sym:          the #GimpSymmetry
 * @stroke:       the stroke number
 * @paint_width:  the width of the painting area
 * @paint_height: the height of the painting area
 *
 * Returns: the operation to apply to the paint buffer for stroke number @stroke.
 * NULL means to copy the original stroke as-is.
 **/
GeglNode *
gimp_symmetry_get_operation (GimpSymmetry *sym,
                             gint          stroke,
                             gint          paint_width,
                             gint          paint_height)
{
  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);

  return GIMP_SYMMETRY_GET_CLASS (sym)->get_operation (sym,
                                                       stroke,
                                                       paint_width,
                                                       paint_height);
}

/*
 * gimp_symmetry_parasite_name:
 * @type: the #GimpSymmetry's #GType
 *
 * Returns: a newly allocated string.
 */
gchar *
gimp_symmetry_parasite_name (GType type)
{
  return g_strconcat ("gimp-image-symmetry:", g_type_name (type), NULL);
}

GimpParasite *
gimp_symmetry_to_parasite (const GimpSymmetry *sym)
{
  GimpParasite *parasite;
  gchar        *parasite_name;
  gchar        *str;

  g_return_val_if_fail (GIMP_IS_SYMMETRY (sym), NULL);

  str = gimp_config_serialize_to_string (GIMP_CONFIG (sym), NULL);
  g_return_val_if_fail (str != NULL, NULL);

  parasite_name = gimp_symmetry_parasite_name (G_TYPE_FROM_INSTANCE (sym));
  parasite = gimp_parasite_new (parasite_name,
                                GIMP_PARASITE_PERSISTENT,
                                strlen (str) + 1, str);
  g_free (parasite_name);
  g_free (str);

  return parasite;
}

GimpSymmetry *
gimp_symmetry_from_parasite (const GimpParasite *parasite,
                             GimpImage          *image,
                             GType               type)
{
  GimpSymmetry    *symmetry;
  gchar           *parasite_name;
  const gchar     *str;
  GError          *error = NULL;

  parasite_name = gimp_symmetry_parasite_name (type);

  g_return_val_if_fail (parasite != NULL, NULL);
  g_return_val_if_fail (strcmp (gimp_parasite_name (parasite),
                                parasite_name) == 0,
                        NULL);

  str = gimp_parasite_data (parasite);

  if (! str)
    {
      g_warning ("Empty symmetry parasite \"%s\"", parasite_name);

      return NULL;
    }

  symmetry = gimp_image_symmetry_new (image, type);

  g_object_set (symmetry,
                "version", -1,
                NULL);

  if (! gimp_config_deserialize_string (GIMP_CONFIG (symmetry),
                                        str,
                                        gimp_parasite_data_size (parasite),
                                        NULL,
                                        &error))
    {
      g_printerr ("Failed to deserialize symmetry parasite: %s\n"
                  "\t- parasite name: %s\n\t- parasite data: %s\n",
                  error->message, parasite_name, str);
      g_error_free (error);

      g_object_unref (symmetry);
      symmetry = NULL;
    }
  g_free (parasite_name);

  if (symmetry)
    {
      gint version;

      g_object_get (symmetry,
                    "version", &version,
                    NULL);
      if (version == -1)
        {
          /* If version has not been updated, let's assume this parasite was
           * not representing symmetry settings.
           */
          g_object_unref (symmetry);
          symmetry = NULL;
        }
      else if (GIMP_SYMMETRY_GET_CLASS (symmetry)->update_version (symmetry) &&
               ! GIMP_SYMMETRY_GET_CLASS (symmetry)->update_version (symmetry))
        {
          g_object_unref (symmetry);
          symmetry = NULL;
        }
    }

  return symmetry;
}
