/*
 *  $Id: gradient.c 29754 2026-04-09 09:35:42Z yeti-dn $
 *  Copyright (C) 2003-2025 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 "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/gradient.h"
#include "libgwyddion/serializable-utils.h"
#include "libgwyddion/serialize.h"

#include "libgwyddion/internal.h"

G_STATIC_ASSERT(sizeof(GwyGradientPoint) == 5*sizeof(gdouble));

#define TYPE_NAME "GwyGradient"

#define point_index(pts, i) g_array_index(pts, GwyGradientPoint, i)

enum {
    ITEM_NAME,
    ITEM_POINTS,
    NUM_ITEMS
};

struct _GwyGradientPrivate {
    GArray *points;
};

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 gpointer         resource_copy          (gpointer);
static gboolean         sanitise_gradient      (GwyGradient *gradient,
                                                gboolean noisy);
static void             refine_interval        (GList *points,
                                                gint n,
                                                const GwyGradientPoint *samples,
                                                gdouble threshold);
static void             data_changed           (GwyGradient *gradient);
static GwyGradient*     create_gradient        (const gchar *name,
                                                gint npoints,
                                                const GwyGradientPoint *points,
                                                gboolean is_const);
static void             resource_dump          (GwyResource *resource,
                                                GString *str);
static GwyResource*     resource_parse         (const gchar *text);
static void             resource_setup_builtins(GwyInventory *inventory);

static GwyResourceClass *parent_class = NULL;
static GwyInventory *inventory = NULL;

static const GwyGradientPoint default_gradient[2] = {
    { 0, { 0, 0, 0, 1 } },
    { 1, { 1, 1, 1, 1 } },
};
static const GwyGradientPoint null_point = { 0, { 0, 0, 0, 0 } };

static const GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "name",   .ctype = GWY_SERIALIZABLE_STRING,       },
    { .name = "points", .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                        .flags = GWY_SERIALIZABLE_BIG_DATA      },
};

G_DEFINE_TYPE_WITH_CODE(GwyGradient, gwy_gradient, GWY_TYPE_RESOURCE,
                        G_ADD_PRIVATE(GwyGradient)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

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

static void
gwy_gradient_class_init(GwyGradientClass *klass)
{
    GwyResourceClass *res_class = GWY_RESOURCE_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_gradient_parent_class;

    gobject_class->finalize = finalize;

    res_class->item_type = *gwy_resource_class_get_item_type(parent_class);
    res_class->item_type.type = G_TYPE_FROM_CLASS(klass);
    res_class->item_type.copy = resource_copy;

    res_class->name = "gradients";
    res_class->inventory = inventory = gwy_inventory_new(&res_class->item_type);
    gwy_inventory_set_default_item_name(inventory, GWY_GRADIENT_DEFAULT);
    res_class->setup_builtins = resource_setup_builtins;
    res_class->dump = resource_dump;
    res_class->parse = resource_parse;
}

static void
gwy_gradient_init(GwyGradient *gradient)
{
    GwyGradientPrivate *priv;

    priv = gradient->priv = gwy_gradient_get_instance_private(gradient);

    priv->points = g_array_new(FALSE, FALSE, sizeof(GwyGradientPoint));
    g_array_append_vals(priv->points, default_gradient, 2);
}

static void
finalize(GObject *object)
{
    GwyGradientPrivate *priv = GWY_GRADIENT(object)->priv;

    g_array_free(priv->points, TRUE);

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

/**
 * gwy_gradient_get_color:
 * @gradient: A color gradient.
 * @x: Position in gradient, in interval [0,1].
 * @color: Color to fill with interpolated color at position @x.
 *
 * Computes the color at a given position of a color gradient.
 **/
