/* LIBGIMP - The GIMP Library
 * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
 *
 * Object properties serialization routines
 * Copyright (C) 2001-2002  Sven Neumann <sven@gimp.org>
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <https://www.gnu.org/licenses/>.
 */

#include "config.h"

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

#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"
#include "libgimpcolor/gimpcolor.h"

#include "gimpconfigtypes.h"

#include "gimpconfigwriter.h"
#include "gimpconfig-iface.h"
#include "gimpconfig-params.h"
#include "gimpconfig-path.h"
#include "gimpconfig-serialize.h"
#include "gimpconfig-utils.h"


/**
 * SECTION: gimpconfig-serialize
 * @title: GimpConfig-serialize
 * @short_description: Serializing for libgimpconfig.
 *
 * Serializing interface for libgimpconfig.
 **/


static gboolean gimp_config_serialize_strv  (const GValue *value,
                                             GString      *str);
static gboolean gimp_config_serialize_array (const GValue *value,
                                             GString      *str);


/**
 * gimp_config_serialize_properties:
 * @config: a #GimpConfig.
 * @writer: a #GimpConfigWriter.
 *
 * This function writes all object properties to the @writer.
 *
 * Returns: %TRUE if serialization succeeded, %FALSE otherwise
 *
 * Since: 2.4
 **/
gboolean
gimp_config_serialize_properties (GimpConfig       *config,
                                  GimpConfigWriter *writer)
{
  GObjectClass  *klass;
  GParamSpec   **property_specs;
  guint          n_property_specs;
  guint          i;
  gboolean       success = TRUE;

  g_return_val_if_fail (G_IS_OBJECT (config), FALSE);

  klass = G_OBJECT_GET_CLASS (config);

  property_specs = g_object_class_list_properties (klass, &n_property_specs);

  if (! property_specs)
    return success;

  for (i = 0; i < n_property_specs; i++)
    {
      GParamSpec *prop_spec = property_specs[i];

      if (! (prop_spec->flags & GIMP_CONFIG_PARAM_SERIALIZE))
        continue;

      /* Some properties may fail writing, which shouldn't break serializing
       * more properties, yet final result would be a (partial) failure.
       */
      if (! gimp_config_serialize_property (config, prop_spec, writer))
        success = FALSE;
    }

  g_free (property_specs);

  return success;
}

/**
 * gimp_config_serialize_changed_properties:
 * @config: a #GimpConfig.
 * @writer: a #GimpConfigWriter.
 *
 * This function writes all object properties that have been changed from
 * their default values to the @writer.
 *
 * Returns: %TRUE if serialization succeeded, %FALSE otherwise
 *
 * Since: 2.4
 **/
gboolean
gimp_config_serialize_changed_properties (GimpConfig       *config,
                                          GimpConfigWriter *writer)
{
  GObjectClass  *klass;
  GParamSpec   **property_specs;
  guint          n_property_specs;
  guint          i;
  GValue         value   = G_VALUE_INIT;
  gboolean       success = TRUE;

  g_return_val_if_fail (G_IS_OBJECT (config), FALSE);

  klass = G_OBJECT_GET_CLASS (config);

  property_specs = g_object_class_list_properties (klass, &n_property_specs);

  if (! property_specs)
    return success;

  for (i = 0; i < n_property_specs; i++)
    {
      GParamSpec *prop_spec = property_specs[i];

      if (! (prop_spec->flags & GIMP_CONFIG_PARAM_SERIALIZE))
        continue;

      g_value_init (&value, prop_spec->value_type);
      g_object_get_property (G_OBJECT (config), prop_spec->name, &value);

      if (! g_param_value_defaults (prop_spec, &value))
        {
          /* Some properties may fail writing, which shouldn't break serializing
           * more properties, yet final result would be a (partial) failure.
           */
          if (! gimp_config_serialize_property (config, prop_spec, writer))
            success = FALSE;
        }

      g_value_unset (&value);
    }

  g_free (property_specs);

  return success;
}

/**
 * gimp_config_serialize_property:
 * @config:     a #GimpConfig.
 * @param_spec: a #GParamSpec.
 * @writer:     a #GimpConfigWriter.
 *
 * This function serializes a single object property to the @writer.
 *
 * Returns: %TRUE if serialization succeeded, %FALSE otherwise
 *
 * Since: 2.4
 **/
