/*
 *  $Id: file.c 30126 2026-06-23 12:03:45Z yeti-dn $
 *  Copyright (C) 2025-2026 David Necas (Yeti).
 *  E-mail: yeti@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/gwyddion.h"
#include "libgwyapp/settings.h"
#include "libgwyapp/file.h"
#include "libgwyapp/types.h"

#include "libgwyapp/internal.h"
#include "libgwyapp/data-browser-internal.h"

G_STATIC_ASSERT(sizeof(GQuark) == sizeof(guint32));

#define TYPE_NAME "GwyFile"

#define gwy_strlen0(s) (s ? strlen(s) : 0)

enum {
    ITEM_LINKS,
    NUM_ITEMS
};

typedef GQuark (*GetKeyFunc)(gint id);

typedef struct {
    /* GSequence would be better for files with many data items. But it has a pretty large overhead. */
    GArray *ids;
    gint *new_ids;
} DataKindInfo;

struct _GwyFilePrivate {
    DataKindInfo data_kind_info[GWY_FILE_N_KINDS];

    GwyErrorList **scan_errlist;
    GArray *toremove;
    GArray *toremap;

    GArray *links;
    const gchar **link_strings;
    GQuark setting_link;

    const gchar *format_name;
    gchar *filename_sys;

    glong id;
    gboolean is_managed;
    gboolean scanned_during_deserialisation;
};

typedef struct {
    const gchar *suffix;
    GwyFilePiece piece;
    GType type;
} FilePieceSpec;

typedef struct {
    GetKeyFunc get_key;
    const gchar *prefix;
    const guint *pieces;
    guint npieces;
    GType type;
} DataKindSpec;

typedef struct {
    GwyDataKind data_kind;
    gint source_id;
    gint dest_id;
} TransferredInfo;

typedef struct {
    GQuark source;
    GQuark dest;
} RemapInfo;

static void             serializable_init     (GwySerializableInterface *iface);
static void             serializable_itemize  (GwySerializable *serializable,
                                               GwySerializableGroup *group);
static void             serializable_done     (GwySerializable *serializable);
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             dispose               (GObject *object);
static void             finalize              (GObject *object);
static void             item_changed          (GwyDict *container,
                                               GQuark key);
static void             construction_finished (GwyDict *container);
static DataKindInfo*    ensure_data_kind_info (GwyFile *file,
                                               GwyDataKind data_kind);
static gint             copy_data             (GwyFile *source,
                                               GwyDataKind data_kind,
                                               gint id,
                                               GwyFile *destination,
                                               gboolean shallow);
static gboolean         file_piece_is_valid   (GwyDataKind data_kind,
                                               GwyFilePiece piece,
                                               const gchar *suffix);
static void             fill_piece_gtypes     (FilePieceSpec *pieces,
                                               guint nspec);
static void             copy_aux_info         (GwyFile *dest,
                                               GwyFile *src);
static GArray*          assign_garray         (GArray *dest,
                                               GArray *src);
static void             set_link              (GwyFile *file,
                                               GQuark key,
                                               GQuark linkquark,
                                               const gchar *linkstr);
static gint             find_link             (GwyFile *file,
                                               GQuark quark);

static GObjectClass *parent_class = NULL;
static GwySerializableInterface *serializable_parent_iface = NULL;

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "__links__", .ctype = GWY_SERIALIZABLE_STRING_ARRAY, },
};

// Fundamental types can be pre-filled because they are constants. Run-time registered types are filled by
// fill_piece_gtypes() during class initialisation.
static FilePieceSpec all_pieces[] = {
    { NULL,            GWY_FILE_PIECE_TITLE,         G_TYPE_STRING,  },
    { NULL,            GWY_FILE_PIECE_VISIBLE,       G_TYPE_BOOLEAN, },
    { NULL,            GWY_FILE_PIECE_PALETTE,       G_TYPE_STRING,  },
    { NULL,            GWY_FILE_PIECE_LOG,           0,              },
    { NULL,            GWY_FILE_PIECE_PICTURE,       0,              },
    { "type",          GWY_FILE_PIECE_PICTURE,       G_TYPE_INT,     },
    { "index",         GWY_FILE_PIECE_PICTURE,       G_TYPE_INT,     },
    { "skip-outliers", GWY_FILE_PIECE_PICTURE,       G_TYPE_BOOLEAN, },
    { NULL,            GWY_FILE_PIECE_META,          0,              },
    { NULL,            GWY_FILE_PIECE_MASK,          0,              },
    { NULL,            GWY_FILE_PIECE_MASK_COLOR,    0,              },
    { NULL,            GWY_FILE_PIECE_COLOR_MAPPING, G_TYPE_INT,     },
    { "max",           GWY_FILE_PIECE_RANGE,         G_TYPE_DOUBLE,  },
    { "min",           GWY_FILE_PIECE_RANGE,         G_TYPE_DOUBLE,  },
    { NULL,            GWY_FILE_PIECE_SELECTION,     0,              },
    { NULL,            GWY_FILE_PIECE_REAL_SQUARE,   G_TYPE_BOOLEAN, },
    { "relative-size", GWY_FILE_PIECE_VIEW,          G_TYPE_DOUBLE,  },
    { "scale",         GWY_FILE_PIECE_VIEW,          G_TYPE_DOUBLE,  },
    { "width",         GWY_FILE_PIECE_VIEW,          G_TYPE_INT,     },
    { "height",        GWY_FILE_PIECE_VIEW,          G_TYPE_INT,     },
    { "spectra",       GWY_FILE_PIECE_REF,           G_TYPE_STRING,  },
};

/* Which pieces from all_pieces[] are valid for given data kind. Use utils/make-file-pieces.py to expand the
 * definitions and generate the numerical id lists. Keep the definitions inside a comment in case something tries to
 * parse it as C because it would get very confused. */
#ifdef __GWY_DEFINE_FILE_PIECES__
// (COMMON) = LOG META VISIBLE
// (IMGLIKE) = (COMMON) TITLE PALETTE PICTURE COLOR_MAPPING RANGE/min,max SELECTION RANGE/min RANGE/max
// (PIXELS) = (IMGLIKE) MASK MASK_COLOR REAL_SQUARE VIEW/relative-size,scale
// (NONPIXEL) = VIEW/relative-size,width,height
// (PREVIEW) = PICTURE/type,index,skip-outliers
// image = (PIXELS) REF/spectra
// volume = (PIXELS) (PREVIEW)
// cmap = (PIXELS) (PREVIEW)
// xyz = (IMGLIKE) (NONPIXEL) (PREVIEW)
// graph = (COMMON) (NONPIXEL)
// spectra = (COMMON) (NONPIXEL)
#else
static const guint image_pieces[] = { 0, 1, 2, 3, 4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20 };
static const guint volume_pieces[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 };
static const guint cmap_pieces[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 };
static const guint xyz_pieces[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 16, 18, 19 };
static const guint graph_pieces[] = { 1, 3, 8, 16, 18, 19 };
static const guint spectra_pieces[] = { 1, 3, 8, 16, 18, 19 };
#endif

/* NB: This array is indexed by GwyDataKind. */
static DataKindSpec data_kind_spec[GWY_FILE_N_KINDS] = {
    { gwy_file_key_image,   GWY_FILE_PREFIX_IMAGE,   image_pieces,   G_N_ELEMENTS(image_pieces),   0, },
    { gwy_file_key_graph,   GWY_FILE_PREFIX_GRAPH,   graph_pieces,   G_N_ELEMENTS(graph_pieces),   0, },
    { gwy_file_key_spectra, GWY_FILE_PREFIX_SPECTRA, spectra_pieces, G_N_ELEMENTS(spectra_pieces), 0, },
    { gwy_file_key_volume,  GWY_FILE_PREFIX_VOLUME,  volume_pieces,  G_N_ELEMENTS(volume_pieces),  0, },
    { gwy_file_key_xyz,     GWY_FILE_PREFIX_XYZ,     xyz_pieces,     G_N_ELEMENTS(xyz_pieces),     0, },
    { gwy_file_key_cmap,    GWY_FILE_PREFIX_CMAP,    cmap_pieces,    G_N_ELEMENTS(cmap_pieces),    0, },
};

static const GwyEnum piece_suffixes[] = {
    { "visible",       GWY_FILE_PIECE_VISIBLE,       },
    { "title",         GWY_FILE_PIECE_TITLE,         },
    { "palette",       GWY_FILE_PIECE_PALETTE,       },
    { "mask",          GWY_FILE_PIECE_MASK,          },
    { "mask-color",    GWY_FILE_PIECE_MASK_COLOR,    },
    { "color-mapping", GWY_FILE_PIECE_COLOR_MAPPING, },
    { "range",         GWY_FILE_PIECE_RANGE,         },
    { "show",          GWY_FILE_PIECE_PICTURE,       },
    { "meta",          GWY_FILE_PIECE_META,          },
    { "log",           GWY_FILE_PIECE_LOG,           },
    { "real-square",   GWY_FILE_PIECE_REAL_SQUARE,   },
    { "view",          GWY_FILE_PIECE_VIEW,          },
    { "selection",     GWY_FILE_PIECE_SELECTION,     },
    { "ref",           GWY_FILE_PIECE_REF,           },
};