void
gwy_gradient_get_color(GwyGradient *gradient,
                       gdouble x,
                       GwyRGBA *color)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(color);
    g_return_if_fail(x >= 0.0 && x <= 1.0);

    GArray *points = gradient->priv->points;
    const GwyGradientPoint *pt = NULL;
    guint i, n = points->len;

    /* Find the correct subinterval. */
    for (i = 0; i < n; i++) {
        pt = &point_index(points, i);
        if (pt->x == x) {
            *color = pt->color;
            return;
        }
        if (pt->x > x)
            break;
    }
    g_assert(i);
    const GwyGradientPoint *pt_prev = &point_index(points, i-1);

    gwy_rgba_interpolate(&pt_prev->color, &pt->color, (x - pt_prev->x)/(pt->x - pt_prev->x), color);
}

/**
 * gwy_gradient_sample:
 * @gradient: A color gradient to sample.
 * @nsamples: Required number of samples.
 * @samples: Pointer to array to be filled.
 *
 * Samples a gradient to an array Gdk pixbuf-like samples.
 *
 * If @samples is not %NULL, it's resized to 4*@nsamples bytes, otherwise a new buffer is allocated.
 *
 * Returns: Sampled @gradient as a sequence of Gdk pixbuf-like RRGGBBAA quadruplets.
 **/
guchar*
gwy_gradient_sample(GwyGradient *gradient,
                    gint nsamples,
                    guchar *samples)
{
    g_return_val_if_fail(GWY_IS_GRADIENT(gradient), NULL);
    g_return_val_if_fail(nsamples > 1, NULL);

    samples = g_renew(guchar, samples, 4*nsamples);

    GArray *points = gradient->priv->points;
    gint i, j, k;
    gdouble q = 1.0/(nsamples - 1.0);
    const GwyGradientPoint *pt = &point_index(points, 0), *pt_prev = pt;
    for (i = j = k = 0; i < nsamples; i++) {
        gdouble x = MIN(i*q, 1.0);
        GwyRGBA color;

        while (G_UNLIKELY(x > pt->x)) {
            j++;
            pt_prev = pt;
            pt = &point_index(points, j);
        }
        if (G_UNLIKELY(x == pt->x))
            color = pt->color;
        else
            gwy_rgba_interpolate(&pt_prev->color, &pt->color, (x - pt_prev->x)/(pt->x - pt_prev->x), &color);

        samples[k++] = float_to_hex(color.r);
        samples[k++] = float_to_hex(color.g);
        samples[k++] = float_to_hex(color.b);
        samples[k++] = float_to_hex(color.a);
    }

    return samples;
}

/**
 * gwy_gradient_get_n_points:
 * @gradient: A color gradient.
 *
 * Returns the number of points in a color gradient.
 *
 * Returns: The number of points in @gradient.
 **/
gint
gwy_gradient_get_n_points(GwyGradient *gradient)
{
    g_return_val_if_fail(GWY_IS_GRADIENT(gradient), 0);
    GArray *points = gradient->priv->points;
    return points->len;
}

/**
 * gwy_gradient_get_point:
 * @gradient: A color gradient.
 * @i: Color point index in @gradient.
 *
 * Returns the point at given index of a color gradient.
 *
 * Returns: (transfer none): Color point at index @i.
 **/
GwyGradientPoint
gwy_gradient_get_point(GwyGradient *gradient,
                       gint i)
{
    g_return_val_if_fail(GWY_IS_GRADIENT(gradient), null_point);
    GArray *points = gradient->priv->points;
    g_return_val_if_fail(i >= 0 && i < points->len, null_point);
    return point_index(points, i);
}

/**
 * fix_rgba:
 * @color: A color.
 *
 * Fixes color components to range 0..1.
 *
 * Returns: The fixed color.
 **/
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;
}

/**
 * fix_position:
 * @points: Array of color points (gradient definition).
 * @i: Index a point should be inserted.
 * @pos: Position the point should be inserted to.
 *
 * Fixes the position of a color point between neighbours and to range 0..1.
 *
 * Returns: Fixed position.
 **/
