/*
 *  $Id: graph-curve-model.c 30146 2026-06-25 12:51:18Z yeti-dn $
 *  Copyright (C) 2005-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 <stdlib.h>
#include <glib/gi18n-lib.h>

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

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

#define TYPE_NAME "GwyGraphCurveModel"

/* Cache operations */
#define CVAL(priv, b)  ((priv)->cache[GWY_GRAPH_CURVE_MODEL_CACHE_##b])
#define CBIT(b)        (1 << GWY_GRAPH_CURVE_MODEL_CACHE_##b)
#define CTEST(priv, b) ((priv)->cached & CBIT(b))

enum {
    SGNL_DATA_CHANGED,
    NUM_SIGNALS
};

enum {
    PROP_0,
    PROP_DESCRIPTION,
    PROP_MODE,
    PROP_POINT_TYPE,
    PROP_POINT_SIZE,
    PROP_LINE_STYLE,
    PROP_LINE_WIDTH,
    PROP_COLOR,
    NUM_PROPERTIES,
};

enum {
    ITEM_XDATA, ITEM_YDATA,
    ITEM_DESCRIPTION,
    ITEM_COLOR,
    ITEM_MODE,
    ITEM_POINT_TYPE, ITEM_POINT_SIZE,
    ITEM_LINE_STYLE, ITEM_LINE_WIDTH,
    NUM_ITEMS
};

static void             finalize              (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             data_changed          (GwyGraphCurveModel *gcmodel);
static void             update_ordering_type  (GwyGraphCurveModel *gcmodel);

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

static const GwyRGBA nice_colors[] = {
    { 0.000, 0.000, 0.000, 1.000 },    /* Black */
    { 1.000, 0.000, 0.000, 1.000 },    /* Light red */
    { 0.000, 0.784, 0.000, 1.000 },    /* Light green */
    { 0.000, 0.000, 1.000, 1.000 },    /* Medium blue */
    { 0.000, 0.650, 0.650, 1.000 },    /* Light azure */
    { 0.529, 0.216, 0.000, 1.000 },    /* Dark brown */
    { 1.000, 0.510, 0.000, 1.000 },    /* Orange */
    { 1.000, 0.784, 0.000, 1.000 },    /* Rich yellow */
    { 0.588, 0.000, 0.588, 1.000 },    /* Dark violet */
    { 1.000, 0.000, 1.000, 1.000 },    /* Pink */
    { 0.095, 0.351, 0.500, 1.000 },    /* Navy blue */
    { 0.232, 0.580, 0.340, 1.000 },    /* Greenish */
    { 0.510, 0.510, 0.510, 1.000 },    /* Grey */
    { 0.780, 0.000, 0.000, 1.000 },    /* Dark red */
    { 0.000, 0.510, 0.000, 1.000 },    /* Dark green */
    { 0.000, 0.000, 0.558, 1.000 },    /* Dark blue */
    { 0.000, 0.467, 0.467, 1.000 },    /* Azure */
    { 0.604, 0.367, 0.095, 1.000 },    /* Dark brown */
    { 0.810, 0.572, 0.000, 1.000 },    /* Dark orange */
    { 1.000, 0.000, 0.510, 1.000 },    /* Purpur */
    { 0.588, 0.588, 0.000, 1.000 },    /* Green-brown */
    { 0.000, 0.510, 1.000, 1.000 },    /* Light greenish blue */
    { 0.681, 0.000, 1.000, 1.000 },    /* Light violet */
    { 0.000, 0.784, 1.000, 1.000 },    /* Greenish blue */
};

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "xdata",       .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                             .flags = GWY_SERIALIZABLE_BIG_DATA      },
    { .name = "ydata",       .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                             .flags = GWY_SERIALIZABLE_BIG_DATA      },
    { .name = "description", .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "color",       .ctype = GWY_SERIALIZABLE_BOXED,        },
    { .name = "mode",        .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "point-type",  .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "point-size",  .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "line-style",  .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "line-width",  .ctype = GWY_SERIALIZABLE_DOUBLE,       },
};

