/*
 *  $Id: dict.c 29741 2026-04-06 08:37:30Z yeti-dn $
 *  Copyright (C) 2003-2026 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  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 2 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, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/dict.h"
#include "libgwyddion/serializable-boxed.h"
#include "libgwyddion/serializable-utils.h"

#define TYPE_NAME "GwyDict"

enum {
    SGNL_ITEM_CHANGED,
    NUM_SIGNALS
};

struct _GwyDictPrivate {
    GHashTable *values;
    gboolean in_construction;
};

typedef struct {
    const gchar *strkey;
    GValue *value;
} SerializeKeyVal;

typedef struct {
    GwyDict *dict;
    const gchar *prefix;
    gsize prefix_length;
    guint count;
    gboolean closed_prefix;
    GwyDictForeachFunc func;
    gpointer user_data;
} PrefixData;

typedef struct {
    const gchar *prefix;
    gsize prefix_length;
    GArray *keys;
    gboolean closed_prefix;
    gboolean by_name;
} PrefixSearchData;

typedef struct {
    GwyDict *dict;
    gint nprefixes;
    const gchar **prefixes;
    gboolean *pfxclosed;
    gsize *pfxlengths;
} PrefixListData;

static void             serializable_init         (GwySerializableInterface *iface);
static void             serializable_itemize      (GwySerializable *serializable,
                                                   GwySerializableGroup *group);
static gboolean         serializable_construct    (GwySerializable *serializable,
                                                   GwySerializableGroup *group,
                                                   GwyErrorList **error_list);
static GwySerializable* serializable_copy         (GwySerializable *serializable);
static void             serializable_assign       (GwySerializable *destination,
                                                   GwySerializable *source);
static void             value_destroy_func        (gpointer data);
static void             dispose                   (GObject *object);
static void             finalize                  (GObject *object);
static GValue*          get_value_of_type         (GwyDict *dict,
                                                   GQuark key,
                                                   GType type);
static GValue*          gis_value_of_type         (GwyDict *dict,
                                                   GQuark key,
                                                   GType type);
static void             hash_foreach_func         (gpointer hkey,
                                                   gpointer hvalue,
                                                   gpointer hdata);
static void             keys_foreach_func         (gpointer hkey,
                                                   gpointer hvalue,
                                                   gpointer hdata);
static void             keys_by_name_foreach_func (gpointer hkey,
                                                   gpointer hvalue,
                                                   gpointer hdata);
static void             keys_prefix_foreach_func  (gpointer hkey,
                                                   gpointer hvalue,
                                                   gpointer hdata);
static GwyDict*         duplicate_by_prefix_valist(GwyDict *dict,
                                                   va_list ap);
static GwyDict*         duplicate_by_prefix_do    (GwyDict *dict,
                                                   PrefixListData *pfxlist);
static void             hash_prefix_duplicate_func(gpointer hkey,
                                                   gpointer hvalue,
                                                   gpointer hdata);
static int              pstring_compare_callback  (const void *p,
                                                   const void *q);
static guint            token_length              (const gchar *text);
static gchar*           dequote_token             (const gchar *tok,
                                                   gsize *len);

static GObjectClass *parent_class = NULL;
static guint signals[NUM_SIGNALS];

G_DEFINE_TYPE_WITH_CODE(GwyDict, gwy_dict, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyDict)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->construct = serializable_construct;
    iface->copy      = serializable_copy;
    iface->assign    = serializable_assign;
}

static void
gwy_dict_class_init(GwyDictClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_dict_parent_class;

    gobject_class->dispose = dispose;
    gobject_class->finalize = finalize;

    /**
    * GwyDict::item-changed:
    * @gwydict: The #GwyDict which received the signal.
    * @arg1: The quark key identifying the changed item.
    *
    * The ::item-changed signal is emitted whenever a dictionary item is changed.  The detail is the string key
    * identifier.
    */
    signals[SGNL_ITEM_CHANGED] = g_signal_new("item-changed", type,
                                              G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED | G_SIGNAL_NO_RECURSE,
                                              G_STRUCT_OFFSET(GwyDictClass, item_changed),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__UINT,
                                              G_TYPE_NONE, 1, G_TYPE_UINT);
    g_signal_set_va_marshaller(signals[SGNL_ITEM_CHANGED], type, g_cclosure_marshal_VOID__UINTv);
}

static void
gwy_dict_init(GwyDict *dict)
{
    GwyDictPrivate *priv;

    priv = dict->priv = gwy_dict_get_instance_private(dict);
    priv->values = g_hash_table_new_full(NULL, NULL, NULL, value_destroy_func);
}

static void
dispose(GObject *object)
{
    GwyDict *dict = (GwyDict*)object;

    /* This breaks any reference cycles but keeps the object functional. */
    g_hash_table_remove_all(dict->priv->values);

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

static void
finalize(GObject *object)
{
    GwyDict *dict = (GwyDict*)object;

    g_hash_table_destroy(dict->priv->values);

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

/**
 * gwy_dict_new: (constructor)
 *
 * Creates a new dictionary-like data container.
 *
 * Returns: (transfer full):
 *          A new dictionary.
 **/
GwyDict*
gwy_dict_new(void)
{
    return (GwyDict*)g_object_new(GWY_TYPE_DICT, NULL);
}

/**
 * gwy_dict_new_in_construction: (constructor)
 *
 * Creates a new dictionary-like data container and marks it as being in construction.
 *
 * This is a convenience function calling gwy_dict_start_construction() on the newly created dictionary.
 *
 * Returns: (transfer full):
 *          A new container.
 **/
GwyDict*
gwy_dict_new_in_construction(void)
{
    GwyDict *dict = (GwyDict*)g_object_new(GWY_TYPE_DICT, NULL);
    dict->priv->in_construction = TRUE;
    return dict;
}

static void
value_destroy_func(gpointer data)
{
    GValue *val = (GValue*)data;
#ifdef DEBUG
    GObject *obj = NULL;

    gwy_debug("unsetting value %p, holds object = %d (%s)", val, G_VALUE_HOLDS_OBJECT(val), G_VALUE_TYPE_NAME(val));
    if (G_VALUE_HOLDS_OBJECT(val)) {
        obj = G_OBJECT(g_value_peek_pointer(val));
        gwy_debug("%s refcount = %d", G_OBJECT_TYPE_NAME(obj), obj->ref_count);
    }
#endif
    g_value_unset(val);
    g_free(val);
#ifdef DEBUG
    if (obj)
        gwy_debug("refcount = %d", obj->ref_count);
#endif
}

/**
 * gwy_dict_get_n_items:
 * @dict: A dictionary-like data container.
 *
 * Gets the number of items in a dictionary.
 *
 * Returns: The number of items.
 **/
guint
gwy_dict_get_n_items(GwyDict *dict)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), 0);
    return g_hash_table_size(dict->priv->values);
}

/**
 * gwy_dict_value_type_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the type of value in container @dict identified by name @name.
 **/

/**
 * gwy_dict_value_type:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the type of value in @dict identified by @key.
 *
 * Returns: The value type as #GType; 0 if there is no such value.
 **/
GType
gwy_dict_value_type(GwyDict *dict, GQuark key)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), 0);
    if (!key)
        return 0;

    GValue *p = g_hash_table_lookup(dict->priv->values, GUINT_TO_POINTER(key));
    return p ? G_VALUE_TYPE(p) : 0;
}

/**
 * gwy_dict_contains_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Expands to %TRUE if container @dict contains a value identified by name @name.
 **/

/**
 * gwy_dict_contains:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns %TRUE if @dict contains a value identified by @key.
 *
 * Returns: Whether @dict contains something identified by @key.
 **/
gboolean
gwy_dict_contains(GwyDict *dict, GQuark key)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), 0);
    return key && g_hash_table_lookup(dict->priv->values, GUINT_TO_POINTER(key)) != NULL;
}

/**
 * gwy_dict_remove_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Removes a value identified by name @name from container @dict.
 *
 * Expands to %TRUE if there was such a value and was removed.
 **/

/**
 * gwy_dict_remove:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Removes a value identified by @key from a dictionary.
 *
 * Returns: %TRUE if there was such a value and was removed.
 **/
gboolean
gwy_dict_remove(GwyDict *dict, GQuark key)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), FALSE);
    if (!key)
        return FALSE;

    GwyDictPrivate *priv = dict->priv;
    GValue *value = g_hash_table_lookup(priv->values, GUINT_TO_POINTER(key));
    if (!value)
        return FALSE;
#ifdef DEBUG
    if (G_VALUE_HOLDS_OBJECT(value)) {
        gwy_debug("refcount = %d", G_OBJECT(g_value_peek_pointer(value))->ref_count);
    }
#endif

#ifdef DEBUG
    gwy_debug("holds object = %d", G_VALUE_HOLDS_OBJECT(value));
    if (G_VALUE_HOLDS_OBJECT(value)) {
        gwy_debug("refcount = %d", G_OBJECT(g_value_peek_pointer(value))->ref_count);
    }
#endif
    /* Keep the object removed but physically alive (if we were holding the last reference) until the signal is
     * handled. This is consistent with how we handle item replacement.  */
    GObject *object = NULL;
    if (G_VALUE_HOLDS_OBJECT(value))
        object = g_object_ref(g_value_get_object(value));

    g_hash_table_remove(priv->values, GUINT_TO_POINTER(key));
    if (!priv->in_construction)
        g_signal_emit(dict, signals[SGNL_ITEM_CHANGED], key, key);
    if (object)
        g_object_unref(object);

    return TRUE;
}

static gboolean
prefix_is_closed(const gchar *prefix, guint prefix_length)
{
    if (!prefix || !prefix_length)
        return TRUE;

    return prefix[prefix_length-1] == GWY_DICT_PATHSEP;
}

static gboolean
prefix_matches_key(const gchar *prefix, guint prefix_length, gboolean is_closed,
                   GQuark key, const gchar **pname)
{
    const gchar *name;

    /* Empty prefix matches everything.  But caller may need to obtain the string name himself. */
    if (!prefix_length) {
        if (pname)
            *pname = NULL;
        return TRUE;
    }

    name = g_quark_to_string(key);
    if (pname)
        *pname = name;

    /* XXX: Odd case. */
    if (!name)
        return FALSE;

    /* No match of the initial string parts means different. */
    if (strncmp(name, prefix, prefix_length) != 0)
        return FALSE;

    /* An unclosed prefix must also terminate at the end or separator. */
    if (!is_closed
        && name[prefix_length] != '\0'
        && name[prefix_length] != GWY_DICT_PATHSEP)
        return FALSE;

    return TRUE;
}