static inline gdouble
fix_position(GArray *points, gint i, gdouble pos)
{
    gdouble xprec, xsucc, x;

    if (i == 0)
        xprec = xsucc = 0.0;
    else if (i == points->len-1)
        xprec = xsucc = 1.0;
    else {
        xprec = point_index(points, i-1).x;
        xsucc = point_index(points, i+1).x;
    }
    x = gwy_clamp(pos, xprec, xsucc);
    if (x != pos)
        g_warning("Point beyond neighbours or outside [0,1] range.");

    return x;
}

/**
 * gwy_gradient_set_point:
 * @gradient: A color gradient.
 * @i: Color point index in @gradient.
 * @point: Color point to replace current point at @i with.
 *
 * Sets a single color point in a color gradient.
 *
 * It is an error to try to move points beyond its neighbours, or to move first (or last) point from 0 (or 1).
 **/
void
gwy_gradient_set_point(GwyGradient *gradient,
                       gint i,
                       const GwyGradientPoint *point)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));
    g_return_if_fail(point);
    GArray *points = gradient->priv->points;
    g_return_if_fail(i >= 0 && i < points->len);

    GwyRGBA color = point->color;
    fix_rgba(&color, TRUE);
    GwyGradientPoint pt = { fix_position(points, i, point->x), color };
    GwyGradientPoint *gradpt = &point_index(points, i);
    if (gradpt->x == pt.x
        && gradpt->color.r == pt.color.r
        && gradpt->color.g == pt.color.g
        && gradpt->color.b == pt.color.b
        && gradpt->color.a == pt.color.a)
        return;

    *gradpt = pt;
    data_changed(gradient);
}

/**
 * gwy_gradient_set_point_color:
 * @gradient: A color gradient.
 * @i: Color point index in @gradient.
 * @color: Color to set the point to.
 *
 * Sets the color of a color gradient point without moving it.
 **/
void
gwy_gradient_set_point_color(GwyGradient *gradient,
                             gint i,
                             const GwyRGBA *color)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));
    g_return_if_fail(color);
    GArray *points = gradient->priv->points;
    g_return_if_fail(i >= 0 && i < points->len);

    GwyGradientPoint *gradpt = &point_index(points, i);
    GwyRGBA color2 = *color;
    fix_rgba(&color2, TRUE);
    if (gradpt->color.r == color2.r
        && gradpt->color.g == color2.g
        && gradpt->color.b == color2.b
        && gradpt->color.a == color2.a)
        return;

    gradpt->color = color2;
    data_changed(gradient);
}

/**
 * gwy_gradient_insert_point:
 * @gradient: A color gradient.
 * @i: Color point index in @gradient.
 * @point: Color point to insert at @i.
 *
 * Inserts a point to a color gradient.
 *
 * It is an error to try to position a outside its future neighbours, or to move the first (or last) point from 0 (or
 * 1). Consider gwy_gradient_insert_point_sorted() if you have the position and want the index to be determined
 * automatically.
 **/
void
gwy_gradient_insert_point(GwyGradient *gradient,
                          gint i,
                          const GwyGradientPoint *point)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));
    g_return_if_fail(point);
    GArray *points = gradient->priv->points;
    g_return_if_fail(i >= 0 && i <= points->len);

    GwyRGBA color = point->color;
    fix_rgba(&color, TRUE);
    GwyGradientPoint pt = { 0.0, color };
    if ((guint)i == points->len) {
        pt.x = 1.0;
        if (point->x != 1.0)
            g_warning("Point beyond neighbours or outside [0,1].");
        g_array_append_val(points, pt);
        data_changed(gradient);
        return;
    }
    if (i == 0) {
        pt.x = 0.0;
        if (point->x != 0.0)
            g_warning("Point beyond neighbours or outside [0,1].");
        data_changed(gradient);
        g_array_prepend_val(points, pt);
    }

    /* Duplicate point at i for position checking. */
    GwyGradientPoint tmp = point_index(points, i);
    g_array_insert_val(points, i, tmp);
    pt.x = fix_position(points, i, point->x);
    point_index(points, i) = pt;

    data_changed(gradient);
}