G_DEFINE_TYPE_WITH_CODE(GwyGraphCurveModel, gwy_graph_curve_model, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyGraphCurveModel)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

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

    properties[PROP_DESCRIPTION] = g_param_spec_string("description", NULL,
                                                       "Curve description. It appears on graph key.",
                                                       "curve",
                                                       GWY_GPARAM_RWE);
    properties[PROP_MODE] = g_param_spec_enum("mode", NULL,
                                              "Curve plotting mode (line, points, ...)",
                                              GWY_TYPE_GRAPH_CURVE_MODE, GWY_GRAPH_CURVE_LINE,
                                              GWY_GPARAM_RWE);
    properties[PROP_POINT_TYPE] = g_param_spec_enum("point-type", NULL,
                                                    "Curve point symbol type. Curve mode has to include points for "
                                                    "the symbols to be visible.",
                                                    GWY_TYPE_SYMBOL_TYPE, GWY_SYMBOL_SQUARE,
                                                    GWY_GPARAM_RWE);
    properties[PROP_POINT_SIZE] = g_param_spec_double("point-size", NULL,
                                                      "Curve point symbol size",
                                                      0.0, 100.0, 8.0,
                                                      GWY_GPARAM_RWE);
    properties[PROP_LINE_STYLE] = g_param_spec_enum("line-style", NULL,
                                                    "Curve line style. Curve mode has to include lines for the line "
                                                    "to be visible.",
                                                    GWY_TYPE_LINE_STYLE, GWY_LINE_SOLID,
                                                    GWY_GPARAM_RWE);
    properties[PROP_LINE_WIDTH] = g_param_spec_double("line-width", NULL,
                                                      "Curve line width.",
                                                      0.0, 100.0, 1.0,
                                                      GWY_GPARAM_RWE);
    properties[PROP_COLOR] = g_param_spec_boxed("color", NULL,
                                                "Curve color",
                                                GWY_TYPE_RGBA,
                                                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;

    GType rgbatype = GWY_TYPE_RGBA;

    define_properties();
    serializable_items[ITEM_MODE].aux.pspec = properties[PROP_MODE];
    serializable_items[ITEM_POINT_TYPE].aux.pspec = properties[PROP_POINT_TYPE];
    serializable_items[ITEM_POINT_SIZE].aux.pspec = properties[PROP_POINT_SIZE];
    serializable_items[ITEM_LINE_STYLE].aux.pspec = properties[PROP_LINE_STYLE];
    serializable_items[ITEM_LINE_WIDTH].aux.pspec = properties[PROP_LINE_WIDTH];
    serializable_items[ITEM_COLOR].aux.boxed_type = rgbatype;
    gwy_fill_serializable_defaults_pspec(serializable_items, NUM_ITEMS, TRUE);
}