gboolean
gimp_config_serialize_property (GimpConfig       *config,
                                GParamSpec       *param_spec,
                                GimpConfigWriter *writer)
{
  GimpConfigInterface *config_iface = NULL;
  GimpConfigInterface *parent_iface = NULL;
  GValue               value        = G_VALUE_INIT;
  gboolean             success      = FALSE;

  if (! (param_spec->flags & GIMP_CONFIG_PARAM_SERIALIZE))
    return FALSE;

  if (param_spec->flags & GIMP_CONFIG_PARAM_IGNORE ||
      param_spec->flags & GIMP_PARAM_DONT_SERIALIZE)
    return TRUE;

  g_value_init (&value, param_spec->value_type);
  g_object_get_property (G_OBJECT (config), param_spec->name, &value);

  if (param_spec->flags & GIMP_CONFIG_PARAM_DEFAULTS &&
      g_param_value_defaults (param_spec, &value))
    {
      g_value_unset (&value);
      return TRUE;
    }

  if (G_TYPE_IS_OBJECT (param_spec->owner_type))
    {
      GTypeClass *owner_class = g_type_class_peek (param_spec->owner_type);

      config_iface = g_type_interface_peek (owner_class, GIMP_TYPE_CONFIG);

      /*  We must call serialize_property() *only* if the *exact* class
       *  which implements it is param_spec->owner_type's class.
       *
       *  Therefore, we ask param_spec->owner_type's immediate parent class
       *  for it's GimpConfigInterface and check if we get a different
       *  pointer.
       *
       *  (if the pointers are the same, param_spec->owner_type's
       *   GimpConfigInterface is inherited from one of it's parent classes
       *   and thus not able to handle param_spec->owner_type's properties).
       */
      if (config_iface)
        {
          GTypeClass *owner_parent_class;

          owner_parent_class = g_type_class_peek_parent (owner_class);

          parent_iface = g_type_interface_peek (owner_parent_class,
                                                GIMP_TYPE_CONFIG);
        }
    }

  if (config_iface                     &&
      config_iface != parent_iface     && /* see comment above */
      config_iface->serialize_property &&
      config_iface->serialize_property (config,
                                        param_spec->param_id,
                                        (const GValue *) &value,
                                        param_spec,
                                        writer))
    {
      success = TRUE;
    }

  /*  If there is no serialize_property() method *or* if it returned
   *  FALSE, continue with the default implementation
   */

  if (! success)
    {
      if (G_VALUE_TYPE (&value) == GIMP_TYPE_PARASITE)
        {
          GimpParasite *parasite = g_value_get_boxed (&value);

          gimp_config_writer_open (writer, param_spec->name);

          if (parasite)
            {
              const gchar   *name;
              gconstpointer  data;
              guint32        data_length;
              gulong         flags;

              name = gimp_parasite_get_name (parasite);
              gimp_config_writer_string (writer, name);

              flags = gimp_parasite_get_flags (parasite);
              data = gimp_parasite_get_data (parasite, &data_length);
              gimp_config_writer_printf (writer, "%lu %u", flags, data_length);
              gimp_config_writer_data (writer, data_length, data);

              success = TRUE;
            }

          if (success)
            gimp_config_writer_close (writer);
          else
            gimp_config_writer_revert (writer);
        }
      else if (G_VALUE_TYPE (&value) == G_TYPE_BYTES)
        {
          GBytes *bytes = g_value_get_boxed (&value);

          gimp_config_writer_open (writer, param_spec->name);

          if (bytes)
            {
              gconstpointer data;
              gsize         data_length;

              data = g_bytes_get_data (bytes, &data_length);

              gimp_config_writer_printf (writer, "%" G_GSIZE_FORMAT, data_length);
              gimp_config_writer_data (writer, data_length, data);
            }
          else
            {
              gimp_config_writer_printf (writer, "%s", "NULL");
            }

          success = TRUE;
          gimp_config_writer_close (writer);
        }
      else if (GIMP_VALUE_HOLDS_COLOR (&value))
        {
          GeglColor *color      = g_value_get_object (&value);
          gboolean   free_color = FALSE;

          gimp_config_writer_open (writer, param_spec->name);

          if (color)
            {
              guint xcf_version = gimp_config_get_xcf_version (config);

              /* If XCF version is before GIMP 3.0, then colors should be saved
               * in legacy GimpRGB format for backwards compatibility */
              if (xcf_version >= 14)
                {
                  const gchar   *encoding;
                  const Babl    *format = gegl_color_get_format (color);
                  const Babl    *space;
                  GBytes        *bytes;
                  gconstpointer  data;
                  gsize          data_length;
                  int            profile_length = 0;

                  gimp_config_writer_open (writer, "color");

                  if (babl_format_is_palette (format))
                    {
                      guint8 pixel[40];

                      /* As a special case, we don't want to serialize
                       * palette colors, because they are just too much
                       * dependent on external data and cannot be
                       * deserialized back safely. So we convert them first.
                       */
                       free_color = TRUE;
                       color = gegl_color_duplicate (color);

                       format = babl_format_with_space ("R'G'B'A u8", format);
                       gegl_color_get_pixel (color, format, pixel);
                       gegl_color_set_pixel (color, format, pixel);
                    }

                  encoding = babl_format_get_encoding (format);
                  gimp_config_writer_string (writer, encoding);

                  bytes = gegl_color_get_bytes (color, format);
                  data  = g_bytes_get_data (bytes, &data_length);

                  gimp_config_writer_printf (writer, "%" G_GSIZE_FORMAT,
                                             data_length);
                  gimp_config_writer_data (writer, data_length, data);

                  space = babl_format_get_space (format);
                  if (space != babl_space ("sRGB"))
                    {
                      guint8 *profile_data;

                      profile_data =
                        (guint8 *) babl_space_get_icc (babl_format_get_space (format),
                                                        &profile_length);
                      gimp_config_writer_printf (writer, "%u", profile_length);
                      if (profile_data)
                        gimp_config_writer_data (writer, profile_length,
                                                 profile_data);
                    }
                  else
                    {
                      gimp_config_writer_printf (writer, "%u", profile_length);
                    }

                  g_bytes_unref (bytes);
                  gimp_config_writer_close (writer);
                }
              else
                {
                  gchar   buf[4][G_ASCII_DTOSTR_BUF_SIZE];
                  gdouble rgba[4];

                  gegl_color_get_pixel (color, babl_format ("R'G'B'A double"),
                                        rgba);

                  for (gint i = 0; i < 4; i++)
                    g_ascii_dtostr (buf[i], G_ASCII_DTOSTR_BUF_SIZE, rgba[i]);

                  gimp_config_writer_printf (writer,
                                             "(color-rgba %s %s %s %s)",
                                             buf[0], buf[1], buf[2], buf[3]);
                }
            }
          else
            {
              gimp_config_writer_printf (writer, "%s", "NULL");
            }

          success = TRUE;
          gimp_config_writer_close (writer);

          if (free_color)
            g_object_unref (color);
        }
      else if (GIMP_VALUE_HOLDS_UNIT (&value))
        {
          GimpUnit *unit = g_value_get_object (&value);

          gimp_config_writer_open (writer, param_spec->name);

          if (unit)
            gimp_config_writer_printf (writer, "%s", gimp_unit_get_name (unit));
          else
            gimp_config_writer_printf (writer, "%s", "NULL");

          success = TRUE;
          gimp_config_writer_close (writer);
        }
      else if (G_VALUE_HOLDS_OBJECT (&value) &&
               G_VALUE_TYPE (&value) != G_TYPE_FILE)
        {
          GimpConfigInterface *config_iface = NULL;
          GimpConfig          *prop_object;

          prop_object = g_value_get_object (&value);

          if (prop_object)
            config_iface = GIMP_CONFIG_GET_IFACE (prop_object);
          else
            success = TRUE;

          if (config_iface)
            {
              gimp_config_writer_open (writer, param_spec->name);

              /*  if the object property is not GIMP_CONFIG_PARAM_AGGREGATE,
               *  deserializing will need to know the exact type
               *  in order to create the object
               */
              if (! (param_spec->flags & GIMP_CONFIG_PARAM_AGGREGATE))
                {
                  GType object_type = G_TYPE_FROM_INSTANCE (prop_object);

                  gimp_config_writer_string (writer, g_type_name (object_type));
                }

              success = config_iface->serialize (prop_object, writer, NULL);

              if (success)
                gimp_config_writer_close (writer);
              else
                gimp_config_writer_revert (writer);
            }
        }
      else
        {
          GString *str = g_string_new (NULL);

          success = gimp_config_serialize_value (&value, str, TRUE);

          if (success)
            {
              gimp_config_writer_open (writer, param_spec->name);
              gimp_config_writer_print (writer, str->str, str->len);
              gimp_config_writer_close (writer);
            }

          g_string_free (str, TRUE);
        }

      if (! success)
        {
          /* don't warn for empty string properties */
          if (G_VALUE_HOLDS_STRING (&value))
            {
              success = TRUE;
            }
          else
            {
              g_warning ("couldn't serialize property %s::%s of type %s",
                         g_type_name (G_TYPE_FROM_INSTANCE (config)),
                         param_spec->name,
                         g_type_name (param_spec->value_type));
            }
        }
    }

  g_value_unset (&value);

  return success;
}