G_DEFINE_TYPE_WITH_CODE(GwyFile, gwy_file, GWY_TYPE_DICT,
                        G_ADD_PRIVATE(GwyFile)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
serializable_init(GwySerializableInterface *iface)
{
    serializable_parent_iface = g_type_interface_peek_parent(iface);

    iface->itemize = serializable_itemize;
    iface->done = serializable_done;
    iface->construct = serializable_construct;
    iface->copy = serializable_copy;
    iface->assign = serializable_assign;
}

static void
gwy_file_class_init(GwyFileClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyDictClass *dict_class = GWY_DICT_CLASS(klass);
    //GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_file_parent_class;

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

    dict_class->item_changed = item_changed;
    dict_class->construction_finished = construction_finished;

    data_kind_spec[GWY_FILE_IMAGE].type = GWY_TYPE_FIELD;
    data_kind_spec[GWY_FILE_GRAPH].type = GWY_TYPE_GRAPH_MODEL;
    data_kind_spec[GWY_FILE_SPECTRA].type = GWY_TYPE_SPECTRA;
    data_kind_spec[GWY_FILE_VOLUME].type = GWY_TYPE_BRICK;
    data_kind_spec[GWY_FILE_XYZ].type = GWY_TYPE_SCATTER;
    data_kind_spec[GWY_FILE_CMAP].type = GWY_TYPE_LAWN;
    fill_piece_gtypes(all_pieces, G_N_ELEMENTS(all_pieces));
}

static void
gwy_file_init(GwyFile *file)
{
    GwyFilePrivate *priv;

    file->priv = priv = gwy_file_get_instance_private(file);

    priv->id = GWY_FILE_ID_NONE;
}

static void
dispose(GObject *object)
{
    G_GNUC_UNUSED GwyFile *file = (GwyFile*)object;

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

static void
finalize(GObject *object)
{
    GwyFile *file = (GwyFile*)object;
    GwyFilePrivate *priv = file->priv;

    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        DataKindInfo *info = priv->data_kind_info + data_kind;
        GWY_FREE_ARRAY(info->ids);
    }
    GWY_FREE_ARRAY(priv->links);
    GWY_FREE_ARRAY(priv->toremove);
    g_free(priv->filename_sys);

    /* Data browser holds a reference. So we should not get here when the file is managed. */
    g_assert(!priv->is_managed);

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

/**
 * gwy_file_new: (constructor)
 *
 * Creates a new #GwyFile.
 *
 * Returns: (transfer full):
 *          A new data file container.
 **/
GwyFile*
gwy_file_new(void)
{
    return (GwyFile*)g_object_new(GWY_TYPE_FILE, NULL);
}

/**
 * gwy_file_new_in_construction: (constructor)
 *
 * Creates a new #GwyFile and marks is as being in construction.
 *
 * This is a convenience function calling gwy_dict_start_construction() on the newly created file.
 *
 * Returns: (transfer full):
 *          A new data file container.
 **/
GwyFile*
gwy_file_new_in_construction(void)
{
    GwyFile *file = (GwyFile*)g_object_new(GWY_TYPE_FILE, NULL);
    gwy_dict_start_construction(GWY_DICT(file));
    return file;
}

static gint
add_data_object(GwyFile *file, gpointer data_object, GwyDataKind data_kind)
{
    GwyDict *dict = GWY_DICT(file);
    if (gwy_dict_is_being_constructed(dict)) {
        g_critical("gwy_file_add_SOMETHING() cannot be used with files still in construction.");
        /* This triggers data enumeration. This will likely break in one way or another anyway, but try to not fail
         * immediately. */
        gwy_dict_finish_construction(dict);
    }

    DataKindInfo *info = ensure_data_kind_info(file, data_kind);
    g_return_val_if_fail(info, -1);
    const DataKindSpec *spec = data_kind_spec + data_kind;
    g_return_val_if_fail(G_TYPE_CHECK_INSTANCE_TYPE(data_object, spec->type), -1);

    GArray *ids = info->ids;
    gint id = ids->len ? g_array_index(ids, gint, ids->len-1)+1 : 0;
    g_array_append_val(ids, id);
    /* Data browser responds to this by immediately updating its data lists and queuing an update of GUI, which
     * preferably happens after the caller finishes setting other things. */
    gwy_dict_set_object(dict, spec->get_key(id), data_object);
    return id;
}

/**
 * gwy_file_add_image:
 * @file: A data file container to add @field to.
 * @field: Data field representing an image to add.
 *
 * Adds a data field representing an image to a file data container.
 *
 * The file cannot be in construction (see gwy_dict_start_construction()). In particular, this function is not
 * suitable for the import of data from third party file formats, use gwy_file_pass_image().
 *
 * A new unused id is assigned to the image and returned. Generally, it is the lowest integer larger than any existing
 * image id (however, there are no specific guarantees).
 *
 * The image is not immediately shown in a new window when the GUI is running. Use gwy_file_set_visible() to show it,
 * preferrably after setting or synchronising visualisation settings like the colour mapping.
 *
 * Returns: The id of the newly added image in the file.
 **/
gint
gwy_file_add_image(GwyFile *file,
                   GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FILE(file), -1);
    return add_data_object(file, field, GWY_FILE_IMAGE);
}

/**
 * gwy_file_add_graph:
 * @file: A data file container to add @graph to.
 * @graph: Graph model to add.
 *
 * Adds a graph model representing a graph to a file data container.
 *
 * The file cannot be in construction (see gwy_dict_start_construction()). In particular, this function is not
 * suitable for the import of data from third party file formats, use gwy_file_pass_graph().
 *
 * A new unused id is assigned to the graph and returned. Generally, it is the lowest integer larger than any existing
 * graph id (however, there are no specific guarantees).
 *
 * The graph is not immediately shown in a new window when the GUI is running. Use gwy_file_set_visible() to show it,
 * preferrably after setting or synchronising other settings.
 *
 * Returns: The id of the newly added graph in the file.
 **/
gint
gwy_file_add_graph(GwyFile *file,
                   GwyGraphModel *graph)
{
    g_return_val_if_fail(GWY_IS_FILE(file), -1);
    return add_data_object(file, graph, GWY_FILE_GRAPH);
}

/**
 * gwy_file_add_spectra:
 * @file: A data file container to add @spectra to.
 * @spectra: Single point spectra to add.
 *
 * Adds a spectra representing a single point spectra to a file data container.
 *
 * The file cannot be in construction (see gwy_dict_start_construction()). In particular, this function is not
 * suitable for the import of data from third party file formats, use gwy_file_pass_spectra().
 *
 * A new unused id is assigned to the spectra and returned. Generally, it is the lowest integer larger than any
 * existing spectra id (however, there are no specific guarantees).
 *
 * The spectra are not immediately shown in a new window when the GUI is running. Use gwy_file_set_visible() to show it,
 * preferrably after setting or synchronising other settings.
 *
 * Returns: The id of the newly added spectra in the file.
 **/
gint
gwy_file_add_spectra(GwyFile *file,
                     GwySpectra *spectra)
{
    g_return_val_if_fail(GWY_IS_FILE(file), -1);
    return add_data_object(file, spectra, GWY_FILE_SPECTRA);
}

/**
 * gwy_file_add_volume:
 * @file: A data file container to add @brick to.
 * @brick: Data brick to add.
 * @preview: (nullable): Data field to use as the preview. You can also pass %NULL to create a preview automatically.
 *
 * Adds a data brick representing an volume data to a file data container.
 *
 * The file cannot be in construction (see gwy_dict_start_construction()). In particular, this function is not
 * suitable for the import of data from third party file formats, use gwy_file_pass_volume().
 *
 * A new unused id is assigned to the volume data and returned. Generally, it is the lowest integer larger than any
 * existing volume data id (however, there are no specific guarantees).
 *
 * The volume data is not immediately shown in a new window when the GUI is running. Use gwy_file_set_visible() to
 * show it, preferrably after setting or synchronising visualisation settings like the colour mapping.
 *
 * Returns: The id of the newly added volume data in the file.
 **/
gint
gwy_file_add_volume(GwyFile *file,
                    GwyBrick *brick,
                    GwyField *preview)
{
    g_return_val_if_fail(GWY_IS_FILE(file), -1);
    gint id = add_data_object(file, brick, GWY_FILE_VOLUME);
    if (id < 0)
        return id;

    /* XXX: This seems unnecessary. But it is so only if the data browser would never create a preview itself (wasting
     * possibly a lot of time because volume data can be huge) before the caller can add one itself. The data browser
     * sort of wants previews immediately, so maybe just keep the 2.x behaviour here. */
    GQuark quark = gwy_file_key_volume_picture(id);
    GwyDict *container = GWY_DICT(file);
    if (preview) {
        if (GWY_IS_FIELD(preview))
            gwy_dict_set_object(container, quark, preview);
        preview = NULL;
    }
    if (!preview) {
        if (!gwy_dict_gis_object(container, quark, &preview) || !GWY_IS_FIELD(preview)) {
            preview = _gwy_app_create_brick_preview_field(brick);
            gwy_dict_pass_object(container, quark, preview);
        }
    }
    return id;
}

/**
 * gwy_file_add_xyz:
 * @file: A data file container to add @scatter to.
 * @scatter: XYZ data scatter to add.
 *
 * Adds a data scatter representing an XYZ data to a file data container.
 *
 * The file cannot be in construction (see gwy_dict_start_construction()). In particular, this function is not
 * suitable for the import of data from third party file formats, use gwy_file_pass_xyz().
 *
 * A new unused id is assigned to the XYZ data and returned. Generally, it is the lowest integer larger than any
 * existing XYZ data id (however, there are no specific guarantees).
 *
 * The XYZ data is not immediately shown in a new window when the GUI is running. Use gwy_file_set_visible() to
 * show it, preferrably after setting or synchronising visualisation settings like the colour mapping.
 *
 * Returns: The id of the newly added XYZ data in the file.
 **/
gint
gwy_file_add_xyz(GwyFile *file,
                 GwyScatter *scatter)
{
    g_return_val_if_fail(GWY_IS_FILE(file), -1);
    return add_data_object(file, scatter, GWY_FILE_XYZ);
}

/**
 * gwy_file_add_cmap:
 * @file: A data file container to add @lawn to.
 * @lawn: Data lawn to add.
 * @preview: (nullable): Data field to use as the preview. You can also pass %NULL to create a preview automatically.
 *
 * Adds a data lawn representing an curve map data to a file data container.
 *
 * The file cannot be in construction (see gwy_dict_start_construction()). In particular, this function is not
 * suitable for the import of data from third party file formats, use gwy_file_pass_cmap().
 *
 * A new unused id is assigned to the curve map data and returned. Generally, it is the lowest integer larger than any
 * existing curve map data id (however, there are no specific guarantees).
 *
 * The curve map data is not immediately shown in a new window when the GUI is running. Use gwy_file_set_visible() to
 * show it, preferrably after setting or synchronising visualisation settings like the colour mapping.
 *
 * Returns: The id of the newly added curve map data in the file.
 **/
gint
gwy_file_add_cmap(GwyFile *file,
                  GwyLawn *lawn,
                  GwyField *preview)
{
    g_return_val_if_fail(GWY_IS_FILE(file), -1);
    gint id = add_data_object(file, lawn, GWY_FILE_CMAP);
    if (id < 0)
        return id;

    GQuark quark = gwy_file_key_cmap_picture(id);
    GwyDict *container = GWY_DICT(file);
    if (preview) {
        if (GWY_IS_FIELD(preview))
            gwy_dict_set_object(container, quark, preview);
        preview = NULL;
    }
    if (!preview) {
        if (!gwy_dict_gis_object(container, quark, &preview) || !GWY_IS_FIELD(preview)) {
            preview = _gwy_app_create_lawn_preview_field(lawn);
            gwy_dict_pass_object(container, quark, preview);
        }
    }
    return id;
}

/**
 * gwy_file_remove:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data in file.
 *
 * Removes a data object from a file data contained together with all auxiliary information.
 *
 * The function can be used with files in construction (see gwy_dict_new_in_construction()), although usually
 * there is no reason for it.
 **/
void
gwy_file_remove(GwyFile *file,
                GwyDataKind data_kind,
                gint id)
{
    g_return_if_fail(data_kind < (guint)GWY_FILE_N_KINDS);
    /* Remove the main data item first. This should, hopefully, cause everything to discard the data and stop caring
     * about it. In particular, everything should ignore the subsequent cleanup of auxiliary items. */
    GQuark quark = data_kind_spec[data_kind].get_key(id);
    g_return_if_fail(quark);
    gwy_dict_remove(GWY_DICT(file), quark);
    const gchar *prefix = g_quark_to_string(quark);
    gwy_dict_remove_by_prefix(GWY_DICT(file), prefix);
}

/**
 * gwy_file_remove_selections:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of image-like data in file.
 *
 * Removes all selections associated with image-like data from a file.
 *
 * This is the preferred selection handling after in-place changes in data geometry as they have generally
 * unpredictable effects on selections. The currently active tool generally needs a selection and will be forced to
 * re-create it.
 *
 * The function can be used with files in construction (see gwy_dict_new_in_construction()), although usually
 * there is no reason for it.
 **/
void
gwy_file_remove_selections(GwyFile *file,
                           GwyDataKind data_kind,
                           gint id)
{
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_SELECTION, .suffix = NULL };
    const gchar *prefix = gwy_form_string_file_key(&parsed);
    g_return_if_fail(prefix);
    gwy_dict_remove_by_prefix(GWY_DICT(file), prefix);
}

/**
 * gwy_file_remove_logs:
 * @file: A data file container.
 *
 * Removes all data processing logs from a file.
 **/
void
gwy_file_remove_logs(GwyFile *file)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFileKeyParsed parsed = { .piece = GWY_FILE_PIECE_LOG, .suffix = NULL };
    GwyFilePrivate *priv = file->priv;
    GwyDict *container = GWY_DICT(file);
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        GArray *ids = priv->data_kind_info->ids;
        guint n = ids->len;
        parsed.data_kind = data_kind;
        for (guint j = 0; j < n; j++) {
            parsed.id = g_array_index(ids, gint, j);
            gwy_dict_remove(container, gwy_form_file_key(&parsed));
        }
    }
}

/* XXX GTK3: Not nice for bindings; there is no way to tell gobject-introspection the array is -1-terminated. However,
 * this style of return value is used in the codebase and can be auto-transformed to the new function. */
/**
 * gwy_file_get_ids:
 * @file: A data file container.
 * @data_kind: Type of data object.
 *
 * Gets the list of ids of all objects of given kind in a file.
 *
 * When there are no data objects of given kind, a non-%NULL array is still returned. It contains the single value -1.
 *
 * The file must not be in construction.
 *
 * Returns: A newly allocated, −1-terminated list of all the ids.
 **/
gint*
gwy_file_get_ids(GwyFile *file,
                 GwyDataKind data_kind)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(data_kind < (guint)GWY_FILE_N_KINDS, NULL);
    GwyFilePrivate *priv = file->priv;
    GArray *ids = priv->data_kind_info[data_kind].ids;
    if (!ids) {
        gint *retval = g_new(gint, 1);
        retval[0] = -1;
        return retval;
    }
    gint *retval = g_new(gint, ids->len+1);
    gwy_assign(retval, &g_array_index(ids, gint, 0), ids->len);
    retval[ids->len] = -1;
    return retval;
}

/**
 * gwy_file_get_n_data:
 * @file: A data file container.
 * @data_kind: Type of data object.
 *
 * Gets the number of data objects of a given kind in a file.
 *
 * This is the preferred function for checking whether there are any data of given kind at all.
 *
 * The file must not be in construction.
 *
 * Returns: The number of data items.
 **/
guint
gwy_file_get_n_data(GwyFile *file,
                   GwyDataKind data_kind)
{
    g_return_val_if_fail(GWY_IS_FILE(file), 0);
    g_return_val_if_fail(data_kind < (guint)GWY_FILE_N_KINDS, 0);
    GwyFilePrivate *priv = file->priv;
    GArray *ids = priv->data_kind_info[data_kind].ids;
    return ids ? ids->len : 0;
}

/* XXX GTK3: This is a function nice for bindings, but incompatible with existing code which assumes the -1 terminated
 * new array.
 * XXX: If we change the internal representation (for instance to GSequence), it becomes quite difficult to return
 * a const array. So returning a newly allocated array is the only future-proof option. */
/**
 * gwy_file_get_idsv:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @n: (optional) (inout): Location to store the number of returned ids.
 *
 * Gets the list of ids of all objects of given kind in a file.
 *
 * The returned pointer remains valid only as long as @file exists and data objects of kind @data_kind are not added
 * or removed. The array is not −1-terminated so unless you obtain the number of data objects by other means you need
 * to get @n.
 *
 * The file must not be in construction.
 *
 * Returns: A newly allocated list of all the ids with @n elements.
 **/
gint*
gwy_file_get_idsv(GwyFile *file,
                  GwyDataKind data_kind,
                  guint *n)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(data_kind < (guint)GWY_FILE_N_KINDS, NULL);
    GwyFilePrivate *priv = file->priv;
    GArray *ids = priv->data_kind_info[data_kind].ids;
    if (!ids) {
        if (n)
            *n = 0;
        return NULL;
    }

    if (n)
        *n = ids->len;

    return g_memdup2(ids->data, ids->len*sizeof(gint));
}

/**
 * gwy_file_get_data:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of data in file.
 *
 * Gets the primary data object of specified type and id.
 *
 * The type of the returned object is whatever is appropriate for given data kind. This function is mainly useful
 * when you need to check generically a data object for some data management purpose. For data processing, usually
 * functions such as gwy_file_get_image() or gwy_file_get_volume() are preferred as they return a specific type.
 *
 * It is not an error to ask for an object which does not exist in the file. The function returns %NULL in such case.
 * However, @data_kind and @id must be valid in principle.
 *
 * Returns: (transfer none) (nullable):
 *          The main data object in the file corresponding to @data_kind and @id, or %NULL if it does not exist.
 **/
GObject*
gwy_file_get_data(GwyFile *file,
                  GwyDataKind data_kind,
                  gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail((guint)data_kind < GWY_FILE_N_KINDS, NULL);
    g_return_val_if_fail(id >= 0, NULL);

    GQuark quark = data_kind_spec[data_kind].get_key(id);
    GObject *object = NULL;
    gwy_dict_gis_object(GWY_DICT(file), quark, &object);
    return object;
}

/**
 * gwy_file_set_image:
 * @file: A data file container.
 * @id: Numerical id of an image in @file.
 * @field: (transfer none): Data field representing an image.
 *
 * Sets a data field as the image in a data file container with no ownership transfer.
 **/
void
gwy_file_set_image(GwyFile *file,
                   gint id,
                   GwyField *field)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(id >= 0);
    gwy_dict_set_object(GWY_DICT(file), gwy_file_key_image(id), field);
}

/**
 * gwy_file_pass_image:
 * @file: A data file container.
 * @id: Numerical id of an image in @file.
 * @field: (transfer full): Data field representing an image.
 *
 * Sets a data field as the image in a data file container passing the ownership to the file.
 **/
void
gwy_file_pass_image(GwyFile *file,
                    gint id,
                    GwyField *field)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(id >= 0);
    gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_image(id), field);
}

/**
 * gwy_file_get_image:
 * @file: A data file container.
 * @id: Numerical id of an image in @file.
 *
 * Gets a data field representing an image in a data file container.
 *
 * It is not an error to ask for an image which does not exist in the file. The function returns %NULL in such case.
 * However, @id must be valid (non-negative).
 *
 * Returns: (transfer none) (nullable): Data field representing an image, or %NULL.
 **/
GwyField*
gwy_file_get_image(GwyFile *file,
                   gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    GwyField *field = NULL;
    gwy_dict_gis_object(GWY_DICT(file), gwy_file_key_image(id), &field);
    return field;
}