static void
gwy_graph_curve_model_class_init(GwyGraphCurveModelClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

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

    parent_class = gwy_graph_curve_model_parent_class;

    /**
     * GwyGraphCurveModel::data-changed:
     * @gwygraphcurvemodel: The #GwyGraphCurveModel which received the signal.
     *
     * The ::data-changed signal is emitted whenever curve data is set with a function like
     * gwy_graph_curve_model_set_data().
     **/
    signals[SGNL_DATA_CHANGED] = g_signal_new("data-changed", type,
                                              G_SIGNAL_RUN_FIRST,
                                              G_STRUCT_OFFSET(GwyGraphCurveModelClass, 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);

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

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyGraphCurveModel *gcmodel = GWY_GRAPH_CURVE_MODEL(object);
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;
    GwyRGBA *color;
    gdouble d;
    guint e;
    gboolean changed = FALSE;

    switch (prop_id) {
        case PROP_DESCRIPTION:
        changed = gwy_assign_gstring(priv->description, g_value_get_string(value));
        break;

        case PROP_MODE:
        if ((changed = (priv->mode != (e = g_value_get_enum(value)))))
            priv->mode = e;
        /* FIXME: Why did we have this here?
        data_changed(gcmodel);
        */
        break;

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

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

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

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

        case PROP_COLOR:
        color = g_value_get_boxed(value);
        if ((changed = !gwy_rgba_equal(color, &priv->color)))
            priv->color = *color;
        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)
{
    GwyGraphCurveModelPrivate *priv = GWY_GRAPH_CURVE_MODEL(object)->priv;

    switch (prop_id) {
        case PROP_DESCRIPTION:
        g_value_set_string(value, priv->description->str);
        break;

        case PROP_MODE:
        g_value_set_enum(value, priv->mode);
        break;

        case PROP_POINT_TYPE:
        g_value_set_enum(value, priv->point_type);
        break;

        case PROP_LINE_STYLE:
        g_value_set_enum(value, priv->line_style);
        break;

        case PROP_LINE_WIDTH:
        g_value_set_double(value, priv->line_width);
        break;

        case PROP_POINT_SIZE:
        g_value_set_double(value, priv->point_size);
        break;

        case PROP_COLOR:
        g_value_set_boxed(value, &priv->color);
        break;

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

static void
gwy_graph_curve_model_init(GwyGraphCurveModel *gcmodel)
{
    GwyGraphCurveModelPrivate *priv;

    gcmodel->priv = priv = gwy_graph_curve_model_get_instance_private(gcmodel);
    priv->description = g_string_new(NULL);
    priv->color = (GwyRGBA){ 0, 0, 0, 1 };

    priv->mode = GWY_GRAPH_CURVE_LINE_POINTS;

    priv->point_type =GWY_SYMBOL_SQUARE;
    priv->point_size = 8;

    priv->line_style = GWY_LINE_SOLID;
    priv->line_width = 1;

    priv->order = 1;
}

/**
 * gwy_graph_curve_model_new:
 *
 * Creates a new graph curve model.
 *
 * Returns: New empty graph curve model as a #GObject.
 **/
GwyGraphCurveModel*
gwy_graph_curve_model_new(void)
{
    return g_object_new(GWY_TYPE_GRAPH_CURVE_MODEL, NULL);
}

/**
 * gwy_graph_curve_model_new_alike:
 * @gcmodel: A graph curve model.
 *
 * Creates new graph curve model object that has the same settings as @gcmodel.
 *
 * Curve data are not duplicated.
 *
 * Returns: New graph curve model.
 **/
GwyGraphCurveModel*
gwy_graph_curve_model_new_alike(GwyGraphCurveModel *gcmodel)
{
    GwyGraphCurveModel *duplicate = gwy_graph_curve_model_new();

    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), duplicate);

    GwyGraphCurveModelPrivate *priv = gcmodel->priv, *dpriv = duplicate->priv;

    g_string_assign(dpriv->description, priv->description->str);
    dpriv->color = priv->color;
    dpriv->mode = priv->mode;
    dpriv->point_type = priv->point_type;
    dpriv->point_size = priv->point_size;
    dpriv->line_style = priv->line_style;
    dpriv->line_width = priv->line_width;

    return duplicate;
}

static void
finalize(GObject *object)
{
    GwyGraphCurveModel *gcmodel = GWY_GRAPH_CURVE_MODEL(object);
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;

    g_string_free(priv->description, TRUE);
    g_free(priv->xdata);
    g_free(priv->ydata);

    parent_class->finalize(object);
}

/**
 * gwy_graph_curve_model_set_data:
 * @gcmodel: A graph curve model.
 * @xdata: X data points (array of size @n).
 * @ydata: Y data points (array of size @n).
 * @n: Number of points, i.e. group in @xdata and @ydata.
 *
 * Sets curve model data from separated X and Y arrays.
 *
 * <warning>The points should be ordered in ascending abscissa order, meaning @xdata values ordered from smallest to
 * largest.  It is not enforced and you can create graphs of data the do not satisfy this condition.  However, various
 * graph functionality may be unavailable or degraded then.  You also can use gwy_graph_curve_model_enforce_order()
 * afterwards to ensure the recommended data point order.</warning>
 **/
void
gwy_graph_curve_model_set_data(GwyGraphCurveModel *gcmodel,
                               const gdouble *xdata,
                               const gdouble *ydata,
                               gint n)
{
    g_return_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel));
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;

    if (priv->n == n) {
        gwy_assign(priv->xdata, xdata, n);
        gwy_assign(priv->ydata, ydata, n);
    }
    else {
        gdouble *old;

        old = priv->xdata;
        priv->xdata = g_memdup(xdata, n*sizeof(gdouble));
        g_free(old);

        old = priv->ydata;
        priv->ydata = g_memdup(ydata, n*sizeof(gdouble));
        g_free(old);

        priv->n = n;
    }

    update_ordering_type(gcmodel);
    data_changed(gcmodel);
}

/**
 * gwy_graph_curve_model_set_data_interleaved:
 * @gcmodel: A graph curve model.
 * @xydata: X and Y data points (array of size 2*@n).
 * @n: Number of points, i.e. half the number of group in @xydata.
 *
 * Sets curve model data from an interleaved array.
 *
 * The array should contain interleaved abscissa and ordinate values: x0, y0, x1, y1, x2, y2, etc.  You can also
 * typecast an array of #GwyXY structs and pass it as @xydata.
 *
 * <warning>The points should be ordered in ascending abscissa order, meaning @xdata values ordered from smallest to
 * largest.  It is not enforced and you can create graphs of data the do not satisfy this condition.  However, various
 * graph functionality may be unavailable or degraded then.  You also can use gwy_graph_curve_model_enforce_order()
 * afterwards to ensure the recommended data point order.</warning>
 **/
void
gwy_graph_curve_model_set_data_interleaved(GwyGraphCurveModel *gcmodel,
                                           const gdouble *xydata,
                                           gint n)
{
    g_return_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel));
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;

    if (priv->n != n) {
        g_free(priv->xdata);
        g_free(priv->ydata);
        priv->xdata = g_new(gdouble, n);
        priv->ydata = g_new(gdouble, n);
    }

    priv->n = n;
    for (gint i = 0; i < n; i++) {
        priv->xdata[i] = *(xydata++);
        priv->ydata[i] = *(xydata++);
    }

    update_ordering_type(gcmodel);
    data_changed(gcmodel);
}