/**
 * gwy_dict_remove_by_prefix:
 * @dict: A dictionary-like data container.
 * @prefix: (nullable): A nul-terminated id prefix.
 *
 * Removes a values whose key start with @prefix from container @dict.
 *
 * @prefix can be %NULL, all values are then removed.
 *
 * Returns: The number of values removed.
 **/
guint
gwy_dict_remove_by_prefix(GwyDict *dict, const gchar *prefix)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), 0);

    GwyDictPrivate *priv = dict->priv;
    guint n;
    GQuark *keys = gwy_dict_keys_with_prefix(dict, prefix, &n);
    for (guint i = 0; i < n; i++)
        g_hash_table_remove(priv->values, GUINT_TO_POINTER(keys[i]));
    if (!priv->in_construction) {
        guint signalid = signals[SGNL_ITEM_CHANGED];
        for (guint i = 0; i < n; i++)
            g_signal_emit(dict, signalid, keys[i], keys[i]);
    }
    g_free(keys);

    return n;
}

/**
 * gwy_dict_foreach:
 * @dict: A dictionary-like data container.
 * @prefix: (nullable):
 *          A string container key prefix.
 * @function: (scope call): The function called on the items.
 * @user_data: The user data passed to @function.
 *
 * Calls a function on each item in a dictionary, possibly limited to a prefix.
 *
 * The function must not modify the container contents (see g_hash_table_foreach()). Modifying individual items by
 * changing the contents of a #GValue is strongly discouraged because it is not detectable and no
 * GwyDict::item-changed signal is emitted.
 *
 * Returns: The number of items @function was called on.
 **/
guint
gwy_dict_foreach(GwyDict *dict,
                 const gchar *prefix,
                 GwyDictForeachFunc function,
                 gpointer user_data)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), 0);
    g_return_val_if_fail(function, 0);

    PrefixData pfdata = {
        .dict = dict,
        .prefix_length = prefix ? strlen(prefix) : 0,
        .closed_prefix = prefix_is_closed(prefix, pfdata.prefix_length),
        .count = 0,
        .func = function,
        .user_data = user_data,
    };
    pfdata.prefix = pfdata.prefix_length ? prefix : NULL;
    g_hash_table_foreach(dict->priv->values, hash_foreach_func, &pfdata);

    return pfdata.count;
}

static void
hash_foreach_func(gpointer hkey, gpointer hvalue, gpointer hdata)
{
    GQuark key = GPOINTER_TO_UINT(hkey);
    PrefixData *pfdata = (PrefixData*)hdata;

    if (prefix_matches_key(pfdata->prefix, pfdata->prefix_length, pfdata->closed_prefix, key, NULL)) {
        pfdata->func(key, (GValue*)hvalue, pfdata->user_data);
        pfdata->count++;
    }
}

/**
 * gwy_dict_keys:
 * @dict: A dictionary-like data container.
 *
 * Gets all quark keys of a dictionary.
 *
 * Returns: (array zero-terminated=1):
 *          A newly allocated 0-terminated array with quark keys of all @dict items, in no particular order.  The
 *          number of items can be obtained with gwy_dict_get_n_items().
 **/
GQuark*
gwy_dict_keys(GwyDict *dict)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);
    GwyDictPrivate *priv = dict->priv;
    guint n = g_hash_table_size(priv->values);
    if (!n)
        return g_new0(GQuark, 1);

    GArray *array = g_array_sized_new(FALSE, FALSE, sizeof(GQuark), n);
    g_hash_table_foreach(priv->values, keys_foreach_func, array);

    GQuark sentinel = 0;
    g_array_append_val(array, sentinel);
    return (GQuark*)g_array_free(array, FALSE);
}

static void
keys_foreach_func(gpointer hkey,
                  G_GNUC_UNUSED gpointer hvalue,
                  gpointer hdata)
{
    GQuark key = GPOINTER_TO_UINT(hkey);
    GArray *array = (GArray*)hdata;

    g_array_append_val(array, key);
}

/**
 * gwy_dict_keys_by_name:
 * @dict: A dictionary-like data container.
 *
 * Gets all string keys of a dictionary.
 *
 * Returns: (transfer container) (array zero-terminated=1):
 *          A newly allocated array with string keys of all @dict items, in no particular order.  The number of
 *          items can be obtained with gwy_dict_get_n_items().  Unlike the array the strings are owned by GLib
 *          and must not be freed.
 **/
const gchar**
gwy_dict_keys_by_name(GwyDict *dict)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);
    GwyDictPrivate *priv = dict->priv;
    guint n = g_hash_table_size(priv->values);
    if (!n)
        return g_new0(const gchar*, 1);

    GPtrArray *array = g_ptr_array_sized_new(n+1);
    g_hash_table_foreach(priv->values, keys_by_name_foreach_func, array);
    g_ptr_array_add(array, NULL);
    const gchar **keys = (const gchar**)g_ptr_array_free(array, FALSE);

    return keys;
}

static void
keys_by_name_foreach_func(gpointer hkey,
                          G_GNUC_UNUSED gpointer hvalue,
                          gpointer hdata)
{
    GQuark key = GPOINTER_TO_UINT(hkey);
    GPtrArray *array = (GPtrArray*)hdata;

    g_ptr_array_add(array, (gpointer)g_quark_to_string(key));
}

/**
 * gwy_dict_keys_with_prefix:
 * @dict: A dictionary-like data container.
 * @prefix: A nul-terminated id prefix.
 * @n: (out): Location to store the number of keys to.  Can be %NULL.
 *
 * Gets quark keys of a dictionary that start with given prefix.
 *
 * Returns: (array zero-terminated=1):
 *          A newly allocated array with quark keys, in no particular order. The array is zero-terminated.
 **/
GQuark*
gwy_dict_keys_with_prefix(GwyDict *dict,
                          const gchar *prefix,
                          guint *n)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);

    PrefixSearchData pfdata = {
        .keys = g_array_sized_new(FALSE, FALSE, sizeof(GQuark), 8),
        .prefix_length = prefix ? strlen(prefix) : 0,
        .closed_prefix = prefix_is_closed(prefix, pfdata.prefix_length),
        .by_name = FALSE,
    };
    pfdata.prefix = pfdata.prefix_length ? prefix : NULL;
    g_hash_table_foreach(dict->priv->values, keys_prefix_foreach_func, &pfdata);
    if (n)
        *n = pfdata.keys->len;

    GQuark nokey = 0;
    g_array_append_val(pfdata.keys, nokey);
    return (GQuark*)g_array_free(pfdata.keys, FALSE);
}

/**
 * gwy_dict_keys_with_prefix_by_name:
 * @dict: A dictionary-like data container.
 * @prefix: A nul-terminated id prefix.
 * @n: (out): Location to store the number of keys to.  Can be %NULL.
 *
 * Gets string keys of a dictionary that start with given prefix.
 *
 * Returns: (transfer none) (array zero-terminated=1):
 *          A newly allocated array with string keys, in no particular order. Unlike the array the strings are owned
 *          by GLib and must not be freed.  The array is %NULL-terminated.
 **/
const gchar**
gwy_dict_keys_with_prefix_by_name(GwyDict *dict,
                                  const gchar *prefix,
                                  guint *n)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);

    PrefixSearchData pfdata = {
        .keys = g_array_sized_new(FALSE, FALSE, sizeof(const gchar*), 8),
        .prefix_length = prefix ? strlen(prefix) : 0,
        .closed_prefix = prefix_is_closed(prefix, pfdata.prefix_length),
        .by_name = TRUE,
    };
    pfdata.prefix = pfdata.prefix_length ? prefix : NULL;
    g_hash_table_foreach(dict->priv->values, keys_prefix_foreach_func, &pfdata);
    if (n)
        *n = pfdata.keys->len;

    const gchar *nokey = NULL;
    g_array_append_val(pfdata.keys, nokey);
    return (const gchar**)g_array_free(pfdata.keys, FALSE);
}

static void
keys_prefix_foreach_func(gpointer hkey,
                         G_GNUC_UNUSED gpointer hvalue,
                         gpointer hdata)
{
    GQuark key = GPOINTER_TO_UINT(hkey);
    PrefixSearchData *pfdata = (PrefixSearchData*)hdata;
    const gchar *name;

    if (!prefix_matches_key(pfdata->prefix, pfdata->prefix_length, pfdata->closed_prefix, key, &name))
        return;

    if (pfdata->by_name) {
        if (!name)
            name = g_quark_to_string(key);
        g_array_append_val(pfdata->keys, name);
    }
    else
        g_array_append_val(pfdata->keys, key);
}

/**
 * gwy_dict_rename_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @nn: A nul-terminated name.
 * @f: Whether to delete existing value at @newkey.
 *
 * Makes a value in container @dict identified by name @name to be identified by new name @nn.
 *
 * See gwy_dict_rename() for details.
 **/

/**
 * gwy_dict_rename:
 * @dict: A dictionary-like data container.
 * @key: The current key.
 * @newkey: A new key for the value.
 * @force: Whether to replace existing value at @newkey.
 *
 * Makes a value in @dict identified by @key to be identified by @newkey.
 *
 * When @force is %TRUE existing value at @newkey is removed from @dict. When it's %FALSE, an existing value
 * @newkey inhibits the rename and %FALSE is returned.
 *
 * Returns: Whether the rename succeeded.
 **/
gboolean
gwy_dict_rename(GwyDict *dict,
                GQuark key,
                GQuark newkey,
                gboolean force)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), FALSE);
    g_return_val_if_fail(key, FALSE);
    if (key == newkey)
        return TRUE;

    GwyDictPrivate *priv = dict->priv;
    GValue *value = g_hash_table_lookup(priv->values, GUINT_TO_POINTER(key));
    if (!value)
        return FALSE;

    if (g_hash_table_lookup(priv->values, GUINT_TO_POINTER(newkey))) {
        if (!force)
            return FALSE;

        g_hash_table_remove(priv->values, GUINT_TO_POINTER(newkey));
    }

    g_hash_table_insert(priv->values, GUINT_TO_POINTER(newkey), value);
    g_hash_table_steal(priv->values, GUINT_TO_POINTER(key));
    if (!priv->in_construction) {
        g_signal_emit(dict, signals[SGNL_ITEM_CHANGED], key, key);
        g_signal_emit(dict, signals[SGNL_ITEM_CHANGED], newkey, newkey);
    }

    return TRUE;
}