/**
 * gwy_gradient_insert_point_sorted:
 * @gradient: A color gradient.
 * @point: Color point to insert.
 *
 * Inserts a point into a color gradient based on its x position.
 *
 * Returns: The index @point was inserted at.
 **/
gint
gwy_gradient_insert_point_sorted(GwyGradient *gradient,
                                 const GwyGradientPoint *point)
{
    g_return_val_if_fail(GWY_IS_GRADIENT(gradient), -1);
    g_return_val_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)), -1);
    g_return_val_if_fail(point, -1);
    g_return_val_if_fail(point->x >= 0.0 && point->x <= 1.0, -1);
    GArray *points = gradient->priv->points;
    GwyRGBA color = point->color;
    fix_rgba(&color, TRUE);
    GwyGradientPoint pt = { point->x, color };

    /* Find the correct subinterval. */
    guint i = 0;
    for (i = 0; i < points->len; i++) {
        if (point_index(points, i).x <= pt.x)
            break;
    }
    g_assert(i < points->len);

    if (i != points->len-1)
        i++;
    g_array_insert_val(points, i, pt);

    data_changed(gradient);

    return i;
}

/**
 * gwy_gradient_delete_point:
 * @gradient: A color gradient.
 * @i: Color point index in @gradient.
 *
 * Deletes a point at given index in a color gradient.
 *
 * It is not possible to delete points in gradients with less than 3 points. The first and last points should not be
 * deleted unless there's another point with @x = 0 or @x = 1 present.
 **/
void
gwy_gradient_delete_point(GwyGradient *gradient,
                          gint i)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));
    GArray *points = gradient->priv->points;
    g_return_if_fail(points->len > 2);
    g_return_if_fail(i >= 0 && i < points->len);

    g_array_remove_index(points, i);
    if (!i) {
        GwyGradientPoint *pt = &point_index(points, 0);
        if (pt->x != 0.0) {
            g_warning("Forced to move first point to 0");
            pt->x = 0.0;
        }
    }
    if (i == points->len) {
        GwyGradientPoint *pt = &point_index(points, points->len-1);
        if (pt->x != 1.0) {
            g_warning("Forced to move last point to 1");
            pt->x = 1.0;
        }
    }
    data_changed(gradient);
}

/**
 * gwy_gradient_reset:
 * @gradient: A color gradient.
 *
 * Resets a gradient to the default two-point gray scale state.
 **/
void
gwy_gradient_reset(GwyGradient *gradient)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));
    GArray *points = gradient->priv->points;

    if (points->len == 2 && !memcmp(points->data, default_gradient, sizeof(default_gradient)))
        return;

    g_array_set_size(points, 0);
    g_array_append_vals(points, default_gradient, 2);
    data_changed(gradient);
}

/**
 * gwy_gradient_get_points:
 * @gradient: A color gradient.
 * @npoints: (out): A location to store the number of color points (or %NULL).
 *
 * Returns the complete set of color points of a gradient.
 *
 * Returns: (array length=npoints):
 *          Complete set @gradient's color points.  The returned array is owned by @gradient and must not be modified
 *          or freed.
 **/
const GwyGradientPoint*
gwy_gradient_get_points(GwyGradient *gradient,
                        gint *npoints)
{
    g_return_val_if_fail(GWY_IS_GRADIENT(gradient), NULL);
    GArray *points = gradient->priv->points;

    if (npoints)
        *npoints = points->len;
    return &point_index(points, 0);
}

