/* LIBPIKA - The PIKA Library
 * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
 *
 * Config file serialization and deserialization interface
 * 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 <string.h>

#include <gio/gio.h>

#include "libpikabase/pikabase.h"

#include "pikaconfigtypes.h"

#include "pikaconfigwriter.h"
#include "pikaconfig-iface.h"
#include "pikaconfig-deserialize.h"
#include "pikaconfig-serialize.h"
#include "pikaconfig-params.h"
#include "pikaconfig-utils.h"
#include "pikascanner.h"

#include "libpika/libpika-intl.h"


/*
 * PikaConfigIface:
 *
 * The [struct@Config] serialization and deserialization interface.
 */


/*  local function prototypes  */

static void         pika_config_iface_default_init (PikaConfigInterface *iface);
static void         pika_config_iface_base_init    (PikaConfigInterface *iface);

static gboolean     pika_config_iface_serialize    (PikaConfig          *config,
                                                    PikaConfigWriter    *writer,
                                                    gpointer             data);
static gboolean     pika_config_iface_deserialize  (PikaConfig          *config,
                                                    GScanner            *scanner,
                                                    gint                 nest_level,
                                                    gpointer             data);
static PikaConfig * pika_config_iface_duplicate    (PikaConfig          *config);
static gboolean     pika_config_iface_equal        (PikaConfig          *a,
                                                    PikaConfig          *b);
static void         pika_config_iface_reset        (PikaConfig          *config);
static gboolean     pika_config_iface_copy         (PikaConfig          *src,
                                                    PikaConfig          *dest,
                                                    GParamFlags          flags);


/*  private functions  */


GType
pika_config_get_type (void)
{
  static GType config_iface_type = 0;

  if (! config_iface_type)
    {
      const GTypeInfo config_iface_info =
      {
        sizeof (PikaConfigInterface),
        (GBaseInitFunc)      pika_config_iface_base_init,
        (GBaseFinalizeFunc)  NULL,
        (GClassInitFunc)     pika_config_iface_default_init,
        (GClassFinalizeFunc) NULL,
      };

      config_iface_type = g_type_register_static (G_TYPE_INTERFACE,
                                                  "PikaConfigInterface",
                                                  &config_iface_info,
                                                  0);

      g_type_interface_add_prerequisite (config_iface_type, G_TYPE_OBJECT);
    }

  return config_iface_type;
}

static void
pika_config_iface_default_init (PikaConfigInterface *iface)
{
  iface->serialize   = pika_config_iface_serialize;
  iface->deserialize = pika_config_iface_deserialize;
  iface->duplicate   = pika_config_iface_duplicate;
  iface->equal       = pika_config_iface_equal;
  iface->reset       = pika_config_iface_reset;
  iface->copy        = pika_config_iface_copy;
}

static void
pika_config_iface_base_init (PikaConfigInterface *iface)
{
  /*  always set these to NULL since we don't want to inherit them
   *  from parent classes
   */
  iface->serialize_property   = NULL;
  iface->deserialize_property = NULL;
}

static gboolean
pika_config_iface_serialize (PikaConfig       *config,
                             PikaConfigWriter *writer,
                             gpointer          data)
{
  return pika_config_serialize_properties (config, writer);
}

static gboolean
pika_config_iface_deserialize (PikaConfig *config,
                               GScanner   *scanner,
                               gint        nest_level,
                               gpointer    data)
{
  return pika_config_deserialize_properties (config, scanner, nest_level);
}