/**
 * gimp_config_serialize_property_by_name:
 * @config:    a #GimpConfig.
 * @prop_name: the property's name.
 * @writer:    a #GimpConfigWriter.
 *
 * This function serializes a single object property to the @writer.
 *
 * Returns: %TRUE if serialization succeeded, %FALSE otherwise
 *
 * Since: 2.6
 **/
gboolean
gimp_config_serialize_property_by_name (GimpConfig       *config,
                                        const gchar      *prop_name,
                                        GimpConfigWriter *writer)
{
  GParamSpec *pspec;

  pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
                                        prop_name);

  if (! pspec)
    return FALSE;

  return gimp_config_serialize_property (config, pspec, writer);
}

/**
 * gimp_config_serialize_value:
 * @value: a #GValue.
 * @str: a #GString.
 * @escaped: whether to escape string values.
 *
 * This utility function appends a string representation of #GValue to @str.
 *
 * Returns: %TRUE if serialization succeeded, %FALSE otherwise.
 *
 * Since: 2.4
 **/
gboolean
gimp_config_serialize_value (const GValue *value,
                             GString      *str,
                             gboolean      escaped)
{
  if (G_VALUE_TYPE (value) == G_TYPE_STRV)
    {
      return gimp_config_serialize_strv (value, str);
    }

  if (GIMP_VALUE_HOLDS_INT32_ARRAY (value) ||
      GIMP_VALUE_HOLDS_DOUBLE_ARRAY (value))
    {
      return gimp_config_serialize_array (value, str);
    }

  if (G_VALUE_HOLDS_BOOLEAN (value))
    {
      gboolean boolean;

      boolean = g_value_get_boolean (value);
      g_string_append (str, boolean ? "yes" : "no");
      return TRUE;
    }

  if (G_VALUE_HOLDS_ENUM (value))
    {
      GEnumClass *enum_class = g_type_class_peek (G_VALUE_TYPE (value));
      GEnumValue *enum_value = g_enum_get_value (enum_class,
                                                 g_value_get_enum (value));

      if (enum_value && enum_value->value_nick)
        {
          g_string_append (str, enum_value->value_nick);
          return TRUE;
        }
      else
        {
          g_warning ("Couldn't get nick for enum_value of %s",
                     G_ENUM_CLASS_TYPE_NAME (enum_class));
          return FALSE;
        }
    }

  if (G_VALUE_HOLDS_STRING (value))
    {
      const gchar *cstr = g_value_get_string (value);

      if (escaped)
        gimp_config_string_append_escaped (str, cstr ? cstr : "");
      else
        g_string_append (str, cstr ? cstr : "");

      return TRUE;
    }

  if (G_VALUE_HOLDS_DOUBLE (value) || G_VALUE_HOLDS_FLOAT (value))
    {
      gdouble v_double;
      gchar   buf[G_ASCII_DTOSTR_BUF_SIZE];

      if (G_VALUE_HOLDS_DOUBLE (value))
        v_double = g_value_get_double (value);
      else
        v_double = (gdouble) g_value_get_float (value);

      g_ascii_dtostr (buf, sizeof (buf), v_double);
      g_string_append (str, buf);
      return TRUE;
    }

  if (GIMP_VALUE_HOLDS_MATRIX2 (value))
    {
      GimpMatrix2 *trafo;

      trafo = g_value_get_boxed (value);

      if (trafo)
        {
          gchar buf[4][G_ASCII_DTOSTR_BUF_SIZE];
          gint  i, j, k;

          for (i = 0, k = 0; i < 2; i++)
            for (j = 0; j < 2; j++, k++)
              g_ascii_dtostr (buf[k], G_ASCII_DTOSTR_BUF_SIZE,
                              trafo->coeff[i][j]);

          g_string_append_printf (str, "(matrix %s %s %s %s)",
                                  buf[0], buf[1], buf[2], buf[3]);
        }
      else
        {
          g_string_append (str, "(matrix 1.0 1.0 1.0 1.0)");
        }

      return TRUE;
    }

  if (G_VALUE_TYPE (value) == GIMP_TYPE_VALUE_ARRAY)
    {
      GimpValueArray *array;

      array = g_value_get_boxed (value);

      if (array)
        {
          gint length = gimp_value_array_length (array);
          gint i;

          g_string_append_printf (str, "%d", length);

          for (i = 0; i < length; i++)
            {
              g_string_append (str, " ");

              if (! gimp_config_serialize_value (gimp_value_array_index (array,
                                                                         i),
                                                 str, TRUE))
                return FALSE;
            }
        }
      else
        {
          g_string_append (str, "0");
        }

      return TRUE;
    }

  if (G_VALUE_TYPE (value) == G_TYPE_FILE)
    {
      GFile *file = g_value_get_object (value);

      if (file)
        {
          gchar *path     = g_file_get_path (file);
          gchar *unexpand = NULL;

          if (path)
            {
              unexpand = gimp_config_path_unexpand (path, TRUE, NULL);
              g_free (path);
            }

          if (unexpand)
            {
              gchar *full_uri;
              gchar *scheme = g_file_get_uri_scheme (file);

              full_uri = g_strconcat (scheme, ":///", unexpand, NULL);
              g_free (scheme);
              g_free (unexpand);

              if (escaped)
                gimp_config_string_append_escaped (str, full_uri);
              else
                g_string_append (str, full_uri);

              g_free (full_uri);
            }
          else
            {
              gchar *uri = g_file_get_uri (file);

              if (uri)
                {
                  if (escaped)
                    gimp_config_string_append_escaped (str, uri);
                  else
                    g_string_append (str, uri);

                  g_free (uri);
                }
              else
                {
                  g_string_append (str, "NULL");
                }
            }
        }
      else
        {
          g_string_append (str, "NULL");
        }

      return TRUE;
    }

  if (g_value_type_transformable (G_VALUE_TYPE (value), G_TYPE_STRING))
    {
      GValue  tmp_value = G_VALUE_INIT;

      g_value_init (&tmp_value, G_TYPE_STRING);
      g_value_transform (value, &tmp_value);

      g_string_append (str, g_value_get_string (&tmp_value));

      g_value_unset (&tmp_value);
      return TRUE;
    }

  return FALSE;
}