/**
 * gwy_file_set_image_mask:
 * @file: A data file container.
 * @id: Numerical id of an image mask in @file.
 * @mask: (transfer none) (nullable): Data field representing a mask.
 *
 * Sets a number field as the image mask in a data file container with no ownership transfer.
 *
 * If @mask is passed as %NULL, any existing mask object is removed.
 **/
void
gwy_file_set_image_mask(GwyFile *file,
                        gint id,
                        GwyNield *mask)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(!mask || GWY_IS_NIELD(mask));
    g_return_if_fail(id >= 0);
    if (mask)
        gwy_dict_set_object(GWY_DICT(file), gwy_file_key_image_mask(id), mask);
    else
        gwy_dict_remove(GWY_DICT(file), gwy_file_key_image_mask(id));
}

/**
 * gwy_file_pass_image_mask:
 * @file: A data file container.
 * @id: Numerical id of an image mask in @file.
 * @mask: (transfer full): Data field representing a mask.
 *
 * Sets a number field as the image mask in a data file container passing the ownership to the file.
 **/
void
gwy_file_pass_image_mask(GwyFile *file,
                         gint id,
                         GwyNield *mask)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_NIELD(mask));
    g_return_if_fail(id >= 0);
    gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_image_mask(id), mask);
}

/**
 * gwy_file_get_image_mask:
 * @file: A data file container.
 * @id: Numerical id of an image mask in @file.
 *
 * Gets a number field representing the image mask in a data file container.
 *
 * It is not an error to ask for a mask which does not exist in the file. The function returns %NULL in such case.
 * However, @id must be valid (non-negative).
 *
 * Returns: (transfer none) (nullable): Data field representing the mask, or %NULL.
 **/
GwyNield*
gwy_file_get_image_mask(GwyFile *file,
                        gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    GwyNield *nield = NULL;
    gwy_dict_gis_object(GWY_DICT(file), gwy_file_key_image_mask(id), &nield);
    return nield;
}

/**
 * gwy_file_set_graph:
 * @file: A data file container.
 * @id: Numerical id of a graph in @file.
 * @gmodel: (transfer none): Graph model representing a graph.
 *
 * Sets a graph model as the graph in a data file container with no ownership transfer.
 **/
void
gwy_file_set_graph(GwyFile *file,
                   gint id,
                   GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));
    g_return_if_fail(id >= 0);
    gwy_dict_set_object(GWY_DICT(file), gwy_file_key_graph(id), gmodel);
}

/**
 * gwy_file_pass_graph:
 * @file: A data file container.
 * @id: Numerical id of a graph in @file.
 * @gmodel: (transfer full): Graph model representing a graph.
 *
 * Sets a graph model as the graph in a data file container passing the ownership to the file.
 **/
void
gwy_file_pass_graph(GwyFile *file,
                    gint id,
                    GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));
    g_return_if_fail(id >= 0);
    gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_graph(id), gmodel);
}

/**
 * gwy_file_get_graph:
 * @file: A data file container.
 * @id: Numerical id of a graph in @file.
 *
 * Gets a graph model representing a graph in a data file container.
 *
 * It is not an error to ask for a graph which does not exist in the file. The function returns %NULL in such case.
 * However, @id must be valid (non-negative).
 *
 * Returns: (transfer none) (nullable): Graph model representing a graph, or %NULL.
 **/
GwyGraphModel*
gwy_file_get_graph(GwyFile *file,
                   gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    GwyGraphModel *gmodel = NULL;
    gwy_dict_gis_object(GWY_DICT(file), gwy_file_key_graph(id), &gmodel);
    return gmodel;
}

/**
 * gwy_file_set_volume:
 * @file: A data file container.
 * @id: Numerical id of volume data in @file.
 * @brick: (transfer none): Data brick representing volume data.
 *
 * Sets a data brick as the volume data in a data file container with no ownership transfer.
 **/
void
gwy_file_set_volume(GwyFile *file,
                    gint id,
                    GwyBrick *brick)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(id >= 0);
    gwy_dict_set_object(GWY_DICT(file), gwy_file_key_volume(id), brick);
}

/**
 * gwy_file_pass_volume:
 * @file: A data file container.
 * @id: Numerical id of volume data in @file.
 * @brick: (transfer full): Data brick representing volume data.
 *
 * Sets a data brick as the volume data in a data file container passing the ownership to the file.
 **/
void
gwy_file_pass_volume(GwyFile *file,
                     gint id,
                     GwyBrick *brick)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(id >= 0);
    gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_volume(id), brick);
}

/**
 * gwy_file_get_volume:
 * @file: A data file container.
 * @id: Numerical id of volume data in @file.
 *
 * Gets a data brick representing volume data in a data file container.
 *
 * It is not an error to ask for volume data which does not exist in the file. The function returns %NULL in such case.
 * However, @id must be valid (non-negative).
 *
 * Returns: (transfer none) (nullable): Data brick representing volume data, or %NULL.
 **/
GwyBrick*
gwy_file_get_volume(GwyFile *file,
                    gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    GwyBrick *brick = NULL;
    gwy_dict_gis_object(GWY_DICT(file), gwy_file_key_volume(id), &brick);
    return brick;
}

/**
 * gwy_file_set_spectra:
 * @file: A data file container.
 * @id: Numerical id of single point spectra in @file.
 * @spectra: (transfer none): Spectra object representing single point spectra.
 *
 * Sets a spectra object as the single point spectra in a data file container with no ownership transfer.
 **/
void
gwy_file_set_spectra(GwyFile *file,
                     gint id,
                     GwySpectra *spectra)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    g_return_if_fail(id >= 0);
    gwy_dict_set_object(GWY_DICT(file), gwy_file_key_spectra(id), spectra);
}

/**
 * gwy_file_pass_spectra:
 * @file: A data file container.
 * @id: Numerical id of single point spectra in @file.
 * @spectra: (transfer full): Spectra object representing single point spectra.
 *
 * Sets a spectra object as the single point spectra in a data file container passing the ownership to the file.
 **/
void
gwy_file_pass_spectra(GwyFile *file,
                      gint id,
                      GwySpectra *spectra)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    g_return_if_fail(id >= 0);
    gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_spectra(id), spectra);
}

/**
 * gwy_file_get_spectra:
 * @file: A data file container.
 * @id: Numerical id of single point spectra in @file.
 *
 * Gets a spectra object representing single point spectra in a data file container.
 *
 * It is not an error to ask for single point spectra which does not exist in the file. The function returns %NULL in
 * such case. However, @id must be valid (non-negative).
 *
 * Returns: (transfer none) (nullable): Spectra object representing single point spectra, or %NULL.
 **/
GwySpectra*
gwy_file_get_spectra(GwyFile *file,
                     gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    GwySpectra *spectra = NULL;
    gwy_dict_gis_object(GWY_DICT(file), gwy_file_key_spectra(id), &spectra);
    return spectra;
}

/**
 * gwy_file_set_xyz:
 * @file: A data file container.
 * @id: Numerical id of XYZ data in @file.
 * @scatter: (transfer none): Scatter object representing XYZ data.
 *
 * Sets a scatter object as the XYZ data in a data file container with no ownership transfer.
 **/
void
gwy_file_set_xyz(GwyFile *file,
                 gint id,
                 GwyScatter *scatter)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(id >= 0);
    gwy_dict_set_object(GWY_DICT(file), gwy_file_key_xyz(id), scatter);
}

/**
 * gwy_file_pass_xyz:
 * @file: A data file container.
 * @id: Numerical id of XYZ data in @file.
 * @scatter: (transfer full): Scatter object representing XYZ data.
 *
 * Sets a scatter object as the XYZ data in a data file container passing the ownership to the file.
 **/
void
gwy_file_pass_xyz(GwyFile *file,
                  gint id,
                  GwyScatter *scatter)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_SCATTER(scatter));
    g_return_if_fail(id >= 0);
    gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_xyz(id), scatter);
}

/**
 * gwy_file_get_xyz:
 * @file: A data file container.
 * @id: Numerical id of XYZ data in @file.
 *
 * Gets a scatter object representing XYZ data in a data file container.
 *
 * It is not an error to ask for XYZ data which does not exist in the file. The function returns %NULL in such case.
 * However, @id must be valid (non-negative).
 *
 * Returns: (transfer none) (nullable): Scatter object representing XYZ data, or %NULL.
 **/
GwyScatter*
gwy_file_get_xyz(GwyFile *file,
                 gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    GwyScatter *scatter = NULL;
    gwy_dict_gis_object(GWY_DICT(file), gwy_file_key_xyz(id), &scatter);
    return scatter;
}

/**
 * gwy_file_set_cmap:
 * @file: A data file container.
 * @id: Numerical id of a curve map in @file.
 * @lawn: (transfer none): Data lawn representing a curve map.
 *
 * Sets a data lawn as the a curve map in a data file container with no ownership transfer.
 **/
void
gwy_file_set_cmap(GwyFile *file,
                  gint id,
                  GwyLawn *lawn)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(id >= 0);
    gwy_dict_set_object(GWY_DICT(file), gwy_file_key_cmap(id), lawn);
}

/**
 * gwy_file_pass_cmap:
 * @file: A data file container.
 * @id: Numerical id of a curve map in @file.
 * @lawn: (transfer full): Data lawn representing a curve map.
 *
 * Sets a data lawn as the a curve map in a data file container passing the ownership to the file.
 **/
void
gwy_file_pass_cmap(GwyFile *file,
                   gint id,
                   GwyLawn *lawn)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(id >= 0);
    gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_cmap(id), lawn);
}

/**
 * gwy_file_get_cmap:
 * @file: A data file container.
 * @id: Numerical id of a curve map in @file.
 *
 * Gets a data lawn representing a curve map in a data file container.
 *
 * It is not an error to ask for a curve map which does not exist in the file. The function returns %NULL in such case.
 * However, @id must be valid (non-negative).
 *
 * Returns: (transfer none) (nullable): Data lawn representing a curve map, or %NULL.
 **/
GwyLawn*
gwy_file_get_cmap(GwyFile *file,
                  gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(id >= 0, NULL);
    GwyLawn *lawn = NULL;
    gwy_dict_gis_object(GWY_DICT(file), gwy_file_key_cmap(id), &lawn);
    return lawn;
}

static void
set_title(GwyFile *file, GwyDataKind data_kind, gint id,
          const gchar *const_name, gchar *allocated_name)
{
    /* TODO: We should clean up this mess. For now, abstract it. */
    GwyDict *container = GWY_DICT(file);
    GQuark quark = 0;

    GObject *object;
    if (!gwy_dict_gis_object(container, data_kind_spec[data_kind].get_key(id), &object)) {
        g_warning("Setting title of a non-existent data item.");
    }

    if (data_kind == GWY_FILE_IMAGE)
        quark = gwy_file_key_image_title(id);
    else if (data_kind == GWY_FILE_GRAPH)
        g_object_set(object, "title", const_name, NULL);
    else if (data_kind == GWY_FILE_SPECTRA)
        gwy_spectra_set_title(GWY_SPECTRA(object), const_name);
    else if (data_kind == GWY_FILE_VOLUME)
        quark = gwy_file_key_volume_title(id);
    else if (data_kind == GWY_FILE_XYZ)
        quark = gwy_file_key_xyz_title(id);
    else if (data_kind == GWY_FILE_CMAP)
        quark = gwy_file_key_cmap_title(id);
    else {
        g_assert_not_reached();
    }

    if (quark) {
        if (allocated_name) {
            gwy_dict_pass_string(container, quark, allocated_name);
            allocated_name = NULL;
        }
        else
            gwy_dict_set_string(container, quark, const_name);
    }
    g_free(allocated_name);
}

/**
 * gwy_file_set_title:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of an existing data object in @file.
 * @name: (nullable): New title to set. It can be %NULL to use something like ‘Untitled’.
 * @numbered: %TRUE to append the numeric id to the title, %FALSE to set it as-is.
 *
 * Sets the title of a data object in a file data container.
 *
 * This is a convenience wrapper, abstracting setting the title for different data types.
 *
 * Depending on @numbered, the id is possibly appended to the name. When @name is %NULL, id is always appended,
 * regardless of @numbered.
 *
 * Passing @numbered as %TRUE is generally recommended for derived data computed in modules with generic names such as
 * ‘Drift-corrected’. Without numbering there would be lots of data with the same name, making it difficult to
 * distinguish them.
 *
 * For channels in imported files, which generally carry file-unique names, it is fine to not append numbers. See also
 * gwy_file_pass_title() which may be more useful on file import.
 *
 * The function can be used with files in construction (see gwy_dict_new_in_construction()), provided that the
 * tiltle is set after the primary data object.
 **/
void
gwy_file_set_title(GwyFile *file,
                   GwyDataKind data_kind,
                   gint id,
                   const gchar *name,
                   gboolean numbered)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(data_kind < (guint)GWY_FILE_N_KINDS);
    g_return_if_fail(id >= 0);

    if (!name) {
        name = _("Untitled");
        numbered = TRUE;
    }
    gchar *freeme = NULL;
    if (numbered)
        name = freeme = g_strdup_printf("%s %d", name, id);

    set_title(file, data_kind, id, name, freeme);
}

/**
 * gwy_file_pass_title:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of an existing data object in @file.
 * @name: (transfer full): Title to set. It may not be %NULL.
 *
 * Sets the title of a data object in a file data container, passing string ownership.
 *
 * This is a convenience wrapper, abstracting setting the title for different data types.
 *
 * Unlike in gwy_file_set_title(), @name is always used unmodified and may not be %NULL. Uts ownership is passed to
 * the file (or other object).
 *
 * The function can be used with files in construction (see gwy_dict_new_in_construction()), provided that the
 * tiltle is set after the primary data object.
 **/
void
gwy_file_pass_title(GwyFile *file,
                    GwyDataKind data_kind,
                    gint id,
                    gchar *name)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(data_kind < (guint)GWY_FILE_N_KINDS);
    g_return_if_fail(id >= 0);
    set_title(file, data_kind, id, name, name);
}

/**
 * gwy_file_get_title:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 *
 * Gets the title of a data object in a file data container.
 *
 * If no title is set this function can return %NULL. Use gwy_file_get_display_title() if you want to always get
 * some useful identifying string.
 *
 * Returns: (nullable):
 *          The title as a string owned by the corresponding data object.
 **/