/* Return positive, negative or zero for ascending, descending or no order, respectively.*/
static void
update_ordering_type(GwyGraphCurveModel *gcmodel)
{
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;

    /* Check if data are either sorted or sorted in the reverse order (which is by far the most common unwanted order
     * because we get it for things like retract curves). If there are not enough points to have any order, consider
     * the curve implicitly sorted. */
    gint n = priv->n;
    if (n < 2) {
        priv->order = 1;
        return;
    }

    gboolean is_sorted = TRUE, is_revsorted = TRUE;
    gdouble *xdata = priv->xdata;
    for (gint i = 1; i < n; i++) {
        if (is_sorted && xdata[i-1] > xdata[i]) {
            is_sorted = FALSE;
            if (!is_revsorted)
                break;
        }
        if (is_revsorted && xdata[i-1] < xdata[i]) {
            is_revsorted = FALSE;
            if (!is_sorted)
                break;
        }
    }

    if (is_sorted)
        priv->order = 1;
    else if (is_revsorted)
        priv->order = -1;
    else
        priv->order = 0;
}

/**
 * gwy_graph_curve_model_enforce_order:
 * @gcmodel: A graph curve model.
 *
 * Ensures curve model data points are ordered by abscissa in ascending order.
 *
 * The function reorders the data points currently present in the model.  It does not prevent functions such as
 * gwy_graph_curve_model_set_data() from disrupting the order again.  See its documentation for further remarks.
 *
 * The "data-changed" signal is emitted if the data order actually changes.
 **/
void
gwy_graph_curve_model_enforce_order(GwyGraphCurveModel *gcmodel)
{
    g_return_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel));
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;
    if (priv->order > 0)
        return;

    /* Reverse order. */
    gint n = priv->n;
    gdouble *xdata = priv->xdata, *ydata = priv->ydata;
    if (priv->order < 0) {
        for (gint i = 0; i < n/2; i++) {
            GWY_SWAP(gdouble, xdata[i], xdata[n-1 - i]);
            GWY_SWAP(gdouble, ydata[i], ydata[n-1 - i]);
        }
        priv->order = 1;
        data_changed(gcmodel);
        return;
    }

    /* The general case. */
    gdouble *bothdata = g_new(gdouble, 2*n);
    for (gint i = 0; i < n; i++) {
        bothdata[2*i + 0] = xdata[i];
        bothdata[2*i + 1] = ydata[i];
    }
    qsort(bothdata, n, 2*sizeof(gdouble), gwy_compare_double);
    for (gint i = 0; i < n; i++) {
        xdata[i] = bothdata[2*i + 0];
        ydata[i] = bothdata[2*i + 1];
    }

    g_free(bothdata);
    priv->order = 1;
    data_changed(gcmodel);
}

/**
 * gwy_graph_curve_model_is_ordered:
 * @gcmodel: A graph curve model.
 *
 * Checks if a curve model data points are ordered by abscissa in ascending order.
 *
 * If the curve model has less than two points it is considered ordered by abscissa.  Two points with the same
 * abscissa are considered correctly ordered in both orders.
 *
 * See gwy_graph_curve_model_enforce_order() for fixing the point order.
 *
 * Returns: %TRUE if the graph curve model points are sorted by abscissa, %FALSE when they are not.
 **/
gboolean
gwy_graph_curve_model_is_ordered(GwyGraphCurveModel *gcmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), FALSE);
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;
    return priv->order > 0;
}

/**
 * gwy_graph_curve_model_get_xdata:
 * @gcmodel: A graph curve model.
 *
 * Gets y data points of a graph curve model.
 *
 * The returned data are owned by the and cannot be modified nor freed.  The returned pointer is valid only so long as
 * the curve model exists and its data do not change.
 *
 * Returns: X data points, owned by the curve model.
 **/
const gdouble*
gwy_graph_curve_model_get_xdata(GwyGraphCurveModel *gcmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), NULL);
    return gcmodel->priv->xdata;
}

/**
 * gwy_graph_curve_model_get_ydata:
 * @gcmodel: A graph curve model.
 *
 * Gets y data points of a graph curve model.
 *
 * The returned data are owned by the and cannot be modified nor freed.  The returned pointer is valid only so long as
 * the curve model exists and its data do not change.
 *
 * Returns: Y data points, owned by the curve model.
 **/
const gdouble*
gwy_graph_curve_model_get_ydata(GwyGraphCurveModel *gcmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), NULL);
    return gcmodel->priv->ydata;
}

/**
 * gwy_graph_curve_model_get_n_data:
 * @gcmodel: A graph curve model.
 *
 * Gets the number of points in a graph curve model.
 *
 * Returns: number of data points within the curve data
 **/
gint
gwy_graph_curve_model_get_n_data(GwyGraphCurveModel *gcmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), 0);
    return gcmodel->priv->n;
}

/**
 * gwy_graph_curve_model_set_data_from_line:
 * @gcmodel: A graph curve model.
 * @dline: A data line.
 * @from_index: Data line index where to start.
 * @to_index: Data line index where to stop.
 *
 * Sets graph curve model data from a data line.
 *
 * The range of import can be modified using parameters @from_index and @to_index that are interpreted directly as
 * data indices within the #GwyLine.  In the case that @from_index == @to_index, the full #GwyLine is used.
 **/