/* Private functions */

/**
 * gimp_config_serialize_strv:
 * @value: source #GValue holding a #GStrv
 * @str:   destination string
 *
 * Appends a string repr of the #GStrv value of #GValue to @str.
 * Repr is an integer literal greater than or equal to zero,
 * followed by a possibly empty sequence
 * of quoted and escaped string literals.
 *
 * Returns: %TRUE always
 *
 * Since: 3.0
 **/
static gboolean
gimp_config_serialize_strv (const GValue *value,
                            GString      *str)
{
  GStrv gstrv;

  gstrv = g_value_get_boxed (value);

  if (gstrv)
    {
      gint length = g_strv_length (gstrv);

      /* Write length */
      g_string_append_printf (str, "%d", length);

      for (gint i = 0; i < length; i++)
        {
          g_string_append (str, " "); /* separator */
          gimp_config_string_append_escaped (str, gstrv[i]);
        }
    }
  else
    {
      /* GValue has NULL value. Not quite the same as an empty GStrv.
       * But handle it quietly as an empty GStrv: write a length of zero.
       */
      g_string_append (str, "0");
    }

  return TRUE;
}

static gboolean
gimp_config_serialize_array (const GValue *value,
                             GString      *str)
{
  GimpArray *array;

  g_return_val_if_fail (GIMP_VALUE_HOLDS_INT32_ARRAY (value) ||
                        GIMP_VALUE_HOLDS_DOUBLE_ARRAY (value), FALSE);

  array = g_value_get_boxed (value);

  if (array)
    {
      gint32 *values = (gint32 *) array->data;
      gint    length;

      if (GIMP_VALUE_HOLDS_INT32_ARRAY (value))
        length = array->length / sizeof (gint32);
      else
        length = array->length / sizeof (gdouble);

      /* Write length */
      g_string_append_printf (str, "%d", length);

      for (gint i = 0; i < length; i++)
        {
          gchar *num_str;

          if (GIMP_VALUE_HOLDS_INT32_ARRAY (value))
            {
              num_str = g_strdup_printf (" %d", values[i]);
            }
          else
            {
              gchar buf[G_ASCII_DTOSTR_BUF_SIZE];

              g_ascii_dtostr (buf, sizeof (buf), ((gdouble *) values)[i]);
              num_str = g_strdup_printf (" %s", buf);
            }

          g_string_append (str, num_str);
          g_free (num_str);
        }
    }
  else
    {
      g_string_append (str, "0");
    }

  return TRUE;
}