/* This is an internal function and does NOT call data_changed(). */
static gboolean
sanitise_gradient(GwyGradient *gradient, gboolean noisy)
{
    g_return_val_if_fail(GWY_IS_GRADIENT(gradient), FALSE);
    GArray *points = gradient->priv->points;
    guint n = points->len;
    gboolean ok = TRUE;
    guint i;

    /* First make sure there are at least two points. */
    if (n < 2) {
        if (!n) {
            GwyGradientPoint pt = default_gradient[0];
            g_array_append_val(points, pt);
        }
        GwyGradientPoint pt = default_gradient[1];
        g_array_append_val(points, pt);
        ok = FALSE;
        if (noisy)
            g_warning("Too few points in gradient.");
    }

    /* Then make the points ordered, in [0,1], starting with 0, ending with 1, and fix colors. */
    for (i = 0; i < n; i++) {
        GwyGradientPoint *pt = &point_index(points, i);
        if (!fix_rgba(&pt->color, noisy))
            ok = FALSE;
        if (i && pt->x < point_index(points, i-1).x) {
            ok = FALSE;
            if (noisy)
                g_warning("Point beyond neighbours or outside 0..1");
            pt->x = point_index(points, i-1).x;
        }
        else if (!i && pt->x != 0.0) {
            ok = FALSE;
            if (noisy)
                g_warning("Forced to move first point to 0");
            pt->x = 0.0;
        }
        else if (pt->x > 1.0) {
            ok = FALSE;
            if (noisy)
                g_warning("Point beyond neighbours or outside 0..1");
            pt->x = 1.0;
        }
    }
    GwyGradientPoint *pt = &point_index(points, n-1);
    if (pt->x != 1.0) {
        ok = FALSE;
        if (noisy)
            g_warning("Forced to move last point to 1");
        pt->x = 1.0;
    }

    /* Then remove redundant points. */
    for (i = n-1; point_index(points, i-1).x == 1.0; i--)
        g_array_remove_index(points, i);
    while (i) {
        pt = &point_index(points, i);
        if (pt->x == point_index(points, i-1).x && pt->x == point_index(points, i+1).x)
            g_array_remove_index(points, i);
        i--;
    }

    while (point_index(points, 1).x == 0.0)
        g_array_remove_index(points, 0);

    return ok;
}

/**
 * gwy_gradient_set_points:
 * @gradient: A color gradient.
 * @npoints: The length of @points.
 * @points: (array length=npoints): Color points to set as new gradient definition.
 *
 * Sets the complete color gradient definition to a given set of points.
 *
 * The point positions should be ordered, and first point should start at 0.0, last end at 1.0.  There should be no
 * redundant points.
 **/
void
gwy_gradient_set_points(GwyGradient *gradient,
                        gint npoints,
                        const GwyGradientPoint *points)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));
    g_return_if_fail(npoints > 1);
    g_return_if_fail(points);
    GwyGradientPrivate *priv = gradient->priv;

    g_array_set_size(priv->points, 0);
    g_array_append_vals(priv->points, points, npoints);
    sanitise_gradient(gradient, TRUE);
    data_changed(gradient);
}

/**
 * gwy_gradient_set_from_samples:
 * @gradient: A color gradient.
 * @nsamples: Number of samples, it must be at least one.
 * @samples: Sampled color gradient in Gdk pixbuf-like RRGGBBAA form.
 * @threshold: Maximum allowed difference (for color components in range 0..1). When negative, default value 1/80
 * suitable for most purposes is used.
 *
 * Reconstructs a color gradient definition from sampled colors.
 *
 * The result is usually approximate.
 **/