/**
 * gwy_dict_get_value_of_type:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @type: (nullable): Value type to get.  Can be %NULL to not check value type.
 *
 * Low level function to get a value from a dictionary.
 *
 * The function should only by used for values which exists and have the expected type (it would be a programmer's
 * error if they did not). Use gis_value_of_type() to get value that may not exist.
 *
 * Returns: The value identified by @key.
 **/
static GValue*
get_value_of_type(GwyDict *dict,
                  GQuark key,
                  GType type)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);
    g_return_val_if_fail(key, NULL);

    GValue *p = g_hash_table_lookup(dict->priv->values, GUINT_TO_POINTER(key));
    if (!p) {
        g_warning("%s: no value for key %u (%s)", TYPE_NAME, key, g_quark_to_string(key));
        return NULL;
    }
    if (type && !G_VALUE_HOLDS(p, type)) {
        g_warning("%s: trying to get %s as %s, key %u (%s)",
                  TYPE_NAME, G_VALUE_TYPE_NAME(p), g_type_name(type), key, g_quark_to_string(key));
        return NULL;
    }

    return p;
}

/**
 * gis_value_of_type:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @type: Value type to get.  Can be %NULL to not check value type.
 *
 * Low level function to get a value from a dictionary.
 *
 * The value must have the expected type (it would be a programmer's error if they did not).
 *
 * Returns: The value identified by @key, or %NULL.
 **/
static GValue*
gis_value_of_type(GwyDict *dict,
                  GQuark key,
                  GType type)
{
    if (!key)
        return NULL;
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);

    GValue *p = g_hash_table_lookup(dict->priv->values, GUINT_TO_POINTER(key));
    if (!p)
        return NULL;
    if (type && !G_VALUE_HOLDS(p, type)) {
        g_warning("%s: trying to get %s as %s, key %u (%s)",
                  TYPE_NAME, G_VALUE_TYPE_NAME(p), g_type_name(type), key, g_quark_to_string(key));
        return NULL;
    }

    return p;
}

/* FIXME: This is a misguided API. */
/**
 * gwy_dict_get_value_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the value in container @dict identified by name @name.
 *
 * The returned #GValue should not be modified since such modification is not detectable and would not emit
 * #GwyDict::item-changed.
 **/

/**
 * gwy_dict_get_value:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the value in @dict identified by @key.
 *
 * The returned #GValue should not be modified since such modification is not detectable and would not emit
 * #GwyDict::item-changed.
 *
 * Returns: The value as a #GValue.
 **/
GValue
gwy_dict_get_value(GwyDict *dict, GQuark key)
{
    GValue gvalue;
    gwy_clear1(gvalue);

    GValue *p = get_value_of_type(dict, key, 0);
    if (G_LIKELY(p)) {
        g_value_init(&gvalue, G_VALUE_TYPE(p));
        g_value_copy(p, &gvalue);
    }

    return gvalue;
}

/**
 * gwy_dict_gis_value_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to a #GValue to update. If item does not exist, it is left untouched.
 *
 * Get-if-set a generic value from a dictionary.
 *
 * On success, @value will contain a copy of the data in @dict.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such value in the container.
 **/

/**
 * gwy_dict_gis_value:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to a #GValue to update. If item does not exist, it is left untouched.
 *
 * Get-if-set a generic value from a dictionary.
 *
 * On success, @value will contain a copy of the data in @dict.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such value in the container.
 **/
gboolean
gwy_dict_gis_value(GwyDict *dict,
                   GQuark key,
                   GValue *value)
{
    GValue *p;

    if (!(p = gis_value_of_type(dict, key, 0)))
        return FALSE;

    g_return_val_if_fail(value, FALSE);
    if (G_VALUE_TYPE(value))
        g_value_unset(value);
    g_value_init(value, G_VALUE_TYPE(p));
    g_value_copy(p, value);

    return TRUE;
}

/**
 * gwy_dict_get_boolean_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the boolean in container @dict identified by name @name.
 **/

/**
 * gwy_dict_get_boolean:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the boolean in @dict identified by @key.
 *
 * Returns: The boolean as #gboolean.
 **/
gboolean
gwy_dict_get_boolean(GwyDict *dict, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, G_TYPE_BOOLEAN);
    return G_LIKELY(p) ? !!g_value_get_boolean(p) : FALSE;
}

/**
 * gwy_dict_gis_boolean_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the boolean to update.
 *
 * Get-if-set a boolean from a dictionary.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such boolean in the container.
 **/

/**
 * gwy_dict_gis_boolean:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the boolean to update.
 *
 * Get-if-set a boolean from a dictionary.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such boolean in the container.
 **/
gboolean
gwy_dict_gis_boolean(GwyDict *dict,
                     GQuark key,
                     gboolean *value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, G_TYPE_BOOLEAN))) {
        g_return_val_if_fail(value, FALSE);
        *value = !!g_value_get_boolean(p);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_uchar_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the unsigned character in container @dict identified by name @name.
 **/

/**
 * gwy_dict_get_uchar:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the unsigned character in @dict identified by @key.
 *
 * Returns: The character as #guchar.
 **/
guchar
gwy_dict_get_uchar(GwyDict *dict, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, G_TYPE_UCHAR);
    return G_LIKELY(p) ? g_value_get_uchar(p) : 0;
}

/**
 * gwy_dict_gis_uchar_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the unsigned char to update.
 *
 * Get-if-set an unsigned char from a dictionary.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such unsigned char in the container.
 **/

/**
 * gwy_dict_gis_uchar:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the unsigned char to update.
 *
 * Get-if-set an unsigned char from a dictionary.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such unsigned char in the container.
 **/
gboolean
gwy_dict_gis_uchar(GwyDict *dict,
                   GQuark key,
                   guchar *value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, G_TYPE_UCHAR))) {
        g_return_val_if_fail(value, FALSE);
        *value = g_value_get_uchar(p);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_int32_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the 32bit integer in container @dict identified by name @name.
 **/

/**
 * gwy_dict_get_int32:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the 32bit integer in @dict identified by @key.
 *
 * Returns: The integer as #guint32.
 **/
gint32
gwy_dict_get_int32(GwyDict *dict, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, G_TYPE_INT);
    return G_LIKELY(p) ? g_value_get_int(p) : 0;
}

/**
 * gwy_dict_gis_int32_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the 32bit integer to update.
 *
 * Get-if-set a 32bit integer from a dictionary.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such 32bit integer in the container.
 **/

/**
 * gwy_dict_gis_int32:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the 32bit integer to update.
 *
 * Get-if-set a 32bit integer from a dictionary.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such 32bit integer in the container.
 **/
gboolean
gwy_dict_gis_int32(GwyDict *dict,
                   GQuark key,
                   gint32 *value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, G_TYPE_INT))) {
        g_return_val_if_fail(value, FALSE);
        *value = g_value_get_int(p);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_enum_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the enum in container @dict identified by name @name.
 *
 * Note enums are treated as 32bit integers.
 **/

/**
 * gwy_dict_get_enum:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the enum in @dict identified by @key.
 *
 * Note enums are treated as 32bit integers.
 *
 * Returns: The enum as #gint.
 **/
guint
gwy_dict_get_enum(GwyDict *dict, GQuark key)
{
    return gwy_dict_get_int32(dict, key);
}

/**
 * gwy_dict_gis_enum_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the enum to update.
 *
 * Get-if-set an enum from a dictionary.
 *
 * Note enums are treated as 32bit integers.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such enum in the container.
 **/

/**
 * gwy_dict_gis_enum:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the enum to update.
 *
 * Get-if-set an enum from a dictionary.
 *
 * Note enums are treated as 32bit integers.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such enum in the container.
 **/
/* FIXME: this is probably wrong.  It's here to localize the problem with enum/int/int32 exchanging in a one place. */
gboolean
gwy_dict_gis_enum(GwyDict *dict,
                  GQuark key,
                  guint *value)
{
    gint32 value32;

    if (gwy_dict_gis_int32(dict, key, &value32)) {
        g_return_val_if_fail(value, FALSE);
        *value = value32;
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_int64_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the 64bit integer in container @dict identified by name @name.
 **/

/**
 * gwy_dict_get_int64:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the 64bit integer in @dict identified by @key.
 *
 * Returns: The 64bit integer as #guint64.
 **/
gint64
gwy_dict_get_int64(GwyDict *dict, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, G_TYPE_INT64);
    return G_LIKELY(p) ? g_value_get_int64(p) : 0;
}

/**
 * gwy_dict_gis_int64_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the 64bit integer to update.
 *
 * Get-if-set a 64bit integer from a dictionary.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such 64bit integer in the container.
 **/

/**
 * gwy_dict_gis_int64:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the 64bit integer to update.
 *
 * Get-if-set a 64bit integer from a dictionary.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such 64bit integer in the container.
 **/
gboolean
gwy_dict_gis_int64(GwyDict *dict,
                   GQuark key,
                   gint64 *value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, G_TYPE_INT64))) {
        g_return_val_if_fail(value, FALSE);
        *value = g_value_get_int64(p);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_double_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the double in container @dict identified by name @name.
 **/

/**
 * gwy_dict_get_double:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the double in @dict identified by @key.
 *
 * Returns: The double as #gdouble.
 **/
gdouble
gwy_dict_get_double(GwyDict *dict, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, G_TYPE_DOUBLE);
    return G_LIKELY(p) ? g_value_get_double(p) : 0.0;
}

/**
 * gwy_dict_gis_double_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the double to update.
 *
 * Get-if-set a double from a dictionary.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such double in the container.
 **/

/**
 * gwy_dict_gis_double:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the double to update.
 *
 * Get-if-set a double from a dictionary.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such double in the container.
 **/
gboolean
gwy_dict_gis_double(GwyDict *dict,
                    GQuark key,
                    gdouble *value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, G_TYPE_DOUBLE))) {
        g_return_val_if_fail(value, FALSE);
        *value = g_value_get_double(p);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_string_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the string in container @dict identified by name @name.
 *
 * The returned string must be treated as constant and never freed or modified.
 **/

/**
 * gwy_dict_get_string:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the string in @dict identified by @key.
 *
 * The returned string must be treated as constant and never freed or modified.
 *
 * Returns: The string.
 **/
const gchar*
gwy_dict_get_string(GwyDict *dict, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, G_TYPE_STRING);
    return G_LIKELY(p) ? g_value_get_string(p) : NULL;
}

/**
 * gwy_dict_gis_string_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the string pointer to update.
 *
 * Get-if-set a string from a dictionary.
 *
 * The string possibly stored in @value must be treated as constant and never freed or modified.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such string in the container.
 **/

