/*
 *  $Id: spectra.c 29753 2026-04-08 17:34:09Z yeti-dn $
 *  Copyright (C) 2006-2025 Owain Davies, David Necas (Yeti), Petr Klapetek.
 *  E-mail: owain.davies@blueyonder.co.uk, yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <glib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/serializable-utils.h"
#include "libgwyddion/spectra.h"
#include "libgwyddion/linestats.h"
#include "libgwyddion/interpolation.h"

#include "libgwyddion/internal.h"

#define TYPE_NAME "GwySpectra"

G_STATIC_ASSERT(sizeof(gboolean) == sizeof(gint32));

enum {
    PROP_0,
    PROP_TITLE,
    NUM_PROPERTIES
};

enum {
    SGNL_DATA_CHANGED,
    NUM_SIGNALS
};

enum {
    ITEM_COORDS, ITEM_DATA,
    ITEM_TITLE, ITEM_UNIT_XY,
    ITEM_SPEC_XLABEL, ITEM_SPEC_YLABEL,
    ITEM_SELECTED,
    NUM_ITEMS,
};

typedef struct {
    gdouble r2;
    guint index;
} CoordPos;

struct _GwySpectraPrivate {
    GPtrArray *spectra;
    GArray *coords;
    GArray *selected;

    gchar *title;
    GwyUnit *unit_xy;

    gchar *spec_xlabel;
    gchar *spec_ylabel;
};

static void             finalize              (GObject *object);
static void             set_property          (GObject *object,
                                               guint prop_id,
                                               const GValue *value,
                                               GParamSpec *pspec);
static void             get_property          (GObject *object,
                                               guint prop_id,
                                               GValue *value,
                                               GParamSpec *pspec);
static void             serializable_init     (GwySerializableInterface *iface);
static void             serializable_itemize  (GwySerializable *serializable,
                                               GwySerializableGroup *group);
static gboolean         serializable_construct(GwySerializable *serializable,
                                               GwySerializableGroup *group,
                                               GwyErrorList **error_list);
static GwySerializable* serializable_copy     (GwySerializable *serializable);
static void             serializable_assign   (GwySerializable *destination,
                                               GwySerializable *source);

static guint signals[NUM_SIGNALS];
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GObjectClass *parent_class = NULL;

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "coords",       .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                              .flags = GWY_SERIALIZABLE_BIG_DATA      },
    { .name = "data",         .ctype = GWY_SERIALIZABLE_OBJECT_ARRAY, },
    { .name = "title",        .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "unit_xy",      .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "spec_xlabel",  .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "spec_ylabel",  .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "selected",     .ctype = GWY_SERIALIZABLE_INT32_ARRAY,  },
};

G_DEFINE_TYPE_WITH_CODE(GwySpectra, gwy_spectra, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwySpectra)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

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

    serializable_items[ITEM_UNIT_XY].aux.object_type = GWY_TYPE_UNIT;
}

static void
gwy_spectra_class_init(GwySpectraClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_spectra_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->get_property = get_property;
    gobject_class->set_property = set_property;

    /**
     * GwySpectra::data-changed:
     * @gwyspectra: The #GwySpectra which received the signal.
     *
     * The ::data-changed signal is never emitted by the object itself.  It is intended as a means to notify object
     * users they should update themselves, usually when a computation modifying the data is finished.
     **/
    signals[SGNL_DATA_CHANGED] = g_signal_new("data-changed", type,
                                              G_SIGNAL_RUN_FIRST,
                                              G_STRUCT_OFFSET(GwySpectraClass, data_changed),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__VOID,
                                              G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_DATA_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);

    properties[PROP_TITLE] = g_param_spec_string("title", NULL,
                                                 "The spectra title",
                                                 NULL,
                                                 GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_spectra_init(GwySpectra *spectra)
{
    GwySpectraPrivate *priv;

    priv = spectra->priv = gwy_spectra_get_instance_private(spectra);

    priv->spectra = g_ptr_array_new_with_free_func(g_object_unref);
    priv->coords = g_array_new(FALSE, FALSE, sizeof(GwyXY));
    priv->selected = g_array_new(FALSE, FALSE, sizeof(gboolean));
}

static void
finalize(GObject *object)
{
    GwySpectra *spectra = (GwySpectra*)object;
    GwySpectraPrivate *priv = spectra->priv;

    g_clear_object(&priv->unit_xy);
    g_ptr_array_free(priv->spectra, TRUE);
    g_array_free(priv->coords, TRUE);
    g_array_free(priv->selected, TRUE);
    g_free(priv->title);
    g_free(priv->spec_xlabel);
    g_free(priv->spec_ylabel);

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

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwySpectra *spectra = GWY_SPECTRA(object);

    switch (prop_id) {
        case PROP_TITLE:
        gwy_spectra_set_title(spectra, g_value_get_string(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwySpectraPrivate *priv = GWY_SPECTRA(object)->priv;

    switch (prop_id) {
        case PROP_TITLE:
        g_value_set_string(value, priv->title);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
copy_info(GwySpectra *src, GwySpectra *dest)
{
    GwySpectraPrivate *spriv = src->priv, *dpriv = dest->priv;

    _gwy_copy_unit(spriv->unit_xy, &dpriv->unit_xy);
    gwy_assign_string(&dpriv->title, spriv->title);
    gwy_assign_string(&dpriv->spec_xlabel, spriv->spec_xlabel);
    gwy_assign_string(&dpriv->spec_ylabel, spriv->spec_ylabel);
}

/**
 * gwy_spectra_new: (constructor)
 *
 * Creates a new spectrum set containing zero spectra.
 *
 * Returns: (transfer full):
 *          A newly created spectra.
 **/
GwySpectra*
gwy_spectra_new(void)
{
    return g_object_new(GWY_TYPE_SPECTRA, NULL);
}

/**
 * gwy_spectra_new_alike:
 * @model: A spectrum set to take units from.
 *
 * Creates a new spectrum set similar to an existing one, but containing zero spectra.
 *
 * Use gwy_spectra_copy() if you want to copy a spectrum set including the spectra in it.
 *
 * Returns: (transfer full):
 *          A newly created spectra.
 **/
GwySpectra*
gwy_spectra_new_alike(GwySpectra *model)
{
    GwySpectra *spectra = g_object_new(GWY_TYPE_SPECTRA, NULL);

    g_return_val_if_fail(GWY_IS_SPECTRA(model), spectra);
    copy_info(model, spectra);

    return spectra;
}

/**
 * gwy_spectra_data_changed:
 * @spectra: A spectrum set.
 *
 * Emits signal "data_changed" on a spectrum set.
 **/
void
gwy_spectra_data_changed(GwySpectra *spectra)
{
    g_signal_emit(spectra, signals[SGNL_DATA_CHANGED], 0);
}

/**
 * gwy_spectra_get_unit_xy:
 * @spectra: A spectra.
 *
 * Gets unit used for the location co-ordinates of spectra.
 *
 * The returned object can be modified to change the spectra lateral units.
 *
 * Returns: (transfer none): Unit corresponding to the location co-ordinates of spectrum set.
 **/
GwyUnit*
gwy_spectra_get_unit_xy(GwySpectra *spectra)
{
    g_return_val_if_fail(GWY_IS_SPECTRA(spectra), NULL);
    GwySpectraPrivate *priv = spectra->priv;

    if (!priv->unit_xy)
        priv->unit_xy = gwy_unit_new(NULL);

    return priv->unit_xy;
}

/**
 * gwy_spectra_getpos:
 * @spectra: A spectrum set.
 * @i: Index of a spectrum.
 * @x: (out): Location to store the physical x coordinate of the spectrum.
 * @y: (out): Location to store the physical x coordinate of the spectrum.
 *
 * Gets the coordinates of one spectrum.
 **/
void
gwy_spectra_getpos(GwySpectra *spectra,
                   guint i,
                   gdouble *x,
                   gdouble *y)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    GwySpectraPrivate *priv = spectra->priv;
    g_return_if_fail(i < priv->coords->len);

    const GwyXY *xy = &g_array_index(priv->coords, GwyXY, i);
    if (x)
        *x = xy->x;
    if (y)
        *y = xy->y;
}

static gint
compare_coord_pos(gconstpointer a, gconstpointer b)
{
    const CoordPos *acp = (const CoordPos*)a;
    const CoordPos *bcp = (const CoordPos*)b;

    if (acp->r2 < bcp->r2)
        return -1;
    if (acp->r2 > bcp->r2)
        return 1;
    return 0;
}

/**
 * gwy_spectra_find_nearest:
 * @spectra: A spectrum set.
 * @x: Point x-coordinate.
 * @y: Point y-coordinate.
 * @n: Number of indices to find.  Array @ilist must have at least this number of items.
 * @ilist: Array to place the spectra indices to.
 *
 * Finds spectra closest to given coordinates.
 *
 * Point indices in @ilist will be sorted by the distance from (@x, @y) from the closes to the farthest.
 *
 * It is not an error to pass @n larger than the number of spectra. However, the positions after the number of spectra
 * in @spectra will be left uninitialised.
 **/
void
gwy_spectra_find_nearest(GwySpectra *spectra,
                         gdouble x,
                         gdouble y,
                         guint n,
                         guint *ilist)
{
    enum { DIRECT_LIMIT = 8 };

    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    g_return_if_fail(ilist || !n);
    GwySpectraPrivate *priv = spectra->priv;

    guint nspec = priv->coords->len;
    n = MIN(n, nspec);
    if (!n)
        return;

    if (n <= DIRECT_LIMIT) {
        CoordPos items[n];

        /* Initialize with sorted initial n items */
        for (guint i = 0; i < n; i++) {
            const GwyXY *xy = &g_array_index(priv->coords, GwyXY, i);
            items[i].index = i;
            items[i].r2 = (xy->x - x)*(xy->x - x) + (xy->y - y)*(xy->y - y);
        }
        qsort(items, n, sizeof(CoordPos), compare_coord_pos);

        /* And let the remaining items compete for positions */
        for (guint i = n; i < nspec; i++) {
            const GwyXY *xy = &g_array_index(priv->coords, GwyXY, i);

            gdouble r2 = ((xy->x - x)*(xy->x - x) + (xy->y - y)*(xy->y - y));
            if (r2 < items[n-1].r2) {
                guint j, k;
                for (j = 1; j < n && r2 < items[n-1 - j].r2; j++)
                    ;
                for (k = 0; k < j-1; k++)
                    items[n-k] = items[n-1 - k];

                items[n-j].index = i;
                items[n-j].r2 = r2;
            }
        }

        /* Move the results to ilist */
        for (guint i = 0; i < n; i++)
            ilist[i] = items[i].index;
    }
    else {
        /* Sort all items and take the head */
        CoordPos *items = g_new(CoordPos, priv->spectra->len);
        for (guint i = 0; i < priv->spectra->len; i++) {
            const GwyXY *xy = &g_array_index(priv->coords, GwyXY, i);
            items[i].index = i;
            items[i].r2 = (xy->x - x)*(xy->x - x) + (xy->y - y)*(xy->y - y);
        }
        qsort(items, nspec, sizeof(CoordPos), compare_coord_pos);
        for (guint i = 0; i < n; i++)
            ilist[i] = items[i].index;

        g_free(items);
    }
}

/**
 * gwy_spectra_setpos:
 * @spectra: A spectrum set.
 * @i: The index of a spectrum.
 * @x: The new x coordinate of the location of the spectrum.
 * @y: The new y coordinate of the location of the spectrum.
 *
 * Sets the location coordinates of a spectrum.
 **/
void
gwy_spectra_setpos(GwySpectra *spectra,
                   guint i,
                   gdouble x, gdouble y)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    GwySpectraPrivate *priv = spectra->priv;
    g_return_if_fail(i < priv->coords->len);

    GwyXY *xy = &g_array_index(priv->coords, GwyXY, i);
    xy->x = x;
    xy->y = y;
}

/**
 * gwy_spectra_get_spectrum:
 * @spectra: A spectrum set.
 * @i: Index of a spectrum
 *
 * Gets a line that contains the spectrum at index i.
 *
 * Returns: (transfer none): A #GwyLine containing the spectrum, owned by @spectra.
 **/
GwyLine*
gwy_spectra_get_spectrum(GwySpectra *spectra, gint i)
{
    g_return_val_if_fail(GWY_IS_SPECTRA(spectra), NULL);
    GwySpectraPrivate *priv = spectra->priv;
    g_return_val_if_fail(i < priv->spectra->len, NULL);
    return g_ptr_array_index(priv->spectra, i);
}

/**
 * gwy_spectra_set_spectrum:
 * @spectra: A spectrum set.
 * @i: Index of a spectrum to replace
 * @new_spectrum: A #GwyLine Object containing the new spectrum.
 *
 * Replaces a spectrum in a spectrum set.
 **/
void
gwy_spectra_set_spectrum(GwySpectra *spectra,
                         guint i,
                         GwyLine *new_spectrum)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    g_return_if_fail(GWY_IS_LINE(new_spectrum));
    GwySpectraPrivate *priv = spectra->priv;
    g_return_if_fail(i < priv->spectra->len);

    GwyLine *spectrum = g_ptr_array_index(priv->spectra, i);
    if (new_spectrum == spectrum)
        return;
    g_ptr_array_index(priv->spectra, i) = g_object_ref(new_spectrum);
    g_object_unref(spectrum);
}

/**
 * gwy_spectra_set_spectrum_selected:
 * @spectra: A spectrum set.
 * @i: Index of a spectrum.
 * @selected: %TRUE to make the spectrum selected, %FALSE to deselect it.
 *
 * Sets selected state of a spectrum in a spectrum set.
 **/
void
gwy_spectra_set_spectrum_selected(GwySpectra *spectra,
                                  guint i,
                                  gboolean selected)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    GwySpectraPrivate *priv = spectra->priv;
    g_return_if_fail(i < priv->selected->len);
    g_array_index(priv->selected, gboolean, i) = !!selected;
}

/**
 * gwy_spectra_get_spectrum_selected:
 * @spectra: A spectrum set.
 * @i: Index of a spectrum.
 *
 * Gets the selected state of a spectrum in a spectrum set.
 *
 * Returns: %TRUE if spectrum is selected.
 **/
gboolean
gwy_spectra_get_spectrum_selected(GwySpectra *spectra,
                                  guint i)
{
    g_return_val_if_fail(GWY_IS_SPECTRA(spectra), FALSE);
    GwySpectraPrivate *priv = spectra->priv;
    g_return_val_if_fail(i < priv->selected->len, FALSE);
    return g_array_index(priv->selected, gboolean, i);
}

/**
 * gwy_spectra_get_n_spectra:
 * @spectra: A spectrum set.
 *
 * Gets the number of spectra in a spectrum set.
 *
 * Returns: The number of spectra.
 **/
guint
gwy_spectra_get_n_spectra(GwySpectra *spectra)
{
    g_return_val_if_fail(GWY_IS_SPECTRA(spectra), 0);
    return spectra->priv->spectra->len;
}

/**
 * gwy_spectra_add_spectrum:
 * @spectra: A spectrum set.
 * @new_spectrum: A GwyLine containing the spectrum to append.
 * @x: The physical x coordinate of the location of the spectrum.
 * @y: The physical y coordinate of the location of the spectrum.
 *
 * Adds a new spectrum to the spectra collection with a position of x, y.
 *
 * The spectra collection takes a reference to @new_spectrum.
 **/
void
gwy_spectra_add_spectrum(GwySpectra *spectra,
                         GwyLine *new_spectrum,
                         gdouble x, gdouble y)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    g_return_if_fail(GWY_IS_LINE(new_spectrum));

    GwySpectraPrivate *priv = spectra->priv;
    g_ptr_array_add(priv->spectra, g_object_ref(new_spectrum));
    GwyXY xy = { x, y };
    g_array_append_val(priv->coords, xy);
    gboolean selected = FALSE;
    g_array_append_val(priv->selected, selected);
}

/**
 * gwy_spectra_remove_spectrum:
 * @spectra: A spectrum set.
 * @i: Index of spectrum to remove.
 *
 * Removes the ith spectrum from the Spectra collection. The subsequent spectra are moved down one place.
 **/
void
gwy_spectra_remove_spectrum(GwySpectra *spectra,
                            guint i)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    GwySpectraPrivate *priv = spectra->priv;
    g_return_if_fail(i < priv->spectra->len);

    g_ptr_array_remove_index(priv->spectra, i);
    g_array_remove_index(priv->coords, i);
    g_array_remove_index(priv->selected, i);
}