static PikaConfig *
pika_config_iface_duplicate (PikaConfig *config)
{
  GObject       *object = G_OBJECT (config);
  GObjectClass  *klass  = G_OBJECT_GET_CLASS (object);
  GParamSpec   **property_specs;
  guint          n_property_specs;
  gint           n_construct_properties = 0;
  const gchar  **construct_names        = NULL;
  GValue        *construct_values       = NULL;
  guint          i;
  GObject       *dup;

  property_specs = g_object_class_list_properties (klass, &n_property_specs);

  construct_names  = g_new0 (const gchar *, n_property_specs);
  construct_values = g_new0 (GValue,        n_property_specs);

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

      if ((prop_spec->flags & G_PARAM_READABLE) &&
          (prop_spec->flags & G_PARAM_WRITABLE) &&
          (prop_spec->flags & G_PARAM_CONSTRUCT_ONLY))
        {
          construct_names[n_construct_properties] = prop_spec->name;

          g_value_init (&construct_values[n_construct_properties],
                        prop_spec->value_type);
          g_object_get_property (object, prop_spec->name,
                                 &construct_values[n_construct_properties]);

          n_construct_properties++;
        }
    }

  g_free (property_specs);

  dup = g_object_new_with_properties (G_TYPE_FROM_INSTANCE (object),
                                      n_construct_properties,
                                      (const gchar **) construct_names,
                                      (const GValue *) construct_values);

  for (i = 0; i < n_construct_properties; i++)
    g_value_unset (&construct_values[i]);

  g_free (construct_names);
  g_free (construct_values);

  pika_config_copy (config, PIKA_CONFIG (dup), 0);

  return PIKA_CONFIG (dup);
}

static gboolean
pika_config_iface_equal (PikaConfig *a,
                         PikaConfig *b)
{
  GObjectClass  *klass;
  GParamSpec   **property_specs;
  guint          n_property_specs;
  guint          i;
  gboolean       equal = TRUE;

  klass = G_OBJECT_GET_CLASS (a);

  property_specs = g_object_class_list_properties (klass, &n_property_specs);

  for (i = 0; equal && i < n_property_specs; i++)
    {
      GParamSpec  *prop_spec;
      GValue       a_value = G_VALUE_INIT;
      GValue       b_value = G_VALUE_INIT;

      prop_spec = property_specs[i];

      if (! (prop_spec->flags & G_PARAM_READABLE) ||
            (prop_spec->flags & PIKA_CONFIG_PARAM_DONT_COMPARE))
        {
          continue;
        }

      g_value_init (&a_value, prop_spec->value_type);
      g_value_init (&b_value, prop_spec->value_type);
      g_object_get_property (G_OBJECT (a), prop_spec->name, &a_value);
      g_object_get_property (G_OBJECT (b), prop_spec->name, &b_value);

      if (g_param_values_cmp (prop_spec, &a_value, &b_value))
        {
          if ((prop_spec->flags & PIKA_CONFIG_PARAM_AGGREGATE) &&
              G_IS_PARAM_SPEC_OBJECT (prop_spec)        &&
              g_type_interface_peek (g_type_class_peek (prop_spec->value_type),
                                     PIKA_TYPE_CONFIG))
            {
              if (! pika_config_is_equal_to (g_value_get_object (&a_value),
                                             g_value_get_object (&b_value)))
                {
                  equal = FALSE;
                }
            }
          else
            {
              equal = FALSE;
            }
        }

      g_value_unset (&a_value);
      g_value_unset (&b_value);
    }

  g_free (property_specs);

  return equal;
}

static void
pika_config_iface_reset (PikaConfig *config)
{
  pika_config_reset_properties (G_OBJECT (config));
}

static gboolean
pika_config_iface_copy (PikaConfig  *src,
                        PikaConfig  *dest,
                        GParamFlags  flags)
{
  return pika_config_sync (G_OBJECT (src), G_OBJECT (dest), flags);
}


/*  public functions  */


/**
 * pika_config_serialize_to_file:
 * @config: an object that implements [iface@ConfigInterface].
 * @file:   the file to write the configuration to.
 * @header: (nullable): optional file header (must be ASCII only)
 * @footer: (nullable): optional file footer (must be ASCII only)
 * @data: user data passed to the serialize implementation.
 * @error: return location for a possible error
 *
 * Serializes the object properties of @config to the file specified
 * by @file. If a file with that name already exists, it is
 * overwritten. Basically this function opens @file for you and calls
 * the serialize function of the @config's [iface@ConfigInterface].
 *
 * Returns: %TRUE if serialization succeeded, %FALSE otherwise.
 *
 * Since: 2.10
 **/
