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

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

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

#include "libgwyui/types.h"
#include "libgwyui/graph-curve-model.h"
#include "libgwyui/graph-model.h"
#include "libgwyui/graph.h"
#include "libgwyui/marshals.h"
#include "libgwyui/graph-internal.h"

#define TYPE_NAME "GwyGraphModel"

enum {
    PROP_0,
    PROP_N_CURVES,
    PROP_TITLE,
    PROP_X_MIN,
    PROP_X_MIN_SET,
    PROP_X_MAX,
    PROP_X_MAX_SET,
    PROP_Y_MIN,
    PROP_Y_MIN_SET,
    PROP_Y_MAX,
    PROP_Y_MAX_SET,
    PROP_AXIS_LABEL_BOTTOM,
    PROP_AXIS_LABEL_LEFT,
    PROP_AXIS_LABEL_RIGHT,
    PROP_AXIS_LABEL_TOP,
    PROP_X_LOGARITHMIC,
    PROP_Y_LOGARITHMIC,
    PROP_UNIT_X,
    PROP_UNIT_Y,
    PROP_LABEL_FRAME_THICKNESS,
    PROP_LABEL_HAS_FRAME,
    PROP_LABEL_POSITION,
    PROP_LABEL_REVERSE,
    PROP_LABEL_VISIBLE,
    PROP_GRID_TYPE,
    PROP_LABEL_RELATIVE_X,
    PROP_LABEL_RELATIVE_Y,
    NUM_PROPERTIES
};

enum {
    SGNL_CURVE_DATA_CHANGED,
    SGNL_CURVE_NOTIFY,
    NUM_SIGNALS
};

enum {
    ITEM_TITLE,
    ITEM_X_MIN, ITEM_X_MIN_SET,
    ITEM_X_MAX, ITEM_X_MAX_SET,
    ITEM_Y_MIN, ITEM_Y_MIN_SET,
    ITEM_Y_MAX, ITEM_Y_MAX_SET,
    ITEM_BOTTOM_LABEL, ITEM_LEFT_LABEL, ITEM_TOP_LABEL, ITEM_RIGHT_LABEL,
    ITEM_X_IS_LOGARITHMIC, ITEM_Y_IS_LOGARITHMIC,
    ITEM_X_UNIT, ITEM_Y_UNIT,
    ITEM_LABEL_HAS_FRAME, ITEM_LABEL_FRAME_THICKNESS, ITEM_LABEL_POSITION, ITEM_LABEL_REVERSE, ITEM_LABEL_VISIBLE,
    ITEM_LABEL_RELATIVE_X, ITEM_LABEL_RELATIVE_Y,
    ITEM_GRID_TYPE,
    ITEM_CURVES,
    NUM_ITEMS
};

typedef struct {
    gulong data_changed_id;
    gulong notify_id;
} GwyGraphModelCurveAux;

static void             finalize                    (GObject *object);
static void             dispose                     (GObject *object);
static void             serializable_init           (GwySerializableInterface *iface);
static void             serializable_itemize        (GwySerializable *serializable,
                                                     GwySerializableGroup *group);
static gboolean         serializable_construct      (GwySerializable *serializable,
                                                     GwySerializableGroup *group,
                                                     GwyErrorList **error_list);
static GwySerializable* serializable_copy           (GwySerializable *serializable);
static void             serializable_assign         (GwySerializable *destination,
                                                     GwySerializable *source);
static void             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             export_with_merged_abscissae(const GwyGraphModel *gmodel,
                                                     GwyGraphModelExportStyle export_style,
                                                     gboolean posix_format,
                                                     gboolean export_units,
                                                     gboolean export_labels,
                                                     gboolean export_metadata,
                                                     GString *string);
static gdouble*         merge_abscissae             (const GwyGraphModel *gmodel,
                                                     guint *ndata);
static gboolean         curve_is_equispaced         (const GwyGraphCurveModel *gcmodel);
static gchar*           ascii_name                  (const gchar *s);
static void             take_curve                  (GwyGraphModel *gmodel,
                                                     GwyGraphCurveModel *curve,
                                                     gint i);
static void             release_curve               (GwyGraphModel *gmodel,
                                                     guint i);
static void             curve_data_changed          (GwyGraphCurveModel *cmodel,
                                                     GwyGraphModel *gmodel);
static void             curve_notify                (GwyGraphCurveModel *cmodel,
                                                     GParamSpec *pspec,
                                                     GwyGraphModel *gmodel);
static void             x_unit_changed              (GwyGraphModel *gmodel,
                                                     GwyUnit *unit);
static void             y_unit_changed              (GwyGraphModel *gmodel,
                                                     GwyUnit *unit);

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

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "title",                 .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "x_min",                 .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "x_min_set",             .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "x_max",                 .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "x_max_set",             .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "y_min",                 .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "y_min_set",             .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "y_max",                 .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "y_max_set",             .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "bottom_label",          .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "left_label",            .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "top_label",             .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "right_label",           .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "x_is_logarithmic",      .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "y_is_logarithmic",      .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "x_unit",                .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "y_unit",                .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "label.has_frame",       .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "label.frame_thickness", .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "label.position",        .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "label.reverse",         .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "label.visible",         .ctype = GWY_SERIALIZABLE_BOOLEAN,      },
    { .name = "label.relative.x",      .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "label.relative.y",      .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "grid-type",             .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "curves",                .ctype = GWY_SERIALIZABLE_OBJECT_ARRAY, },
};

G_DEFINE_TYPE_WITH_CODE(GwyGraphModel, gwy_graph_model, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyGraphModel)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
define_properties(void)
{
    if (properties[PROP_N_CURVES])
        return;

    properties[PROP_N_CURVES] = g_param_spec_uint("n-curves", NULL,
                                                  "The number of curves in graph model",
                                                  0, G_MAXUINT, 0,
                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    properties[PROP_TITLE] = g_param_spec_string("title", NULL,
                                                 "The graph title",
                                                 "New graph",
                                                 GWY_GPARAM_RWE);
    properties[PROP_X_MIN] = g_param_spec_double("x-min", NULL,
                                                 "Requested minimum x value",
                                                 -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                 GWY_GPARAM_RWE);
    properties[PROP_X_MIN_SET] = g_param_spec_boolean("x-min-set", NULL,
                                                      "Whether x-min is set",
                                                      FALSE,
                                                      GWY_GPARAM_RWE);
    properties[PROP_X_MAX] = g_param_spec_double("x-max", NULL,
                                                 "Requested maximum x value",
                                                 -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                 GWY_GPARAM_RWE);
    properties[PROP_X_MAX_SET] = g_param_spec_boolean("x-max-set", NULL,
                                                      "Whether x-max is set",
                                                      FALSE,
                                                      GWY_GPARAM_RWE);
    properties[PROP_Y_MIN] = g_param_spec_double("y-min", NULL,
                                                 "Requested minimum y value",
                                                 -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                 GWY_GPARAM_RWE);
    properties[PROP_Y_MIN_SET] = g_param_spec_boolean("y-min-set", NULL,
                                                      "Whether y-min is set",
                                                      FALSE,
                                                      GWY_GPARAM_RWE);
    properties[PROP_Y_MAX] = g_param_spec_double("y-max", NULL,
                                                 "Requested maximum y value",
                                                 -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                 GWY_GPARAM_RWE);
    properties[PROP_Y_MAX_SET] = g_param_spec_boolean("y-max-set", NULL,
                                                      "Whether y-max is set",
                                                      FALSE,
                                                      GWY_GPARAM_RWE);
    properties[PROP_AXIS_LABEL_BOTTOM] = g_param_spec_string("axis-label-bottom", NULL,
                                                             "The label of the bottom axis",
                                                             "x",
                                                             GWY_GPARAM_RWE);
    properties[PROP_AXIS_LABEL_LEFT] = g_param_spec_string("axis-label-left", NULL,
                                                           "The label of the left axis",
                                                           "y",
                                                           GWY_GPARAM_RWE);
    properties[PROP_AXIS_LABEL_RIGHT] = g_param_spec_string("axis-label-right", NULL,
                                                            "The label of the right axis",
                                                            "",
                                                            GWY_GPARAM_RWE);
    properties[PROP_AXIS_LABEL_TOP] = g_param_spec_string("axis-label-top", NULL,
                                                          "The label of the top axis",
                                                          "",
                                                          GWY_GPARAM_RWE);
    properties[PROP_X_LOGARITHMIC] = g_param_spec_boolean("x-logarithmic", NULL,
                                                          "TRUE if x coordinate is logarithimic",
                                                          FALSE,
                                                          GWY_GPARAM_RWE);
    properties[PROP_Y_LOGARITHMIC] = g_param_spec_boolean("y-logarithmic", NULL,
                                                          "TRUE if y coordinate is logarithimic",
                                                          FALSE,
                                                          GWY_GPARAM_RWE);

    /**
     * GwyGraphModel:unit-x
     *
     * The unit of horizontal axis values.
     *
     * Units are set by value. The unit object does not change. Setting the property is the same as getting the unit
     * object and using gwy_unit_assign() to modify it.
     **/
    properties[PROP_UNIT_X] = g_param_spec_object("unit-x", NULL,
                                                  "Unit of x axis.",
                                                  GWY_TYPE_UNIT,
                                                  GWY_GPARAM_RWE);

    /**
     * GwyGraphModel:unit-y
     *
     * The unit of vertical axis values.
     *
     * Units are set by value. The unit object does not change. Setting the property is the same as getting the unit
     * object and using gwy_unit_assign() to modify it.
     **/
    properties[PROP_UNIT_Y] = g_param_spec_object("unit-y", NULL,
                                                  "Unit of y axis.",
                                                  GWY_TYPE_UNIT,
                                                  GWY_GPARAM_RWE);

    properties[PROP_LABEL_REVERSE] = g_param_spec_boolean("label-reverse", NULL,
                                                          "TRUE if text and curve sample is switched in the key.",
                                                          FALSE,
                                                          GWY_GPARAM_RWE);
    properties[PROP_LABEL_VISIBLE] = g_param_spec_boolean("label-visible", NULL,
                                                          "TRUE if key label is visible",
                                                          TRUE,
                                                          GWY_GPARAM_RWE);
    properties[PROP_LABEL_HAS_FRAME] = g_param_spec_boolean("label-has-frame", NULL,
                                                            "TRUE if key label has frame",
                                                            TRUE,
                                                            GWY_GPARAM_RWE);
    properties[PROP_LABEL_FRAME_THICKNESS] = g_param_spec_int("label-frame-thickness", NULL,
                                                              "Thickness of key label frame",
                                                              0, 16, 1,
                                                              GWY_GPARAM_RWE);
    properties[PROP_LABEL_POSITION] = g_param_spec_enum("label-position", NULL,
                                                        "Position type of key label",
                                                        GWY_TYPE_GRAPH_KEY_POSITION,
                                                        GWY_GRAPH_KEY_NORTHEAST,
                                                        GWY_GPARAM_RWE);
    properties[PROP_GRID_TYPE] = g_param_spec_enum("grid-type", NULL,
                                                   "Type of grid drawn on main graph area",
                                                   GWY_TYPE_GRAPH_GRID_TYPE,
                                                   GWY_GRAPH_GRID_AUTO,
                                                   GWY_GPARAM_RWE);
    properties[PROP_LABEL_RELATIVE_X] = g_param_spec_double("label-relative-x", NULL,
                                                            "Relative screen x-coordinate of label inside the area "
                                                            "for user label position.",
                                                            0.0, 1.0, 1.0,
                                                            GWY_GPARAM_RWE);
    properties[PROP_LABEL_RELATIVE_Y] = g_param_spec_double("label-relative-y", NULL,
                                                            "Relative screen y-coordinate of label inside the area "
                                                            "for user label position.",
                                                            0.0, 1.0, 0.0,
                                                            GWY_GPARAM_RWE);
}

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

    define_properties();
    /* Do not need to add x-min and x-min-set because there would be no validation beyond the one already done. */
    serializable_items[ITEM_LABEL_HAS_FRAME].aux.pspec = properties[PROP_LABEL_HAS_FRAME];
    serializable_items[ITEM_LABEL_REVERSE].aux.pspec = properties[PROP_LABEL_REVERSE];
    serializable_items[ITEM_LABEL_VISIBLE].aux.pspec = properties[PROP_LABEL_VISIBLE];
    serializable_items[ITEM_LABEL_FRAME_THICKNESS].aux.pspec = properties[PROP_LABEL_FRAME_THICKNESS];
    serializable_items[ITEM_LABEL_POSITION].aux.pspec = properties[PROP_LABEL_POSITION];
    serializable_items[ITEM_GRID_TYPE].aux.pspec = properties[PROP_GRID_TYPE];
    serializable_items[ITEM_LABEL_RELATIVE_X].aux.pspec = properties[PROP_LABEL_RELATIVE_X];
    serializable_items[ITEM_LABEL_RELATIVE_Y].aux.pspec = properties[PROP_LABEL_RELATIVE_Y];
    /* Basically none of the property names matches the serialisation item. We cannot change property names freely
     * because they need to be canonical-strings. Changing the serialisation group is possible, but it is a file
     * format change. */
    gwy_fill_serializable_defaults_pspec(serializable_items, NUM_ITEMS, TRUE);
    serializable_items[ITEM_X_UNIT].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_Y_UNIT].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_CURVES].aux.object_type = GWY_TYPE_GRAPH_CURVE_MODEL;
}