/**
 * gwy_spectra_get_title:
 * @spectra: A spectrum set.
 *
 * Gets the title of spectra.
 *
 * Returns: A pointer to the title string (owned by the spectrum set).
 **/
const gchar*
gwy_spectra_get_title(GwySpectra *spectra)
{
    g_return_val_if_fail(GWY_IS_SPECTRA(spectra), NULL);
    return spectra->priv->title;
}

/**
 * gwy_spectra_set_title:
 * @spectra: A spectrum set.
 * @title: The new title string.
 *
 * Sets the title of the spectra collection.
 **/
void
gwy_spectra_set_title(GwySpectra *spectra,
                      const gchar *title)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));

    GwySpectraPrivate *priv = spectra->priv;
    if (gwy_assign_string(&priv->title, title))
        g_object_notify_by_pspec(G_OBJECT(spectra), properties[PROP_TITLE]);
}

/**
 * gwy_spectra_get_spectrum_x_label:
 * @spectra: A spectrum set.
 *
 * Gets the spectrum abscissa label of a spectrum set.
 *
 * Returns: The abscissa label.  The string is owned by @spectra and must not be modified nor freed.
 **/
const gchar*
gwy_spectra_get_spectrum_x_label(GwySpectra *spectra)
{
    g_return_val_if_fail(GWY_IS_SPECTRA(spectra), NULL);
    return spectra->priv->spec_xlabel;
}