gboolean
pika_config_serialize_to_file (PikaConfig   *config,
                               GFile        *file,
                               const gchar  *header,
                               const gchar  *footer,
                               gpointer      data,
                               GError      **error)
{
  PikaConfigWriter *writer;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);
  g_return_val_if_fail (G_IS_FILE (file), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  writer = pika_config_writer_new_from_file (file, TRUE, header, error);
  if (!writer)
    return FALSE;

  PIKA_CONFIG_GET_IFACE (config)->serialize (config, writer, data);

  return pika_config_writer_finish (writer, footer, error);
}

/**
 * pika_config_serialize_to_stream:
 * @config: an object that implements [iface@ConfigInterface].
 * @output: the #GOutputStream to write the configuration to.
 * @header: (nullable): optional file header (must be ASCII only)
 * @footer: (nullable): optional file footer (must be ASCII only)
 * @data: user data passed to the serialize implementation.
 * @error: return location for a possible error
 *
 * Serializes the object properties of @config to the stream specified
 * by @output.
 *
 * Returns: Whether serialization succeeded.
 *
 * Since: 2.10
 **/
gboolean
pika_config_serialize_to_stream (PikaConfig     *config,
                                 GOutputStream  *output,
                                 const gchar    *header,
                                 const gchar    *footer,
                                 gpointer        data,
                                 GError        **error)
{
  PikaConfigWriter *writer;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);
  g_return_val_if_fail (G_IS_OUTPUT_STREAM (output), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  writer = pika_config_writer_new_from_stream (output, header, error);
  if (!writer)
    return FALSE;

  PIKA_CONFIG_GET_IFACE (config)->serialize (config, writer, data);

  return pika_config_writer_finish (writer, footer, error);
}

/**
 * pika_config_serialize_to_fd:
 * @config: an object that implements [iface@ConfigInterface].
 * @fd: a file descriptor, opened for writing
 * @data: user data passed to the serialize implementation.
 *
 * Serializes the object properties of @config to the given file
 * descriptor.
 *
 * Returns: %TRUE if serialization succeeded, %FALSE otherwise.
 *
 * Since: 2.4
 **/
gboolean
pika_config_serialize_to_fd (PikaConfig *config,
                             gint        fd,
                             gpointer    data)
{
  PikaConfigWriter *writer;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);
  g_return_val_if_fail (fd > 0, FALSE);

  writer = pika_config_writer_new_from_fd (fd);
  if (!writer)
    return FALSE;

  PIKA_CONFIG_GET_IFACE (config)->serialize (config, writer, data);

  return pika_config_writer_finish (writer, NULL, NULL);
}

/**
 * pika_config_serialize_to_string:
 * @config: an object that implements the [iface@ConfigInterface].
 * @data: user data passed to the serialize implementation.
 *
 * Serializes the object properties of @config to a string.
 *
 * Returns: a newly allocated NUL-terminated string.
 *
 * Since: 2.4
 **/
gchar *
pika_config_serialize_to_string (PikaConfig *config,
                                 gpointer    data)
{
  PikaConfigWriter *writer;
  GString          *str;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), NULL);

  str = g_string_new (NULL);
  writer = pika_config_writer_new_from_string (str);

  PIKA_CONFIG_GET_IFACE (config)->serialize (config, writer, data);

  pika_config_writer_finish (writer, NULL, NULL);

  return g_string_free (str, FALSE);
}

/**
 * pika_config_serialize_to_parasite:
 * @config:         an object that implements the [iface@ConfigInterface].
 * @parasite_name:  the new parasite's name
 * @parasite_flags: the new parasite's flags
 * @data:           user data passed to the serialize implementation.
 *
 * Serializes the object properties of @config to a [struct@Parasite].
 *
 * Returns: (transfer full): the newly allocated parasite.
 *
 * Since: 3.0
 **/