static void
gwy_graph_model_class_init(GwyGraphModelClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_graph_model_parent_class;

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

    /**
     * GwyGraphModel::curve-data-changed:
     * @gwygraphmodel: The #GwyGraphModel which received the signal.
     * @arg1: The index of the changed curve in the model.
     *
     * The ::curve-data-changed signal is emitted whenever any of the curves in a graph model emits
     * #GwyGraphCurveModel::data-changed.
     **/
    signals[SGNL_CURVE_DATA_CHANGED] = g_signal_new("curve-data-changed", type,
                                                    G_SIGNAL_RUN_FIRST,
                                                    G_STRUCT_OFFSET(GwyGraphModelClass, curve_data_changed),
                                                    NULL, NULL,
                                                    g_cclosure_marshal_VOID__INT,
                                                    G_TYPE_NONE, 1, G_TYPE_INT);
    g_signal_set_va_marshaller(signals[SGNL_CURVE_DATA_CHANGED], type, g_cclosure_marshal_VOID__INTv);

    /**
     * GwyGraphModel::curve-notify:
     * @gwygraphmodel: The #GwyGraphModel which received the signal.
     * @arg1: The index of the changed curve in the model.
     * @arg2: The #GParamSpec of the property that has changed.
     *
     * The ::curve-data-changed signal is emitted whenever any of the curves in a graph model emits #GObject::notify.
     **/
    signals[SGNL_CURVE_NOTIFY] = g_signal_new("curve-notify", type,
                                              G_SIGNAL_RUN_FIRST,
                                              G_STRUCT_OFFSET(GwyGraphModelClass, curve_notify),
                                              NULL, NULL,
                                              _gwyui_marshal_VOID__INT_PARAM,
                                              G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_PARAM);
    g_signal_set_va_marshaller(signals[SGNL_CURVE_NOTIFY], type, _gwyui_marshal_VOID__INT_PARAMv);

    define_properties();
    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_graph_model_init(GwyGraphModel *gmodel)
{
    GwyGraphModelPrivate *priv;

    priv = gmodel->priv = gwy_graph_model_get_instance_private(gmodel);

    priv->curves = g_ptr_array_new();
    priv->curveaux = g_array_new(FALSE, FALSE, sizeof(GwyGraphModelCurveAux));

    priv->x_unit = gwy_unit_new(NULL);
    priv->y_unit = gwy_unit_new(NULL);
    priv->x_unit_changed_id = g_signal_connect_swapped(priv->x_unit, "value-changed",
                                                       G_CALLBACK(x_unit_changed), gmodel);
    priv->y_unit_changed_id = g_signal_connect_swapped(priv->y_unit, "value-changed",
                                                       G_CALLBACK(y_unit_changed), gmodel);

    priv->title = NULL;
    priv->bottom_label = g_strdup("x");
    priv->top_label = NULL;
    priv->left_label = g_strdup("y");
    priv->right_label = NULL;

    priv->label_position = GWY_GRAPH_KEY_NORTHEAST;
    priv->grid_type = GWY_GRAPH_GRID_AUTO;
    priv->label_has_frame = TRUE;
    priv->label_frame_thickness = 1;
    priv->label_reverse = FALSE;
    priv->label_visible = TRUE;

    priv->label_xy = (GwyXY){ 1.0, 0.0 };
}

/**
 * gwy_graph_model_new:
 *
 * Creates a new graph model.
 *
 * Returns: New graph model as a #GObject.
 **/
GwyGraphModel*
gwy_graph_model_new(void)
{
    return g_object_new(GWY_TYPE_GRAPH_MODEL, NULL);
}

static void
finalize(GObject *object)
{
    GwyGraphModel *gmodel = GWY_GRAPH_MODEL(object);
    GwyGraphModelPrivate *priv = gmodel->priv;

    g_clear_object(&priv->x_unit);
    g_clear_object(&priv->y_unit);

    GWY_FREE(priv->title);
    GWY_FREE(priv->top_label);
    GWY_FREE(priv->bottom_label);
    GWY_FREE(priv->left_label);
    GWY_FREE(priv->right_label);

    for (guint i = 0; i < priv->curves->len; i++)
        release_curve(gmodel, i);
    g_ptr_array_free(priv->curves, TRUE);
    g_array_free(priv->curveaux, TRUE);

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

static void
dispose(GObject *object)
{
    GwyGraphModel *gmodel = GWY_GRAPH_MODEL(object);
    GwyGraphModelPrivate *priv = gmodel->priv;

    g_clear_signal_handler(&priv->x_unit_changed_id, priv->x_unit);
    g_clear_signal_handler(&priv->y_unit_changed_id, priv->y_unit);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyGraphModel *gmodel = GWY_GRAPH_MODEL(object);
    GwyGraphModelPrivate *priv = gmodel->priv;
    gdouble d;
    gboolean b;
    gint i;
    guint e;
    GwyUnit *unit;
    gboolean changed = FALSE;

    switch (prop_id) {
        case PROP_TITLE:
        changed = gwy_assign_string(&priv->title, g_value_get_string(value));
        break;

        case PROP_X_MIN:
        if ((changed = (priv->x_min != (d = g_value_get_double(value)))))
            priv->x_min = d;
        break;

        case PROP_X_MIN_SET:
        if ((changed = (priv->x_min_set != (b = g_value_get_boolean(value)))))
            priv->x_min_set = b;
        break;

        case PROP_X_MAX:
        if ((changed = (priv->x_max != (d = g_value_get_double(value)))))
            priv->x_max = d;
        break;

        case PROP_X_MAX_SET:
        if ((changed = (priv->x_max_set != (b = g_value_get_boolean(value)))))
            priv->x_max_set = b;
        break;

        case PROP_Y_MIN:
        if ((changed = (priv->y_min != (d = g_value_get_double(value)))))
            priv->y_min = d;
        break;

        case PROP_Y_MIN_SET:
        if ((changed = (priv->y_min_set != (b = g_value_get_boolean(value)))))
            priv->y_min_set = b;
        break;

        case PROP_Y_MAX:
        if ((changed = (priv->y_max != (d = g_value_get_double(value)))))
            priv->y_max = d;
        break;

        case PROP_Y_MAX_SET:
        if ((changed = (priv->y_max_set != (b = g_value_get_boolean(value)))))
            priv->y_max_set = b;
        break;

        case PROP_AXIS_LABEL_BOTTOM:
        changed = gwy_assign_string(&priv->bottom_label, g_value_get_string(value));
        break;

        case PROP_AXIS_LABEL_LEFT:
        changed = gwy_assign_string(&priv->left_label, g_value_get_string(value));
        break;

        case PROP_AXIS_LABEL_RIGHT:
        changed = gwy_assign_string(&priv->right_label, g_value_get_string(value));
        break;

        case PROP_AXIS_LABEL_TOP:
        changed = gwy_assign_string(&priv->top_label, g_value_get_string(value));
        break;

        case PROP_UNIT_X:
        unit = g_value_get_object(value);
        if ((changed = !gwy_unit_equal(priv->x_unit, unit))) {
            gwy_unit_assign(priv->x_unit, unit);
            g_object_notify_by_pspec(object, properties[PROP_UNIT_X]);
        }
        break;

        case PROP_UNIT_Y:
        unit = g_value_get_object(value);
        if ((changed = !gwy_unit_equal(priv->y_unit, unit))) {
            gwy_unit_assign(priv->y_unit, unit);
            g_object_notify_by_pspec(object, properties[PROP_UNIT_Y]);
        }
        break;

        case PROP_X_LOGARITHMIC:
        if ((changed = (priv->x_is_logarithmic != (b = g_value_get_boolean(value)))))
            priv->x_is_logarithmic = b;
        break;

        case PROP_Y_LOGARITHMIC:
        if ((changed = (priv->y_is_logarithmic != (b = g_value_get_boolean(value)))))
            priv->y_is_logarithmic = b;
        break;

        case PROP_LABEL_FRAME_THICKNESS:
        if ((changed = (priv->label_frame_thickness != (i = g_value_get_int(value)))))
            priv->label_frame_thickness = i;
        break;

        case PROP_LABEL_HAS_FRAME:
        if ((changed = (priv->label_has_frame != (b = g_value_get_boolean(value)))))
            priv->label_has_frame = b;
        break;

        case PROP_LABEL_REVERSE:
        if ((changed = (priv->label_reverse != (b = g_value_get_boolean(value)))))
            priv->label_reverse = b;
        break;

        case PROP_LABEL_VISIBLE:
        if ((changed = (priv->label_visible != (b = g_value_get_boolean(value)))))
            priv->label_visible = b;
        break;

        case PROP_LABEL_POSITION:
        if ((changed = (priv->label_position != (e = g_value_get_enum(value)))))
            priv->label_position = e;
        break;

        case PROP_GRID_TYPE:
        if ((changed = (priv->grid_type != (e = g_value_get_enum(value)))))
            priv->grid_type = e;
        break;

        case PROP_LABEL_RELATIVE_X:
        if ((changed = (priv->label_xy.x != (d = g_value_get_double(value)))))
            priv->label_xy.x = d;
        break;

        case PROP_LABEL_RELATIVE_Y:
        if ((changed = (priv->label_xy.y != (d = g_value_get_double(value)))))
            priv->label_xy.y = d;
        break;

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

    if (changed)
        g_object_notify_by_pspec(object, properties[prop_id]);
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyGraphModel *gmodel = GWY_GRAPH_MODEL(object);
    GwyGraphModelPrivate *priv = gmodel->priv;

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

        case PROP_N_CURVES:
        g_value_set_uint(value, priv->curves->len);
        break;

        case PROP_X_MIN:
        g_value_set_double(value, priv->x_min);
        break;

        case PROP_X_MIN_SET:
        g_value_set_boolean(value, priv->x_min_set);
        break;

        case PROP_X_MAX:
        g_value_set_double(value, priv->x_max);
        break;

        case PROP_X_MAX_SET:
        g_value_set_boolean(value, priv->x_max_set);
        break;

        case PROP_Y_MIN:
        g_value_set_double(value, priv->y_min);
        break;

        case PROP_Y_MIN_SET:
        g_value_set_boolean(value, priv->y_min_set);
        break;

        case PROP_Y_MAX:
        g_value_set_double(value, priv->y_max);
        break;

        case PROP_Y_MAX_SET:
        g_value_set_boolean(value, priv->y_max_set);
        break;

        case PROP_AXIS_LABEL_BOTTOM:
        g_value_set_string(value, priv->bottom_label);
        break;

        case PROP_AXIS_LABEL_LEFT:
        g_value_set_string(value, priv->left_label);
        break;

        case PROP_AXIS_LABEL_RIGHT:
        g_value_set_string(value, priv->right_label);
        break;

        case PROP_AXIS_LABEL_TOP:
        g_value_set_string(value, priv->top_label);
        break;

        case PROP_UNIT_X:
        g_value_set_object(value, priv->x_unit);
        break;

        case PROP_UNIT_Y:
        g_value_set_object(value, priv->y_unit);
        break;

        case PROP_X_LOGARITHMIC:
        g_value_set_boolean(value, priv->x_is_logarithmic);
        break;

        case PROP_Y_LOGARITHMIC:
        g_value_set_boolean(value, priv->y_is_logarithmic);
        break;

        case PROP_LABEL_FRAME_THICKNESS:
        g_value_set_int(value, priv->label_frame_thickness);
        break;

        case PROP_LABEL_HAS_FRAME:
        g_value_set_boolean(value, priv->label_has_frame);
        break;

        case PROP_LABEL_REVERSE:
        g_value_set_boolean(value, priv->label_reverse);
        break;

        case PROP_LABEL_VISIBLE:
        g_value_set_boolean(value, priv->label_visible);
        break;

        case PROP_LABEL_POSITION:
        g_value_set_enum(value, priv->label_position);
        break;

        case PROP_GRID_TYPE:
        g_value_set_enum(value, priv->grid_type);
        break;

        case PROP_LABEL_RELATIVE_X:
        g_value_set_double(value, priv->label_xy.x);
        break;

        case PROP_LABEL_RELATIVE_Y:
        g_value_set_double(value, priv->label_xy.y);
        break;

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

/**
 * gwy_graph_model_new_alike:
 * @gmodel: A graph model.
 *
 * Creates new graph model object that has the same settings as @gmodel.
 *
 * This includes axis/label visibility, actual plotting range, etc. Curves are not duplicated or referenced.
 *
 * Returns: New graph model.
 **/
GwyGraphModel*
gwy_graph_model_new_alike(GwyGraphModel *gmodel)
{
    GwyGraphModel *duplicate = gwy_graph_model_new();
    GwyGraphModelPrivate *priv = gmodel->priv, *dpriv = duplicate->priv;

    gwy_assign_string(&dpriv->title, priv->title);
    dpriv->x_is_logarithmic = priv->x_is_logarithmic;
    dpriv->y_is_logarithmic = priv->y_is_logarithmic;
    dpriv->x_min = priv->x_min;
    dpriv->x_min_set = priv->x_min_set;
    dpriv->x_max = priv->x_max;
    dpriv->x_max_set = priv->x_max_set;
    dpriv->y_min = priv->y_min;
    dpriv->y_min_set = priv->y_min_set;
    dpriv->y_max = priv->y_max;
    dpriv->y_max_set = priv->y_max_set;
    dpriv->label_has_frame = priv->label_has_frame;
    dpriv->label_frame_thickness = priv->label_frame_thickness;
    dpriv->label_visible = priv->label_visible;
    dpriv->label_position = priv->label_position;
    dpriv->grid_type = priv->grid_type;
    gwy_unit_assign(dpriv->x_unit, priv->x_unit);
    gwy_unit_assign(dpriv->y_unit, priv->y_unit);
    gwy_assign_string(&dpriv->top_label, priv->top_label);
    gwy_assign_string(&dpriv->bottom_label, priv->bottom_label);
    gwy_assign_string(&dpriv->left_label, priv->left_label);
    gwy_assign_string(&dpriv->right_label, priv->right_label);
    dpriv->label_xy = priv->label_xy;

    return duplicate;
}

/**
 * gwy_graph_model_add_curve:
 * @gmodel: A graph model.
 * @curve: A #GwyGraphCurveModel representing the curve to add.
 *
 * Adds a new curve to a graph model.
 *
 * Returns: The index of the added curve in @gmodel.
 **/
gint
gwy_graph_model_add_curve(GwyGraphModel *gmodel,
                          GwyGraphCurveModel *curve)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), -1);
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(curve), -1);

    gint idx = gmodel->priv->curves->len;
    take_curve(gmodel, curve, idx);
    /* In principle, this can change priv->curves->len, so we have to save the index in idx. */
    g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_N_CURVES]);

    return idx;
}