/**
 * gwy_spectra_set_spectrum_x_label:
 * @spectra: A spectrum set.
 * @label: New abscissa label.
 *
 * Sets the spectrum abscissa label of a spectrum set.
 **/
void
gwy_spectra_set_spectrum_x_label(GwySpectra *spectra,
                                 const gchar *label)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    GwySpectraPrivate *priv = spectra->priv;
    gwy_assign_string(&priv->spec_xlabel, label);
}

/**
 * gwy_spectra_get_spectrum_y_label:
 * @spectra: A spectrum set.
 *
 * Gets the spectrum ordinate label of a spectrum set.
 *
 * Returns: The ordinate label.  The string is owned by @spectra and must not be modified nor freed.
 **/
const gchar*
gwy_spectra_get_spectrum_y_label(GwySpectra *spectra)
{
    g_return_val_if_fail(GWY_IS_SPECTRA(spectra), NULL);
    return spectra->priv->spec_ylabel;
}

/**
 * gwy_spectra_set_spectrum_y_label:
 * @spectra: A spectrum set.
 * @label: New ordinate label.
 *
 * Sets the spectrum ordinate label of a spectrum set.
 **/
void
gwy_spectra_set_spectrum_y_label(GwySpectra *spectra,
                                 const gchar *label)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    GwySpectraPrivate *priv = spectra->priv;
    gwy_assign_string(&priv->spec_ylabel, label);
}