void
gwy_graph_curve_model_set_data_from_line(GwyGraphCurveModel *gcmodel,
                                         GwyLine *dline,
                                         gint from_index,
                                         gint to_index)
{
    g_return_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel));
    g_return_if_fail(GWY_IS_LINE(dline));

    gdouble realmin, realmax;
    gint res;

    if (from_index == to_index || from_index > to_index) {
        res = gwy_line_get_res(dline);
        realmin = 0;
        realmax = gwy_line_get_real(dline);
        from_index = 0;
    }
    else {
        res = to_index - from_index;
        realmin = gwy_line_itor(dline, from_index);
        realmax = gwy_line_itor(dline, to_index);
    }

    gdouble offset = gwy_line_get_offset(dline);
    const gdouble *ldata = gwy_line_get_data_const(dline);
    gdouble *xdata = gwy_math_linspace(NULL, res, realmin + offset, (realmax - realmin)/res);
    gdouble *ydata = g_new(gdouble, res);
    gwy_assign(ydata, ldata + from_index, res);
    if (realmin > realmax) {
        /* XXX: Data lines with negative step should not actually exist.  But file modules are still prone to
         * producing them for spectroscopy... */
        for (gint i = 0; i < res/2; i++) {
            GWY_SWAP(gdouble, xdata[i], xdata[res-1 - i]);
            GWY_SWAP(gdouble, ydata[i], ydata[res-1 - i]);
        }
    }

    gwy_graph_curve_model_set_data(gcmodel, xdata, ydata, res);
    g_free(xdata);
    g_free(ydata);
}

/**
 * gwy_graph_curve_model_get_x_range:
 * @gcmodel: A graph curve 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 curve.
 *
 * The values are cached in the curve model therefore repeated calls to this function (with unchanged data) are cheap.
 *
 * If there are no data points in the curve, @x_min and @x_max are untouched and the function returns %FALSE.
 *
 * See also gwy_graph_curve_model_get_ranges() for a more high-level function.
 *
 * Returns: %TRUE if there are any data points in the curve and @x_min, @x_max were set.
 **/
gboolean
gwy_graph_curve_model_get_x_range(GwyGraphCurveModel *gcmodel,
                                  gdouble *x_min,
                                  gdouble *x_max)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), FALSE);

    GwyGraphCurveModelPrivate *priv = gcmodel->priv;
    gdouble xmin, xmax;
    gboolean must_calculate = FALSE;

    if (!priv->n)
        return FALSE;

    if (x_min) {
        if (CTEST(priv, XMIN))
            *x_min = CVAL(priv, XMIN);
        else
            must_calculate = TRUE;
    }

    if (x_max) {
        if (CTEST(priv, XMAX))
            *x_max = CVAL(priv, XMAX);
        else
            must_calculate = TRUE;
    }

    if (!must_calculate)
        return TRUE;

    xmin = xmax = priv->xdata[0];
    for (gint i = 1; i < priv->n; i++) {
        xmin = fmin(xmin, priv->xdata[i]);
        xmax = fmax(xmax, priv->xdata[i]);
    }

    CVAL(priv, XMIN) = xmin;
    CVAL(priv, XMAX) = xmax;
    priv->cached |= CBIT(XMIN) | CBIT(XMAX);

    if (x_min)
        *x_min = xmin;
    if (x_max)
        *x_max = xmax;

    return TRUE;
}

/**
 * gwy_graph_curve_model_get_y_range:
 * @gcmodel: A graph curve 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 curve.
 *
 * The values are cached in the curve model therefore repeated calls to this function (with unchanged data) are cheap.
 *
 * If there are no data points in the curve, @x_min and @x_max are untouched and the function returns %FALSE.
 *
 * See also gwy_graph_curve_model_get_ranges() for a more high-level function.
 *
 * Returns: %TRUE if there are any data points in the curve and @x_min, @x_max were set.
 **/
gboolean
gwy_graph_curve_model_get_y_range(GwyGraphCurveModel *gcmodel,
                                  gdouble *y_min,
                                  gdouble *y_max)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), FALSE);

    GwyGraphCurveModelPrivate *priv = gcmodel->priv;
    gdouble ymin, ymax;
    gboolean must_calculate = FALSE;

    if (!priv->n)
        return FALSE;

    if (y_min) {
        if (CTEST(priv, YMIN))
            *y_min = CVAL(priv, YMIN);
        else
            must_calculate = TRUE;
    }

    if (y_max) {
        if (CTEST(priv, YMAX))
            *y_max = CVAL(priv, YMAX);
        else
            must_calculate = TRUE;
    }

    if (!must_calculate)
        return TRUE;

    ymin = ymax = priv->ydata[0];
    for (gint i = 1; i < priv->n; i++) {
        ymin = fmin(ymin, priv->ydata[i]);
        ymax = fmax(ymax, priv->ydata[i]);
    }

    CVAL(priv, YMIN) = ymin;
    CVAL(priv, YMAX) = ymax;
    priv->cached |= CBIT(YMIN) | CBIT(YMAX);

    if (y_min)
        *y_min = ymin;
    if (y_max)
        *y_max = ymax;

    return TRUE;
}