/**
 * gwy_dict_gis_string:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the string pointer to update.
 *
 * Get-if-set a string from a dictionary.
 *
 * The string possibly stored in @value must be treated as constant and never freed or modified.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such string in the container.
 **/
gboolean
gwy_dict_gis_string(GwyDict *dict,
                    GQuark key,
                    const gchar **value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, G_TYPE_STRING))) {
        g_return_val_if_fail(value, FALSE);
        *value = g_value_get_string(p);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_object_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 *
 * Gets the object in container @dict identified by name @name.
 *
 * The returned object does not have its reference count increased, use g_object_ref() if you want to access it even
 * when @dict may cease to exist.
 **/

/**
 * gwy_dict_get_object:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 *
 * Returns the object in @dict identified by @key.
 *
 * The returned object does not have its reference count increased, use g_object_ref() if you want to access it even
 * when @dict may cease to exist.
 *
 * Returns: The object as #gpointer.
 **/
gpointer
gwy_dict_get_object(GwyDict *dict, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, G_TYPE_OBJECT);
    return G_LIKELY(p) ? g_value_get_object(p) : NULL;
}

/**
 * gwy_dict_gis_object_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to the object pointer to update.
 *
 * Get-if-set an object from a dictionary.
 *
 * The object possibly stored in @value doesn't have its reference count increased, use g_object_ref() if you want
 * to access it even when @dict may cease to exist.
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such object in the container.
 **/

/**
 * gwy_dict_gis_object:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: (out): Pointer to the object pointer to update.
 *
 * Get-if-set an object from a dictionary.
 *
 * The object possibly stored in @value doesn't have its reference count increased, use g_object_ref() if you want
 * to access it even when @dict may cease to exist.
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such object in the container.
 **/
gboolean
gwy_dict_gis_object(GwyDict *dict,
                    GQuark key,
                    gpointer value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, G_TYPE_OBJECT))) {
        g_return_val_if_fail(value, FALSE);
        *(GObject**)value = g_value_get_object(p);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_dict_get_boxed_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @type: The boxed type.
 *
 * Gets the boxed pointer in container @dict identified by name @name.
 *
 * See gwy_dict_get_boxed().
 **/

/**
 * gwy_dict_get_boxed:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @type: The boxed type.
 *
 * Returns the boxed pointer in @dict identified by @key.
 *
 * It is an error to request a boxed pointer of different type than is actually stored.
 *
 * Returns: The boxed pointer as #gpointer.
 **/
gpointer
gwy_dict_get_boxed(GwyDict *dict, GType type, GQuark key)
{
    GValue *p = get_value_of_type(dict, key, type);
    return G_LIKELY(p) ? g_value_get_boxed(p) : NULL;
}

/**
 * gwy_dict_gis_boxed_by_name:
 * @dict: A dictionary-like data container.
 * @type: The boxed type.
 * @name: A nul-terminated name.
 * @value: The boxed pointer to update.
 *
 * Get-if-set a boxed pointer from a dictionary.
 *
 * It is an error to request a boxed pointer of different type than is actually stored.
 *
 * The boxed pointer is copied to @value by value, which is always possbile as only serialisable boxed types can be
 * stored in a #GwyDict. The @value pointer thus does not have the extra level of indirection objects and strings
 * have:
 * |[
 * GwyXYZ xyz;
 * gwy_dict_gis_boxed_by_name(dict, GWY_TYPE_XYZ, "/some/name", &xyz);
 * ]|
 *
 * Expands to %TRUE if @value was actually updated, %FALSE when there is no such boxed in the container.
 **/

/**
 * gwy_dict_gis_boxed:
 * @dict: A dictionary-like data container.
 * @type: The boxed type.
 * @key: A #GQuark key.
 * @value: (out): The boxed value to update.
 *
 * Get-if-set a boxed pointer from a dictionary.
 *
 * It is an error to request a boxed pointer of different type than is actually stored.
 *
 * The boxed pointer is copied to @value by value, which is always possbile as only serialisable boxed types can be
 * stored in a #GwyDict. The @value pointer thus does not have the extra level of indirection objects and strings
 * have:
 * |[
 * GwyXYZ xyz;
 * gwy_dict_gis_boxed(dict, GWY_TYPE_XYZ, quark, &xyz);
 * ]|
 *
 * Returns: %TRUE if @value was actually updated, %FALSE when there is no such boxed in the container.
 **/
gboolean
gwy_dict_gis_boxed(GwyDict *dict,
                   GType type,
                   GQuark key,
                   gpointer value)
{
    GValue *p;

    if ((p = gis_value_of_type(dict, key, type))) {
        g_return_val_if_fail(value, FALSE);
        gwy_serializable_boxed_assign(type, value, g_value_get_boxed(p));
        return TRUE;
    }
    return FALSE;
}

static gboolean
values_equal(GValue *value1, GValue *value2)
{
    g_return_val_if_fail(value1 || value2, TRUE);
    if (!value1 || !value2)
        return FALSE;

    GType type = G_VALUE_TYPE(value1);
    if (type != G_VALUE_TYPE(value2))
        return FALSE;

    if (type == G_TYPE_BOOLEAN)
        return !g_value_get_boolean(value1) == !g_value_get_boolean(value2);
    if (type == G_TYPE_UCHAR)
        return g_value_get_uchar(value1) == g_value_get_uchar(value2);
    if (type == G_TYPE_INT)
        return g_value_get_int(value1) == g_value_get_int(value2);
    if (type == G_TYPE_INT64)
        return g_value_get_int64(value1) == g_value_get_int64(value2);
    if (type == G_TYPE_DOUBLE)
        return g_value_get_double(value1) == g_value_get_double(value2);
    if (type == G_TYPE_STRING)
        return gwy_strequal(g_value_get_string(value1), g_value_get_string(value2));
    /* Objects must be identical (not just equal-valued), so compare addresses. */
    if (g_type_is_a(type, G_TYPE_OBJECT))
        return g_value_get_object(value1) == g_value_get_object(value2);
    /* Boxed are compared by content. */
    if (g_type_is_a(type, G_TYPE_BOXED))
        return gwy_serializable_boxed_equal(type, g_value_get_boxed(value1), g_value_get_boxed(value2));

    g_return_val_if_reached(FALSE);
}

static GValue*
check_one_value(GwyDict *dict, GQuark key, GType type,
                gboolean *modify_existing)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);
    g_return_val_if_fail(key, NULL);
    g_return_val_if_fail(type, NULL);

    GwyDictPrivate *priv = dict->priv;
    GValue *value = g_hash_table_lookup(priv->values, GUINT_TO_POINTER(key));
    if (value && g_type_is_a(G_VALUE_TYPE(value), type)) {
        /* This is the only case when setting a value can in fact be a no-op. The curent value is of the correct
         * type so just return it, noting that we do not want to do g_hash_table_insert(). */
        *modify_existing = TRUE;
    }
    else {
        /* If the value does not exist or is of the wrong type, it cannot be unchanged by setting the new value.
         * Create a new GValue. Postpone setting it because that can free the current one, which we do not want to do
         * before setting the new one. */
        *modify_existing = FALSE;
        value = g_new0(GValue, 1);
        g_value_init(value, type);
    }
    return value;
}

static void
finish_setting_one_value(GwyDict *dict, GQuark key, GValue *value, gboolean modify_existing)
{
    GwyDictPrivate *priv = dict->priv;
    if (!modify_existing) {
        /* At this point value_destroy_func() can be invoked, freeing the old value. It is OK, because at this point
         * we are done. */
        g_hash_table_insert(priv->values, GUINT_TO_POINTER(key), value);
    }
    if (!priv->in_construction)
        g_signal_emit(dict, signals[SGNL_ITEM_CHANGED], key, key);
}

/**
 * gwy_dict_set_value_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: Pointer to a #GValue to set.
 *
 * Inserts or updates one value in a dictionary.
 *
 * The value is copied and the caller remains responsible for unsetting @value.
 **/
/**
 * gwy_dict_set_value:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: Pointer to a #GValue to set.
 *
 * Inserts or updates one value in a dictionary.
 *
 * The value is copied and the caller remains responsible for unsetting @value.
 **/
void
gwy_dict_set_value(GwyDict *dict,
                   GQuark key,
                   GValue *value)
{
    g_return_if_fail(G_IS_VALUE(value));

    /* Restrict the values to the usual supported types. */
    GType type = G_VALUE_TYPE(value);
    if (g_type_is_a(type, G_TYPE_OBJECT)) {
        GObject *object = g_value_peek_pointer(value);
        g_return_if_fail(GWY_IS_SERIALIZABLE(object));
    }
    else if (g_type_is_a(type, G_TYPE_BOXED)) {
        g_return_if_fail(gwy_boxed_type_is_serializable(type));
    }
    else {
        g_return_if_fail(type == G_TYPE_CHAR || type == G_TYPE_UCHAR
                         || type == G_TYPE_INT || type == G_TYPE_UINT
                         || type == G_TYPE_INT64 || type == G_TYPE_UINT64
                         || type == G_TYPE_ENUM || type == G_TYPE_FLAGS
                         || type == G_TYPE_DOUBLE
                         || type == G_TYPE_STRING
                         || type == G_TYPE_BOOLEAN);
    }

    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, type, &modify_existing)))
        return;

    /* We could reuse the existing GValue in more cases here. However, we still want to invoke value_destroy_func() at
     * the very end, i.e. need the old and new coexist. */
    if (modify_existing && values_equal(gvalue, value))
        return;
    g_value_copy(value, gvalue);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_set_boolean_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: A boolean.
 *
 * Stores a boolean into container @dict, identified by name @name.
 **/

/**
 * gwy_dict_set_boolean:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: A boolean.
 *
 * Stores a boolean into @dict, identified by @key.
 **/