const gchar*
gwy_file_get_title(GwyFile *file,
                   GwyDataKind data_kind,
                   gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(data_kind < (guint)GWY_FILE_N_KINDS, NULL);
    g_return_val_if_fail(id >= 0, NULL);

    GQuark quark = data_kind_spec[data_kind].get_key(id);
    GwyDict *container = GWY_DICT(file);
    GObject *object = NULL;
    gwy_dict_gis_object(container, quark, &object);

    /* TODO: We should clean up this mess. For now, abstract it. */
    const gchar *title = NULL;
    if (data_kind == GWY_FILE_IMAGE)
        gwy_dict_gis_string(container, gwy_file_key_image_title(id), &title);
    else if (data_kind == GWY_FILE_GRAPH) {
        if (object)
            title = gwy_graph_model_get_title(GWY_GRAPH_MODEL(object));
    }
    else if (data_kind == GWY_FILE_SPECTRA) {
        if (object)
            title = gwy_spectra_get_title(GWY_SPECTRA(object));
    }
    else if (data_kind == GWY_FILE_VOLUME)
        gwy_dict_gis_string(container, gwy_file_key_volume_title(id), &title);
    else if (data_kind == GWY_FILE_XYZ)
        gwy_dict_gis_string(container, gwy_file_key_xyz_title(id), &title);
    else if (data_kind == GWY_FILE_CMAP)
        gwy_dict_gis_string(container, gwy_file_key_cmap_title(id), &title);
    else {
        g_assert_not_reached();
    }

    return title;
}

/**
 * gwy_file_get_display_title:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 *
 * Gets a suitable title of a data object in a file data container.
 *
 * The function returns a non-%NULL string even if no data title has been set or is empty. It is something like
 * ‘Untitled 123’ in such case. It makes it useful for having at least some identifying label for all data objects,
 * regardless if they have any title set. However, if you really need to know whether a title has been set use
 * gwy_file_get_title().
 *
 * Returns: The title as a newly allocated string.
 **/
gchar*
gwy_file_get_display_title(GwyFile *file,
                           GwyDataKind data_kind,
                           gint id)
{
    const gchar *title = gwy_file_get_title(file, data_kind, id);

    if (title)
        return g_strdup(title);
    return g_strdup_printf("%s %d", _("Untitled"), id);
}

/**
 * gwy_file_find_by_title:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @titleglob: (nullable):
 *             Pattern, as used by #GPatternSpec, to match the data titles against.
 *
 * Gets the list of all data objects in a data file container whose titles match the specified pattern.
 *
 * If titleglob is %NULL the function is identical to gwy_file_get_ids().
 *
 * For data objects with no title the synthetic title produced by gwy_file_get_display_title() is used.
 *
 * Returns: A newly allocated, −1-terminated list of all the ids.
 **/
gint*
gwy_file_find_by_title(GwyFile *file,
                       GwyDataKind data_kind,
                       const gchar *titleglob)
{
    if (!titleglob || !*titleglob)
        return gwy_file_get_ids(file, data_kind);

    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    g_return_val_if_fail(data_kind < (guint)GWY_FILE_N_KINDS, NULL);

    GwyFilePrivate *priv = file->priv;
    GArray *ids = priv->data_kind_info[data_kind].ids;
    if (!ids) {
        gint *retval = g_new(gint, 1);
        retval[0] = -1;
        return retval;
    }

    GPatternSpec *pattern = g_pattern_spec_new(titleglob);
    GArray *filtered_ids = g_array_new(FALSE, FALSE, sizeof(gint));
    guint n = ids->len;
    for (guint i = 0; i < n; i++) {
        gint id = g_array_index(ids, gint, i);
        const gchar *title = gwy_file_get_title(file, data_kind, id);
        if (title) {
            if (g_pattern_match_string(pattern, title))
                g_array_append_val(filtered_ids, id);
        }
        else {
            /* This is the traditional behaviour – use the display title if no real title is set. */
            gchar *t = g_strdup_printf("%s %d", _("Untitled"), id);
            if (g_pattern_match_string(pattern, t))
                g_array_append_val(filtered_ids, id);
            g_free(t);
        }
    }
    g_pattern_spec_free(pattern);

    gint id = -1;
    g_array_append_val(filtered_ids, id);
    return (gint*)g_array_free(filtered_ids, FALSE);
}

/**
 * gwy_file_set_visible:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 * @visible: %TRUE to display the data in a window, %FALSE to hide it.
 *
 * Sets the visibility of a data item in a data file container.
 *
 * The function can be used to either just mark the visibility status of data objects in files or actually control the
 * visibility in files open in the GUI. Closing the last window displaying any data of a particular file can close the
 * file entirely.
 *
 * The function can be used with files in construction (see gwy_dict_new_in_construction()).
 **/
void
gwy_file_set_visible(GwyFile *file,
                     GwyDataKind data_kind,
                     gint id,
                     gboolean visible)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_VISIBLE, .suffix = NULL };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_if_fail(quark);
    if (visible)
        gwy_dict_set_boolean(GWY_DICT(file), quark, TRUE);
    else
        gwy_dict_remove(GWY_DICT(file), quark);
    /* And data browser does the rest if the GUI is showing the file. */
}

/**
 * gwy_file_get_visible:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 *
 * Gets the visivility status of a data object in a data file container.
 *
 * The function returning %TRUE does not mean the data object is actually displayed in some window. It simply returns
 * the value of the flag and can be used with standalone files with no GUI running.
 *
 * Returns: %TRUE if the object is marked visible, %FALSE otherwise.
 **/
gboolean
gwy_file_get_visible(GwyFile *file,
                     GwyDataKind data_kind,
                     gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), FALSE);
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_VISIBLE, .suffix = NULL };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_val_if_fail(quark, FALSE);
    gboolean visible = FALSE;
    gwy_dict_gis_boolean(GWY_DICT(file), quark, &visible);
    return visible;
}

/**
 * gwy_file_pass_meta:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 * @meta: (transfer full): A data container holding data object's metadata. The reference is passed to @file.
 *
 * Sets the metadata for a data object in a data file container.
 *
 * This is a convenience wrapper for gwy_dict_pass_object() with slightly more checking. It can be used with
 * files in construction (see gwy_dict_new_in_construction()) and is most useful in file import modules.
 *
 * If @meta has still the in-construction flag set, the function calls gwy_dict_finish_construction() on it.
 * Preferably set the metadata after @meta has been filled with values.
 *
 * Analogous function gwy_file_set_meta() is missing intentionally. It creates too big temptation to use the same
 * object as metadata for multiple data, which is an error. Use #GwyFile functions to get the key and #GwyDict
 * functions to set the object if you really need to keep the metadata around after puttng it to the file. The useful
 * idioms are the following.
 *
 * With a function which creates metadata:
 * |[
 * gwy_file_pass_meta(file, GWY_FILE_IMAGE, id, create_metadata(...));
 * ]|
 *
 * With the same metadata for many data items:
 * |[
 * meta = create_metadata(...);
 * ...
 * gwy_file_pass_meta(file, GWY_FILE_IMAGE, id1, gwy_dict_copy(meta));
 * gwy_file_pass_meta(file, GWY_FILE_IMAGE, id2, gwy_dict_copy(meta));
 * gwy_file_pass_meta(file, GWY_FILE_IMAGE, id3, gwy_dict_copy(meta));
 * ...
 * g_object_unref(meta);
 * ]|
 *
 * With optional metadata:
 * |[
 * meta = gwy_dict_new();
 * ...
 * if (gwy_dict_get_n_items(meta))
 *     gwy_file_pass_meta(file, GWY_FILE_IMAGE, id, meta));
 * else
 *     g_object_unref(meta);
 * ]|
 **/
void
gwy_file_pass_meta(GwyFile *file,
                   GwyDataKind data_kind,
                   gint id,
                   GwyDict *meta)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_DICT(meta));
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_META, .suffix = NULL };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_if_fail(quark);
    /* FIXME: Why was this here? The function should basically only be called from file import modules with the file
     * still in construction. That is the point.
    if (gwy_dict_is_being_constructed(GWY_DICT(meta)))
        gwy_dict_finish_construction(GWY_DICT(meta));
        */
    gwy_dict_pass_object(GWY_DICT(file), quark, meta);
}

/**
 * gwy_file_get_meta:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 *
 * Gets the metadata for a data object in a data file container.
 *
 * Returns: (transfer none) (nullable):
 *          A data container holding data object's metadata.
 **/
GwyDict*
gwy_file_get_meta(GwyFile *file,
                  GwyDataKind data_kind,
                  gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), FALSE);
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_META, .suffix = NULL };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_val_if_fail(quark, FALSE);
    GwyDict *meta = NULL;
    gwy_dict_gis_object(GWY_DICT(file), quark, &meta);
    return meta;
}

/**
 * gwy_file_set_palette:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 * @name: (nullable):
 *        Gradient name. It can be %NULL to unset the gradient (causing the current default gradient to be used).
 *
 * Sets the gradient name for data visualisation in a data file container.
 **/
void
gwy_file_set_palette(GwyFile *file,
                     GwyDataKind data_kind,
                     gint id,
                     const gchar *name)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_PALETTE, .suffix = NULL };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_if_fail(quark);
    if (name)
        gwy_dict_set_string(GWY_DICT(file), quark, name);
    else
        gwy_dict_remove(GWY_DICT(file), quark);
}

/**
 * gwy_file_get_palette:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 *
 * Gets the gradient name for data visualisation in a data file container.
 *
 * Returns: (nullable):
 *          The gradient name as a string owned by the file. It can be %NULL if no gradient is set.
 **/
const gchar*
gwy_file_get_palette(GwyFile *file,
                     GwyDataKind data_kind,
                     gint id)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_PALETTE, .suffix = NULL };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_val_if_fail(quark, NULL);
    const gchar *gradient = NULL;
    gwy_dict_gis_string(GWY_DICT(file), quark, &gradient);
    return gradient;
}

/**
 * gwy_file_set_color_mapping:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 * @mapping: Mapping type to set.
 *
 * Sets the colour mapping type for data visualisation in a data file container.
 **/
void
gwy_file_set_color_mapping(GwyFile *file,
                           GwyDataKind data_kind,
                           gint id,
                           GwyColorMappingType mapping)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFileKeyParsed parsed = {
        .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_COLOR_MAPPING, .suffix = NULL
    };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_if_fail(quark);
    gwy_dict_set_enum(GWY_DICT(file), quark, mapping);
}

/**
 * gwy_file_get_color_mapping:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 *
 * Gets the colour mapping type for data visualisation in a data file container.
 *
 * If no mapping is set, the current default mapping type from user settings is returned.
 *
 * Returns: The colour mapping type.
 **/
GwyColorMappingType
gwy_file_get_color_mapping(GwyFile *file,
                           GwyDataKind data_kind,
                           gint id)
{
    GwyColorMappingType mapping = GWY_COLOR_MAPPING_FULL;
    g_return_val_if_fail(GWY_IS_FILE(file), mapping);
    GwyFileKeyParsed parsed = {
        .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_COLOR_MAPPING, .suffix = NULL
    };
    GQuark quark = gwy_form_file_key(&parsed);
    g_return_val_if_fail(quark, mapping);
    if (!gwy_dict_gis_enum(GWY_DICT(file), quark, &mapping))
        gwy_dict_gis_enum_by_name(gwy_app_settings_get(), "/app/default-range-type", &mapping);
    return mapping;
}

/**
 * gwy_file_set_fixed_range:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 * @min: Fixed range minimum.
 * @max: Fixed range maximum.
 *
 * Sets the fixed colour mapping range for data visualisation in a data file container.
 **/
void
gwy_file_set_fixed_range(GwyFile *file,
                         GwyDataKind data_kind,
                         gint id,
                         gdouble min,
                         gdouble max)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_RANGE, .suffix = NULL };
    parsed.suffix = "min";
    gwy_dict_set_double(GWY_DICT(file), gwy_form_file_key(&parsed), min);
    parsed.suffix = "max";
    gwy_dict_set_double(GWY_DICT(file), gwy_form_file_key(&parsed), max);
}

/**
 * gwy_file_get_fixed_range:
 * @file: A data file container.
 * @data_kind: Type of data object.
 * @id: Numerical id of a data object in @file.
 * @min: (out) (nullable): Location to store fixed range minimum.
 * @max: (out) (nullable): Location to store fixed range maximum.
 *
 * Gets the fixed colour mapping range for data visualisation in a data file container.
 *
 * It is possible (although unusual) to request just one of the values, or even none.
 *
 * Returns: %TRUE if all requested values were retrieved, %FALSE otherwise.
 **/
gboolean
gwy_file_get_fixed_range(GwyFile *file,
                         GwyDataKind data_kind,
                         gint id,
                         gdouble *min,
                         gdouble *max)
{
    g_return_val_if_fail(GWY_IS_FILE(file), FALSE);
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_RANGE, .suffix = NULL };
    gboolean ok = TRUE;
    if (min) {
        parsed.suffix = "min";
        if (!gwy_dict_gis_double(GWY_DICT(file), gwy_form_file_key(&parsed), min))
            ok = FALSE;
    }
    if (max) {
        parsed.suffix = "max";
        if (!gwy_dict_gis_double(GWY_DICT(file), gwy_form_file_key(&parsed), max))
            ok = FALSE;
    }
    return ok;
}

// Do not use shallow=TRUE unless it is a transfer-and-destroy-original situation (e.g. gwy_file_merge()).
static void
sync_one_item(GwyDict *src, GwyDict *dest,
              GwyDataKind src_data_kind, GwyDataKind dest_data_kind,
              gint from_id, gint to_id,
              GwyFilePiece piece, const gchar *suffix,
              gboolean shallow, gboolean delete_too)
{
    // Be lenient for to simplify generic copying calls. If the piece is not valid for either destination or source,
    // just skip it.
    if (!file_piece_is_valid(dest_data_kind, piece, suffix)
        || (src_data_kind != dest_data_kind && !file_piece_is_valid(src_data_kind, piece, suffix)))
        return;

    GwyFileKeyParsed srcparsed = { .id = from_id, .data_kind = src_data_kind, .piece = piece, .suffix = suffix };
    GwyFileKeyParsed destparsed = { .id = to_id, .data_kind = dest_data_kind, .piece = piece, .suffix = suffix };
    GQuark srckey = gwy_form_file_key(&srcparsed), destkey = gwy_form_file_key(&destparsed);

    gboolean src_exists = gwy_dict_contains(src, srckey);
    if (!src_exists) {
        if (delete_too)
            gwy_dict_remove(dest, destkey);
        return;
    }

    if (shallow) {
        GValue value = gwy_dict_get_value(src, srckey);
        gwy_dict_set_value(dest, destkey, &value);
    }
    else
        gwy_dict_copy_value(src, dest, srckey, destkey);
}