/**
 * gwy_graph_model_get_n_curves:
 * @gmodel: A graph model.
 *
 * Reports the number of curves in a graph model.
 *
 * Returns: Number of curves in graph model.
 **/
gint
gwy_graph_model_get_n_curves(GwyGraphModel *gmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), 0);
    return gmodel->priv->curves->len;
}

/**
 * gwy_graph_model_remove_all_curves:
 * @gmodel: A graph model.
 *
 * Removes all the curves from graph model
 **/
void
gwy_graph_model_remove_all_curves(GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));

    GwyGraphModelPrivate *priv = gmodel->priv;
    for (guint i = 0; i < priv->curves->len; i++)
        release_curve(gmodel, i);
    g_ptr_array_set_size(priv->curves, 0);
    g_array_set_size(priv->curveaux, 0);
    g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_N_CURVES]);
}

/**
 * gwy_graph_model_append_curves:
 * @gmodel: A graph model.
 * @source: Graph model containing the curves to append.
 * @colorstep: Block size for curve color updating.
 *
 * Appends all curves from another graph model to a graph model.
 *
 * The colors of the curves can be updated, presumably to continue a preset color sequence.  This is controlled by
 * argument @colorstep.  When @colorstep is zero no curve color modification is done.  When it is positive, a block of
 * curves of size @colorstep is always given the same color, the first color being the first preset color
 * corresponding to the number of curves already in @gmodel.  So pass @colorstep=1 for individual curves, @colorstep=2
 * for couples of curves (e.g. data and fit) that should have the same color, etc.
 **/
void
gwy_graph_model_append_curves(GwyGraphModel *gmodel,
                              GwyGraphModel *source,
                              gint colorstep)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));
    g_return_if_fail(GWY_IS_GRAPH_MODEL(source));

    GwyGraphModelPrivate *priv = gmodel->priv, *spriv = source->priv;
    guint n = priv->curves->len;
    guint ns = spriv->curves->len;
    if (!ns)
        return;

    for (guint i = 0; i < ns; i++) {
        GwyGraphCurveModel *gcmodel = g_ptr_array_index(spriv->curves, i);
        gcmodel = gwy_graph_curve_model_copy(gcmodel);
        if (colorstep > 0) {
            gint c = (n + colorstep-1)/colorstep + i/colorstep;
            const GwyRGBA *color = gwy_graph_get_preset_color(c);
            g_object_set(gcmodel, "color", color, NULL);
        }
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);
    }
}

/**
 * gwy_graph_model_remove_curve:
 * @gmodel: A graph model.
 * @cindex: Curve index in graph model.
 *
 * Removes the curve having given index.
 **/
void
gwy_graph_model_remove_curve(GwyGraphModel *gmodel,
                             gint cindex)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));
    GwyGraphModelPrivate *priv = gmodel->priv;
    g_return_if_fail(cindex >= 0 && cindex < priv->curves->len);

    release_curve(gmodel, cindex);
    g_ptr_array_remove_index(priv->curves, cindex);
    g_array_remove_index(priv->curveaux, cindex);
    g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_N_CURVES]);
}