void
gwy_dict_set_boolean(GwyDict *dict,
                     GQuark key,
                     gboolean value)
{
    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_TYPE_BOOLEAN, &modify_existing)))
        return;

    value = !!value;
    if (modify_existing && g_value_get_boolean(gvalue) == value)
        return;
    g_value_set_boolean(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_set_uchar_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: An unsigned character.
 *
 * Stores an unsigned character into container @dict, identified by name @name.
 **/

/**
 * gwy_dict_set_uchar:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: An unsigned character.
 *
 * Stores an unsigned character into @dict, identified by @key.
 **/
void
gwy_dict_set_uchar(GwyDict *dict,
                   GQuark key,
                   guchar value)
{
    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_TYPE_UCHAR, &modify_existing)))
        return;

    if (modify_existing && g_value_get_uchar(gvalue) == value)
        return;
    g_value_set_uchar(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_set_int32_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: A 32bit integer.
 *
 * Stores a 32bit integer into container @dict, identified by name @name.
 **/

/**
 * gwy_dict_set_int32:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: A 32bit integer.
 *
 * Stores a 32bit integer into @dict, identified by @key.
 **/
void
gwy_dict_set_int32(GwyDict *dict,
                   GQuark key,
                   gint32 value)
{
    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_TYPE_INT, &modify_existing)))
        return;

    if (modify_existing && g_value_get_int(gvalue) == value)
        return;
    g_value_set_int(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_set_enum_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: An enum.
 *
 * Stores an enum into container @dict, identified by name @name.
 *
 * Note enums are treated as 32bit integers.
 **/

/**
 * gwy_dict_set_enum:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: An enum integer.
 *
 * Stores an enum into @dict, identified by @key.
 *
 * Note enums are treated as 32bit integers.
 **/
void
gwy_dict_set_enum(GwyDict *dict,
                  GQuark key,
                  guint value)
{
    gint32 value32 = value;

    gwy_dict_set_int32(dict, key, value32);
}

/**
 * gwy_dict_set_int64_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: A 64bit integer.
 *
 * Stores a 64bit integer into container @dict, identified by name @name.
 **/

/**
 * gwy_dict_set_int64:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: A 64bit integer.
 *
 * Stores a 64bit integer into @dict, identified by @key.
 **/
void
gwy_dict_set_int64(GwyDict *dict,
                   GQuark key,
                   gint64 value)
{
    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_TYPE_INT64, &modify_existing)))
        return;

    if (modify_existing && g_value_get_int64(gvalue) == value)
        return;
    g_value_set_int64(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_set_double_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: A double integer.
 *
 * Stores a double into container @dict, identified by name @name.
 **/

/**
 * gwy_dict_set_double:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: A double.
 *
 * Stores a double into @dict, identified by @key.
 **/
void
gwy_dict_set_double(GwyDict *dict,
                    GQuark key,
                    gdouble value)
{
    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_TYPE_DOUBLE, &modify_existing)))
        return;

    if (modify_existing && g_value_get_double(gvalue) == value)
        return;
    g_value_set_double(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_pass_string_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: A nul-terminated string.
 *
 * Stores a string into container @dict, identified by name @name.
 *
 * See gwy_dict_pass_string() for details.
 **/

/**
 * gwy_dict_pass_string:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: A nul-terminated string.
 *
 * Stores a string into @dict, identified by @key.
 *
 * Since the ownership is passed to the container, you must not access the string data afterwards. The container is
 * allowed to immediately free @value, for example, when it already holds an identical string at @key.
 **/
void
gwy_dict_pass_string(GwyDict *dict,
                     GQuark key,
                     gchar *value)
{
    g_return_if_fail(value);

    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_TYPE_STRING, &modify_existing)))
        return;

    if (modify_existing && gwy_strequal(g_value_get_string(gvalue), value)) {
        /* XXX: There is apparently code relying on @value being consumed but kept alive, not just immediately freed
         * by this function. Said code is wrong. But for now, replace physically the string, just do not emit any
         * signals. */
        g_value_take_string(gvalue, value);
        return;
    }
    g_value_take_string(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_set_string_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: A nul-terminated string.
 *
 * Stores a string into container @dict, identified by name @name.
 *
 * The container makes a copy of the string, so it can be used on static strings and strings owned by other entities.
 **/

/**
 * gwy_dict_set_string:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: A nul-terminated string.
 *
 * Stores a string into @dict, identified by @key.
 *
 * The container makes a copy of the string, so it can be used on static strings and strings owned by other entities.
 **/
void
gwy_dict_set_string(GwyDict *dict,
                    GQuark key,
                    const gchar *value)
{
    g_return_if_fail(value);

    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_TYPE_STRING, &modify_existing)))
        return;

    if (modify_existing && gwy_strequal(g_value_get_string(gvalue), value))
        return;
    g_value_set_string(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

static void
set_or_pass_object(GwyDict *dict,
                   GQuark key,
                   gpointer value,
                   gboolean pass_ownership)
{
    g_return_if_fail(value && GWY_IS_SERIALIZABLE(value));

    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, G_OBJECT_TYPE(value), &modify_existing)))
        return;

    gpointer old_object = NULL;
    if (modify_existing) {
        old_object = g_value_get_object(gvalue);
        if (old_object == value) {
            /* This is kind of weird because the caller must be somehow getting new references to the object to pass
             * them to us again and again. Be consistent and implement ‘caller does not need to unref herself’.
             * Keep holding just the one references we already hold though. */
            if (pass_ownership)
                g_object_unref(value);
            return;
        }
        /* Be extra paranoid and keep the old object alive until we are completely done. If something bad happens
         * anyway it is clearly caller's bug then. */
        g_object_ref(old_object);
    }
    if (pass_ownership)
        g_value_take_object(gvalue, value);
    else
        g_value_set_object(gvalue, value);

    finish_setting_one_value(dict, key, gvalue, modify_existing);
    g_clear_object(&old_object);
}

/**
 * gwy_dict_set_object_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: An object to store into container.
 *
 * Stores an object into container @dict, identified by name @name.
 *
 * See gwy_dict_set_object() for details.
 **/

/**
 * gwy_dict_set_object:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: An object to store into container.
 *
 * Stores an object into @dict, identified by @key.
 *
 * The container claims ownership on the object, i.e. its reference count is incremented. Use
 * gwy_dict_pass_object() to pass your reference to @dict.
 *
 * The object must implement #GwySerializable interface to allow serialization of the container.
 **/
void
gwy_dict_set_object(GwyDict *dict,
                    GQuark key,
                    gpointer value)
{
    set_or_pass_object(dict, key, value, FALSE);
}

/**
 * gwy_dict_pass_object_by_name:
 * @dict: A dictionary-like data container.
 * @name: A nul-terminated name.
 * @value: An object to store into container.
 *
 * Stores an object into container @dict, identified by name @name, passing reference to the container.
 *
 * See gwy_dict_pass_object() for details.
 **/

/**
 * gwy_dict_pass_object:
 * @dict: A dictionary-like data container.
 * @key: A #GQuark key.
 * @value: An object to store into container.
 *
 * Stores an object into @dict, identified by @key, passing reference to the container.
 *
 * The reference on the object you hold is passed to @dict. This is often useful when importing data as you do
 * not have to (in fact must not) release your reference afterwards. Use gwy_dict_set_object() if you want to
 * keep your reference.
 *
 * The object must implement #GwySerializable interface to allow serialization of the container.
 **/
void
gwy_dict_pass_object(GwyDict *dict,
                     GQuark key,
                     gpointer value)
{
    set_or_pass_object(dict, key, value, TRUE);
}

static void
set_or_pass_boxed(GwyDict *dict,
                  GQuark key,
                  GType type,
                  gpointer value,
                  gboolean pass_ownership)
{
    g_return_if_fail(value && G_TYPE_IS_BOXED(type) && gwy_boxed_type_is_serializable(type));

    gboolean modify_existing;
    GValue *gvalue;
    if (!(gvalue = check_one_value(dict, key, type, &modify_existing)))
        return;

    gpointer old_boxed = NULL;
    if (modify_existing) {
        old_boxed = g_value_get_boxed(gvalue);
        if (gwy_serializable_boxed_equal(type, old_boxed, value)) {
            if (pass_ownership)
                g_boxed_free(type, value);
            return;
        }
    }
    if (pass_ownership)
        g_value_take_boxed(gvalue, value);
    else
        g_value_take_boxed(gvalue, g_boxed_copy(type, value));

    finish_setting_one_value(dict, key, gvalue, modify_existing);
}

/**
 * gwy_dict_set_boxed_by_name:
 * @dict: A dictionary-like data container.
 * @type: The boxed type.
 * @name: A nul-terminated name.
 * @value: An boxed to store into container.
 *
 * Stores a boxed pointer into container @dict, identified by name @name.
 *
 * See gwy_dict_set_boxed() for details.
 **/

/**
 * gwy_dict_set_boxed:
 * @dict: A dictionary-like data container.
 * @type: The type of @value.
 * @key: A #GQuark key.
 * @value: An boxed pointer to store into container.
 *
 * Stores a boxed pointer into @dict, identified by @key.
 *
 * The container makes a copy of the boxed pointer using g_boxed_copy(). The caller remains responsible for freeing
 * @value with g_boxed_free().
 *
 * The boxed type must be serializable to allow serialization of the container.
 **/
void
gwy_dict_set_boxed(GwyDict *dict,
                   GType type,
                   GQuark key,
                   gpointer value)
{
    set_or_pass_boxed(dict, key, type, value, FALSE);
}

/**
 * gwy_dict_pass_boxed_by_name:
 * @dict: A dictionary-like data container.
 * @type: The boxed type.
 * @name: A nul-terminated name.
 * @value: An boxed to store into container.
 *
 * Stores an boxed pointer into container @dict, identified by name @name, passing ownership to the container.
 *
 * See gwy_dict_pass_boxed() for details.
 **/

/**
 * gwy_dict_pass_boxed:
 * @dict: A dictionary-like data container.
 * @type: The type of @value.
 * @key: A #GQuark key.
 * @value: An boxed pointer to store into container.
 *
 * Stores a boxed pointer into @dict, identified by @key, passing ownership to the container.
 *
 * Since the ownership is passed to the container, you must not access the boxed data afterwards. The container is
 * allowed to immediately free @value using g_boxed_free(), for example, when it already holds an identical value at
 * @key. In general, since boxed pointers are not reference counted, passing the ownership is prone to subtle errors.
 * Unless you created the boxed pointer in order to just pass it to @dict and forget about it, it is safer to use
 * gwy_dict_set_boxed().
 *
 * The boxed type must be serializable to allow serialization of the container.
 **/
void
gwy_dict_pass_boxed(GwyDict *dict,
                    GType type,
                    GQuark key,
                    gpointer value)
{
    set_or_pass_boxed(dict, key, type, value, TRUE);
}

static int
compare_keyvals(const void *pa, const void *pb)
{
    const SerializeKeyVal *a = (const SerializeKeyVal*)pa;
    const SerializeKeyVal *b = (const SerializeKeyVal*)pb;
    return strcmp(a->strkey, b->strkey);
}

static void
remember_keyvals_func(gpointer hkey, gpointer hvalue, gpointer hdata)
{
    GQuark key = GPOINTER_TO_UINT(hkey);
    GValue *value = (GValue*)hvalue;
    GArray *keyvals = (GArray*)hdata;
    SerializeKeyVal kv = { .strkey = g_quark_to_string(key), .value = value };
    g_array_append_val(keyvals, kv);
}