// Do not sync REF. It does not work across files.
static void
sync_one_file_piece(GwyDict *source, GwyDict *dest,
                    GwyDataKind src_data_kind, GwyDataKind dest_data_kind,
                    gint from_id, gint to_id,
                    GwyFilePiece piece, gboolean shallow, gboolean delete_too)
{
    if (piece == GWY_FILE_PIECE_PALETTE
        || piece == GWY_FILE_PIECE_MASK
        || piece == GWY_FILE_PIECE_MASK_COLOR
        || piece == GWY_FILE_PIECE_COLOR_MAPPING
        || piece == GWY_FILE_PIECE_REAL_SQUARE
        || piece == GWY_FILE_PIECE_PICTURE
        || piece == GWY_FILE_PIECE_META) {
        sync_one_item(source, dest, src_data_kind, dest_data_kind, from_id, to_id, piece, NULL, shallow, delete_too);
        const gchar *suffixes[] = { "type", "index", "skip-outliers" };
        for (guint i = 0; i < G_N_ELEMENTS(suffixes); i++) {
            sync_one_item(source, dest, src_data_kind, dest_data_kind, from_id, to_id,
                          GWY_FILE_PIECE_PICTURE, suffixes[i], shallow, delete_too);
        }
    }
    else if (piece == GWY_FILE_PIECE_TITLE) {
        const gchar *title = gwy_file_get_title(GWY_FILE(source), src_data_kind, from_id);
        if (title)
            gwy_file_set_title(GWY_FILE(dest), dest_data_kind, to_id, title, FALSE);
        else if (delete_too) {
            g_warning("Implement me! We do not have a generic function to unset the underlying title.");
        }
    }
    else if (piece == GWY_FILE_PIECE_RANGE) {
        sync_one_item(source, dest, src_data_kind, dest_data_kind, from_id, to_id,
                      GWY_FILE_PIECE_COLOR_MAPPING, NULL, shallow, delete_too);
        sync_one_item(source, dest, src_data_kind, dest_data_kind, from_id, to_id,
                      GWY_FILE_PIECE_RANGE, "min", shallow, delete_too);
        sync_one_item(source, dest, src_data_kind, dest_data_kind, from_id, to_id,
                      GWY_FILE_PIECE_RANGE, "max", shallow, delete_too);
    }
    else if (piece == GWY_FILE_PIECE_VIEW) {
        const gchar *suffixes[] = { "relative-size", "scale", "width", "height" };
        for (guint i = 0; i < G_N_ELEMENTS(suffixes); i++) {
            sync_one_item(source, dest, src_data_kind, dest_data_kind, from_id, to_id,
                          GWY_FILE_PIECE_VIEW, suffixes[i], shallow, delete_too);
        }
    }
    else if (piece == GWY_FILE_PIECE_SELECTION) {
        GwyFileKeyParsed srcparsed = { .id = from_id, .data_kind = src_data_kind, .piece = piece, .suffix = NULL };
        GwyFileKeyParsed destparsed = { .id = to_id, .data_kind = dest_data_kind, .piece = piece, .suffix = NULL };
        const gchar *srcprefix = gwy_form_string_file_key(&srcparsed);
        const gchar *destprefix = gwy_form_string_file_key(&destparsed);
        gwy_dict_transfer(source, dest, srcprefix, destprefix, shallow ? FALSE : TRUE, delete_too);
    }
    else {
        g_critical("Unhandled data piece type %u.", piece);
    }
}

/**
 * gwy_file_copy_data:
 * @source: Source data file container.
 * @data_kind: Type of data object.
 * @id: Source numerical id of data object.
 * @destination: Destination data file container.
 *
 * Completely duplicates data in a data file container including all auxiliary information.
 *
 * The source and destination files may be the same file. Visibility is not copied; the new data object is created as
 * not visible. The log is not copied either. See also gwy_data_browser_copy_data() for somewhat a more high-level
 * function for copying data in managed files.
 *
 * Returns: The id of the newly added data in file @destination.
 **/
gint
gwy_file_copy_data(GwyFile *source,
                   GwyDataKind data_kind,
                   gint id,
                   GwyFile *destination)
{
    g_return_val_if_fail(GWY_IS_FILE(source), -1);
    g_return_val_if_fail(GWY_IS_FILE(destination), -1);
    g_return_val_if_fail(data_kind < (guint)GWY_FILE_N_KINDS, -1);

    return copy_data(source, data_kind, id, destination, FALSE);
}

/**
 * gwy_file_sync_items:
 * @source: Source data file container.
 * @src_data_kind: Type of source data object.
 * @from_id: Source numerical id of data object.
 * @destination: Destination data file container.
 * @dest_data_kind: Type of destination data object.
 * @to_id: Destination numerical id of data object.
 * @items: Flags indicating the items to synchronise.
 * @delete_too: %TRUE to delete items in target if source does not contain them, %FALSE to copy only.
 *
 * Synchronizes auxiliary data items between data file containers.
 *
 * Use a bit mask formed from predefined constants such as %GWY_FILE_ITEM_REAL_SQUARE or unsigned 1 shifted left by
 * %GWY_FILE_PIECE_REAL_SQUARE for the flags. Note that the 1 may need to be 64bit if you are doing the shifting
 * yourself.
 *
 * The most common use is with @dest_data_kind = @src_data_kind. Nevertheless, some items make sense for multiple data
 * kinds and can be used with @dest_data_kind ≠ @src_data_kind. For example, the false colour gradient can be
 * synchronised between an image and volume data.
 **/
void
gwy_file_sync_items(GwyFile *source,
                    GwyDataKind src_data_kind,
                    gint from_id,
                    GwyFile *destination,
                    GwyDataKind dest_data_kind,
                    gint to_id,
                    guint64 items,
                    gboolean delete_too)
{
    g_return_if_fail(GWY_IS_FILE(source));
    g_return_if_fail(GWY_IS_FILE(destination));
    g_return_if_fail(from_id >= 0 && to_id >= 0);
    if (source == destination && from_id == to_id)
        return;

    GwyDict *src = GWY_DICT(source), *dest = GWY_DICT(destination);
    for (GwyFilePiece p = 0; items; p++) {
        if (items & 1)
            sync_one_file_piece(src, dest, src_data_kind, dest_data_kind, from_id, to_id, p, FALSE, delete_too);
        items >>= 1;
    }
}

/**
 * gwy_file_merge:
 * @file: A data file container.
 * @otherfile: (transfer full):
 *             A file to merge into @file. It must not be @file itself and must not be managed by the data browser.
 * @merged: (nullable) (scope call) (closure user_data) (destroy destroy):
 *          Optional function to call for each copied data item.
 * @user_data: Data passed to @merged.
 * @destroy: Function to destroy user data when done, possibly %NULL.
 *
 * Merges a data file container into another.
 *
 * The caller passes its reference on @otherfile to the function. The function releases the reference when it is
 * finished. The underlying assumption is that @otherfile is then destroyed. This enables an efficient merge as
 * objects in @otherfile can simply be added to @file and expensive copying is avoided. Keeping @otherfile around
 * after merging is not recommended. Use gwy_file_copy() on @otherfile beforhand if you want to add a copy of all its
 * data to @file.
 *
 * This is a low-level function and does not handle logs. The caller can use the @merge callback to copy logs and
 * add new log entries for the merge operation if appropriate.
 *
 * If @merged is given, it is called for each transferred item after the transfer is completely done, but before
 * visibility is set (i.e. before any widgets would be created if @file is displayed in the GUI). Source file will be
 * @otherfile and destination file @file. Since the transfer is already done, @merge can modify the @file. However,
 * caution is advised as changes can have cascading effects.
 **/
void
gwy_file_merge(GwyFile *file,
               GwyFile *otherfile,
               GwyFileTransferFunc merged,
               gpointer user_data,
               GDestroyNotify destroy)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(GWY_IS_FILE(otherfile));
    g_return_if_fail(otherfile != file);
    GwyFilePrivate *opriv = otherfile->priv;
    g_return_if_fail(!opriv->is_managed);

    GArray *visible_keys = g_array_new(FALSE, FALSE, sizeof(GQuark));
    GArray *transferred_info = merged ? g_array_new(FALSE, FALSE, sizeof(TransferredInfo)) : NULL;
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        DataKindInfo *oinfo = opriv->data_kind_info + data_kind;
        if (!oinfo->ids)
            continue;

        DataKindInfo *info = ensure_data_kind_info(file, data_kind);
        guint len = info->ids->len, ntoadd = oinfo->ids->len;
        gint first_free_id = len ? g_array_index(info->ids, gint, len-1) : 0;
        if ((guint)(G_MAXINT - first_free_id) < ntoadd) {
            g_critical("Too large data id %d. Cannot merge files.", first_free_id);
            break;
        }
        for (guint i = 0; i < ntoadd; i++) {
            gint oldid = g_array_index(oinfo->ids, gint, i);
            gint newid = copy_data(otherfile, data_kind, oldid, file, TRUE);
            if (gwy_file_get_visible(otherfile, data_kind, oldid)) {
                GwyFileKeyParsed parsed = {
                    .data_kind = data_kind, .id = newid, .piece = GWY_FILE_PIECE_VISIBLE, .suffix = NULL
                };
                GQuark quark = gwy_form_file_key(&parsed);
                g_array_append_val(visible_keys, quark);
            }
            if (transferred_info) {
                TransferredInfo ti = { .data_kind = data_kind, .source_id = oldid, .dest_id = newid };
                g_array_append_val(transferred_info, ti);
            }
        }
    }

    // This exists mainly to implement log handling in higher-level operations. But anyone is free to do any other
    // update or accouting.
    if (transferred_info) {
        for (guint i = 0; i < transferred_info->len; i++) {
            const TransferredInfo *ti = &g_array_index(transferred_info, TransferredInfo, i);
            merged(otherfile, file, ti->data_kind, ti->source_id, ti->dest_id, user_data);
        }
        g_array_free(transferred_info, TRUE);
    }
    if (destroy)
        destroy(user_data);

    // Once we have copied all data, set visibility flags. This can make widgets pop up if run in GUI. It is up to the
    // caller to do something along the lines of gwy_data_browser_reset_visibility(DEFAULT) on the unmanaged
    // @otherfile beforehand if something from the file should become visible.
    GwyDict *dict = GWY_DICT(file);
    for (guint i = 0; i < visible_keys->len; i++)
        gwy_dict_set_boolean(dict, g_array_index(visible_keys, GQuark, i), TRUE);
    g_array_free(visible_keys, TRUE);

    // TODO: Handle GWY_FILE_PIECE_REF stuff. But first we should make them actual links.
}

static guint
append_uint(gchar *buf, guint u)
{
    static const gchar digits[11] = "0123456789";

    guint len = 0;
    do {
        buf[len++] = digits[u % 10];
        u /= 10;
    } while (u);

    for (guint i = 0; i < len/2; i++)
        GWY_SWAP(gchar, buf[i], buf[len-1 - i]);

    return len;
}

/**
 * gwy_form_file_key:
 * @parsed: Broken down key in a data file.
 *
 * Constructs a data file quark key from its broken down representation.
 *
 * Although the function does not accept complete nonsense, it can form keys corresponding to data type and piece
 * combinations which do not actually exists. In other words, if @parsed was obtained using gwy_parse_file_key(), this
 * function can be used to construct the string (quark) key back. However, a key constructed by this function is not
 * guaranteed to be parseable using gwy_parse_file_key().
 *
 * See gwy_form_string_file_key() for a string version.
 *
 * Returns: The corresponding key as a quark.
 **/
GQuark
gwy_form_file_key(const GwyFileKeyParsed *parsed)
{
    g_return_val_if_fail(parsed, 0);

    if (parsed->piece == GWY_FILE_PIECE_FILENAME) {
        g_assert(parsed->data_kind == GWY_FILE_NONE);
        g_assert(parsed->id < 0);
        g_assert(!parsed->suffix);
        return g_quark_from_static_string("/filename");
    }

    g_return_val_if_fail(parsed->data_kind >= 0 && parsed->data_kind < (guint)GWY_FILE_N_KINDS, 0);
    g_return_val_if_fail(parsed->id >= 0, 0);

    const DataKindSpec *spec = data_kind_spec + parsed->data_kind;
    const gchar *piece_suffix = gwy_enum_to_string(parsed->piece, piece_suffixes, G_N_ELEMENTS(piece_suffixes));
    guint plen = strlen(spec->prefix), pslen = gwy_strlen0(piece_suffix), slen = gwy_strlen0(parsed->suffix);
    if (slen && !pslen) {
        g_warning("Key suffix given for unknown piece %u.", parsed->piece);
    }
    gchar key[plen + pslen + slen + 14];
    memcpy(key, spec->prefix, plen);
    key[plen] = '/';
    guint n = append_uint(key + plen + 1, parsed->id) + 1;
    if (pslen) {
        key[plen + n] = '/';
        memcpy(key + plen + n + 1, piece_suffix, pslen);
        pslen++;
    }
    if (slen) {
        key[plen + n + pslen] = '/';
        memcpy(key + plen + n + pslen + 1, parsed->suffix, slen);
        slen++;
    }
    key[plen + n + pslen + slen] = '\0';

    return g_quark_from_string(key);
}

/**
 * gwy_form_string_file_key:
 * @parsed: Broken down key in a data file.
 *
 * Constructs a data file string key from its broken down representation.
 *
 * See gwy_form_file_key() for a quark version and remarks.
 *
 * Returns: The corresponding key as a static string.
 **/
const gchar*
gwy_form_string_file_key(const GwyFileKeyParsed *parsed)
{
    g_return_val_if_fail(parsed, 0);

    if (parsed->piece == GWY_FILE_PIECE_FILENAME) {
        g_assert(parsed->data_kind == GWY_FILE_NONE);
        g_assert(parsed->id < 0);
        g_assert(!parsed->suffix);
        return "/filename";
    }

    /* Laundering through the quark effectively interns the string. */
    return g_quark_to_string(gwy_form_file_key(parsed));
}

/**
 * gwy_parse_file_key:
 * @strkey: String data file container key.
 * @parsed: (out):
 *          Location to store the parsed information. The contents of @parsed is undefined if the function return
 *          %FALSE.
 *
 * Parses a data file container key.
 *
 * The parsing usually identifies the data kind, id and the specific setting or property for the data (if applicable).
 * If a specific combination is impossible, for instance mask colour with graphs, the function returns %FALSE even
 * though the key may seem syntactically valid.
 *
 * Upon successful parsing all fields are always set (possibly to values meaning ‘none’). There are some special
 * cases. The piece %GWY_FILE_PIECE_FILENAME is file-global and has neither data kind nor id.
 *
 * When @parsed.suffix is not set to %NULL will generally point inside @strkey. Since #GwyDict keys are backed by
 * #GQuark, the corresponding string key is a constant string which always exists. Hence, the pointer will be always
 * valid. However, if you use gwy_parse_file_key() with an arbitrary string, be aware that modifying or freeing it
 * invalidates the suffix.
 *
 * See also gwy_form_file_key() for constructing the key back.
 *
 * Returns: %TRUE if the key was sucessfully parsed; %FALSE if it was not.
 **/