PikaParasite *
pika_config_serialize_to_parasite (PikaConfig  *config,
                                   const gchar *parasite_name,
                                   guint        parasite_flags,
                                   gpointer     data)
{
  PikaParasite *parasite;
  gchar        *str;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), NULL);
  g_return_val_if_fail (parasite_name != NULL, NULL);

  str = pika_config_serialize_to_string (config, data);

  if (! str)
    return NULL;

  parasite = pika_parasite_new (parasite_name,
                                parasite_flags,
                                0, NULL);

  parasite->size = strlen (str) + 1;
  parasite->data = str;

  return parasite;
}

/**
 * pika_config_deserialize_file:
 * @config: an object that implements the #PikaConfigInterface.
 * @file: the file to read configuration from.
 * @data: user data passed to the deserialize implementation.
 * @error: return location for a possible error
 *
 * Opens the file specified by @file, reads configuration data from it
 * and configures @config accordingly. Basically this function creates
 * a properly configured [struct@GLib.Scanner] for you and calls the deserialize
 * function of the @config's [iface@ConfigInterface].
 *
 * Returns: Whether deserialization succeeded.
 *
 * Since: 2.10
 **/
gboolean
pika_config_deserialize_file (PikaConfig  *config,
                              GFile       *file,
                              gpointer     data,
                              GError     **error)
{
  GScanner *scanner;
  gboolean  success;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);
  g_return_val_if_fail (G_IS_FILE (file), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  scanner = pika_scanner_new_file (file, error);
  if (! scanner)
    return FALSE;

  g_object_freeze_notify (G_OBJECT (config));

  success = PIKA_CONFIG_GET_IFACE (config)->deserialize (config,
                                                         scanner, 0, data);

  g_object_thaw_notify (G_OBJECT (config));

  pika_scanner_unref (scanner);

  if (! success)
    /* If we get this assert, it means we have a bug in one of the
     * deserialize() implementations. Any failure case should report the
     * error condition with g_scanner_error() which will populate the
     * error object passed in pika_scanner_new*().
     */
    g_assert (error == NULL || *error != NULL);

  return success;
}

/**
 * pika_config_deserialize_stream:
 * @config: an object that implements the #PikaConfigInterface.
 * @input: the input stream to read configuration from.
 * @data: user data passed to the deserialize implementation.
 * @error: return location for a possible error
 *
 * Reads configuration data from @input and configures @config
 * accordingly. Basically this function creates a properly configured
 * #GScanner for you and calls the deserialize function of the
 * @config's #PikaConfigInterface.
 *
 * Returns: Whether deserialization succeeded.
 *
 * Since: 2.10
 **/
gboolean
pika_config_deserialize_stream (PikaConfig    *config,
                                GInputStream  *input,
                                gpointer       data,
                                GError       **error)
{
  GScanner *scanner;
  gboolean  success;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);
  g_return_val_if_fail (G_IS_INPUT_STREAM (input), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  scanner = pika_scanner_new_stream (input, error);
  if (! scanner)
    return FALSE;

  g_object_freeze_notify (G_OBJECT (config));

  success = PIKA_CONFIG_GET_IFACE (config)->deserialize (config,
                                                         scanner, 0, data);

  g_object_thaw_notify (G_OBJECT (config));

  pika_scanner_unref (scanner);

  if (! success)
    g_assert (error == NULL || *error != NULL);

  return success;
}

/**
 * pika_config_deserialize_string:
 * @config:   a #GObject that implements the #PikaConfigInterface.
 * @text: (array length=text_len): string to deserialize (in UTF-8 encoding)
 * @text_len: length of @text in bytes or -1
 * @data:     client data
 * @error:    return location for a possible error
 *
 * Configures @config from @text. Basically this function creates a
 * properly configured #GScanner for you and calls the deserialize
 * function of the @config's #PikaConfigInterface.
 *
 * Returns: %TRUE if deserialization succeeded, %FALSE otherwise.
 *
 * Since: 2.4
 **/