/**
 * gwy_graph_model_find_curve_by_description:
 * @gmodel: A graph model.
 * @description: Curve description (label).
 *
 * Finds the first graph curve model in a graph model with matching description string.
 *
 * Returns: The curve index if found, a negative value (-1) if not found.
 **/
gint
gwy_graph_model_find_curve_by_description(GwyGraphModel *gmodel,
                                          const gchar *description)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), -1);
    g_return_val_if_fail(description, -1);

    GPtrArray *curves = gmodel->priv->curves;
    for (guint i = 0; i < curves->len; i++) {
        GwyGraphCurveModel *cmodel = g_ptr_array_index(curves, i);
        if (gwy_strequal(description, cmodel->priv->description->str))
            return i;
    }

    return -1;
}

/**
 * gwy_graph_model_get_curve:
 * @gmodel: A graph model.
 * @cindex: Curve index in graph model.
 *
 * Gets a graph curve model in a graph model by its index.
 *
 * Returns: The curve with index @cindex (no reference is added).
 **/
GwyGraphCurveModel*
gwy_graph_model_get_curve(GwyGraphModel *gmodel,
                          gint cindex)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), NULL);
    g_return_val_if_fail(cindex >= 0 && cindex < gmodel->priv->curves->len, NULL);

    return g_ptr_array_index(gmodel->priv->curves, cindex);
}

/**
 * gwy_graph_model_get_curve_index:
 * @gmodel: A graph model.
 * @curve: A curve model present in @gmodel to find.
 *
 * Finds the index of a graph model curve.
 *
 * Returns: The index of @curve in @gmodel, -1 if it is not present there.
 **/
gint
gwy_graph_model_get_curve_index(GwyGraphModel *gmodel,
                                GwyGraphCurveModel *curve)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), -1);
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(curve), -1);

    GwyGraphModelPrivate *priv = gmodel->priv;

    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *cmodel = g_ptr_array_index(priv->curves, i);
        if (cmodel == curve)
            return (gint)i;
    }

    return -1;
}

/**
 * gwy_graph_model_replace_curve:
 * @gmodel: A graph model.
 * @cindex: Curve index in graph model.
 * @curve: A curve model to put into @gmodel at position @cindex.
 *
 * Replaces a curve in a graph model.
 **/
void
gwy_graph_model_replace_curve(GwyGraphModel *gmodel,
                              gint cindex,
                              GwyGraphCurveModel *curve)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));
    g_return_if_fail(GWY_IS_GRAPH_CURVE_MODEL(curve));
    GwyGraphModelPrivate *priv = gmodel->priv;
    g_return_if_fail(cindex >= 0 && cindex < priv->curves->len);

    GwyGraphCurveModel *cmodel = g_ptr_array_index(priv->curves, cindex);
    /* Avoid messy work when the curve is already there. */
    if (curve == cmodel)
        return;

    g_object_ref(cmodel);
    release_curve(gmodel, cindex);
    take_curve(gmodel, curve, cindex);
    g_object_unref(cmodel);
}

/**
 * gwy_graph_model_get_unit_x:
 * @gmodel: A graph model.
 *
 * Returns ordinate SI unit of a graph model.
 *
 * The returned object can be modified to change the graph ordinate units.
 *
 * Returns: SI unit corresponding to the ordinate dimensions of the graph.  Its reference count is not incremented.
 **/
GwyUnit*
gwy_graph_model_get_unit_x(GwyGraphModel *gmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), NULL);
    return gmodel->priv->x_unit;
}

/**
 * gwy_graph_model_get_unit_y:
 * @gmodel: A graph model.
 *
 * Returns abscissa SI unit of a graph model.
 *
 * The returned object can be modified to change the graph abscissa units.
 *
 * Returns: SI unit corresponding to the abscissa dimensions of the graph.  Its reference count is not incremented.
 **/
GwyUnit*
gwy_graph_model_get_unit_y(GwyGraphModel *gmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), NULL);
    return gmodel->priv->y_unit;
}

/**
 * gwy_graph_model_set_units_from_line:
 * @gmodel: A graph model.
 * @line: A data line to take units from.
 *
 * Sets x and y graph model units to match a data line.
 **/
void
gwy_graph_model_set_units_from_line(GwyGraphModel *gmodel,
                                    GwyLine *line)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));
    g_return_if_fail(GWY_IS_LINE(line));

    GwyUnit *unitx = gwy_line_get_unit_x(line);
    GwyUnit *unity = gwy_line_get_unit_y(line);
    g_object_set(gmodel, "unit-x", unitx, "unit-y", unity, NULL);
}

/**
 * gwy_graph_model_set_units_from_field:
 * @gmodel: A graph model.
 * @field: A data field.
 * @power_xy_in_x: Power of field's lateral units to appear in graph's abscissa units.
 * @power_z_in_x: Power of field's value units to appear in graph's abscissa units.
 * @power_xy_in_y: Power of field's lateral units to appear in graph's ordinate units.
 * @power_z_in_y: Power of field's value units to appear in graph's ordinate units.
 *
 * Sets x and y graph model units to units derived from a data field.
 **/
void
gwy_graph_model_set_units_from_field(GwyGraphModel *gmodel,
                                     GwyField *field,
                                     gint power_xy_in_x,
                                     gint power_z_in_x,
                                     gint power_xy_in_y,
                                     gint power_z_in_y)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));
    g_return_if_fail(GWY_IS_FIELD(field));

    GwyGraphModelPrivate *priv = gmodel->priv;
    GwyUnit *xyunit = gwy_field_get_unit_xy(field);
    GwyUnit *zunit = gwy_field_get_unit_z(field);
    gwy_unit_power_multiply(priv->x_unit, xyunit, power_xy_in_x, zunit, power_z_in_x);
    gwy_unit_power_multiply(priv->y_unit, xyunit, power_xy_in_y, zunit, power_z_in_y);
    g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_UNIT_X]);
    g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_UNIT_Y]);
}

/**
 * gwy_graph_model_units_are_compatible:
 * @gmodel: A graph model.
 * @othergmodel: Another graph model.
 *
 * Checks if the units of two graph models are compatible.
 *
 * This function is useful namely as a pre-check for moving curves between graphs.
 *
 * Returns: %TRUE if the abscissa and ordinate units of the two graphs are compatible.
 **/
gboolean
gwy_graph_model_units_are_compatible(GwyGraphModel *gmodel,
                                     GwyGraphModel *othergmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), FALSE);
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(othergmodel), FALSE);

    GwyUnit *xunit, *yunit, *otherxunit, *otheryunit;

    g_object_get(gmodel,
                 "unit-x", &xunit,
                 "unit-y", &yunit,
                 NULL);
    g_object_get(othergmodel,
                 "unit-x", &otherxunit,
                 "unit-y", &otheryunit,
                 NULL);

    gboolean ok = (gwy_unit_equal(xunit, otherxunit) && gwy_unit_equal(yunit, otheryunit));

    g_object_unref(xunit);
    g_object_unref(yunit);
    g_object_unref(otherxunit);
    g_object_unref(otheryunit);

    return ok;
}

/**
 * gwy_graph_model_x_data_can_be_logarithmed:
 * @gmodel: A graph model.
 *
 * Checks whehter x axis can be lograrithmed.
 *
 * Returns: TRUE if all x-values are greater than zero (thus logarithmic display of x-data is feasible).
 **/
gboolean
gwy_graph_model_x_data_can_be_logarithmed(GwyGraphModel *gmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), FALSE);

    GwyGraphModelPrivate *priv = gmodel->priv;

    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *cmodel = g_ptr_array_index(priv->curves, i);
        const gdouble *data = gwy_graph_curve_model_get_xdata(cmodel);
        guint n = gwy_graph_curve_model_get_n_data(cmodel);
        for (guint j = 0; j < n; j++) {
            if (data[j] <= 0)
                return FALSE;
        }
    }
    return TRUE;
}

/**
 * gwy_graph_model_y_data_can_be_logarithmed:
 * @gmodel: A graph model.
 *
 * Checks whehter y axis can be lograrithmed.
 *
 * Returns: TRUE if all y-values are greater than zero (thus logarithmic display of y-data is feasible).
 **/
gboolean
gwy_graph_model_y_data_can_be_logarithmed(GwyGraphModel *gmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), FALSE);

    GwyGraphModelPrivate *priv = gmodel->priv;

    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *cmodel = g_ptr_array_index(priv->curves, i);
        const gdouble *data = gwy_graph_curve_model_get_ydata(cmodel);
        guint n = gwy_graph_curve_model_get_n_data(cmodel);
        for (guint j = 0; j < n; j++) {
            if (data[j] <= 0)
                return FALSE;
        }
    }
    return TRUE;
}

/**
 * gwy_graph_model_get_axis_label:
 * @gmodel: A graph model.
 * @pos: Axis position.
 *
 * Gets the label of a one graph model axis.
 *
 * Returns: (nullable): The label as a string owned by the model.
 **/
const gchar*
gwy_graph_model_get_axis_label(GwyGraphModel *gmodel,
                               GtkPositionType pos)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), NULL);

    GwyGraphModelPrivate *priv = gmodel->priv;
    if (pos == GTK_POS_BOTTOM)
        return priv->bottom_label;
    if (pos == GTK_POS_LEFT)
        return priv->left_label;
    if (pos == GTK_POS_RIGHT)
        return priv->right_label;
    if (pos == GTK_POS_TOP)
        return priv->top_label;

    g_return_val_if_reached(NULL);
}

/**
 * gwy_graph_model_get_title:
 * @gmodel: A graph model.
 *
 * Gets the graph model title.
 *
 * Returns: (nullable): The title as a string owned by the model.
 **/
const gchar*
gwy_graph_model_get_title(GwyGraphModel *gmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), NULL);
    return gmodel->priv->title;
}

/**
 * gwy_graph_model_set_axis_label:
 * @gmodel: A graph model.
 * @pos: Axis position.
 * @label: (nullable): The new label.
 *
 * Sets one axis label of a graph model.
 **/