static void
serialize_one_keyval(SerializeKeyVal *kv, GwySerializableGroup *group)
{
    GValue *value = kv->value;
    GwySerializableItem it;
    GType type = G_VALUE_TYPE(value);

    it.flags = 0;
    it.aux.pspec = NULL;
    it.name = kv->strkey;
    if (type == G_TYPE_BOOLEAN) {
        it.ctype = GWY_SERIALIZABLE_BOOLEAN;
        it.value.v_boolean = !!g_value_get_boolean(value);
    }
    else if (type == G_TYPE_UCHAR) {
        it.ctype = GWY_SERIALIZABLE_INT8;
        it.value.v_uint8  = g_value_get_uchar(value);
    }
    else if (type == G_TYPE_INT) {
        it.ctype = GWY_SERIALIZABLE_INT32;
        it.value.v_int32 = g_value_get_int(value);
    }
    else if (type == G_TYPE_INT64) {
        it.ctype = GWY_SERIALIZABLE_INT64;
        it.value.v_int64 = g_value_get_int64(value);
    }
    else if (type == G_TYPE_DOUBLE) {
        it.ctype = GWY_SERIALIZABLE_DOUBLE;
        it.value.v_double = g_value_get_double(value);
    }
    else if (type == G_TYPE_STRING) {
        it.ctype = GWY_SERIALIZABLE_STRING;
        /* Const-cast. It is private to the serialisation and no one is going to free it. */
        it.value.v_string = (gchar*)g_value_get_string(value);
    }
    else if (g_type_is_a(type, G_TYPE_OBJECT)) {
        it.ctype = GWY_SERIALIZABLE_OBJECT;
        it.value.v_object = g_value_get_object(value);
    }
    else if (g_type_is_a(type, G_TYPE_BOXED)) {
        it.ctype = GWY_SERIALIZABLE_BOXED;
        it.value.v_boxed = g_value_get_boxed(value);
        it.aux.boxed_type = type;
    }
    else {
        /* This is not technically true. We can pack a few other types. However, GwyDict does not have values of
         * these types. */
        g_warning("Cannot pack GValue holding %s", g_type_name(type));
        return;
    }
    gwy_serializable_group_append(group, &it, 1);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyDictPrivate *priv = GWY_DICT(serializable)->priv;
    guint n = g_hash_table_size(priv->values);

    /* Resize to a slightly larger size than necessary for GwyFile's itemize(). */
    gwy_serializable_group_alloc_size(group, n+2);
    /* We want to store the container items sorted. It is an extra hassle because we cannot simply sort the resulting
     * items. They will already contain items from children objects which we would mess up.
     *
     * Make a list of the (string-key, GValue) pairs, sort that and go through it without involving the hash table any
     * further. Also just take the GValue pointers to avoid copying GValues and doing things like refcounting. */
    GArray *keyvals = g_array_sized_new(FALSE, FALSE, sizeof(SerializeKeyVal), n);
    g_hash_table_foreach(priv->values, remember_keyvals_func, keyvals);
    g_assert(keyvals->len == n);
    qsort(keyvals->data, n, sizeof(SerializeKeyVal), compare_keyvals);
    for (guint i = 0; i < n; i++)
        serialize_one_keyval(&g_array_index(keyvals, SerializeKeyVal, i), group);
    g_array_free(keyvals, TRUE);
    gwy_serializable_group_itemize(group);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwyDict *dict = GWY_DICT(serializable);
    GwyDictPrivate *priv = dict->priv;
    gsize n;
    GwySerializableItem *items = gwy_serializable_group_items(group, &n);
    gboolean was_in_construction = priv->in_construction;

    priv->in_construction = TRUE;
    for (gsize i = 0; i < n; i++) {
        GwySerializableItem *it = items + i;
        gwy_debug("value: #%" G_GSIZE_FORMAT ": <%s> of <%c>", i, it->name, it->ctype);
        GwySerializableCType ctype = it->ctype;
        /* A child class has already consumed the item. */
        if (ctype == GWY_SERIALIZABLE_CONSUMED)
            continue;

        GQuark key = g_quark_from_string(it->name);
        if (ctype == GWY_SERIALIZABLE_BOOLEAN)
            gwy_dict_set_boolean(dict, key, it->value.v_boolean);
        else if (ctype == GWY_SERIALIZABLE_INT8)
            gwy_dict_set_uchar(dict, key, it->value.v_uint8);
        else if (ctype == GWY_SERIALIZABLE_INT32)
            gwy_dict_set_int32(dict, key, it->value.v_int32);
        else if (ctype == GWY_SERIALIZABLE_INT64)
            gwy_dict_set_int64(dict, key, it->value.v_int64);
        else if (ctype == GWY_SERIALIZABLE_DOUBLE)
            gwy_dict_set_double(dict, key, it->value.v_double);
        else if (ctype == GWY_SERIALIZABLE_STRING)
            gwy_dict_pass_string(dict, key, it->value.v_string);
        else if (ctype == GWY_SERIALIZABLE_OBJECT) {
            if (it->value.v_object)
                set_or_pass_object(dict, key, it->value.v_object, TRUE);
        }
        else if (ctype == GWY_SERIALIZABLE_BOXED) {
            if (it->value.v_boxed)
                set_or_pass_boxed(dict, key, it->aux.boxed_type, it->value.v_boxed, TRUE);
        }
        else {
            gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                               // TRANSLATORS: Error message.
                               _("GwyDict cannot store data of type 0x%02x."), it->ctype);
            continue;
        }
        it->ctype = GWY_SERIALIZABLE_CONSUMED;
    }
    priv->in_construction = was_in_construction;

    return TRUE;
}

static inline void
duplicate_one_value(GQuark key, GValue *value, GwyDict *target)
{
    GType type = G_VALUE_TYPE(value);

    if (type == G_TYPE_BOOLEAN || type == G_TYPE_UCHAR
        || type == G_TYPE_INT || type == G_TYPE_INT64 || type == G_TYPE_DOUBLE
        || type == G_TYPE_STRING)
        gwy_dict_set_value(target, key, value);
    else if (g_type_is_a(type, G_TYPE_OBJECT)) {
        /* Objects have to be handled separately since we want a deep copy. */
        gwy_dict_pass_object(target, key, gwy_serializable_copy(GWY_SERIALIZABLE(g_value_get_object(value))));
    }
    else if (g_type_is_a(type, G_TYPE_BOXED)) {
        /* Boxed are set normally by value, but do the test at the end as it is more expensive. */
        gwy_dict_set_value(target, key, value);
    }
    else {
        g_warning("Cannot duplicate item of type %s", g_type_name(type));
    }
}

static void
copy_value_func(gpointer hkey, gpointer hvalue, gpointer hdata)
{
    duplicate_one_value(GPOINTER_TO_UINT(hkey), (GValue*)hvalue, (GwyDict*)hdata);
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    /* Create an object of @serializable's type for easier subclassing. */
    GwyDict *copy = g_object_new(G_OBJECT_TYPE(serializable), NULL);
    copy->priv->in_construction = TRUE;
    g_hash_table_foreach(GWY_DICT(serializable)->priv->values, copy_value_func, copy);
    copy->priv->in_construction = FALSE;
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GHashTable *srcvalues = GWY_DICT(source)->priv->values;
    GHashTable *destvalues = GWY_DICT(destination)->priv->values;
    g_hash_table_remove_all(destvalues);
    g_hash_table_foreach(srcvalues, copy_value_func, GWY_DICT(destination));
}

/**
 * gwy_dict_copy:
 * @dict: A dictionary-like data container to duplicate.
 *
 * Create a new container as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Signal ::item-changed is not emitted during the construction of the copy (in case you manage to get hold on the
 * returned object before the copying is finished). The new object is not marked as being in construction, regardless
 * of the state of @dict.
 *
 * Returns: (transfer full):
 *          A copy of the container.
 **/
GwyDict*
gwy_dict_copy(GwyDict *dict)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_DICT(dict)) {
        g_assert(GWY_IS_DICT(dict));
        return g_object_new(GWY_TYPE_DICT, NULL);
    }
    return GWY_DICT(gwy_serializable_copy(GWY_SERIALIZABLE(dict)));
}

/**
 * gwy_dict_assign:
 * @destination: Target container.
 * @source: Source container.
 *
 * Makes one container equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 *
 * Signal ::item-changed is emitted as usual, making the operation possibly relatively expensive. The in-construction
 * state of either object is unchanged.
 **/
void
gwy_dict_assign(GwyDict *destination, GwyDict *source)
{
    g_return_if_fail(GWY_IS_DICT(destination));
    g_return_if_fail(GWY_IS_DICT(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}

/**
 * gwy_dict_duplicate_by_prefix:
 * @dict: A dictionary-like data container.
 * @...: A %NULL-terminated list of string keys.
 *
 * Duplicates a dictionary keeping only values under given prefixes.
 *
 * Like gwy_dict_copy(), this method creates a deep copy, that is contained object are physically duplicated
 * too, not just referenced again.
 *
 * Signal ::item-changed is not emitted during the construction of the copy (in case you manage to get hold on the
 * returned object before the copying is finished).
 *
 * Returns: (transfer full): A newly created container.
 **/
GwyDict*
gwy_dict_duplicate_by_prefix(GwyDict *dict, ...)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);

    va_list ap;
    va_start(ap, dict);
    GwyDict *duplicate = duplicate_by_prefix_valist(dict, ap);
    va_end(ap);

    return duplicate;
}

/**
 * gwy_dict_duplicate_by_prefixv:
 * @dict: A dictionary-like data container.
 * @n: Length of @prefixes array.
 * @prefixes: (array length=n): List of prefixes.
 *
 * Duplicates a dictionary keeping only values under given prefixes.
 *
 * Like gwy_dict_copy(), this method creates a deep copy, that is contained object are physically duplicated
 * too, not just referenced again.
 *
 * Signal ::item-changed is not emitted during the construction of the copy (in case you manage to get hold on the
 * returned object before the copying is finished).
 *
 * Returns: (transfer full): A newly created container.
 **/
GwyDict*
gwy_dict_duplicate_by_prefixv(GwyDict *dict,
                              guint n,
                              const gchar **prefixes)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);
    PrefixListData pfxlist = {
        .prefixes = prefixes,
        .nprefixes = n,
    };
    return duplicate_by_prefix_do(dict, &pfxlist);
}