/**
 * gwy_graph_curve_model_get_ranges:
 * @gcmodel: A graph curve 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 to, or %NULL.
 * @x_max: Location to store the maximum abscissa value to, or %NULL.
 * @y_min: Location to store the minimum ordinate value to, or %NULL.
 * @y_max: Location to store the maximum ordinate value to, or %NULL.
 *
 * Gets the log-scale suitable range minima of a graph curve.
 *
 * Parameters @x_logscale and @y_logscale determine which axis or axes are intended to use logarithmical scale.  The
 * range of displayble values for an axis generally depends on the other axis too as it acts as a filter. When both
 * @x_logscale and @y_logscale are %FALSE, the returned minima are identical to those returned by
 * gwy_graph_curve_model_get_x_range() and gwy_graph_curve_model_get_y_range().
 *
 * The return values are cached in the curve model therefore repeated calls to this function (with unchanged data) are
 * cheap.
 *
 * If there are no data points that would be displayable with the intended logarithmical scale setup, the output
 * arguments are untouched and %FALSE is returned.
 *
 * Returns: %TRUE if the output arguments were filled with the ranges.
 **/
gboolean
gwy_graph_curve_model_get_ranges(GwyGraphCurveModel *gcmodel,
                                 gboolean x_logscale,
                                 gboolean y_logscale,
                                 gdouble *x_min,
                                 gdouble *x_max,
                                 gdouble *y_min,
                                 gdouble *y_max)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), FALSE);

    if (!x_logscale && !y_logscale) {
        return (gwy_graph_curve_model_get_x_range(gcmodel, x_min, x_max)
                && gwy_graph_curve_model_get_y_range(gcmodel, y_min, y_max));
    }

    GwyGraphCurveModelPrivate *priv = gcmodel->priv;
    if (!priv->n)
        return FALSE;

    gdouble xmin_xpos, ymin_xpos, ya0min, ya0min_xpos;
    gdouble xret = 0.0, yret = 0.0;

    /* We know all cache2 values are always calculated and cleared at once, so test just one of them. */
    if (CTEST(priv, XMIN_XPOS)) {
        xmin_xpos = CVAL(priv, XMIN_XPOS);
        ymin_xpos = CVAL(priv, YMIN_XPOS);
        ya0min = CVAL(priv, YAMIN);
        ya0min_xpos = CVAL(priv, YAMIN_XPOS);
    }
    else {
        xmin_xpos = ymin_xpos = ya0min = ya0min_xpos = G_MAXDOUBLE;
        for (gint i = 0; i < priv->n; i++) {
            gdouble x = priv->xdata[i];
            gdouble y = priv->ydata[i];

            if (x > 0.0) {
                if (x < xmin_xpos)
                    xmin_xpos = x;
                if (y < ymin_xpos)
                    ymin_xpos = y;
            }

            y = fabs(y);
            if (y > 0.0) {
                if (x > 0.0 && y < ya0min_xpos)
                    ya0min_xpos = y;
                if (y < ya0min)
                    ya0min = y;
            }
        }

        /* Note we cache failures too. */
        CVAL(priv, XMIN_XPOS) = xmin_xpos;
        CVAL(priv, YMIN_XPOS) = ymin_xpos;
        CVAL(priv, YAMIN) = ya0min;
        CVAL(priv, YAMIN_XPOS) = ya0min_xpos;
        priv->cached |= (CBIT(XMIN_XPOS) | CBIT(YMIN_XPOS) | CBIT(YAMIN) | CBIT(YAMIN_XPOS));
    }

    gboolean ok = TRUE;
    if (x_logscale) {
        if (xmin_xpos != G_MAXDOUBLE) {
            xret = xmin_xpos;
            if (y_logscale) {
                if (ya0min_xpos != G_MAXDOUBLE)
                    yret = ya0min_xpos;
                else
                    ok = FALSE;
            }
            else {
                if (ymin_xpos != G_MAXDOUBLE)
                    yret = ymin_xpos;
                else
                    ok = FALSE;
            }
        }
        else
            ok = FALSE;
    }
    else {
        if ((ok = gwy_graph_curve_model_get_x_range(gcmodel, &xret, NULL))) {
            if (y_logscale) {
                if (ya0min != G_MAXDOUBLE)
                    yret = ya0min;
                else
                    ok = FALSE;
            }
            else
                ok = ok && gwy_graph_curve_model_get_y_range(gcmodel, &yret, NULL);
        }
    }

    if (ok) {
        if (x_min)
            *x_min = xret;
        if (y_min)
            *y_min = yret;

        /* If ok is TRUE, minimum is positive so maximum is always safe. */
        if (x_max)
            gwy_graph_curve_model_get_x_range(gcmodel, NULL, x_max);

        if (y_max) {
            if (y_logscale) {
                gdouble ym, yp;

                gwy_graph_curve_model_get_y_range(gcmodel, &ym, &yp);
                *y_max = MAX(fabs(ym), fabs(yp));
            }
            else
                gwy_graph_curve_model_get_y_range(gcmodel, NULL, y_max);
        }
    }

    return ok;
}