void
gwy_graph_model_set_axis_label(GwyGraphModel *gmodel,
                               GtkPositionType pos,
                               const gchar *label)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(gmodel));

    GwyGraphModelPrivate *priv = gmodel->priv;
    if (pos == GTK_POS_BOTTOM) {
        if (g_strcmp0(priv->bottom_label, label)) {
            gwy_assign_string(&priv->bottom_label, label);
            g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_AXIS_LABEL_BOTTOM]);
        }
    }
    else if (pos == GTK_POS_LEFT) {
        if (g_strcmp0(priv->left_label, label)) {
            gwy_assign_string(&priv->left_label, label);
            g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_AXIS_LABEL_LEFT]);
        }
    }
    else if (pos == GTK_POS_RIGHT) {
        if (g_strcmp0(priv->right_label, label)) {
            gwy_assign_string(&priv->right_label, label);
            g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_AXIS_LABEL_RIGHT]);
        }
    }
    else if (pos == GTK_POS_TOP) {
        if (g_strcmp0(priv->top_label, label)) {
            gwy_assign_string(&priv->top_label, label);
            g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_AXIS_LABEL_TOP]);
        }
    }
    else {
        g_return_if_reached();
    }
}

/**
 * gwy_graph_model_get_x_range:
 * @gmodel: A graph model.
 * @x_min: Location to store the minimum abscissa value, or %NULL.
 * @x_max: Location to store the maximum abscissa value, or %NULL.
 *
 * Gets the abscissa range of a graph.
 *
 * Explicitly set minimum and maximum range properties take precedence over values calculated from curve abscissa
 * ranges.
 *
 * Returns: %TRUE if the requested values were filled, %FALSE is there are no data points and the ranges are not
 *          explicitly set.
 **/
gboolean
gwy_graph_model_get_x_range(GwyGraphModel *gmodel,
                            gdouble *x_min,
                            gdouble *x_max)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), FALSE);

    GwyGraphModelPrivate *priv = gmodel->priv;

    gdouble xmin = G_MAXDOUBLE;
    gdouble xmax = -G_MAXDOUBLE;
    gboolean xmin_ok = FALSE, xmax_ok = FALSE;
    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *gcmodel = g_ptr_array_index(priv->curves, i);
        gdouble cmin, cmax;
        if (gwy_graph_curve_model_get_x_range(gcmodel, &cmin, &cmax)) {
            xmin_ok = xmax_ok = TRUE;
            xmin = fmin(cmin, xmin);
            xmax = fmax(cmax, xmax);
        }
    }

    if (priv->x_min_set) {
        xmin = priv->x_min;
        xmin_ok = TRUE;
    }
    if (priv->x_max_set) {
        xmax = priv->x_max;
        xmax_ok = TRUE;
    }

    if (x_min && xmin_ok)
        *x_min = xmin;
    if (x_max && xmax_ok)
        *x_max = xmax;

    return (xmin_ok || !x_min) && (xmax_ok || !x_max);
}

/**
 * gwy_graph_model_get_y_range:
 * @gmodel: A graph model.
 * @y_min: Location to store the minimum ordinate value, or %NULL.
 * @y_max: Location to store the maximum ordinate value, or %NULL.
 *
 * Gets the ordinate range of a graph.
 *
 * Explicitly set minimum and maximum range properties take precedence over values calculated from curve ordinate
 * ranges.
 *
 * Returns: %TRUE if the requested values were filled, %FALSE is there are no data points and the ranges are not
 *          explicitly set.
 **/
gboolean
gwy_graph_model_get_y_range(GwyGraphModel *gmodel,
                            gdouble *y_min,
                            gdouble *y_max)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), FALSE);

    GwyGraphModelPrivate *priv = gmodel->priv;

    gdouble ymin = G_MAXDOUBLE;
    gdouble ymax = -G_MAXDOUBLE;
    gboolean ymin_ok = FALSE, ymax_ok = FALSE;
    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *gcmodel = g_ptr_array_index(priv->curves, i);
        gdouble cmin, cmax;
        if (gwy_graph_curve_model_get_y_range(gcmodel, &cmin, &cmax)) {
            ymin_ok = ymax_ok = TRUE;
            ymin = fmin(cmin, ymin);
            ymax = fmax(cmax, ymax);
        }
    }

    if (priv->x_min_set) {
        ymin = priv->x_min;
        ymin_ok = TRUE;
    }
    if (priv->x_max_set) {
        ymax = priv->x_max;
        ymax_ok = TRUE;
    }

    if (y_min && ymin_ok)
        *y_min = ymin;
    if (y_max && ymax_ok)
        *y_max = ymax;

    return (ymin_ok || !y_min) && (ymax_ok || !y_max);
}

/**
 * gwy_graph_model_get_ranges:
 * @gmodel: A graph model.
 * @x_logscale: %TRUE if logarithmical scale is intended for the abscissa.
 * @y_logscale: %TRUE if logarithmical scale is intended for the ordinate.
 * @x_min: Location to store the minimum abscissa value, or %NULL.
 * @x_max: Location to store the maximum abscissa value, or %NULL.
 * @y_min: Location to store the minimum ordinate value, or %NULL.
 * @y_max: Location to store the maximum ordinate value, or %NULL.
 *
 * Gets the log-scale suitable range minima of a graph curve.
 *
 * See gwy_graph_curve_model_get_ranges() for discussion.
 *
 * Returns: %TRUE if all requested output arguments were filled with the ranges.
 **/
gboolean
gwy_graph_model_get_ranges(GwyGraphModel *gmodel,
                           gboolean x_logscale,
                           gboolean y_logscale,
                           gdouble *x_min,
                           gdouble *x_max,
                           gdouble *y_min,
                           gdouble *y_max)
{
    enum { XMIN = 1, XMAX = 2, YMIN = 4, YMAX = 8, ALL = 15 };

    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), FALSE);

    GwyGraphModelPrivate *priv = gmodel->priv;

    guint req = ((x_min ? XMIN : 0) | (x_max ? XMAX : 0) | (y_min ? YMIN : 0) | (y_max ? YMAX : 0));
    if (!req)
        return TRUE;

    guint ok = 0;
    gdouble xmin = G_MAXDOUBLE, ymin = G_MAXDOUBLE;
    gdouble xmax = -G_MAXDOUBLE, ymax = -G_MAXDOUBLE;
    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *gcmodel = g_ptr_array_index(priv->curves, i);
        gdouble cxmin, cymin, cxmax, cymax;
        if (gcmodel->priv->mode != GWY_GRAPH_CURVE_HIDDEN
            && gwy_graph_curve_model_get_ranges(gcmodel, x_logscale, y_logscale, &cxmin, &cxmax, &cymin, &cymax)) {
            ok |= ALL;
            xmin = MIN(cxmin, xmin);
            xmax = MAX(cxmax, xmax);
            ymin = MIN(cymin, ymin);
            ymax = MAX(cymax, ymax);
        }
    }

    if (priv->x_min_set && (!x_logscale || priv->x_min > 0.0)) {
        xmin = priv->x_min;
        ok |= XMIN;
    }
    if (priv->x_max_set && (!x_logscale || priv->x_max > 0.0)) {
        xmax = priv->x_max;
        ok |= XMAX;
    }
    if (priv->y_min_set && (!y_logscale || fabs(priv->y_min) > 0.0)) {
        if (y_logscale)
            ymin = fabs(priv->y_min);
        else
            ymin = priv->y_min;
        ok |= YMIN;
    }
    if (priv->y_max_set && (!y_logscale || priv->y_max > 0.0)) {
        ymax = priv->y_max;
        ok |= YMIN;
    }

    if (x_min && (ok & XMIN))
        *x_min = xmin;
    if (x_max && (ok & XMAX))
        *x_max = xmax;
    if (y_min && (ok & YMIN))
        *y_min = ymin;
    if (y_max && (ok & YMAX))
        *y_max = ymax;

    return (req & ok) == req;
}

static inline void
append_number(GString *str,
              gdouble value,
              gboolean posix_format)
{
    if (posix_format) {
        gchar buf[48];
        g_ascii_formatd(buf, sizeof(buf), "%.8g", value);
        g_string_append(str, buf);
    }
    else
        g_string_append_printf(str, "%.8g", value);
}

/**
 * gwy_graph_model_export_ascii:
 * @model: A graph model.
 * @export_units: %TRUE to export units in the column header.
 * @export_labels: %TRUE to export labels in the column header.
 * @export_metadata: %TRUE to export all graph metadata within file header.
 * @export_style: File format subtype to export to (e. g. plain, csv, gnuplot, etc.).
 * @string: A string to append the text dump to, or %NULL to allocate a new string.
 *
 * Exports a graph model data to a string.
 *
 * The export format is specified by parameter @export_style.
 *
 * Returns: Either @string itself if it was not %NULL, or a newly allocated #GString.
 **/