static GwyDict*
duplicate_by_prefix_valist(GwyDict *dict, va_list ap)
{
    gsize n = 16;
    PrefixListData pfxlist = {
        .prefixes = g_new(const gchar*, n),
        .nprefixes = 0,
    };
    const gchar *prefix = va_arg(ap, const gchar*);
    while (prefix) {
        if (pfxlist.nprefixes == n) {
            n += 16;
            pfxlist.prefixes = g_renew(const gchar*, pfxlist.prefixes, n);
        }
        pfxlist.prefixes[pfxlist.nprefixes] = prefix;
        pfxlist.nprefixes++;
        prefix = va_arg(ap, const gchar*);
    }

    GwyDict *duplicate = duplicate_by_prefix_do(dict, &pfxlist);
    g_free(pfxlist.prefixes);
    return duplicate;
}

static GwyDict*
duplicate_by_prefix_do(GwyDict *dict, PrefixListData *pfxlist)
{
    pfxlist->pfxlengths = g_new(gsize, pfxlist->nprefixes);
    pfxlist->pfxclosed = g_new(gboolean, pfxlist->nprefixes);
    for (gsize n = 0; n < pfxlist->nprefixes; n++) {
        const gchar *prefix = pfxlist->prefixes[n];
        gsize len;
        len = pfxlist->pfxlengths[n] = strlen(prefix);
        pfxlist->pfxclosed[n] = prefix_is_closed(prefix, len);
    }

    /* Don't emit signals when no one can be connected. */
    GwyDict *duplicate = gwy_dict_new();
    duplicate->priv->in_construction = TRUE;
    pfxlist->dict = duplicate;
    g_hash_table_foreach(dict->priv->values, hash_prefix_duplicate_func, pfxlist);
    duplicate->priv->in_construction = FALSE;

    g_free(pfxlist->pfxclosed);
    g_free(pfxlist->pfxlengths);

    return duplicate;
}

static void
hash_prefix_duplicate_func(gpointer hkey, gpointer hvalue, gpointer hdata)
{
    GQuark key = GPOINTER_TO_UINT(hkey);
    GValue *value = (GValue*)hvalue;
    PrefixListData *pfxlist = (PrefixListData*)hdata;
    GwyDict *duplicate;
    gsize n;

    duplicate = pfxlist->dict;
    for (n = 0; n < pfxlist->nprefixes; n++) {
        if (prefix_matches_key(pfxlist->prefixes[n], pfxlist->pfxlengths[n], pfxlist->pfxclosed[n], key, NULL)) {
            duplicate_one_value(key, value, duplicate);
            return;
        }
    }
}

/**
 * gwy_dict_transfer:
 * @source: Source container.
 * @dest: Destination container. It may be the same container as @source, but @source_prefix and @dest_prefix may not
 *        overlap then.
 * @source_prefix: Prefix in @source to take values from.
 * @dest_prefix: Prefix in @dest to put values to.
 * @deep: %TRUE to perform a deep copy, %FALSE to perform a shallow copy. This option pertains only to contained
 *        objects, strings are always duplicated.
 * @force: %TRUE to replace existing values in @dest.
 *
 * Copies a items from one place in container to another place.
 *
 * The copies are shallow, objects are not physically duplicated, only referenced in @dest, unless @deep is %TRUE.
 *
 * The order in which items are transferred is not defined. Signals for all changed items are emitted after the
 * transfer is finished (not during transfer).
 *
 * Source items within @source_prefix are not allowed to change during the transfer. This is mostly relevant when
 * @force is %TRUE and existing destination objects are finalised or @deep is %TRUE and new objects are created,
 * either of which could have cascading effects.
 *
 * Returns: The number of actually transferred items, excluding unchanged items (even when @force is %TRUE).
 **/
gint
gwy_dict_transfer(GwyDict *source,
                  GwyDict *dest,
                  const gchar *source_prefix,
                  const gchar *dest_prefix,
                  gboolean deep,
                  gboolean force)
{
    g_return_val_if_fail(GWY_IS_DICT(source), 0);
    g_return_val_if_fail(GWY_IS_DICT(dest), 0);
    if (!source_prefix)
        source_prefix = "";
    if (!dest_prefix)
        dest_prefix = "";
    if (source == dest) {
        if (gwy_strequal(source_prefix, dest_prefix))
            return 0;

        g_return_val_if_fail(!g_str_has_prefix(source_prefix, dest_prefix), 0);
        g_return_val_if_fail(!g_str_has_prefix(dest_prefix, source_prefix), 0);
    }

    guint n = 0, count = 0;
    GQuark *srckeys = gwy_dict_keys_with_prefix(source, source_prefix, &n);
    if (!n) {
        g_free(srckeys);
        return 0;
    }

    guint spflen = strlen(source_prefix);
    if (spflen && prefix_is_closed(source_prefix, spflen))
        spflen--;

    GString *key = g_string_new(dest_prefix);
    guint dpflen = key->len;
    if (dpflen && dest_prefix[dpflen - 1] == GWY_DICT_PATHSEP)
        dpflen--;

    GwyDictPrivate *dpriv = dest->priv, *spriv = source->priv;
    GArray *changed_quarks = dpriv->in_construction ? NULL : g_array_new(FALSE, FALSE, sizeof(GQuark));
    for (guint i = 0; i < n; i++) {
        GValue *value = (GValue*)g_hash_table_lookup(spriv->values, GUINT_TO_POINTER(srckeys[i]));
        if (!value) {
            g_critical("Source container contents is changing during gwy_dict_transfer(). Aborting.");
            break;
        }
        GType type = G_VALUE_TYPE(value);
        gboolean is_object = g_type_is_a(type, G_TYPE_OBJECT);

        g_string_truncate(key, dpflen);
        g_string_append(key, g_quark_to_string(srckeys[i]) + spflen);
        GQuark quark = g_quark_from_string(key->str);
        GValue *copy = (GValue*)g_hash_table_lookup(dpriv->values, GUINT_TO_POINTER(quark));
        gboolean exists = !!copy;

        /* This is a bit messy. We do not replace existing values with !force. This part is easy.
         *
         * We do not need to replace values that are equal. In fact, we do not want to in order to avoid redundant
         * signals.
         *
         * However, when a deep copy is requested we have to physically duplicate destination objects even if they are
         * already there because a deep copy promises the objects in destination will be copies of source objects. */
        if (exists) {
            if (!force || ((!deep || !is_object) && values_equal(value, copy)))
                continue;
            if (G_VALUE_TYPE(copy) != type) {
                g_value_unset(copy);
                g_value_init(copy, type);
            }
        }
        else {
            copy = g_new0(GValue, 1);
            g_value_init(copy, type);
        }

        /* This g_value_copy() makes a real copy of everything, including strings, but it only references objects
         * while deep copy requires duplication. */
        if (deep & is_object)
            g_value_take_object(copy, G_OBJECT(gwy_serializable_copy(GWY_SERIALIZABLE(g_value_get_object(value)))));
        else
            g_value_copy(value, copy);

        if (!exists)
            g_hash_table_insert(dpriv->values, GUINT_TO_POINTER(quark), copy);
        if (changed_quarks)
            g_array_append_val(changed_quarks, quark);
        count++;
    }
    g_free(srckeys);
    g_string_free(key, TRUE);

    if (changed_quarks) {
        guint nq = changed_quarks->len;
        for (guint i = 0; i < nq; i++) {
            GQuark quark = g_array_index(changed_quarks, GQuark, i);
            g_signal_emit(dest, signals[SGNL_ITEM_CHANGED], quark, quark);
        }
        g_array_free(changed_quarks, TRUE);
    }

    return count;
}

/**
 * gwy_dict_copy_value_by_name:
 * @dict1: Source dictionary.
 * @dict2: Destination dictionary. It may be the same container as @source.
 * @name1: String key in @source.
 * @name2: String key in @dest.
 *
 * Copies one value identified by a string key from one container to another one.
 *
 * The source value must exist. If it is an object or boxed, it is copied by value, i.e. the functions always does a
 * deep copy.
 **/

/**
 * gwy_dict_copy_value:
 * @source: Source container.
 * @dest: Destination container. It may be the same container as @source.
 * @srckey: Quark key in @source.
 * @destkey: Quark key in @dest.
 *
 * Copies one value from a dictionary to another one.
 *
 * The source value must exist. If it is an object or boxed, it is copied by value, i.e. the functions always does a
 * deep copy.
 **/
void
gwy_dict_copy_value(GwyDict *source,
                    GwyDict *dest,
                    GQuark srckey,
                    GQuark destkey)
{
    g_return_if_fail(GWY_IS_DICT(source));
    g_return_if_fail(GWY_IS_DICT(dest));
    g_return_if_fail(srckey);
    g_return_if_fail(destkey);
    if (source == dest && srckey == destkey)
        return;
    GValue *value = (GValue*)g_hash_table_lookup(source->priv->values, GUINT_TO_POINTER(srckey));
    duplicate_one_value(destkey, value, dest);
}

/**
 * gwy_dict_start_construction:
 * @dict: A dictionary-like data container.
 *
 * Marks a dictionary as being initially constructed.
 *
 * It is an error to call the function on @dict which is not empty. It is an error to call it when @dict is
 * already marked as being constructed. It is undefined behaviour to start and finish the construction multiple times,
 * even if @dict remains empty.
 *
 * When a dictionary is constructed by internal means, for instance during deserialisation or
 * gwy_dict_copy(), it does not emit any signals, in particular ::item-changed. However, data containers are also
 * frequently constructed for instance to represent files in file import modules, where emitting signals is pointless
 * and can slow down the import (more substantially for subclasses like #GwyFile which respond to changed items). You
 * can use this function to prevent signal emission in such case.
 *
 * Remember to call gwy_dict_finish_construction() when you are done. A #GwyDict marked as still being in
 * construction can be considered an invalid object.
 **/
void
gwy_dict_start_construction(GwyDict *dict)
{
    g_return_if_fail(GWY_IS_DICT(dict));
    GwyDictPrivate *priv = dict->priv;
    g_return_if_fail(!priv->in_construction);
    g_return_if_fail(!g_hash_table_size(priv->values));
    priv->in_construction = TRUE;
}

/**
 * gwy_dict_finish_construction:
 * @dict: A dictionary-like data container.
 *
 * Marks the construction of a dictionary as finished.
 *
 * The data container must be marked as being constructed with gwy_dict_start_construction(). The call may result
 * in a subclass or other user scanning the contents. See gwy_dict_start_construction() for more discussion.
 **/