void
gwy_gradient_set_from_samples(GwyGradient *gradient,
                              gint nsamples,
                              const guchar *samples,
                              gdouble threshold)
{
    g_return_if_fail(GWY_IS_GRADIENT(gradient));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));
    g_return_if_fail(samples);
    g_return_if_fail(nsamples > 0);

    GwyGradientPoint *spoints = g_new(GwyGradientPoint, MAX(nsamples, 2));
    gint i, k;

    if (threshold < 0)
        threshold = 1.0/80.0;

    /* Preprocess guchar data to doubles */
    for (k = i = 0; i < nsamples; i++) {
        spoints[i].x = i/(nsamples - 1.0);
        spoints[i].color.r = samples[k++]/255.0;
        spoints[i].color.g = samples[k++]/255.0;
        spoints[i].color.b = samples[k++]/255.0;
        spoints[i].color.a = samples[k++]/255.0;
    }

    /* Handle special silly case */
    if (nsamples == 1) {
        spoints[0].x = 0.0;
        spoints[1].x = 1.0;
        spoints[1].color = spoints[0].color;
        nsamples = 2;
    }

    /* Start with first and last point and recurse */
    GList *list = NULL;
    list = g_list_append(list, spoints + nsamples-1);
    list = g_list_prepend(list, spoints);
    refine_interval(list, nsamples, spoints, threshold);

    /* Set the new points */
    GArray *points = gradient->priv->points;
    g_array_set_size(points, 0);
    for (GList *l = list; l; l = g_list_next(l)) {
        GwyGradientPoint pt = *(GwyGradientPoint*)l->data;
        g_array_append_val(points, pt);
    }
    g_list_free(list);
    g_free(spoints);
    data_changed(gradient);
}

static void
refine_interval(GList *points,
                gint n,
                const GwyGradientPoint *samples,
                gdouble threshold)
{
    GList *item;
    const GwyRGBA *first, *last;
    GwyRGBA color;
    gint i, mi;
    gdouble max, s, d;

    if (n <= 2)
        return;

    first = &samples[0].color;
    last = &samples[n-1].color;
    gwy_debug("Working on %d samples from {%f: %f, %f, %f, %f} "
              "to {%f: %f, %f, %f, %f}",
              n,
              samples[0].x, first->r, first->g, first->b, first->a,
              samples[n-1].x, last->r, last->g, last->b, last->a);

    max = 0.0;
    mi = 0;
    for (i = 1; i < n-1; i++) {
        gwy_rgba_interpolate(first, last, i/(n - 1.0), &color);
        /* Maximum distance is the crucial metric */
        s = fabs(color.r - samples[i].color.r);
        d = fabs(color.g - samples[i].color.g);
        s = fmax(s, d);
        d = fabs(color.b - samples[i].color.b);
        s = fmax(s, d);
        d = fabs(color.a - samples[i].color.a);
        s = fmax(s, d);

        if (s > max) {
            max = s;
            mi = i;
        }
    }
    gwy_debug("Max. difference %f located at %d, {%f: %f, %f, %f, %f}",
              max, mi,
              samples[mi].x,
              samples[mi].color.r,
              samples[mi].color.g,
              samples[mi].color.b,
              samples[mi].color.a);

    if (max < threshold) {
        gwy_debug("Max. difference small enough, stopping recursion");
        return;
    }
    gwy_debug("Inserting new point at %f", samples[mi].x);

    /* Use g_list_alloc() manually, GList functions care too much about list
     * head which is something we don't want here, because we always work
     * in the middle of some list. */
    item = g_list_alloc();
    item->data = (gpointer)(samples + mi);
    item->prev = points;
    item->next = points->next;
    item->prev->next = item;
    item->next->prev = item;

    /* Recurse. */
    refine_interval(points, mi + 1, samples, threshold);
    refine_interval(item, n - mi, samples + mi, threshold);
}

static void
data_changed(GwyGradient *gradient)
{
    gwy_debug("%s", GWY_RESOURCE(gradient)->name->str);
    //GwyGradientPrivate *priv = gradient->priv;
    gwy_resource_data_changed(GWY_RESOURCE(gradient));
}

static GwyGradient*
create_gradient(const gchar *name,
                gint npoints,
                const GwyGradientPoint *points,
                gboolean is_const)
{
    GwyGradient *gradient = g_object_new(GWY_TYPE_GRADIENT,
                                         "name", name,
                                         "const", is_const,
                                         NULL);

    GwyGradientPrivate *priv = gradient->priv;
    if (npoints && points) {
        g_array_set_size(priv->points, 0);
        g_array_append_vals(priv->points, points, npoints);
    }

    return gradient;
}