gboolean
pika_config_deserialize_string (PikaConfig   *config,
                                const gchar  *text,
                                gint          text_len,
                                gpointer      data,
                                GError      **error)
{
  GScanner *scanner;
  gboolean  success;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);
  g_return_val_if_fail (text != NULL || text_len == 0, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  scanner = pika_scanner_new_string (text, text_len, error);

  g_object_freeze_notify (G_OBJECT (config));

  success = PIKA_CONFIG_GET_IFACE (config)->deserialize (config,
                                                         scanner, 0, data);

  g_object_thaw_notify (G_OBJECT (config));

  pika_scanner_unref (scanner);

  if (! success)
    g_assert (error == NULL || *error != NULL);

  return success;
}

/**
 * pika_config_deserialize_parasite:
 * @config:   a #GObject that implements the #PikaConfigInterface.
 * @parasite: parasite containing a serialized config string
 * @data:     client data
 * @error:    return location for a possible error
 *
 * Configures @config from @parasite. Basically this function creates
 * a properly configured #GScanner for you and calls the deserialize
 * function of the @config's #PikaConfigInterface.
 *
 * Returns: %TRUE if deserialization succeeded, %FALSE otherwise.
 *
 * Since: 3.0
 **/
gboolean
pika_config_deserialize_parasite (PikaConfig          *config,
                                  const PikaParasite  *parasite,
                                  gpointer             data,
                                  GError             **error)
{
  const gchar *parasite_data;
  guint32      parasite_size;

  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);
  g_return_val_if_fail (parasite != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  parasite_data = pika_parasite_get_data (parasite, &parasite_size);
  if (! parasite_data)
    return TRUE;

  return pika_config_deserialize_string (config, parasite_data, parasite_size,
                                         data, error);
}

/**
 * pika_config_deserialize_return:
 * @scanner:        a #GScanner
 * @expected_token: the expected token
 * @nest_level:     the nest level
 *
 * Returns:
 *
 * Since: 2.4
 **/
gboolean
pika_config_deserialize_return (GScanner     *scanner,
                                GTokenType    expected_token,
                                gint          nest_level)
{
  GTokenType next_token;

  g_return_val_if_fail (scanner != NULL, FALSE);

  next_token = g_scanner_peek_next_token (scanner);

  if (expected_token != G_TOKEN_LEFT_PAREN)
    {
      g_scanner_get_next_token (scanner);
      g_scanner_unexp_token (scanner, expected_token, NULL, NULL, NULL,
                             _("fatal parse error"), TRUE);
      return FALSE;
    }
  else
    {
      if (nest_level > 0 && next_token == G_TOKEN_RIGHT_PAREN)
        {
          return TRUE;
        }
      else if (next_token != G_TOKEN_EOF)
        {
          g_scanner_get_next_token (scanner);
          g_scanner_unexp_token (scanner, expected_token, NULL, NULL, NULL,
                                 _("fatal parse error"), TRUE);
          return FALSE;
        }
    }

  return TRUE;
}


/**
 * pika_config_serialize:
 * @config: an object that implements the #PikaConfigInterface.
 * @writer: the #PikaConfigWriter to use.
 * @data: client data
 *
 * Serialize the #PikaConfig object.
 *
 * Returns: Whether serialization succeeded.
 *
 * Since: 2.8
 **/
gboolean
pika_config_serialize (PikaConfig       *config,
                       PikaConfigWriter *writer,
                       gpointer          data)
{
  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);

  return PIKA_CONFIG_GET_IFACE (config)->serialize (config, writer, data);
}

/**
 * pika_config_deserialize:
 * @config: a #GObject that implements the #PikaConfigInterface.
 * @scanner: the #GScanner to use.
 * @nest_level: the nest level.
 * @data: client data.
 *
 * Deserialize the #PikaConfig object.
 *
 * Returns: Whether serialization succeeded.
 *
 * Since: 2.8
 **/