void
gwy_dict_finish_construction(GwyDict *dict)
{
    g_return_if_fail(GWY_IS_DICT(dict));
    GwyDictPrivate *priv = dict->priv;
    g_return_if_fail(priv->in_construction);
    GwyDictClass *klass = GWY_DICT_GET_CLASS(dict);
    if (klass->construction_finished)
        klass->construction_finished(dict);
    priv->in_construction = FALSE;
}

/**
 * gwy_dict_is_being_constructed:
 * @dict: A dictionary-like data container.
 *
 * Queries if a dictionary is being initially constructed.
 *
 * Returns: %TRUE if @dict was marked with gwy_dict_start_construction().
 **/
gboolean
gwy_dict_is_being_constructed(GwyDict *dict)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), FALSE);
    return dict->priv->in_construction;
}

static int
pstring_compare_callback(const void *p, const void *q)
{
    return strcmp(*(gchar**)p, *(gchar**)q);
}

static void
hash_text_serialize_func(gpointer hkey, gpointer hvalue, gpointer hdata)
{
    GQuark key = GPOINTER_TO_UINT(hkey);
    GValue *value = (GValue*)hvalue;
    GPtrArray *pa = (GPtrArray*)hdata;
    GType type = G_VALUE_TYPE(value);
    gchar *k = g_strescape(g_quark_to_string(key), NULL);
    gchar *v = NULL;

    if (type == G_TYPE_BOOLEAN) {
        v = g_strdup_printf("\"%s\" boolean %s", k, g_value_get_boolean(value) ? "True" : "False");
    }
    else if (type == G_TYPE_UCHAR) {
        gint c = g_value_get_uchar(value);
        if (g_ascii_isprint(c) && !g_ascii_isspace(c))
            v = g_strdup_printf("\"%s\" char %c", k, c);
        else
            v = g_strdup_printf("\"%s\" char 0x%02x", k, c);
    }
    else if (type == G_TYPE_INT)
        v = g_strdup_printf("\"%s\" int32 %d", k, g_value_get_int(value));
    else if (type == G_TYPE_INT64)
        v = g_strdup_printf("\"%s\" int64 %" G_GINT64_FORMAT, k, g_value_get_int64(value));
    else if (type == G_TYPE_DOUBLE) {
        static gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
        g_ascii_dtostr(buf, G_ASCII_DTOSTR_BUF_SIZE, g_value_get_double(value));
        v = g_strdup_printf("\"%s\" double %s", k, buf);
    }
    else if (type == G_TYPE_STRING) {
        gchar *s = gwy_utf8_strescape(g_value_get_string(value), NULL);
        v = g_strdup_printf("\"%s\" string \"%s\"", k, s);
        g_free(s);
    }
    else if (g_type_is_a(type, G_TYPE_BOXED)) {
        g_warning("Boxed values can perhaps be dumped to text, but it is not implemented.");
    }
    else {
        g_critical("Cannot pack GValue holding %s", g_type_name(type));
    }
    if (v)
        g_ptr_array_add(pa, v);

    g_free(k);
}

/* FIXME: Return gchar** just by freeing the container array. */
/**
 * gwy_dict_serialize_to_text:
 * @dict: A dictionary-like data container.
 *
 * Creates a text representation of @dict contents.
 *
 * Note that only simple data types are supported as serialization of compound objects is not controllable.
 *
 * The strings in the returned array are owned by the array by means of element destroy function which is set to
 * g_free() (which you need to take into account if you intend to add more strings yourself). Usually, you can free
 * the array including all the strings with g_ptr_array_free(). If you want to take the ownership of the strings, use
 * g_ptr_array_steal().
 *
 * Returns: A pointer array, each item containing string with one container item representation (name, type, value).
 *          The array is sorted by name.
 **/
GPtrArray*
gwy_dict_serialize_to_text(GwyDict *dict)
{
    g_return_val_if_fail(GWY_IS_DICT(dict), NULL);

    GPtrArray *pa = g_ptr_array_new_full(g_hash_table_size(dict->priv->values), g_free);
    g_hash_table_foreach(dict->priv->values, hash_text_serialize_func, pa);
    g_ptr_array_sort(pa, pstring_compare_callback);

    return pa;
}

static guint
token_length(const gchar *text)
{
    guint i;

    g_assert(!g_ascii_isspace(*text));
    if (*text == '"') {
        /* quoted string */
        for (i = 1; text[i] != '"'; i++) {
            if (text[i] == '\\') {
               i++;
               if (!text[i])
                   return (guint)-1;
            }
        }
        i++;
    }
    else {
        /* normal token */
        for (i = 0; text[i] && !g_ascii_isspace(text[i]); i++)
            ;
    }

    return i;
}

static gchar*
dequote_token(const gchar *tok, gsize *len)
{
    gchar *s, *t;

    if (*len < 2 || *tok != '"')
        return g_strndup(tok, *len);

    g_assert(tok[*len-1] == '"');
    s = g_strndup(tok+1, *len-2);
    t = g_strcompress(s);
    g_free(s);

    return t;
}

/**
 * gwy_dict_deserialize_from_text:
 * @text: Text containing serialized container contents as dumped by gwy_dict_serialize_to_text().
 *
 * Restores a dictionary from is text representation.
 *
 * Signal ::item-changed is not emitted during the construction (in case you manage to get hold on the returned object
 * before the deserialisation is finished).
 *
 * Returns: (transfer full): The restored container, or %NULL on failure.
 **/
GwyDict*
gwy_dict_deserialize_from_text(const gchar *text)
{
    const gchar *tok, *type;
    gchar *name = NULL;
    gsize len, namelen, typelen;
    GQuark key;

    GwyDict *dict = gwy_dict_new();
    GwyDictPrivate *priv = dict->priv;
    priv->in_construction = TRUE;

    for (tok = text; g_ascii_isspace(*tok); tok++)
        ;
    while ((len = token_length(tok))) {
        /* name */
        if (len == (guint)-1)
            goto fail;
        namelen = len;
        name = dequote_token(tok, &namelen);
        key = g_quark_from_string(name);
        gwy_debug("got name <%s>", name);

        /* type */
        for (tok = tok + len; g_ascii_isspace(*tok); tok++)
            ;
        if (!(len = token_length(tok)) || len == (guint)-1)
            goto fail;
        type = tok;
        typelen = len;
        gwy_debug("got type <%.*s>", (guint)typelen, type);

        /* value */
        for (tok = tok + len; g_ascii_isspace(*tok); tok++)
            ;
        if (!(len = token_length(tok)) || len == (guint)-1)
            goto fail;
        /* boolean */
        if (typelen+1 == sizeof("boolean")
            && g_str_has_prefix(type, "boolean")) {
            if (len == 4 && g_str_has_prefix(tok, "True"))
                gwy_dict_set_boolean(dict, key, TRUE);
            else if (len == 5 && g_str_has_prefix(tok, "False"))
                gwy_dict_set_boolean(dict, key, FALSE);
            else
                goto fail;
        }
        /* char */
        else if (typelen+1 == sizeof("char") && g_str_has_prefix(type, "char")) {
            guint c;

            if (len == 1)
                c = *tok;
            else {
                if (len != 4)
                    goto fail;
                sscanf(tok+2, "%x", &c);
            }
            gwy_dict_set_uchar(dict, key, (guchar)c);
        }
        /* int32 */
        else if (typelen+1 == sizeof("int32") && g_str_has_prefix(type, "int32")) {
            gwy_dict_set_int32(dict, key, strtol(tok, NULL, 0));
        }
        /* int64 */
        else if (typelen+1 == sizeof("int64") && g_str_has_prefix(type, "int64")) {
            gwy_dict_set_int64(dict, key, g_ascii_strtoull(tok, NULL, 0));
        }
        /* double */
        else if (typelen+1 == sizeof("double") && g_str_has_prefix(type, "double")) {
            gwy_dict_set_double(dict, key, g_ascii_strtod(tok, NULL));
        }
        /* string */
        else if (typelen+1 == sizeof("string") && g_str_has_prefix(type, "string")) {
            gsize vallen = len;
            gwy_dict_pass_string(dict, key, dequote_token(tok, &vallen));
        }
        /* UFO */
        else {
            tok = type;  /* for warning */
            goto fail;
        }
        g_free(name);
        name = NULL;

        /* skip space */
        for (tok = tok + len; g_ascii_isspace(*tok); tok++)
            ;
    }
    priv->in_construction = FALSE;

    return dict;

fail:
    g_free(name);
    g_warning("parsing failed at <%.18s...>", tok);
    g_object_unref(dict);
    return NULL;
}

/**
 * SECTION: dict
 * @title: GwyDict
 * @short_description: A dictionary with items identified by a GQuark or string
 * @see_also: #GwyInventory
 *
 * #GwyDict is a general-purpose dictionary-like data container, it can hold atomic types, strings and objects.
 * However, objects must implement the #GwySerializable interface, because the dictionary itself is serializable.
 *
 * A new dictionary can be created with gwy_dict_new(), items can be stored with function like
 * gwy_dict_set_double(), read with gwy_dict_get_double(), and removed with gwy_dict_remove() or
 * gwy_dict_remove_by_prefix(). A presence of a value can be tested with gwy_dict_contains(), convenience
 * functions for reading (updating) a value only if it is present like gwy_dict_gis_double(), are available too.
 *
 * #GwyDict takes ownership of stored non-atomic items. For strings, this means you cannot store static strings
 * (use g_strdup() to duplicate them), and must not free stored dynamic strings, as the dictionary will free them
 * itself when they are removed or when the dictionary is finalized. For objects, this means it takes a reference on
 * the object (released when the object is removed or the dictionary is finalized), so you usually want to
 * g_object_unref() objects after storing them to a dictionary.
 *
 * Items in a #GwyDict can be identified by a #GQuark or the corresponding string.  While #GQuark's are atomic
 * values and allow faster access, they are less convenient for casual usage -- each #GQuark key function like
 * gwy_dict_set_double() thus has a string-key counterpart gwy_dict_set_double_by_name().
 **/

/**
 * GwyDict:
 *
 * The #GwyDict struct contains private data only and should be accessed using the functions below.
 **/

/**
 * GWY_DICT_PATHSEP:
 *
 * Path separator to be used for hierarchical structures in the container.
 **/

/**
 * GWY_DICT_PATHSEP_STR:
 *
 * Path separator to be used for hierarchical structures in the container, as a string.
 **/

/**
 * GwyDictForeachFunc:
 * @key: A #GQuark key.
 * @value: Value at @key.
 * @user_data: User data passed to the for-each function.
 *
 * Type of callback function for container for-each operation.
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