GString*
gwy_graph_model_export_ascii(GwyGraphModel *gmodel,
                             gboolean export_units,
                             gboolean export_labels,
                             gboolean export_metadata,
                             GwyGraphModelExportStyle export_style,
                             GString* string)
{
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), string);

    GwyGraphModelPrivate *priv = gmodel->priv;

    GwyGraphCurveModel *cmodel;
    GString *labels, *descriptions, *units;
    gint i, j, max, ndata;
    gboolean posix_format = export_style & GWY_GRAPH_MODEL_EXPORT_ASCII_POSIX;
    gboolean merged_x = export_style & GWY_GRAPH_MODEL_EXPORT_ASCII_MERGED;
    gchar *xname = NULL, *yname = NULL, *xunitstr = NULL, *yunitstr = NULL;

    if (!string)
        string = g_string_new(NULL);

    export_style &= ~(GWY_GRAPH_MODEL_EXPORT_ASCII_POSIX | GWY_GRAPH_MODEL_EXPORT_ASCII_MERGED);
    if (export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_IGORPRO) {
        if (merged_x) {
            g_warning("Merged abscissae is not available for IGOR PRO style.");
            merged_x = FALSE;
        }
        export_units = TRUE;
        posix_format = TRUE;
    }

    if (merged_x) {
        export_with_merged_abscissae(gmodel, export_style, posix_format,
                                     export_units, export_labels,
                                     export_metadata,
                                     string);
        return string;
    }

    if (export_units) {
        xunitstr = gwy_unit_get_string(priv->x_unit, GWY_UNIT_FORMAT_MARKUP);
        yunitstr = gwy_unit_get_string(priv->y_unit, GWY_UNIT_FORMAT_MARKUP);
    }

    const gchar *xlabel = priv->bottom_label ? priv->bottom_label : "";
    const gchar *ylabel = priv->left_label ? priv->left_label : "";

    /* FIXME: export_with_merged_abscissae() is already much cleaner; try to avoid the repeated mess also here. */
    if (export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_PLAIN || export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_ORIGIN) {
        labels = g_string_new(NULL);
        descriptions = g_string_new(NULL);
        units = g_string_new(NULL);
        for (i = 0; i < priv->curves->len; i++) {
            cmodel = g_ptr_array_index(priv->curves, i);
            if (export_metadata) {
                g_string_append_printf(descriptions, "%s             ", cmodel->priv->description->str);
            }
            if (export_labels) {
                g_string_append_printf(labels, "%s       %s           ", xlabel, ylabel);
            }
            if (export_units) {
                g_string_append_printf(units, "[%s]     [%s]         ", xunitstr, yunitstr);
            }
        }
        if (export_metadata)
            g_string_append_printf(string, "%s\n", descriptions->str);
        if (export_labels)
            g_string_append_printf(string, "%s\n", labels->str);
        if (export_units)
            g_string_append_printf(string, "%s\n", units->str);
        g_string_free(descriptions, TRUE);
        g_string_free(labels, TRUE);
        g_string_free(units, TRUE);

        max = 0;
        for (i = 0; i < priv->curves->len; i++) {
            cmodel = g_ptr_array_index(priv->curves, i);
            if ((ndata = gwy_graph_curve_model_get_n_data(cmodel)) > max)
                max = ndata;
        }

        for (j = 0; j < max; j++) {
            for (i = 0; i < priv->curves->len; i++) {
                cmodel = g_ptr_array_index(priv->curves, i);
                if (gwy_graph_curve_model_get_n_data(cmodel) > j) {
                    append_number(string, cmodel->priv->xdata[j], posix_format);
                    g_string_append(string, "  ");
                    append_number(string, cmodel->priv->ydata[j], posix_format);
                    g_string_append(string, "            ");
                }
                else
                    g_string_append(string, "-          -              ");
            }
            g_string_append_c(string, '\n');
        }
    }
    else if (export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_GNUPLOT) {
        for (i = 0; i < priv->curves->len; i++) {
            cmodel = g_ptr_array_index(priv->curves, i);
            if (export_metadata)
                g_string_append_printf(string, "# %s\n", cmodel->priv->description->str);
            if (export_labels)
                g_string_append_printf(string, "# %s      %s\n", xlabel, ylabel);
            if (export_units)
                g_string_append_printf(string, "# [%s]    [%s]\n", xunitstr, yunitstr);
            for (j = 0; j < cmodel->priv->n; j++) {
                append_number(string, cmodel->priv->xdata[j], posix_format);
                g_string_append(string, "   ");
                append_number(string, cmodel->priv->ydata[j], posix_format);
                g_string_append_c(string, '\n');
            }
            g_string_append(string, "\n\n");
        }
    }
    else if (export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_CSV) {
        labels = g_string_new(NULL);
        descriptions = g_string_new(NULL);
        units = g_string_new(NULL);
        for (i = 0; i < priv->curves->len; i++) {
            cmodel = g_ptr_array_index(priv->curves, i);
            if (export_metadata) {
                g_string_append_printf(descriptions, "%s;%s;",
                                       cmodel->priv->description->str, cmodel->priv->description->str);
            }
            if (export_labels)
                g_string_append_printf(labels, "%s;%s;", xlabel, ylabel);
            if (export_units)
                g_string_append_printf(units, "[%s];[%s];", xunitstr, yunitstr);
        }
        if (export_metadata)
            g_string_append_printf(string, "%s\n", descriptions->str);
        if (export_labels)
            g_string_append_printf(string, "%s\n", labels->str);
        if (export_units)
            g_string_append_printf(string, "%s\n", units->str);
        g_string_free(descriptions, TRUE);
        g_string_free(labels, TRUE);
        g_string_free(units, TRUE);

        max = 0;
        for (i = 0; i < priv->curves->len; i++) {
            cmodel = g_ptr_array_index(priv->curves, i);
            if ((ndata = gwy_graph_curve_model_get_n_data(cmodel)) > max)
                max = ndata;
        }

        for (j = 0; j < max; j++) {
            for (i = 0; i < priv->curves->len; i++) {
                cmodel = g_ptr_array_index(priv->curves, i);
                if (gwy_graph_curve_model_get_n_data(cmodel) > j) {
                    append_number(string, cmodel->priv->xdata[j], posix_format);
                    g_string_append_c(string, ';');
                    append_number(string, cmodel->priv->ydata[j], posix_format);
                    g_string_append_c(string, ';');
                }
                else
                    g_string_append(string, ";;");
            }
            g_string_append_c(string, '\n');
        }
    }
    else if (export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_IGORPRO) {
        xname = ascii_name(xlabel);
        if (!xname)
            xname = g_strdup_printf("x");

        yname = ascii_name(ylabel);
        if (!yname)
            yname = g_strdup_printf("y");

        g_string_append(string, "IGOR\n");
        for (i = 0; i < priv->curves->len; i++) {
            cmodel = g_ptr_array_index(priv->curves, i);
            if (curve_is_equispaced(cmodel)) {
                g_string_append_printf(string, "WAVES/D %s%d\n", yname, i+1);
                g_string_append(string, "BEGIN\n");
                for (j = 0; j < cmodel->priv->n; j++) {
                    append_number(string, cmodel->priv->ydata[j], posix_format);
                    g_string_append_c(string, '\n');
                }
                g_string_append(string, "END\n");
                g_string_append(string, "X SetScale/I x ");
                append_number(string, cmodel->priv->xdata[0], posix_format);
                g_string_append_c(string, ',');
                append_number(string, cmodel->priv->xdata[cmodel->priv->n-1], posix_format);
                g_string_append_printf(string, ",\"%s\", %s%d\n", xunitstr, yname, i+1);
            }
            else {
                g_string_append_printf(string, "WAVES/D %s%d %s%d\n", xname, i+1, yname, i+1);
                g_string_append(string, "BEGIN\n");
                for (j = 0; j < cmodel->priv->n; j++) {
                    append_number(string, cmodel->priv->xdata[j], posix_format);
                    g_string_append_c(string, ' ');
                    append_number(string, cmodel->priv->ydata[j], posix_format);
                    g_string_append_c(string, '\n');
                }
                g_string_append(string, "END\n");
                g_string_append_printf(string, "X SetScale d,0,0,\"%s\", %s%d\n", xunitstr, xname, i+1);
            }
            g_string_append_printf(string, "X SetScale d,0,0,\"%s\", %s%d\n", yunitstr, yname, i+1);
            g_string_append_c(string, '\n');
        }
    }
    else {
        g_return_val_if_reached(string);
    }

    g_free(xunitstr);
    g_free(yunitstr);
    g_free(xname);
    g_free(yname);

    return string;
}

static void
export_with_merged_abscissae(const GwyGraphModel *gmodel,
                             GwyGraphModelExportStyle export_style,
                             gboolean posix_format,
                             gboolean export_units,
                             gboolean export_labels,
                             gboolean export_metadata,
                             GString *string)
{
    GwyGraphModelPrivate *priv = gmodel->priv;
    GwyGraphCurveModel *gcmodel, **gcmodels;
    gdouble *mergedxdata;
    const gdouble **xdata, **ydata;
    guint n, ncurves, i, k, *j, *ndata;
    gchar sep = '\t';
    const gchar *nodata = "---";
    const gchar *eol = "\n";
    const gchar *comment = "";

    if (export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_CSV) {
        sep = ';';
        eol = ";\n";
        nodata = "";
    }
    if (export_style == GWY_GRAPH_MODEL_EXPORT_ASCII_GNUPLOT) {
        comment = "# ";
    }

    const gchar *xlabel = priv->bottom_label ? priv->bottom_label : "";
    const gchar *ylabel = priv->left_label ? priv->left_label : "";

    ncurves = priv->curves->len;
    if (export_metadata) {
        g_string_append(string, comment);
        g_string_append(string, _("Abscissa"));
        for (i = 0; i < ncurves; i++) {
            gcmodel = g_ptr_array_index(priv->curves, i);
            g_string_append_c(string, sep);
            g_string_append(string, gcmodel->priv->description->str);
        }
        g_string_append(string, eol);
    }

    if (export_labels) {
        g_string_append(string, comment);
        g_string_append(string, xlabel);
        for (i = 0; i < ncurves; i++) {
            gcmodel = g_ptr_array_index(priv->curves, i);
            g_string_append_c(string, sep);
            g_string_append(string, ylabel);
        }
        g_string_append(string, eol);
    }

    if (export_units) {
        gchar *xunitstr = gwy_unit_get_string(priv->x_unit, GWY_UNIT_FORMAT_MARKUP);
        gchar *yunitstr = gwy_unit_get_string(priv->y_unit, GWY_UNIT_FORMAT_MARKUP);

        g_string_append(string, comment);
        g_string_append_printf(string, "[%s]", yunitstr);
        for (i = 0; i < ncurves; i++) {
            gcmodel = g_ptr_array_index(priv->curves, i);
            g_string_append_c(string, sep);
            g_string_append_printf(string, "[%s]", yunitstr);
        }
        g_string_append(string, eol);

        g_free(xunitstr);
        g_free(yunitstr);
    }

    mergedxdata = merge_abscissae(gmodel, &n);
    if (!mergedxdata)
        return;

    xdata = g_new(const gdouble*, ncurves);
    ydata = g_new(const gdouble*, ncurves);
    ndata = g_new(guint, ncurves);
    gcmodels = g_new0(GwyGraphCurveModel*, ncurves);
    for (i = 0; i < ncurves; i++) {
        gcmodel = g_ptr_array_index(priv->curves, i);
        if (!gwy_graph_curve_model_is_ordered(gcmodel)) {
            gcmodels[i] = gwy_graph_curve_model_copy(gcmodel);
            gcmodel = gcmodels[i];
            gwy_graph_curve_model_enforce_order(gcmodel);
        }
        ndata[i] = gwy_graph_curve_model_get_n_data(gcmodel);
        xdata[i] = gwy_graph_curve_model_get_xdata(gcmodel);
        ydata[i] = gwy_graph_curve_model_get_ydata(gcmodel);
    }

    j = g_new0(guint, ncurves);
    for (k = 0; k < n; k++) {
        append_number(string, mergedxdata[k], posix_format);
        for (i = 0; i < ncurves; i++) {
            g_string_append_c(string, sep);
            if (j[i] >= ndata[i] || mergedxdata[k] < xdata[i][j[i]])
                g_string_append(string, nodata);
            else {
                append_number(string, ydata[i][j[i]], posix_format);
                j[i]++;
            }
        }
        g_string_append(string, eol);
    }

    g_free(j);
    g_free(xdata);
    g_free(ydata);
    g_free(ndata);
    g_free(mergedxdata);
    for (i = 0; i < ncurves; i++)
        g_clear_object(gcmodels + i);
    g_free(gcmodels);
}