gboolean
pika_config_deserialize (PikaConfig *config,
                         GScanner   *scanner,
                         gint        nest_level,
                         gpointer    data)
{
  g_return_val_if_fail (PIKA_IS_CONFIG (config), FALSE);

  return PIKA_CONFIG_GET_IFACE (config)->deserialize (config,
                                                      scanner,
                                                      nest_level,
                                                      data);
}

/**
 * pika_config_duplicate:
 * @config: a #GObject that implements the #PikaConfigInterface.
 *
 * Creates a copy of the passed object by copying all object
 * properties. The default implementation of the #PikaConfigInterface
 * only works for objects that are completely defined by their
 * properties.
 *
 * Returns: the duplicated #PikaConfig object
 *
 * Since: 2.4
 **/
gpointer
pika_config_duplicate (PikaConfig *config)
{
  g_return_val_if_fail (PIKA_IS_CONFIG (config), NULL);

  return PIKA_CONFIG_GET_IFACE (config)->duplicate (config);
}

/**
 * pika_config_is_equal_to:
 * @a: a #GObject that implements the #PikaConfigInterface.
 * @b: another #GObject of the same type as @a.
 *
 * Compares the two objects. The default implementation of the
 * #PikaConfigInterface compares the object properties and thus only
 * works for objects that are completely defined by their
 * properties.
 *
 * Returns: %TRUE if the two objects are equal.
 *
 * Since: 2.4
 **/
gboolean
pika_config_is_equal_to (PikaConfig *a,
                         PikaConfig *b)
{
  g_return_val_if_fail (PIKA_IS_CONFIG (a), FALSE);
  g_return_val_if_fail (PIKA_IS_CONFIG (b), FALSE);
  g_return_val_if_fail (G_TYPE_FROM_INSTANCE (a) == G_TYPE_FROM_INSTANCE (b),
                        FALSE);

  return PIKA_CONFIG_GET_IFACE (a)->equal (a, b);
}

/**
 * pika_config_reset:
 * @config: a #GObject that implements the #PikaConfigInterface.
 *
 * Resets the object to its default state. The default implementation of the
 * #PikaConfigInterface only works for objects that are completely defined by
 * their properties.
 *
 * Since: 2.4
 **/
void
pika_config_reset (PikaConfig *config)
{
  g_return_if_fail (PIKA_IS_CONFIG (config));

  g_object_freeze_notify (G_OBJECT (config));

  PIKA_CONFIG_GET_IFACE (config)->reset (config);

  g_object_thaw_notify (G_OBJECT (config));
}

/**
 * pika_config_copy:
 * @src: a #GObject that implements the #PikaConfigInterface.
 * @dest: another #GObject of the same type as @a.
 * @flags: a mask of GParamFlags
 *
 * Compares all read- and write-able properties from @src and @dest
 * that have all @flags set. Differing values are then copied from
 * @src to @dest. If @flags is 0, all differing read/write properties.
 *
 * Properties marked as "construct-only" are not touched.
 *
 * Returns: %TRUE if @dest was modified, %FALSE otherwise
 *
 * Since: 2.6
 **/
gboolean
pika_config_copy (PikaConfig  *src,
                  PikaConfig  *dest,
                  GParamFlags  flags)
{
  gboolean changed;

  g_return_val_if_fail (PIKA_IS_CONFIG (src), FALSE);
  g_return_val_if_fail (PIKA_IS_CONFIG (dest), FALSE);
  g_return_val_if_fail (G_TYPE_FROM_INSTANCE (src) == G_TYPE_FROM_INSTANCE (dest),
                        FALSE);

  g_object_freeze_notify (G_OBJECT (dest));

  changed = PIKA_CONFIG_GET_IFACE (src)->copy (src, dest, flags);

  g_object_thaw_notify (G_OBJECT (dest));

  return changed;
}