static void
resource_setup_builtins(GwyInventory *res_inventory)
{
    GwyGradient *gradient = create_gradient(GWY_GRADIENT_DEFAULT, 0, NULL, TRUE);
    gwy_inventory_insert_item(res_inventory, gradient);
    g_object_unref(gradient);
}

static gpointer
resource_copy(gpointer item)
{
    g_return_val_if_fail(GWY_IS_GRADIENT(item), NULL);

    GwyGradient *gradient = GWY_GRADIENT(item);
    GArray *points = gradient->priv->points;
    GwyGradient *copy = create_gradient(gwy_resource_get_name(GWY_RESOURCE(item)),
                                        points->len, &point_index(points, 0), FALSE);

    return copy;
}

static void
resource_dump(GwyResource *resource, GString *str)
{
    g_return_if_fail(GWY_IS_GRADIENT(resource));
    GwyGradient *gradient = GWY_GRADIENT(resource);
    GArray *points = gradient->priv->points;
    g_return_if_fail(points->len > 0);

    for (guint i = 0; i < points->len; i++) {
        GwyGradientPoint *pt = &point_index(points, i);

        gwy_append_doubles_to_gstring(str, (gdouble*)pt, 5, 6, " ", TRUE);
        g_string_append_c(str, '\n');
    }
}

static GwyResource*
resource_parse(const gchar *text)
{
    GwyGradient *gradient = NULL;
    gint npoints = -1, ncols = 5;
    gdouble *data;

    g_return_val_if_fail(text, NULL);
    GwyGradientClass *klass = g_type_class_peek(GWY_TYPE_GRADIENT);
    g_return_val_if_fail(klass, NULL);

    if (!(data = gwy_parse_doubles(text, NULL, 0, &npoints, &ncols, NULL, NULL))) {
        g_warning("Cannot parse color points.");
        goto fail;
    }

    gradient = create_gradient("", npoints, (GwyGradientPoint*)data, FALSE);
    sanitise_gradient(gradient, TRUE);

fail:
    g_free(data);
    return GWY_RESOURCE(gradient);
}

/**
 * gwy_gradients:
 *
 * Gets inventory with all the gradients.
 *
 * Returns: (transfer none): Gradient inventory.
 **/
GwyInventory*
gwy_gradients(void)
{
    return inventory;
}

/**
 * gwy_gradients_get_gradient:
 * @name: (nullable): Gradient name.  May be %NULL to get the default gradient.
 *
 * Convenience function to get a gradient from gwy_gradients() by name.
 *
 * Returns: (transfer none): Gradient identified by @name or the default gradient if @name does not exist.
 **/
GwyGradient*
gwy_gradients_get_gradient(const gchar *name)
{
    g_return_val_if_fail(inventory, NULL);
    return (GwyGradient*)gwy_inventory_get_item_or_default(inventory, name);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyGradient *gradient = GWY_GRADIENT(serializable);
    GwyGradientPrivate *priv = gradient->priv;

    gwy_serializable_group_append_string(group, serializable_items + ITEM_NAME,
                                         gwy_resource_get_name(GWY_RESOURCE(gradient)));
    gdouble *pointdata = (gdouble*)priv->points->data;
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_POINTS, pointdata, 5*priv->points->len);
}

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);

    GwyGradient *gradient = GWY_GRADIENT(serializable);
    GwyGradientPrivate *priv = gradient->priv;

    if (!gwy_check_data_length_multiple(error_list, TYPE_NAME, its[ITEM_POINTS].array_size, 5))
        goto fail;

    it = its + ITEM_POINTS;
    g_array_set_size(priv->points, 0);
    g_array_append_vals(priv->points, it->value.v_double_array, it->array_size/5);
    if (!sanitise_gradient(gradient, FALSE)) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                           _("Invalid gradient data."));
    }

    const gchar *name;
    if ((name = its[ITEM_NAME].value.v_string)) {
        if (strchr(name, '/') || strchr(name, '\\')) {
            gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                               _("Invalid resource name."));
        }
        else
            gwy_resource_rename(GWY_RESOURCE(gradient), name);
    }