gboolean
gwy_parse_file_key(const gchar *strkey,
                   GwyFileKeyParsed *parsed)
{
    g_return_val_if_fail(parsed, FALSE);
    *parsed = (GwyFileKeyParsed){ -1, GWY_FILE_NONE, GWY_FILE_PIECE_NONE, NULL };
    g_return_val_if_fail(strkey, FALSE);

    if (strkey[0] != GWY_DICT_PATHSEP) {
        gwy_debug("malformed key %s", strkey);
        return FALSE;
    }

    /* Identify the data kind prefix if it is data (a few things might not be). */
    const DataKindSpec *spec = NULL;
    guint pos = 0;
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        spec = data_kind_spec + data_kind;
        pos = strlen(spec->prefix);
        if (strncmp(strkey, spec->prefix, pos) == 0) {
            parsed->data_kind = data_kind;
            break;
        }
        spec = NULL;
    }

    if (!spec) {
        /* Handle special non-data stuff. */
        if (gwy_strequal(strkey, "/filename")) {
            parsed->piece = GWY_FILE_PIECE_FILENAME;
            return TRUE;
        }
        gwy_debug("unknown no-data key %s", strkey);
        return FALSE;
    }

    /* If it is a data item then it must be followed with /123456. */
    if (strkey[pos++] != GWY_DICT_PATHSEP) {
        gwy_debug("malformed key %s", strkey);
        return FALSE;
    }

    /* Parse the number. Do not use strtol directly. It allows weird stuff like spaces. */
    guint len = 0;
    while (g_ascii_isdigit(strkey[pos + len]))
        len++;

    if (!len || len > 9) {
        gwy_debug("bad numerical id in %s", strkey);
        return FALSE;
    }

    parsed->id = atoi(strkey + pos);
    pos += len;
    if (parsed->id < 0) {
        gwy_debug("negative numerical id in %s", strkey);
        return FALSE;
    }

    /* If nothing follows the number, it is the main data item key. */
    if (!strkey[pos])
        return TRUE;
    if (strkey[pos++] != GWY_DICT_PATHSEP) {
        gwy_debug("malformed key %s", strkey);
        return FALSE;
    }

    for (guint i = 0; i < G_N_ELEMENTS(piece_suffixes); i++) {
        len = strlen(piece_suffixes[i].name);
        /* Compare piece name. The string must either end there or have a separator (do not confuse mask with
         * mask-color). */
        if (strncmp(strkey + pos, piece_suffixes[i].name, len) == 0) {
            if (!strkey[pos + len] || strkey[pos + len] == GWY_DICT_PATHSEP) {
                parsed->piece = piece_suffixes[i].value;
                break;
            }
        }
    }
    pos += len;
    if (parsed->piece == GWY_FILE_PIECE_NONE) {
        gwy_debug("no corresponding piece for %s", strkey);
        return FALSE;
    }

    /* Find the suffix. */
    if (strkey[pos]) {
        if (strkey[pos++] != GWY_DICT_PATHSEP) {
            gwy_debug("malformed key %s", strkey);
            return FALSE;
        }
        parsed->suffix = strkey + pos;
    }

    gboolean valid_piece = file_piece_is_valid(parsed->data_kind, parsed->piece, parsed->suffix);
    if (!valid_piece) {
        /* Either we did not find the piece at all for this data kind, or it has an unknown suffix. */
        gwy_debug("fell completely through %s", strkey);
    }
    return valid_piece;
}

/* Check if the data_kind/piece/suffix combination is valid. */
static gboolean
file_piece_is_valid(GwyDataKind data_kind, GwyFilePiece piece, const gchar *suffix)
{
    const DataKindSpec *spec = data_kind_spec + data_kind;
    const guint *piece_ids = spec->pieces;
    guint npieces = spec->npieces;
    for (guint i = 0; i < npieces; i++) {
        const FilePieceSpec *pspec = all_pieces + piece_ids[i];
        if (pspec->piece != piece)
            continue;

        /* Special-case selections. They must have a name but the name can be anything. */
        if (piece == GWY_FILE_PIECE_SELECTION)
            return suffix && strlen(suffix) && !strchr(suffix, '/');

        if (!pspec->suffix && !suffix)
            return TRUE;
        /* There are pieces (PICTURE) which have both base and suffixed keys. When just one suffix is NULL, just
         * consider it a mismatch and try another. */
        if (!pspec->suffix || !suffix)
            continue;
        if (gwy_strequal(pspec->suffix, suffix))
            return TRUE;
    }
    return FALSE;
}

/**
 * gwy_file_set_link_quark:
 * @file: A data file container.
 * @key: Link source in @file, as a quark.
 * @value: Link target in @file, as a quark.
 *
 * Creates a link in a data file container, specified using quarks.
 *
 * The link value is always a string. Use this function instead of gwy_file_set_link() for convenience when
 * you already have quarks. See its description for remarks.
 **/
void
gwy_file_set_link_quark(GwyFile *file,
                        GQuark key,
                        GQuark value)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(key);
    g_return_if_fail(value);
    set_link(file, key, value, NULL);
}

/**
 * gwy_file_set_link:
 * @file: A data file container.
 * @key: Link source in @file, as a string.
 * @value: Link target in @file, as a string.
 *
 * Creates a link in a data file container, specified using string keys.
 *
 * Link is a string which holds the key of some other data in the file. Its value can be obtained using
 * gwy_dict_get_string() or gwy_dict_gis_string(). The difference between links and plain strings is that
 * when data are moved around or merged, they can be automatically updated to point to the new location.
 *
 * Modifying the value by a #GwyDict function, even to another string, makes it a non-link. Therefore, do not do
 * it. There are no file items that can be both links and non-links. One of the two options is always invalid.
 **/
void
gwy_file_set_link(GwyFile *file,
                  const gchar *key,
                  const gchar *value)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(key);
    g_return_if_fail(value);
    set_link(file, g_quark_from_string(key), 0, value);
}

/**
 * gwy_file_value_is_link:
 * @file: A data file container.
 * @key: Key sin @file, as a string.
 *
 * Checks if a value in a data file container is a link.
 *
 * It is not an error to call this function with keys which do not corresponds to strings at all. It will return
 * %FALSE.
 *
 * Returns: %TRUE if @key is a link, %FALSE if it is not.
 **/
gboolean
gwy_file_value_is_link(GwyFile *file,
                       const gchar *key)
{
    g_return_val_if_fail(GWY_IS_FILE(file), FALSE);
    g_return_val_if_fail(key, FALSE);
    return find_link(file, g_quark_from_string(key)) >= 0;
}

/**
 * gwy_file_get_format_name:
 * @file: A data file container.
 *
 * Gets the file format name for a data file container.
 *
 * The file format name is the name of file import function used to import the data.
 *
 * Returns: (nullable): The file format name, possibly %NULL.
 **/
const gchar*
gwy_file_get_format_name(GwyFile *file)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    return file->priv->format_name;
}

/**
 * gwy_file_get_filename_sys:
 * @file: A data file
 *
 * Gets the file name corresponding to a data file container.
 *
 * The file name is set on two ocasions: file load and successful file save. File export does not set it.
 *
 * Returns: File name of @data (in GLib encoding), or %NULL. The returned string is owned by @file and is valid only
 *          until it is destroyed or saved again.
 **/
const gchar*
gwy_file_get_filename_sys(GwyFile *file)
{
    g_return_val_if_fail(GWY_IS_FILE(file), NULL);
    return file->priv->filename_sys;
}

void
_gwy_file_set_info(GwyFile *file, const gchar *name, const gchar *filename_sys)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFilePrivate *priv = file->priv;
    /* The function names are normally static, but it is currently not guaranteed. */
    priv->format_name = g_intern_string(name);
    gwy_assign_string(&priv->filename_sys, filename_sys);
}

/**
 * gwy_file_get_id:
 * @file: (nullable) (transfer none): A data file container.
 *
 * Gets the numerical identifier of a file.
 *
 * Only files managed by the data browser have an id. %GWY_FILE_ID_NONE is returned for free-standing files. You can
 * check whether a file is managed by calling this function and comparing the returned id to %GWY_FILE_ID_NONE.
 *
 * The identifier is not reused and does not change during program's lifetime. See also gwy_data_browser_get_file().
 *
 * For convenience, %NULL can be also passed. The function then returns a negative value (%GWY_FILE_ID_NONE).
 *
 * Returns: The id (a nonnegative integer), possibly %GWY_FILE_ID_NONE.
 **/
gint
gwy_file_get_id(GwyFile *file)
{
    if (!file)
        return GWY_FILE_ID_NONE;
    g_return_val_if_fail(GWY_IS_FILE(file), GWY_FILE_ID_NONE);
    /* Return GWY_FILE_ID_NONE for unmanaged files, but preserve the id even when some repeatedly adds the file to
     * the data browser and removes it again. */
    GwyFilePrivate *priv = file->priv;
    return priv->is_managed ? priv->id : GWY_FILE_ID_NONE;
}

void
_gwy_file_set_id(GwyFile *file, gint id)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFilePrivate *priv = file->priv;
    g_assert(priv->id == GWY_FILE_ID_NONE);
    priv->id = id;
}

void
_gwy_file_set_managed(GwyFile *file, gboolean is_managed)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GwyFilePrivate *priv = file->priv;
    priv->is_managed = is_managed;
}

static const gchar*
get_enum_nick(guint value, GType gtype)
{
    GEnumClass *klass = g_type_class_ref(gtype);
    const GEnumValue *enumval = g_enum_get_value(klass, value);
    const gchar *nick = "???";
    if (enumval)
        nick = enumval->value_nick;
    else {
        g_assert(enumval);
    }
    g_type_class_unref(klass);
    return nick;
}

const gchar*
_gwy_data_kind_name(GwyDataKind data_kind)
{
    return get_enum_nick(data_kind, GWY_TYPE_DATA_KIND);
}

const gchar*
_gwy_file_piece_name(GwyFilePiece piece)
{
    return get_enum_nick(piece, GWY_TYPE_FILE_PIECE);
}

static DataKindInfo*
ensure_data_kind_info(GwyFile *file, GwyDataKind data_kind)
{
    g_return_val_if_fail(data_kind < (guint)GWY_FILE_N_KINDS, NULL);

    GwyFilePrivate *priv = file->priv;
    DataKindInfo *info = priv->data_kind_info + data_kind;
    if (!info->ids) {
        info->ids = g_array_new(FALSE, FALSE, sizeof(gint));
    }
    return info;
}

static void
fill_piece_gtypes(FilePieceSpec *pieces, guint nspec)
{
    for (guint i = 0; i < nspec; i++) {
        FilePieceSpec *fps = pieces + i;
        GwyFilePiece piece = fps->piece;
        if (piece == GWY_FILE_PIECE_PICTURE && !fps->suffix)
            fps->type = GWY_TYPE_FIELD;
        else if (piece == GWY_FILE_PIECE_MASK)
            fps->type = GWY_TYPE_NIELD;
        else if (piece == GWY_FILE_PIECE_LOG)
            fps->type = GWY_TYPE_STRING_LIST;
        else if (piece == GWY_FILE_PIECE_META)
            fps->type = GWY_TYPE_DICT;
        else if (piece == GWY_FILE_PIECE_MASK_COLOR)
            fps->type = GWY_TYPE_RGBA;
        else if (piece == GWY_FILE_PIECE_SELECTION)
            fps->type = GWY_TYPE_SELECTION;
        else {
            g_assert(fps->type != 0);
        }
    }
}

/* Preferrably, this is not happening when file import modules construct the imported file as they use
 * gwy_dict_start_construction(). But if they do not, we keep up. It is just slower. */
static void
item_changed(GwyDict *container, GQuark key)
{
    GType type_after_the_change = gwy_dict_value_type(container, key);
    GwyFile *file = GWY_FILE(container);
    GwyFilePrivate *priv = file->priv;

    if (priv->setting_link != key) {
        gint linkpos = find_link(file, key);
        if (linkpos >= 0)
            g_array_remove_index_fast(priv->links, linkpos);
    }

    // If we got here, we must no longer be being constructed. Otherwise we can add data twice and similar nonsense.
    g_assert(!gwy_dict_is_being_constructed(container));
    const gchar *strkey = g_quark_to_string(key);
    GwyFileKeyParsed parsed;
    if (!gwy_parse_file_key(strkey, &parsed)) {
        g_warning("Cannot parse file key %s.", strkey);
        return;
    }

    if (parsed.data_kind == GWY_FILE_NONE)
        return;
    g_assert(parsed.id >= 0);

    /* We only care about the main data items here. */
    if (parsed.piece != GWY_FILE_PIECE_NONE)
        return;

    DataKindInfo *info = ensure_data_kind_info(file, parsed.data_kind);
    GArray *ids = info->ids;
    guint i, n = ids->len;
    for (i = 0; i < n; i++) {
        if (g_array_index(ids, gint, i) >= parsed.id)
            break;
    }
    if (i == n) {
        /* If we did not find the id in our list it had to be newly added, not removed (or changed). */
        g_assert(type_after_the_change);
        g_array_append_val(ids, parsed.id);
    }
    else if (g_array_index(ids, gint, i) != parsed.id) {
        /* If we did not find the id in our list it had to be newly added, not removed (or changed). */
        g_assert(type_after_the_change);
        g_array_insert_val(ids, i, parsed.id);
    }
    else if (!type_after_the_change) {
        /* The item was found but no longer exists. So it was deleted. */
        g_array_remove_index(ids, i);
    }
    /* Otherwise, the item was found and exists. It has only changed and we do not need to update the lists. */
}

static inline gboolean
check_piece_type(GType value_type, GType expected_type,
                 const gchar *strkey, GQuark quark, GArray *toremove,
                 GwyErrorList **error_list)
{
    if (g_type_is_a(value_type, expected_type))
        return TRUE;

    gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_TYPE,
                       _("Wrong type of ‘%s’ value (%s instead of %s)."),
                       strkey, g_type_name(value_type), g_type_name(expected_type));
    g_array_append_val(toremove, quark);
    return FALSE;
}