static gdouble*
merge_abscissae(const GwyGraphModel *gmodel,
                guint *ndata)
{
    GwyGraphModelPrivate *priv = gmodel->priv;
    GwyGraphCurveModel *gcmodel;
    gdouble *xdata;
    guint n, i, j;
    guint ncurves = priv->curves->len;

    n = 0;
    for (i = 0; i < ncurves; i++) {
        gcmodel = g_ptr_array_index(priv->curves, i);
        n += gwy_graph_curve_model_get_n_data(gcmodel);
    }

    *ndata = n;
    if (!n)
        return NULL;

    xdata = g_new(gdouble, n);
    n = 0;
    for (i = 0; i < ncurves; i++) {
        gcmodel = g_ptr_array_index(priv->curves, i);
        j = gwy_graph_curve_model_get_n_data(gcmodel);
        gwy_assign(xdata + n, gwy_graph_curve_model_get_xdata(gcmodel), j);
        n += j;
    }

    g_assert(n == *ndata);

    gwy_math_sort(xdata, n);
    for (i = 1, j = 0; i < n; i++) {
        if (xdata[i] != xdata[j]) {
            j++;
            xdata[j] = xdata[i];
        }
    }

    *ndata = j+1;

    return xdata;
}

static gboolean
curve_is_equispaced(const GwyGraphCurveModel *gcmodel)
{
    const gdouble *xdata = gcmodel->priv->xdata;
    gdouble step, eps;
    gint i, n = gcmodel->priv->n;

    if (n < 3)
        return TRUE;

    step = (xdata[n-1] - xdata[0])/(n - 1);
    eps = 1e-9*fabs(step);
    for (i = 1; i < n-1; i++) {
        if (fabs(xdata[i] - xdata[0] - i*step) > eps)
            return FALSE;
    }

    return TRUE;
}

static gchar*
ascii_name(const gchar *s)
{
    guint i, start, n = 0;
    gchar *as;

    for (i = 0; s[i]; i++) {
        if (g_ascii_isalpha(s[i]))
            break;
    }

    start = i;
    while (s[i]) {
        if (g_ascii_isalnum(s[i]))
            n++;
        i++;
    }

    if (!n)
        return NULL;

    as = g_new(gchar, n+1);

    i = start;
    n = 0;
    while (s[i]) {
        if (g_ascii_isalnum(s[i]))
            as[n++] = s[i];
        i++;
    }

    as[n] = '\0';
    return as;
}

static void
take_curve(GwyGraphModel *gmodel,
           GwyGraphCurveModel *curve,
           gint i)
{
    GwyGraphModelPrivate *priv = gmodel->priv;
    GPtrArray *curves = priv->curves;
    gboolean appending = FALSE;

    g_object_ref(curve);
    if (i == curves->len)
        appending = TRUE;

    if (appending)
        g_ptr_array_add(curves, curve);
    else {
        g_assert(g_ptr_array_index(curves, i) == NULL);
        g_ptr_array_index(curves, i) = curve;
    }

    GwyGraphModelCurveAux aux = {
        .data_changed_id = g_signal_connect(curve, "data-changed", G_CALLBACK(curve_data_changed), gmodel),
        .notify_id = g_signal_connect(curve, "notify", G_CALLBACK(curve_notify), gmodel),
    };

    if (appending)
        g_array_append_val(priv->curveaux, aux);
    else
        g_array_index(priv->curveaux, GwyGraphModelCurveAux, i) = aux;
}

static void
release_curve(GwyGraphModel *gmodel,
              guint i)
{
    GwyGraphModelPrivate *priv = gmodel->priv;
    GwyGraphCurveModel *cmodel = g_ptr_array_index(priv->curves, i);
    GwyGraphModelCurveAux *aux = &g_array_index(priv->curveaux, GwyGraphModelCurveAux, i);

    g_signal_handler_disconnect(cmodel, aux->data_changed_id);
    g_signal_handler_disconnect(cmodel, aux->notify_id);
    g_object_unref(cmodel);

    g_ptr_array_index(priv->curves, i) = NULL;
}

static void
curve_data_changed(GwyGraphCurveModel *cmodel,
                   GwyGraphModel *gmodel)
{
    gint i;

    /* FIXME: This scales bad although for a reasonable number of curves it's quite fast. */
    i = gwy_graph_model_get_curve_index(gmodel, cmodel);
    g_return_if_fail(i > -1);
    g_signal_emit(gmodel, signals[SGNL_CURVE_DATA_CHANGED], 0, i);
}

static void
curve_notify(GwyGraphCurveModel *cmodel,
             GParamSpec *pspec,
             GwyGraphModel *gmodel)
{
    gint i;

    /* FIXME: This scales bad although for a reasonable number of curves it's quite fast. */
    i = gwy_graph_model_get_curve_index(gmodel, cmodel);
    g_return_if_fail(i > -1);
    g_signal_emit(gmodel, signals[SGNL_CURVE_NOTIFY], 0, i, pspec);
}

static void
x_unit_changed(GwyGraphModel *gmodel,
               G_GNUC_UNUSED GwyUnit *unit)
{
    g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_UNIT_X]);
}