/**
 * gwy_graph_curve_model_get_color:
 * @gcmodel: A graph curve model.
 * @color: Location where to fill the curve color.
 *
 * Gets the color of a graph curve model.
 *
 * This is a convenience function for getting GwyGraphCurveModel:color because g_object_get() is unwieldy for getting
 * structs.
 **/
void
gwy_graph_curve_model_get_color(GwyGraphCurveModel *gcmodel,
                                GwyRGBA *color)
{
    g_return_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel));
    g_return_if_fail(color);
    *color = gcmodel->priv->color;
}

/**
 * gwy_graph_curve_model_get_description:
 * @gcmodel: A graph curve model
 *
 * Gets the description of a graph curve model.
 *
 * Returns: The description as a string owned by the model.
 **/
const gchar*
gwy_graph_curve_model_get_description(GwyGraphCurveModel *gcmodel)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVE_MODEL(gcmodel), NULL);
    return gcmodel->priv->description->str;
}

/**
 * gwy_graph_curve_mode_get_enum:
 *
 * Returns #GwyEnum for #GwyGraphCurveMode enum type.
 *
 * Returns: %NULL-terminated #GwyEnum which must not be modified nor freed.
 **/
const GwyEnum*
gwy_graph_curve_mode_get_enum(void)
{
    static const GwyEnum entries[] = {
        { N_("Hidden"),        GWY_GRAPH_CURVE_HIDDEN,      },
        { N_("Points"),        GWY_GRAPH_CURVE_POINTS,      },
        { N_("Line"),          GWY_GRAPH_CURVE_LINE,        },
        { N_("Line + points"), GWY_GRAPH_CURVE_LINE_POINTS, },
        { NULL,                0,                           },
    };
    return entries;
}

static inline gboolean
fix_rgba(GwyRGBA *color, gboolean noisy)
{
    GwyRGBA rgba;

    rgba.r = gwy_clamp(color->r, 0.0, 1.0);
    rgba.g = gwy_clamp(color->g, 0.0, 1.0);
    rgba.b = gwy_clamp(color->b, 0.0, 1.0);
    rgba.a = gwy_clamp(color->a, 0.0, 1.0);
    if (!gwy_rgba_equal(color, &rgba)) {
        if (noisy)
            g_warning("Color component outside [0,1] range.");
        *color = rgba;
        return FALSE;
    }

    return TRUE;
}

static void
data_changed(GwyGraphCurveModel *gcmodel)
{
    gcmodel->priv->cached = 0;
    g_signal_emit(gcmodel, signals[SGNL_DATA_CHANGED], 0);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyGraphCurveModel *gcmodel = GWY_GRAPH_CURVE_MODEL(serializable);
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_XDATA, priv->xdata, priv->n);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_YDATA, priv->ydata, priv->n);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_DESCRIPTION, priv->description->str);
    gwy_serializable_group_append_rgba(group, serializable_items + ITEM_COLOR, &priv->color);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_MODE, priv->mode);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_POINT_TYPE, priv->point_type);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_POINT_SIZE, priv->point_size);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_LINE_STYLE, priv->line_style);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_LINE_WIDTH, priv->line_width);
    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);

    GwyGraphCurveModel *gcmodel = GWY_GRAPH_CURVE_MODEL(serializable);
    GwyGraphCurveModelPrivate *priv = gcmodel->priv;

    /* Botched up data dimensions is a hard fail. */
    guint nx = its[ITEM_XDATA].array_size, ny = its[ITEM_YDATA].array_size;
    gsize x_dim[1] = { nx }, y_dim[1] = { ny };
    if ((nx || ny) && (!gwy_check_data_dimension(error_list, TYPE_NAME, 1, 0, x_dim)
                       || !gwy_check_data_dimension(error_list, TYPE_NAME, 1, nx, y_dim)))
        goto fail;

    priv->n = nx;
    priv->xdata = its[ITEM_XDATA].value.v_double_array;
    its[ITEM_XDATA].value.v_double_array = NULL;
    priv->ydata = its[ITEM_YDATA].value.v_double_array;
    its[ITEM_YDATA].value.v_double_array = NULL;
    update_ordering_type(gcmodel);

    /* The rest is already validated by pspec. */
    gwy_assign_gstring(priv->description, its[ITEM_DESCRIPTION].value.v_string);

    priv->mode = its[ITEM_MODE].value.v_int32;
    priv->point_type = its[ITEM_POINT_TYPE].value.v_int32;
    priv->point_size = its[ITEM_POINT_SIZE].value.v_double;
    priv->line_style = its[ITEM_LINE_STYLE].value.v_int32;
    priv->line_width = its[ITEM_LINE_WIDTH].value.v_double;

    it = its + ITEM_COLOR;
    if (it->value.v_boxed) {
        priv->color = *(GwyRGBA*)it->value.v_boxed;
        if (!fix_rgba(&priv->color, FALSE)) {
            gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                               _("Invalid color data."));
        }
    }

    ok = TRUE;