static gboolean
check_value_type(GType gtype, const GwyFileKeyParsed *parsed,
                 const gchar *strkey, GQuark quark,
                 GwyFilePrivate *priv)
{
    // Check the type by looking it up in all_pieces[].
    GwyDataKind data_kind = parsed->data_kind;
    if (data_kind == GWY_FILE_NONE)
        return check_piece_type(gtype, G_TYPE_STRING, strkey, quark, priv->toremove, priv->scan_errlist);
    g_assert(parsed->id >= 0);

    GwyFilePiece piece = parsed->piece;
    if (piece == GWY_FILE_PIECE_NONE) {
        return check_piece_type(gtype, data_kind_spec[data_kind].type, strkey, quark,
                                priv->toremove, priv->scan_errlist);
    }

    guint loose_match = G_MAXUINT, match = G_MAXUINT;
    const gchar *suffix = parsed->suffix;
    for (guint i = 0; i < G_N_ELEMENTS(all_pieces); i++) {
        if (piece == all_pieces[i].piece) {
            loose_match = i;
            if ((!suffix && !all_pieces[i].suffix)
                || (suffix && all_pieces[i].suffix && !strcmp(suffix, all_pieces[i].suffix))) {
                match = i;
                break;
            }
        }
    }
    if (match == G_MAXUINT) {
        if (loose_match == G_MAXUINT) {
            g_critical("Key parser understands %s but there does not seem to be a matching file piece.", strkey);
            g_array_append_val(priv->toremove, quark);
            return FALSE;
        }
        match = loose_match;
    }
    if (!check_piece_type(gtype, all_pieces[match].type, strkey, quark, priv->toremove, priv->scan_errlist))
        return FALSE;
    return TRUE;
}

static void
enumerate_data(GQuark quark, GValue *gvalue, gpointer user_data)
{
    GwyFile *file = (GwyFile*)user_data;
    GwyFilePrivate *priv = file->priv;
    const gchar *strkey = g_quark_to_string(quark);

    // This already catches all ways a key alone can be wrong: malformed, invalid piece/data combination, etc.
    GwyFileKeyParsed parsed;
    if (!gwy_parse_file_key(strkey, &parsed)) {
        gwy_error_list_add(priv->scan_errlist, GWY_FILE_ERROR, GWY_FILE_ERROR_KEY,
                           _("Uknown/invalid file key ‘%s’."), strkey);
        g_array_append_val(priv->toremove, quark);
        return;
    }

    if (!check_value_type(G_VALUE_TYPE(gvalue), &parsed, strkey, quark, priv))
        return;

    // We only care about the main data items for enumeration. For non-main data, we check orphaned items.
    if (parsed.piece == GWY_FILE_PIECE_NONE) {
        DataKindInfo *info = ensure_data_kind_info(file, parsed.data_kind);
        g_array_append_val(info->ids, parsed.id);
    }
    else if (parsed.data_kind == GWY_FILE_NONE) {
        // Valid global data cannot be orphaned.
    }
    else {
        g_assert(parsed.id >= 0);
        parsed.piece = GWY_FILE_PIECE_NONE;
        parsed.suffix = NULL;

        GObject *main_object;
        if (!gwy_dict_gis_object(GWY_DICT(file), gwy_form_file_key(&parsed), &main_object)) {
            gwy_error_list_add(priv->scan_errlist, GWY_FILE_ERROR, GWY_FILE_ERROR_ORPHAN,
                               _("Orphaned file item ‘%s’."), strkey);
            g_array_append_val(priv->toremove, quark);
        }
    }
}

static gint*
remap_huge_ids(const gint *oldids, guint n)
{
    gint *newids = g_new(gint, n);
    guint i;
    // Do not rename in an initial segment of ids starting from 0.
    for (i = 0; i < n && (guint)oldids[i] == i; i++)
        newids[i] = i;

    // We are only called when ids are very sparse, so this should not happen.
    if (i == n)
        return newids;

    // Choose new ids exclusively from empty blocks in old ids to avoid overwriting anything.
    guint iold = i;
    while (i < n) {
        gint nextid = (iold == n ? G_MAXINT : oldids[iold+1]);
        // Assing ids from the empty block between oldids[iold] and oldids[iold+1]. At the beginning, they may be
        // consecutive forcing mapping ids upwards. But we should see some large gaps eventually.
        for (gint j = oldids[iold]; j < nextid && i < n; j++)
            newids[i++] = j;
    }

    return newids;
}

static void
remap_id(GQuark quark, G_GNUC_UNUSED GValue *gvalue, gpointer user_data)
{
    const gchar *strkey = g_quark_to_string(quark);

    GwyFileKeyParsed parsed;
    if (!gwy_parse_file_key(strkey, &parsed)) {
        g_critical("Cannot parse key %s after all keys have been validated.", strkey);
        return;
    }

    GwyFile *file = (GwyFile*)user_data;
    GwyFilePrivate *priv = file->priv;
    DataKindInfo *info = priv->data_kind_info + parsed.data_kind;
    if (parsed.data_kind < 0 || !info->new_ids)
        return;

    guint n = info->ids->len;
    const gint *ids = &g_array_index(info->ids, gint, 0);
    for (guint i = 0; i < n; i++) {
        if (ids[i] == parsed.id) {
            parsed.id = info->new_ids[i];
            if (parsed.id == ids[i])
                return;

            RemapInfo remap = { .source = quark, .dest = gwy_form_file_key(&parsed) };
            g_array_append_val(priv->toremap, remap);
            return;
        }
    }
    g_return_if_reached();
}

static void
fix_huge_ids(GwyFile *file, GwyErrorList **error_list)
{
    GwyFilePrivate *priv = file->priv;
    gboolean has_any_huge_ids = FALSE;
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        DataKindInfo *info = priv->data_kind_info + data_kind;
        if (!info->ids)
            continue;

        const gint *ids = &g_array_index(info->ids, gint, 0);
        guint n = info->ids->len;
        if ((guint)ids[n-1] > 1000*n) {
            info->new_ids = remap_huge_ids(ids, n);
            has_any_huge_ids = TRUE;

            // Report all primary data items with huge ids (but not auxiliary as that would cause many times more
            // pointless messages).
            GwyFileKeyParsed parsed = {
                .data_kind = data_kind, .piece = GWY_FILE_PIECE_NONE, .suffix = NULL
            };
            for (guint i = 0; i < n; i++) {
                if (info->new_ids[i] == ids[i])
                    continue;

                parsed.id = ids[i];
                GQuark quark = gwy_form_file_key(&parsed);
                gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_HUGE_ID,
                                   _("Disproportionally huge data id in ‘%s’."), g_quark_to_string(quark));
            }
        }
    }
    if (!has_any_huge_ids)
        return;

    GArray *toremap = priv->toremap = g_array_new(FALSE, FALSE, sizeof(RemapInfo));
    GwyDict *dict = GWY_DICT(file);
    gwy_dict_foreach(dict, NULL, remap_id, file);
    for (guint i = 0; i < toremap->len; i++) {
        RemapInfo *info = &g_array_index(toremap, RemapInfo, i);
#ifdef DEBUG
        gwy_debug("fixing huge id <%s> -> <%s>\n", g_quark_to_string(info->source), g_quark_to_string(info->dest));
#endif
        gboolean remapped = gwy_dict_rename(dict, info->source, info->dest, FALSE);
        g_assert(remapped);
    }
    g_array_free(toremap, TRUE);
    priv->toremap = NULL;

    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        DataKindInfo *info = priv->data_kind_info + data_kind;
        if (!info->new_ids)
            continue;

        gint *ids = &g_array_index(info->ids, gint, 0);
        gwy_assign(ids, info->new_ids, info->ids->len);
        GWY_FREE(info->new_ids);
    }
}

typedef gboolean (*DataMismatchFunc)(GObject *item, GObject *main_item);

static gboolean
check_image_mask_mismatch(GObject *item, GObject *main_item)
{
    GwyDataMismatchFlags flags = GWY_DATA_MISMATCH_RES;
    return !gwy_nield_is_incompatible_with_field(GWY_NIELD(item), GWY_FIELD(main_item), flags);
}

static gboolean
check_image_show_mismatch(GObject *item, GObject *main_item)
{
    GwyDataMismatchFlags flags = GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_LATERAL;
    return !gwy_field_is_incompatible(GWY_FIELD(item), GWY_FIELD(main_item), flags);
}

static gboolean
check_volume_preview_mismatch(GObject *item, GObject *main_item)
{
    GwyDataMismatchFlags flags = GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_LATERAL;
    return !gwy_field_is_incompatible_with_brick_xy(GWY_FIELD(item), GWY_BRICK(main_item), flags);
}

static gboolean
check_cmap_preview_mismatch(GObject *item, GObject *main_item)
{
    GwyDataMismatchFlags flags = GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_LATERAL;
    return !gwy_field_is_incompatible_with_lawn(GWY_FIELD(item), GWY_LAWN(main_item), flags);
}

static void
check_compatibilty(GwyFile *file, GwyFileKeyParsed item_template,
                   DataMismatchFunc check_func, GwyErrorList **error_list)
{
    GwyFilePrivate *priv = file->priv;
    GArray *ids;

    if (!(ids = priv->data_kind_info[item_template.data_kind].ids))
        return;

    GwyFileKeyParsed main_template = item_template;
    main_template.suffix = NULL;
    main_template.piece = GWY_FILE_PIECE_NONE;

    GwyDict *dict = GWY_DICT(file);
    for (guint i = 0; i < ids->len; i++) {
        item_template.id = main_template.id = g_array_index(ids, gint, i);
        GObject *main_item = gwy_dict_get_object(dict, gwy_form_file_key(&main_template));
        g_assert(main_item);

        GObject *item = NULL;
        GQuark quark = gwy_form_file_key(&item_template);
        if (!gwy_dict_gis_object(dict, quark, &item))
            continue;

        if (!check_func(item, main_item)) {
            gwy_error_list_add(error_list, GWY_FILE_ERROR, GWY_FILE_ERROR_SIZE_MISMATCH,
                               _("Item ‘%s’ is incompatible with its main data."),
                               g_quark_to_string(quark));
            g_array_append_val(priv->toremove, quark);
        }
    }
}

static void
fix_mismatched_sizes(GwyFile *file, GwyErrorList **error_list)
{
    GwyFileKeyParsed item_template = { .suffix = NULL };

    item_template.data_kind = GWY_FILE_IMAGE;
    item_template.piece = GWY_FILE_PIECE_MASK;
    check_compatibilty(file, item_template, check_image_mask_mismatch, error_list);

    item_template.piece = GWY_FILE_PIECE_PICTURE;
    check_compatibilty(file, item_template, check_image_show_mismatch, error_list);

    item_template.data_kind = GWY_FILE_VOLUME;
    check_compatibilty(file, item_template, check_volume_preview_mismatch, error_list);

    item_template.data_kind = GWY_FILE_CMAP;
    check_compatibilty(file, item_template, check_cmap_preview_mismatch, error_list);
}

static gint
compare_int(gconstpointer pa, gconstpointer pb)
{
    gint a = *(const gint*)pa;
    gint b = *(const gint*)pb;

    if (a < b)
        return -1;
    if (a > b)
        return 1;
    return 0;
}

static void
remove_bad(GwyFile *file)
{
    GwyFilePrivate *priv = file->priv;
    GwyDict *dict = GWY_DICT(file);
    guint len = priv->toremove->len;
    for (guint i = 0; i < len; i++)
        gwy_dict_remove(dict, g_array_index(priv->toremove, GQuark, i));
    g_array_set_size(priv->toremove, 0);
}

static void
scan_file_contents(GwyFile *file, GwyErrorList **error_list)
{
    GwyFilePrivate *priv = file->priv;
    if (priv->scanned_during_deserialisation) {
        // If we ever get here again it is because someone removed all items and started the file construction afresh.
        // In such case we have to scan again.
        priv->scanned_during_deserialisation = FALSE;
        return;
    }

    if (!priv->toremove)
        priv->toremove = g_array_new(FALSE, FALSE, sizeof(GQuark));
    else {
        g_assert(!priv->toremove->len);
    }

    // enumerate_data() handles completely:
    // - weird keys
    // - unexpected items types
    // - orphaned items
    priv->scan_errlist = error_list;
    gwy_dict_foreach(GWY_DICT(file), NULL, enumerate_data, file);
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        DataKindInfo *info = priv->data_kind_info + data_kind;
        if (info->ids)
            g_array_sort(info->ids, compare_int);
    }
    priv->scan_errlist = NULL;
    remove_bad(file);

    // What can be checked after enumeration:
    // - maybe soNaNs, but where it matters, it should be done by individual objects (in fact, even Dict should reject
    //   double values which are not normal)
    fix_mismatched_sizes(file, error_list);
    remove_bad(file);

    // Fix huge ids after removals. We can hope offending items have been removed.
    fix_huge_ids(file, error_list);

    // FIXME: What to do if, after the removals, no valid data remain?
}

static void
construction_finished(GwyDict *container)
{
    GwyErrorList *errlist = NULL;
    scan_file_contents(GWY_FILE(container), &errlist);
    // Priting g_warning()s is not exactly nice. But this is not a deserialisation error. Errors here occur when
    // someone creates a File object and fills it with rubbish on purpose.
    for (GSList *l = errlist; l; l = g_slist_next(l))
        g_warning("File construction problem: %s", ((GError*)l->data)->message);
    gwy_error_list_clear(&errlist);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    serializable_parent_iface->itemize(serializable, group);
    g_assert(!gwy_dict_contains_by_name(GWY_DICT(serializable), "__links__"));
    GwyFilePrivate *priv = GWY_FILE(serializable)->priv;
    if (priv->links && priv->links->len) {
        g_assert(!priv->link_strings);
        GQuark *quarks = &g_array_index(priv->links, GQuark, 0);
        gsize n = priv->links->len;
        priv->link_strings = g_new(const gchar*, n);
        for (gsize i = 0; i < n; i++)
            priv->link_strings[i] = g_quark_to_string(quarks[i]);
        gwy_serializable_group_append_string_array(group, serializable_items + ITEM_LINKS,
                                                   (gchar**)priv->link_strings, n);
    }
}