/**
 * gwy_spectra_clear:
 * @spectra: A spectrum set.
 *
 * Removes all spectra from the collection.
 **/
void
gwy_spectra_clear(GwySpectra *spectra)
{
    g_return_if_fail(GWY_IS_SPECTRA(spectra));
    GwySpectraPrivate *priv = spectra->priv;
    g_ptr_array_set_size(priv->spectra, 0);
    g_array_set_size(priv->coords, 0);
    g_array_set_size(priv->selected, 0);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwySpectra *spectra = GWY_SPECTRA(serializable);
    GwySpectraPrivate *priv = spectra->priv;

    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_COORDS,
                                               (gdouble*)priv->coords->data, 2*priv->coords->len);
    gwy_serializable_group_append_object_array(group, serializable_items + ITEM_DATA,
                                               (GObject**)priv->spectra->pdata, priv->spectra->len);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_TITLE, priv->title);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_XY, priv->unit_xy);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_SPEC_XLABEL, priv->spec_xlabel);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_SPEC_YLABEL, priv->spec_ylabel);
    gwy_serializable_group_append_int32_array(group, serializable_items + ITEM_SELECTED,
                                              (gint32*)priv->selected->data, priv->selected->len);
    gwy_serializable_group_itemize(group);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwySerializableItem its[NUM_ITEMS], *it;
    gboolean ok = FALSE;
    gwy_assign(its, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, TYPE_NAME, error_list);

    GwySpectra *spectra = GWY_SPECTRA(serializable);
    GwySpectraPrivate *priv = spectra->priv;

    /* Botched up data dimensions is a hard fail. But in this case an empty object is OK. */
    guint ncoords = its[ITEM_COORDS].array_size;
    guint nspectra = its[ITEM_DATA].array_size;
    guint nselected = its[ITEM_SELECTED].array_size;

    /* Constraint: 2*len(data) = len(coords); len(data) = len(selected). */
    gsize spectra_dim[1] = { nspectra }, coord_dim[1] = { ncoords }, selected_dim[1] = { nselected };
    if (nspectra || ncoords || nselected) {
        if (!gwy_check_data_dimension(error_list, TYPE_NAME, 1, 0, spectra_dim)
            || !gwy_check_data_dimension(error_list, TYPE_NAME ".coords", 1, 2*nspectra, coord_dim)
            || !gwy_check_data_dimension(error_list, TYPE_NAME ".selected", 1, nspectra, selected_dim))
            goto fail;

        it = its + ITEM_DATA;
        if (!gwy_check_object_component(it, TYPE_NAME, GWY_TYPE_LINE, error_list))
            goto fail;

        g_ptr_array_set_size(priv->spectra, 0);
        for (guint i = 0; i < nspectra; i++) {
            g_ptr_array_add(priv->spectra, it->value.v_object_array[i]);
            it->value.v_object_array[i] = NULL;
        }
        it->array_size = 0;

        it = its + ITEM_COORDS;
        g_array_set_size(priv->coords, 0);
        g_array_append_vals(priv->coords, it->value.v_double_array, nspectra);  /* NB: Not ncoords which 2× larger! */

        it = its + ITEM_SELECTED;
        g_array_set_size(priv->selected, 0);
        g_array_append_vals(priv->selected, it->value.v_int32_array, nspectra);
    }

    /* The rest is non-fatal. Struct members are already initialised to defaults. Only set them when they are sane. */
    /* Take advantage of string field being initialised to NULLs. Meanig swapping does the right thing. */
    GWY_SWAP(gchar*, priv->title, its[ITEM_TITLE].value.v_string);
    GWY_SWAP(gchar*, priv->spec_xlabel, its[ITEM_SPEC_XLABEL].value.v_string);
    GWY_SWAP(gchar*, priv->spec_ylabel, its[ITEM_SPEC_YLABEL].value.v_string);

    priv->unit_xy = (GwyUnit*)its[ITEM_UNIT_XY].value.v_object;
    its[ITEM_UNIT_XY].value.v_object = NULL;

    ok = TRUE;