fail:
    g_free(its[ITEM_XDATA].value.v_double_array);
    g_free(its[ITEM_YDATA].value.v_double_array);
    g_free(its[ITEM_DESCRIPTION].value.v_string);

    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyGraphCurveModel *gcmodel = GWY_GRAPH_CURVE_MODEL(serializable);
    GwyGraphCurveModel *copy = gwy_graph_curve_model_new_alike(gcmodel);
    GwyGraphCurveModelPrivate *priv = gcmodel->priv, *cpriv = copy->priv;
    if ((cpriv->n = priv->n)) {
        cpriv->xdata = g_memdup2(priv->xdata, priv->n*sizeof(gdouble));
        cpriv->ydata = g_memdup2(priv->ydata, priv->n*sizeof(gdouble));
    }
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyGraphCurveModel *destgcmodel = GWY_GRAPH_CURVE_MODEL(destination), *srcgcmodel = GWY_GRAPH_CURVE_MODEL(source);
    GwyGraphCurveModelPrivate *dpriv = destgcmodel->priv, *spriv = srcgcmodel->priv;

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

    if (!gwy_strequal(dpriv->description->str, spriv->description->str)) {
        g_string_assign(dpriv->description, spriv->description->str);
        g_object_notify_by_pspec(object, properties[PROP_DESCRIPTION]);
    }

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

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

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

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

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

    if (!gwy_rgba_equal(&dpriv->color, &spriv->color)) {
        dpriv->color = spriv->color;
        g_object_notify_by_pspec(object, properties[PROP_COLOR]);
    }

    if (dpriv->n != spriv->n) {
        g_free(dpriv->xdata);
        g_free(dpriv->ydata);
        dpriv->xdata = g_new(gdouble, spriv->n);
        dpriv->ydata = g_new(gdouble, spriv->n);
        dpriv->n = spriv->n;
    }
    gwy_assign(dpriv->xdata, spriv->xdata, spriv->n);
    gwy_assign(dpriv->ydata, spriv->ydata, spriv->n);

    g_object_thaw_notify(object);
    data_changed(destgcmodel);
}

/**
 * gwy_graph_curve_model_copy:
 * @gcmodel: 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.
 **/
GwyGraphCurveModel*
gwy_graph_curve_model_copy(GwyGraphCurveModel *gcmodel)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_GRAPH_CURVE_MODEL(gcmodel)) {
        g_assert(GWY_IS_GRAPH_CURVE_MODEL(gcmodel));
        return g_object_new(GWY_TYPE_GRAPH_CURVE_MODEL, NULL);
    }
    return GWY_GRAPH_CURVE_MODEL(gwy_serializable_copy(GWY_SERIALIZABLE(gcmodel)));
}

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

/**
 * gwy_graph_get_preset_color:
 * @i: Colour number, starting from 0 which is always black.  It can be any number but colours start to repeat after
 *     gwy_graph_get_n_preset_colors() colours.
 *
 * Gets a preset graph curve colour.
 *
 * Preset colours are a set of selected colours one can use to distingush graph curves when there is no reason to
 * prefer a particular colour. Note that the preset colours are not guaranteed to never change (including their total
 * number), although it happens rarely.
 *
 * Returns: A constant colour that must not be neither modified nor freed.
 **/
const GwyRGBA*
gwy_graph_get_preset_color(guint i)
{
    return nice_colors + (i % G_N_ELEMENTS(nice_colors));
}

/**
 * gwy_graph_get_n_preset_colors:
 *
 * Gets the number of distinct colors gwy_graph_get_preset_color() can return.
 *
 * Returns: The number of distinct colors.
 **/
guint
gwy_graph_get_n_preset_colors(void)
{
    return G_N_ELEMENTS(nice_colors);
}

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

/**
 * GwyGraphCurveMode:
 * @GWY_GRAPH_CURVE_HIDDEN: Curve is invisible.
 * @GWY_GRAPH_CURVE_POINTS: Curve data is plotted with symbols.
 * @GWY_GRAPH_CURVE_LINE: Curve data is plotted with a line.
 * @GWY_GRAPH_CURVE_LINE_POINTS: Curve data is plotted with symbols and a line.
 *
 * Graph curve plotting type.
 **/

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