static void
serializable_done(GwySerializable *serializable)
{
    GwyFilePrivate *priv = GWY_FILE(serializable)->priv;
    GWY_FREE(priv->link_strings);
    if (serializable_parent_iface->done)
        serializable_parent_iface->done(serializable);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwyDict *dict = GWY_DICT(serializable);
    gboolean was_in_construction = gwy_dict_is_being_constructed(dict);

    /* Handle our extra items first. The parent class would try to gobble up everything and complain. */
    GwySerializableItem its[NUM_ITEMS];
    gwy_assign(its, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, TYPE_NAME, error_list);

    gboolean ok = FALSE;
    // Mark self as under construction so that parent's construct() will not finish it when it is done. If you
    // subclass us, you need to handle your items after the parents', as we do.
    if (!was_in_construction)
        gwy_dict_start_construction(dict);

    if (!serializable_parent_iface->construct(serializable, group, error_list))
        goto finish;

    GwyFile *file = GWY_FILE(serializable);
    GwyFilePrivate *priv = GWY_FILE(serializable)->priv;
    if (its[ITEM_LINKS].array_size) {
        gsize n = its[ITEM_LINKS].array_size;
        priv->links = g_array_sized_new(FALSE, FALSE, sizeof(GQuark), n);
        for (gsize i = 0; i < n; i++) {
            GQuark quark = g_quark_from_string(its[ITEM_LINKS].value.v_string_array[i]);
            g_array_append_val(priv->links, quark);
        }
    }

    scan_file_contents(file, error_list);
    priv->scanned_during_deserialisation = TRUE;
    ok = TRUE;

finish:
    if (its[ITEM_LINKS].array_size) {
        gsize n = its[ITEM_LINKS].array_size;
        for (gsize i = 0; i < n; i++)
            g_free(its[ITEM_LINKS].value.v_string_array[i]);
        g_free(its[ITEM_LINKS].value.v_string_array);
    }

    if (!was_in_construction)
        gwy_dict_finish_construction(dict);

    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    /* GwyDict is made for this kind of subclassing and its copy() creates an object of the correct type. */
    GwySerializable *copy = serializable_parent_iface->copy(serializable);
    copy_aux_info(GWY_FILE(copy), GWY_FILE(serializable));
    return copy;
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    serializable_parent_iface->assign(destination, source);
    copy_aux_info(GWY_FILE(destination), GWY_FILE(source));
}

static void
copy_aux_info(GwyFile *dest, GwyFile *src)
{
    GwyFilePrivate *dpriv = dest->priv, *spriv = src->priv;
    for (guint i = 0; i < GWY_FILE_N_KINDS; i++)
        dpriv->data_kind_info[i].ids = assign_garray(dpriv->data_kind_info[i].ids, spriv->data_kind_info[i].ids);
    dpriv->links = assign_garray(dpriv->links, spriv->links);
}

static GArray*
assign_garray(GArray *dest, GArray *src)
{
    if (dest && src && src->len) {
        g_assert(g_array_get_element_size(dest) == g_array_get_element_size(src));
        g_array_set_size(dest, 0);
        g_array_append_vals(dest, src->data, src->len);
    }
    else if (dest) {
        g_array_free(dest, TRUE);
        return NULL;
    }
    else if (src && src->len) {
        dest = g_array_sized_new(FALSE, FALSE, g_array_get_element_size(src), src->len);
        g_array_append_vals(dest, src->data, src->len);
    }

    return dest;
}

static gint
copy_data(GwyFile *source,
          GwyDataKind data_kind,
          gint id,
          GwyFile *destination,
          gboolean shallow)
{
    GQuark srcquark = data_kind_spec[data_kind].get_key(id);
    g_return_val_if_fail(srcquark, -1);

    GwyDict *srcdata = GWY_DICT(source), *destdata = GWY_DICT(destination);
    GObject *object = gwy_dict_get_object(srcdata, srcquark);
    GObject *otherobject = NULL;
    gboolean visualisation_done = FALSE;
    g_return_val_if_fail(object, -1);

    if (!shallow)
        object = G_OBJECT(gwy_serializable_copy(GWY_SERIALIZABLE(object)));

    gint newid = -1;
    if (data_kind == GWY_FILE_IMAGE)
        newid = gwy_file_add_image(destination, GWY_FIELD(object));
    else if (data_kind == GWY_FILE_GRAPH)
        newid = gwy_file_add_graph(destination, GWY_GRAPH_MODEL(object));
    else if (data_kind == GWY_FILE_SPECTRA)
        newid = gwy_file_add_spectra(destination, GWY_SPECTRA(object));
    else if (data_kind == GWY_FILE_VOLUME || data_kind == GWY_FILE_CMAP) {
        GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_PICTURE, .suffix = NULL };
        if (gwy_dict_gis_object(srcdata, gwy_form_file_key(&parsed), &otherobject) && !shallow)
            otherobject = G_OBJECT(gwy_serializable_copy(GWY_SERIALIZABLE(otherobject)));

        if (data_kind == GWY_FILE_VOLUME)
            newid = gwy_file_add_volume(destination, GWY_BRICK(object), (GwyField*)otherobject);
        else
            newid = gwy_file_add_cmap(destination, GWY_LAWN(object), (GwyField*)otherobject);

        if (!shallow && otherobject)
            g_object_unref(otherobject);
        visualisation_done = TRUE;
    }
    else if (data_kind == GWY_FILE_XYZ)
        newid = gwy_file_add_xyz(destination, GWY_SCATTER(object));
    else {
        g_assert_not_reached();
    }

    if (!shallow)
        g_object_unref(object);

    const DataKindSpec *spec = data_kind_spec + data_kind;
    for (guint i = 0; i < spec->npieces; i++) {
        GwyFilePiece piece = all_pieces[spec->pieces[i]].piece;
        if (piece < 64 && (!visualisation_done || piece != GWY_FILE_PIECE_PICTURE)) {
            const gchar *suffix = all_pieces[spec->pieces[i]].suffix;
            sync_one_item(srcdata, destdata, data_kind, data_kind, id, newid, piece, suffix, FALSE, FALSE);
        }
    }

    return newid;
}

static void
set_link(GwyFile *file, GQuark key, GQuark linkquark, const gchar *linkstr)
{
    GwyDict *container = GWY_DICT(file);
    GwyFilePrivate *priv = file->priv;
    gint linkpos = -1;
    if (gwy_dict_value_type(container, key) == G_TYPE_STRING)
        linkpos = find_link(file, key);

    if (!linkstr)
        linkstr = g_quark_to_string(linkquark);

    /* First mark the key as holding a link, then actually set the string. This way anything that responds to
     * "item-changed" signal has the correct information whether the changed item is a link. */
    if (linkpos == -1) {
        if (!priv->links)
            priv->links = g_array_new(FALSE, FALSE, sizeof(GQuark));
        g_array_append_val(priv->links, key);
    }

    g_assert(!priv->setting_link);
    priv->setting_link = key;
    gwy_dict_set_string(container, key, linkstr);
    priv->setting_link = 0;
}

static gint
find_link(GwyFile *file, GQuark quark)
{
    GwyFilePrivate *priv = file->priv;
    if (!priv->links)
        return -1;

    guint n = priv->links->len;
    GQuark *quarks = &g_array_index(priv->links, GQuark, 0);
    for (guint i = 0; i < n; i++) {
        if (quarks[i] == quark)
            return i;
    }
    return -1;
}

/**
 * gwy_file_copy:
 * @file: A data file to duplicate.
 *
 * Create a new data file as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Returns: (transfer full):
 *          A copy of the data file.
 **/
GwyFile*
gwy_file_copy(GwyFile *file)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_FILE(file)) {
        g_assert(GWY_IS_FILE(file));
        return g_object_new(GWY_TYPE_FILE, NULL);
    }
    return GWY_FILE(gwy_serializable_copy(GWY_SERIALIZABLE(file)));
}

/**
 * GWY_FILE_ERROR:
 *
 * Error domain for file structure.
 *
 * Errors in this domain will be from the #GwyFileError enumeration. See #GError for information on error domains.
 **/

/**
 * GwyFileError:
 * @GWY_FILE_ERROR_KEY: Malformed or unrecognised item key.
 * @GWY_FILE_ERROR_TYPE: Item has the wrong type for given key.
 * @GWY_FILE_ERROR_ORPHAN: Secondary item does not correspond to any primary data.
 * @GWY_FILE_ERROR_SIZE_MISMATCH: Secondary item does not match its primary data.
 * @GWY_FILE_ERROR_HUGE_ID: Unreasonably large data id.
 *
 * Error codes returned by scanning of file structure.
 *
 * Invalid file structure should not result from normal program operation. Therefore, the way you would get this
 * errors, is the deserialisation of a #GwyFile with incorrect/unrecognised structure. They would be include in the
 * error list among (possibly) other deserialisation errors.
 *
 * Error %GWY_FILE_ERROR_SIZE_MISMATCH can occur for example when a mask has different pixel dimensions than the
 * corresponding image. A similar error can happen with volume or curve map data previews.
 *
 * For %GWY_FILE_ERROR_HUGE_ID, reasonble depends on the amount of data. For a file with 100 images, reasonable ids
 * are larger than for a single-image file. If the ids are several times larger the number of data, you are perfectly
 * fine. If you try to add an image with id %G_MAXINT just to cause trouble, do not do it then.
 **/

/**
 * gwy_file_error_quark:
 *
 * Gets the error domain for file structure.
 *
 * See and use %GWY_FILE_ERROR.
 *
 * Returns: The error domain.
 **/
G_DEFINE_QUARK(gwy-file-error-quark, gwy_file_error);

/**
 * SECTION: file
 * @title: GwyFile
 * @short_description: Dictionary representing a data file
 * @see_also: #GwyDict
 *
 * #GwyFile is a subclass of #GwyDict, assuming a specific organisation of items inside to represent scanning
 * probe microscopy data, more specifically a Gwyddion GWY file.
 *
 * The class implements #GwySerializable copy() and assign() methods. A convenience wrapper gwy_file_copy() is
 * provided only for the first one. Assigning files by value is less efficient than simply destroying the existing
 * #GwyFile and creating a new data file. It also generates lots of signals about changed items. So, generally, do not
 * do it.
 **/

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

/**
 * GWY_FILE_ID_NONE:
 *
 * Invalid file id value indicating an unmanaged file.
 *
 * Only files managed by the data browser have an id. A free-standing file object does not have any. Using
 * gwy_file_get_id() on such file gives this value. The value can also represent an error.
 **/

/**
 * GWY_FILE_PREFIX_IMAGE:
 *
 * Prefix of image string keys in Gwyddion data files.
 **/

/**
 * GWY_FILE_PREFIX_GRAPH:
 *
 * Prefix of graph string keys in Gwyddion data files.
 **/

/**
 * GWY_FILE_PREFIX_SPECTRA:
 *
 * Prefix of single point spectra string keys in Gwyddion data files.
 **/

/**
 * GWY_FILE_PREFIX_VOLUME:
 *
 * Prefix of volume data string keys in Gwyddion data files.
 **/

/**
 * GWY_FILE_PREFIX_XYZ:
 *
 * Prefix of XYZ data string keys in Gwyddion data files.
 **/

/**
 * GWY_FILE_PREFIX_CMAP:
 *
 * Prefix of curve map string keys in Gwyddion data files.
 **/

/**
 * GwyFileKeyParsed:
 * @id: Numerical id of a data in file, -1 for no data item.
 * @data_kind: Type of the data object, %GWY_FILE_NONE for no data item.
 * @piece: Particular piece related or belonging to the data, %GWY_FILE_PIECE_NONE for the main data object.
 * @suffix: Remaning part of the key (pointer to @strkey), or %NULL.
 *
 * Broken down representation of a piece of information in a data file container.
 **/

/**
 * GwyFileTransferFunc:
 * @source: Source file.
 * @destination: Destination file.
 * @data_kind: Type of the data object.
 * @source_id: Numerical id of a data in source file.
 * @destination_id: Numerical id of a data in destination file.
 * @user_data: (closure): User data passed to the function.
 *
 * Type of callback function for file item transfer.
 **/

/**
 * GwyDataKind:
 * @GWY_FILE_NONE: No data. Often meaning an error, sometimes also special data keys not related to any specific
 *                 data.
 * @GWY_FILE_IMAGE: An image, primarily represented as #GwyField.
 * @GWY_FILE_GRAPH: A graph, primarily represented as #GwyGraphModel.
 * @GWY_FILE_SPECTRA: Single point spectra, primarily represented as #GwySpectra.
 * @GWY_FILE_VOLUME: Volume data, primarily represented as #GwyBrick.
 * @GWY_FILE_XYZ: XYZ data, primarily represented as #GwyScatter.
 * @GWY_FILE_CMAP: A curve map, primarily represented as #GwyLawn.
 *
 * Basic kind of data which can be found in Gwyddion data files.
 **/

/**
 * GwyFilePiece:
 * @GWY_FILE_PIECE_PALETTE: False colour mapping gradient.
 * @GWY_FILE_PIECE_MASK: Image mask.
 * @GWY_FILE_PIECE_MASK_COLOR: Mask colour.
 * @GWY_FILE_PIECE_RANGE: False colour mapping range.
 * @GWY_FILE_PIECE_COLOR_MAPPING: False colour mapping type.
 * @GWY_FILE_PIECE_REAL_SQUARE: Pixel/real aspect ratio.
 * @GWY_FILE_PIECE_SELECTION: Image selections.
 * @GWY_FILE_PIECE_META: Data item metadata.
 * @GWY_FILE_PIECE_PICTURE: Object representing the visualisation or preview of the data.
 * @GWY_FILE_PIECE_VIEW: Window size or scaling.
 * @GWY_FILE_PIECE_TITLE: Data item title.
 * @GWY_FILE_PIECE_LOG: Log of operations on the data item.
 * @GWY_FILE_PIECE_VISIBLE: Data item visibility.
 * @GWY_FILE_PIECE_FILENAME: Name of the file.
 * @GWY_FILE_PIECE_REF: References to other data in the file.
 * @GWY_FILE_PIECE_NONE: It usually indicates the main data object, but it can indicate an invalid data piece.
 *
 * Type of auxiliary data in a Gwyddion data file.
 **/

/**
 * GWY_FILE_ITEM_PALETTE:
 *
 * False colour mapping gradient. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_MASK:
 *
 * Image mask. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_MASK_COLOR:
 *
 * Mask colour. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_RANGE:
 *
 * False colour mapping range. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_COLOR_MAPPING:
 *
 * False colour mapping type. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_REAL_SQUARE:
 *
 * Pixel/real aspect ratio. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_SELECTIONS:
 *
 * Image selections. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_META:
 *
 * Data item metadata. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_PICTURE:
 *
 * Object representing the visualisation or preview of the data. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_VIEW:
 *
 * Window size or scaling. Predefined item synchonisation flag.
 **/

/**
 * GWY_FILE_ITEM_TITLE:
 *
 * Data item title. Predefined item synchonisation flag.
 **/

/* 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 : */