fail:
    g_free(its[ITEM_NAME].value.v_string);
    g_free(its[ITEM_POINTS].value.v_double_array);

    return TRUE;
}

static void
copy_points(GwyGradient *source, GwyGradient *destination)
{
    GArray *pointssrc = source->priv->points, *pointsdest = destination->priv->points;
    g_array_set_size(pointsdest, 0);
    g_array_append_vals(pointsdest, &point_index(pointssrc, 0), pointssrc->len);
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GArray *points = GWY_GRADIENT(serializable)->priv->points;
    GwyGradient *copy = create_gradient(gwy_resource_get_name(GWY_RESOURCE(serializable)),
                                        points->len, &point_index(points, 0), FALSE);
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    copy_points(GWY_GRADIENT(source), GWY_GRADIENT(destination));
    /* We do not rename the destination resource. It would be wrong for managed resources and it would be confusing to
     * do it only for unmanaged resources. */
    gwy_resource_data_changed(GWY_RESOURCE(destination));
}

#if 0
/**
 * gwy_gradient_copy:
 * @gradient: A gradient to duplicate.
 *
 * Create a new gradient as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * It creates a new unmanaged gradient. Use gwy_inventory_new_item() to copy managed gradients.
 *
 * Returns: (transfer full):
 *          A copy of the gradient.
 **/
GwyGradient*
gwy_gradient_copy(GwyGradient *gradient)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_GRADIENT(gradient)) {
        g_assert(GWY_IS_GRADIENT(gradient));
        return g_object_new(GWY_TYPE_GRADIENT, NULL);
    }
    return GWY_GRADIENT(serializable_copy(GWY_SERIALIZABLE(gradient)));
}

/**
 * gwy_gradient_assign:
 * @destination: Target gradient.
 * @source: Source gradient.
 *
 * Makes one gradient equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 *
 * The resource name of @destination is not changed. It makes the function useful for both unmanaged and managed
 * gradients (whose name cannot be changed to an already existing gradient). However, if you need to assign both the
 * data and name to an unmanaged gradient you need to complete the operation with gwy_resource_rename() to @source's
 * name.
 **/
void
gwy_gradient_assign(GwyGradient *destination, GwyGradient *source)
{
    g_return_if_fail(GWY_IS_GRADIENT(destination));
    g_return_if_fail(GWY_IS_GRADIENT(source));
    if (destination != source)
        serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}
#endif

/**
 * SECTION: gradient
 * @title: GwyGradient
 * @short_description: A map from numbers to RGBA colors
 * @see_also: <link linkend="libgwyui-pixbuf-render">pixbuf-render</link> --
 *            low level functions for painting data fields,
 *            #GwyInventory -- the container holding all gradients,
 *            #GwyDataView -- 2D data display widget,
 *            #GwyColorAxis -- false color axis widget
 *
 * Gradient is a map from interval [0,1] to RGB(A) color space. Each gradient is defined by an ordered set of color
 * points, the first of them is always at 0.0, the last at 1.0 (thus each gradient must consist of at least two
 * points).  Between them, the color is interpolated.  Color points of modifiable gradients (see #GwyResource) can be
 * edited with functions like gwy_gradient_insert_point(), gwy_gradient_set_point_color(), or
 * gwy_gradient_set_points().
 *
 * Gradient objects can be obtained from gwy_gradients_get_gradient(). New gradients can be created with
 * gwy_inventory_new_item() on the #GwyInventory returned by gwy_gradients().
 **/

/**
 * GwyGradientPoint:
 * @x: Color point position (in interval [0,1]).
 * @color: The color at position @x.
 *
 * Gradient color point struct.
 **/

/**
 * GWY_GRADIENT_DEFAULT:
 *
 * The name of the default gray color gradient.
 *
 * It is guaranteed to always exist.
 *
 * Note this is not the same as user's default gradient which corresponds to the default item in gwy_gradients()
 * inventory and it changes over time.
 **/

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