static void
y_unit_changed(GwyGraphModel *gmodel,
               G_GNUC_UNUSED GwyUnit *unit)
{
    g_object_notify_by_pspec(G_OBJECT(gmodel), properties[PROP_UNIT_Y]);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyGraphModel *gmodel = GWY_GRAPH_MODEL(serializable);
    GwyGraphModelPrivate *priv = gmodel->priv;

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_TITLE, priv->title);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_X_MIN, priv->x_min);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_X_MIN_SET, priv->x_min_set);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_X_MAX, priv->x_max);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_X_MAX_SET, priv->x_max_set);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_Y_MIN, priv->y_min);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_Y_MIN_SET, priv->y_min_set);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_Y_MAX, priv->y_max);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_Y_MAX_SET, priv->y_max_set);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_BOTTOM_LABEL, priv->bottom_label);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_LEFT_LABEL, priv->left_label);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_TOP_LABEL, priv->top_label);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_RIGHT_LABEL, priv->right_label);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_X_IS_LOGARITHMIC, priv->x_is_logarithmic);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_Y_IS_LOGARITHMIC, priv->y_is_logarithmic);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_X_UNIT, priv->x_unit);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_Y_UNIT, priv->y_unit);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_LABEL_HAS_FRAME, priv->label_has_frame);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_LABEL_FRAME_THICKNESS,
                                        priv->label_frame_thickness);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_LABEL_REVERSE, priv->label_reverse);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_LABEL_VISIBLE, priv->label_visible);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_LABEL_POSITION, priv->label_position);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_LABEL_RELATIVE_X, priv->label_xy.x);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_LABEL_RELATIVE_Y, priv->label_xy.y);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_GRID_TYPE, priv->grid_type);
    gwy_serializable_group_append_object_array(group, serializable_items + ITEM_CURVES,
                                               (GObject**)priv->curves->pdata, priv->curves->len);
    gwy_serializable_group_itemize(group);
}

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

    GwyGraphModel *gmodel = GWY_GRAPH_MODEL(serializable);
    GwyGraphModelPrivate *priv = gmodel->priv;

    /* Nothing can hard-fail here. */
    it = its + ITEM_CURVES;
    for (gsize i = 0; i < it->array_size; i++) {
        take_curve(gmodel, GWY_GRAPH_CURVE_MODEL(it->value.v_object_array[i]), i);
        g_object_unref(it->value.v_object_array[i]);
    }

    GWY_SWAP(gchar*, priv->bottom_label, its[ITEM_BOTTOM_LABEL].value.v_string);
    GWY_SWAP(gchar*, priv->left_label, its[ITEM_LEFT_LABEL].value.v_string);
    GWY_SWAP(gchar*, priv->top_label, its[ITEM_TOP_LABEL].value.v_string);
    GWY_SWAP(gchar*, priv->right_label, its[ITEM_RIGHT_LABEL].value.v_string);
    GWY_SWAP(gchar*, priv->title, its[ITEM_TITLE].value.v_string);

    /* FIXME: Are we OK with getting xmax < xmin? Or should we sanitise further? */
    priv->x_min = its[ITEM_X_MIN].value.v_double;
    priv->x_max = its[ITEM_X_MAX].value.v_double;
    priv->y_min = its[ITEM_Y_MIN].value.v_double;
    priv->y_max = its[ITEM_Y_MAX].value.v_double;
    priv->x_min_set = its[ITEM_X_MIN_SET].value.v_boolean;
    priv->x_max_set = its[ITEM_X_MAX_SET].value.v_boolean;
    priv->y_min_set = its[ITEM_Y_MIN_SET].value.v_boolean;
    priv->y_max_set = its[ITEM_Y_MAX_SET].value.v_boolean;
    priv->x_is_logarithmic = its[ITEM_X_IS_LOGARITHMIC].value.v_boolean;
    priv->y_is_logarithmic = its[ITEM_Y_IS_LOGARITHMIC].value.v_boolean;

    it = its + ITEM_X_UNIT;
    if (it->value.v_object) {
        gwy_unit_assign(priv->x_unit, GWY_UNIT(it->value.v_object));
        g_clear_object(&it->value.v_object);
    }

    it = its + ITEM_Y_UNIT;
    if (it->value.v_object) {
        gwy_unit_assign(priv->y_unit, GWY_UNIT(it->value.v_object));
        g_clear_object(&it->value.v_object);
    }

    priv->label_has_frame = its[ITEM_LABEL_HAS_FRAME].value.v_boolean;
    priv->label_frame_thickness = its[ITEM_LABEL_FRAME_THICKNESS].value.v_int32;
    priv->label_reverse = its[ITEM_LABEL_REVERSE].value.v_boolean;
    priv->label_visible = its[ITEM_LABEL_VISIBLE].value.v_boolean;
    priv->label_position = its[ITEM_LABEL_POSITION].value.v_int32;
    priv->label_xy.x = its[ITEM_LABEL_RELATIVE_X].value.v_double;
    priv->label_xy.y = its[ITEM_LABEL_RELATIVE_Y].value.v_double;
    priv->grid_type = its[ITEM_GRID_TYPE].value.v_int32;

    g_free(its[ITEM_CURVES].value.v_object_array);
    g_free(its[ITEM_TITLE].value.v_string);
    g_free(its[ITEM_BOTTOM_LABEL].value.v_string);
    g_free(its[ITEM_LEFT_LABEL].value.v_string);
    g_free(its[ITEM_RIGHT_LABEL].value.v_string);
    g_free(its[ITEM_TOP_LABEL].value.v_string);

    return TRUE;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyGraphModel *gmodel = GWY_GRAPH_MODEL(serializable);
    GwyGraphModel *copy = gwy_graph_model_new_alike(gmodel);
    GwyGraphModelPrivate *priv = gmodel->priv;
    guint n = priv->curves->len;
    for (guint i = 0; i < n; i++) {
        GwyGraphCurveModel *gcmodel = gwy_graph_curve_model_copy(g_ptr_array_index(priv->curves, i));
        take_curve(copy, gcmodel, i);
        g_object_unref(gcmodel);
    }
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyGraphModel *destgmodel = GWY_GRAPH_MODEL(destination), *srcgmodel = GWY_GRAPH_MODEL(source);
    GwyGraphModelPrivate *dpriv = destgmodel->priv, *spriv = srcgmodel->priv;

    GObject *object = G_OBJECT(destination);
    g_object_freeze_notify(object);

    if (g_strcmp0(dpriv->title, spriv->title)) {
        gwy_assign_string(&dpriv->title, spriv->title);
        g_object_notify_by_pspec(object, properties[PROP_TITLE]);
    }

    if (dpriv->x_min != spriv->x_min) {
        dpriv->x_min = spriv->x_min;
        g_object_notify_by_pspec(object, properties[PROP_X_MIN]);
    }

    if (dpriv->x_max != spriv->x_max) {
        dpriv->x_max = spriv->x_max;
        g_object_notify_by_pspec(object, properties[PROP_X_MAX]);
    }

    if (dpriv->y_min != spriv->y_min) {
        dpriv->y_min = spriv->y_min;
        g_object_notify_by_pspec(object, properties[PROP_Y_MIN]);
    }

    if (dpriv->y_max != spriv->y_max) {
        dpriv->y_max = spriv->y_max;
        g_object_notify_by_pspec(object, properties[PROP_Y_MAX]);
    }

    if (dpriv->x_min_set != spriv->x_min_set) {
        dpriv->x_min_set = spriv->x_min_set;
        g_object_notify_by_pspec(object, properties[PROP_X_MIN_SET]);
    }

    if (dpriv->x_max_set != spriv->x_max_set) {
        dpriv->x_max_set = spriv->x_max_set;
        g_object_notify_by_pspec(object, properties[PROP_X_MAX_SET]);
    }

    if (dpriv->y_min_set != spriv->y_min_set) {
        dpriv->y_min_set = spriv->y_min_set;
        g_object_notify_by_pspec(object, properties[PROP_Y_MIN_SET]);
    }

    if (dpriv->y_max_set != spriv->y_max_set) {
        dpriv->y_max_set = spriv->y_max_set;
        g_object_notify_by_pspec(object, properties[PROP_Y_MAX_SET]);
    }

    if (g_strcmp0(dpriv->bottom_label, spriv->bottom_label)) {
        gwy_assign_string(&dpriv->bottom_label, spriv->bottom_label);
        g_object_notify_by_pspec(object, properties[PROP_AXIS_LABEL_BOTTOM]);
    }

    if (g_strcmp0(dpriv->left_label, spriv->left_label)) {
        gwy_assign_string(&dpriv->left_label, spriv->left_label);
        g_object_notify_by_pspec(object, properties[PROP_AXIS_LABEL_LEFT]);
    }

    if (g_strcmp0(dpriv->top_label, spriv->top_label)) {
        gwy_assign_string(&dpriv->top_label, spriv->top_label);
        g_object_notify_by_pspec(object, properties[PROP_AXIS_LABEL_TOP]);
    }

    if (g_strcmp0(dpriv->right_label, spriv->right_label)) {
        gwy_assign_string(&dpriv->right_label, spriv->right_label);
        g_object_notify_by_pspec(object, properties[PROP_AXIS_LABEL_RIGHT]);
    }

    if (!gwy_unit_equal(dpriv->x_unit, spriv->x_unit)) {
        gwy_unit_assign(dpriv->x_unit, spriv->x_unit);
        g_object_notify_by_pspec(object, properties[PROP_UNIT_X]);
    }

    if (!gwy_unit_equal(dpriv->y_unit, spriv->y_unit)) {
        gwy_unit_assign(dpriv->y_unit, spriv->y_unit);
        g_object_notify_by_pspec(object, properties[PROP_UNIT_Y]);
    }

    if (dpriv->x_is_logarithmic != spriv->x_is_logarithmic) {
        dpriv->x_is_logarithmic = spriv->x_is_logarithmic;
        g_object_notify_by_pspec(object, properties[PROP_X_LOGARITHMIC]);
    }

    if (dpriv->y_is_logarithmic != spriv->y_is_logarithmic) {
        dpriv->y_is_logarithmic = spriv->y_is_logarithmic;
        g_object_notify_by_pspec(object, properties[PROP_Y_LOGARITHMIC]);
    }

    if (dpriv->label_frame_thickness != spriv->label_frame_thickness) {
        dpriv->label_frame_thickness = spriv->label_frame_thickness;
        g_object_notify_by_pspec(object, properties[PROP_LABEL_FRAME_THICKNESS]);
    }

    if (dpriv->label_visible != spriv->label_visible) {
        dpriv->label_visible = spriv->label_visible;
        g_object_notify_by_pspec(object, properties[PROP_LABEL_VISIBLE]);
    }

    if (dpriv->label_reverse != spriv->label_reverse) {
        dpriv->label_reverse = spriv->label_reverse;
        g_object_notify_by_pspec(object, properties[PROP_LABEL_REVERSE]);
    }

    if (dpriv->label_has_frame != spriv->label_has_frame) {
        dpriv->label_has_frame = spriv->label_has_frame;
        g_object_notify_by_pspec(object, properties[PROP_LABEL_HAS_FRAME]);
    }

    if (dpriv->label_position != spriv->label_position) {
        dpriv->label_position = spriv->label_position;
        g_object_notify_by_pspec(object, properties[PROP_LABEL_POSITION]);
    }

    if (dpriv->grid_type != spriv->grid_type) {
        dpriv->grid_type = spriv->grid_type;
        g_object_notify_by_pspec(object, properties[PROP_GRID_TYPE]);
    }

    if (dpriv->label_xy.x != spriv->label_xy.x) {
        dpriv->label_xy.x = spriv->label_xy.x;
        g_object_notify_by_pspec(object, properties[PROP_LABEL_RELATIVE_X]);
    }

    if (dpriv->label_xy.y != spriv->label_xy.y) {
        dpriv->label_xy.y = spriv->label_xy.y;
        g_object_notify_by_pspec(object, properties[PROP_LABEL_RELATIVE_Y]);
    }

    guint sncurves = spriv->curves->len, dncurves = dpriv->curves->len;
    while (dncurves > sncurves) {
        gwy_graph_model_remove_curve(destgmodel, dncurves-1);
        dncurves--;
    }
    while (dncurves < sncurves) {
        GwyGraphCurveModel *gcmodel = gwy_graph_curve_model_new();
        gwy_graph_model_add_curve(destgmodel, gcmodel);
        g_object_unref(gcmodel);
        dncurves++;
    }
    for (guint i = 0; i < dncurves; i++) {
        gwy_graph_curve_model_assign(g_ptr_array_index(dpriv->curves, i), g_ptr_array_index(spriv->curves, i));
    }

    g_object_thaw_notify(object);
    for (guint i = 0; i < dncurves; i++) {
        /* XXX: In principle, we should also emit some curve-notify signals, except we would have to catch them above
         * and there would be hundreds of them. So, let's hope anyone listening is happy seeing all data changing? */
        g_signal_emit(destgmodel, signals[SGNL_CURVE_DATA_CHANGED], 0, i);
    }
}

/**
 * gwy_graph_model_copy:
 * @gmodel: A graph curve model to duplicate.
 *
 * Create a new graph curve model as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Returns: (transfer full):
 *          A copy of the graph curve model.
 **/
GwyGraphModel*
gwy_graph_model_copy(GwyGraphModel *gmodel)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_GRAPH_MODEL(gmodel)) {
        g_assert(GWY_IS_GRAPH_MODEL(gmodel));
        return g_object_new(GWY_TYPE_GRAPH_MODEL, NULL);
    }
    return GWY_GRAPH_MODEL(gwy_serializable_copy(GWY_SERIALIZABLE(gmodel)));
}

/**
 * gwy_graph_model_assign:
 * @destination: Target data gmodel.
 * @source: Source data gmodel.
 *
 * Makes one data gmodel equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 **/
void
gwy_graph_model_assign(GwyGraphModel *destination, GwyGraphModel *source)
{
    g_return_if_fail(GWY_IS_GRAPH_MODEL(destination));
    g_return_if_fail(GWY_IS_GRAPH_MODEL(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}

/**
 * SECTION: graph-model
 * @title: GwyGraphModel
 * @short_description: Representation of a graph
 *
 * #GwyGraphModel represents information about a graph necessary to fully reconstruct it.
 **/

/**
 * GwyGraphModelExportStyle:
 * @GWY_GRAPH_MODEL_EXPORT_ASCII_PLAIN: White-space separated data values, plain description lines and column headers,
 *                                      missing data represented with dashes.
 * @GWY_GRAPH_MODEL_EXPORT_ASCII_GNUPLOT: White-space separated data values, curves serialised, description lines and
 *                                        column headers prefixed with <literal>#</literal>.
 * @GWY_GRAPH_MODEL_EXPORT_ASCII_CSV: Semicolon separated data values and column headers, missing data represented as
 *                                    empty columns.
 * @GWY_GRAPH_MODEL_EXPORT_ASCII_ORIGIN: Presently, the same as the plain format.
 * @GWY_GRAPH_MODEL_EXPORT_ASCII_IGORPRO: Text wave format of Igor Pro (.itx).
 * @GWY_GRAPH_MODEL_EXPORT_ASCII_POSIX: Flag that can be combined with the other formats, meaning locale-independent
 *                                      C/POSIX format of floating point numbers.
 * @GWY_GRAPH_MODEL_EXPORT_ASCII_MERGED: Flag that can be combined with the other formats (except Igor Pro),
 *                                       requesting multi-column output with a single merged abscissa in the first
 *                                       column.
 *
 * Graph ASCII export style.
 **/

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