fail:
    it = its + ITEM_DATA;
    for (guint i = 0; i < it->array_size; i++)
        g_clear_object(it->value.v_object_array + i);
    g_free(it->value.v_object_array);
    g_clear_object(&its[ITEM_UNIT_XY].value.v_object);
    g_free(its[ITEM_COORDS].value.v_double_array);
    g_free(its[ITEM_SELECTED].value.v_int32_array);
    g_free(its[ITEM_TITLE].value.v_string);
    g_free(its[ITEM_SPEC_XLABEL].value.v_string);
    g_free(its[ITEM_SPEC_YLABEL].value.v_string);
    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwySpectra *spectra = GWY_SPECTRA(serializable);
    GwySpectra *copy = gwy_spectra_new_alike(spectra);
    GwySpectraPrivate *priv = spectra->priv, *cpriv = copy->priv;
    guint nspectra = priv->spectra->len;
    g_ptr_array_set_size(cpriv->spectra, 0);
    for (guint i = 0; i < nspectra; i++)
        g_ptr_array_add(cpriv->spectra, gwy_line_copy(g_ptr_array_index(priv->spectra, i)));
    g_array_set_size(cpriv->coords, 0);
    g_array_append_vals(cpriv->coords, priv->coords->data, nspectra);
    g_array_set_size(cpriv->selected, 0);
    g_array_append_vals(cpriv->selected, priv->selected->data, nspectra);
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwySpectra *destspectra = GWY_SPECTRA(destination), *srcspectra = GWY_SPECTRA(source);
    GwySpectraPrivate *dpriv = destspectra->priv, *spriv = srcspectra->priv;
    guint dnspec = dpriv->spectra->len, snspec = spriv->spectra->len;

    g_ptr_array_set_size(dpriv->spectra, snspec);
    for (guint i = 0; i < MIN(snspec, dnspec); i++) {
        GwyLine *dspectrum = g_ptr_array_index(dpriv->spectra, i);
        GwyLine *sspectrum = g_ptr_array_index(spriv->spectra, i);
        gwy_line_assign(dspectrum, sspectrum);
    }
    for (guint i = dnspec; i < snspec; i++)
        g_ptr_array_index(dpriv->spectra, i) = gwy_line_copy(g_ptr_array_index(spriv->spectra, i));

    g_array_set_size(dpriv->coords, snspec);
    gwy_assign(&g_array_index(dpriv->coords, GwyXY, 0), &g_array_index(spriv->coords, GwyXY, 0), snspec);
    g_array_set_size(dpriv->selected, snspec);
    gwy_assign(&g_array_index(dpriv->selected, gboolean, 0), &g_array_index(spriv->selected, gboolean, 0), snspec);
    copy_info(srcspectra, destspectra);
}

/**
 * gwy_spectra_copy:
 * @spectra: A spectrum set to duplicate.
 *
 * Create a new spectrum set as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Returns: (transfer full):
 *          A copy of the spectrum set.
 **/
GwySpectra*
gwy_spectra_copy(GwySpectra *spectra)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_SPECTRA(spectra)) {
        g_assert(GWY_IS_SPECTRA(spectra));
        return g_object_new(GWY_TYPE_SPECTRA, NULL);
    }
    return GWY_SPECTRA(gwy_serializable_copy(GWY_SERIALIZABLE(spectra)));
}

/**
 * gwy_spectra_assign:
 * @destination: Target spectrum set.
 * @source: Source spectrum set.
 *
 * Makes one spectrum set equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 **/
void
gwy_spectra_assign(GwySpectra *destination, GwySpectra *source)
{
    g_return_if_fail(GWY_IS_SPECTRA(destination));
    g_return_if_fail(GWY_IS_SPECTRA(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}

/**
 * SECTION:spectra
 * @title: GwySpectra
 * @short_description: Collection of line representing point spectra.
 *
 * #GwySpectra contains an array of #GwyLine<!-- -->s and coordinates representing where in a data field the
 * spectrum was acquired.
 **/

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