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

#include "tests/testlibgwy.h"

#define gwy_unit_is_empty(u) gwy_unit_equal_string(u, NULL)

G_GNUC_UNUSED
static void
dump_scatter(GwyScatter *scatter, const gchar *name)
{
    gsize n = gwy_scatter_get_n_points(scatter);
    gint nvalues = gwy_scatter_get_n_values(scatter);
    g_printerr("=====[ %s (%lux%d) ]=====\n", name, (gulong)n, nvalues);
    const GwyXY *xy = gwy_scatter_get_xy_const(scatter);
    const gdouble *d = nvalues ? gwy_scatter_get_data_const(scatter, 0) : NULL;
    for (gsize i = 0; i < n; i++) {
        g_printerr("[%6g %6g]", xy[i].x, xy[i].y);
        for (gint j = 0; j < nvalues; j++)
            g_printerr(" %6g",d[j*n + i]);
        g_printerr("\n");
    }
}

static void
assert_scatter_xy(GwyScatter *scatter, const GwyXY *ref_xy, guint ref_n, gdouble eps)
{
    g_assert_true(GWY_IS_SCATTER(scatter));
    guint n = gwy_scatter_get_n_points(scatter);
    g_assert_cmpuint(n, ==, ref_n);
    if (!n)
        return;

    const GwyXY *xy = gwy_scatter_get_xy_const(scatter);
    g_assert_nonnull(xy);

    if (eps > 0.0) {
        for (guint i = 0; i < ref_n; i++) {
            g_assert_cmpfloat_with_epsilon(xy[i].x, ref_xy[i].x, eps);
            g_assert_cmpfloat_with_epsilon(xy[i].y, ref_xy[i].y, eps);
        }
    }
    else {
        for (guint i = 0; i < ref_n; i++) {
            g_assert_cmpfloat(xy[i].x, ==, ref_xy[i].x);
            g_assert_cmpfloat(xy[i].y, ==, ref_xy[i].y);
        }
    }
}

static void
assert_scatter_data(GwyScatter *scatter, const gdouble *ref_data, guint ref_n, guint ref_nvalues, gdouble eps)
{
    g_assert_true(GWY_IS_SCATTER(scatter));
    guint n = gwy_scatter_get_n_points(scatter);
    guint nvalues = gwy_scatter_get_n_values(scatter);
    g_assert_cmpuint(n, ==, ref_n);
    g_assert_cmpuint(nvalues, ==, ref_nvalues);
    if (!n || !nvalues)
        return;

    for (guint i = 0; i < ref_nvalues; i++) {
        const gdouble *data = gwy_scatter_get_data_const(scatter, i);
        g_assert_nonnull(data);

        if (eps > 0.0) {
            for (guint j = 0; j < ref_n; j++)
                g_assert_cmpfloat_with_epsilon(data[j], ref_data[i*n + j], eps);
        }
        else {
            for (guint j = 0; j < ref_n; j++)
                g_assert_cmpfloat(data[j], ==, ref_data[i*n + j]);
        }
    }
}

static void
scatter_assert_equal(GObject *object, GObject *reference)
{
    g_assert_true(GWY_IS_SCATTER(object));
    g_assert_true(GWY_IS_SCATTER(reference));

    GwyScatter *scatter = GWY_SCATTER(object), *scatter_ref = GWY_SCATTER(reference);
    g_assert_cmpint(gwy_scatter_get_n_points(scatter), ==, gwy_scatter_get_n_points(scatter_ref));
    g_assert_cmpint(gwy_scatter_get_n_values(scatter), ==, gwy_scatter_get_n_values(scatter_ref));
    guint nvalues = gwy_scatter_get_n_values(scatter_ref);
    guint npoints = gwy_scatter_get_n_points(scatter_ref);
    g_assert_true(gwy_unit_equal(gwy_scatter_get_unit_xy(scatter), gwy_scatter_get_unit_xy(scatter_ref)));
    for (guint i = 0; i < nvalues; i++) {
        g_assert_true(gwy_unit_equal(gwy_scatter_get_unit_data(scatter, i), gwy_scatter_get_unit_data(scatter_ref, i)));
        g_assert_cmpstr(gwy_scatter_get_data_label(scatter, i), ==, gwy_scatter_get_data_label(scatter_ref, i));
    }
    assert_scatter_xy(scatter, gwy_scatter_get_xy_const(scatter_ref), npoints, 0.0);
    // NB: Here we assume completely flat internal representation, which the API does not really officially guarantee.
    assert_scatter_data(scatter, gwy_scatter_get_data_const(scatter_ref, 0), npoints, nvalues, 0.0);
}

void
test_scatter_basic(void)
{
    const GwyXYZ xyz[5] = {
        { 1, 2, 3 },
        { G_PI, G_PI, G_PI },
        { -1.3, 0.0, 330.0 },
        { 10.5, 11.5, -4.25 },
        { 1e-6, 1e-7, 1e-9 },
    };
    guint n = G_N_ELEMENTS(xyz);
    GwyXY xy[n];
    gdouble data[n];

    for (guint i = 0; i < n; i++) {
        xy[i].x = xyz[i].x;
        xy[i].y = xyz[i].y;
        data[i] = xyz[i].z;
    }

    GwyScatter *scatter1 = gwy_scatter_new_from_xyz(xyz, 5);
    assert_scatter_xy(scatter1, xy, 5, 0.0);
    assert_scatter_data(scatter1, data, 5, 1, 0.0);
    g_assert_finalize_object(scatter1);

    GwyScatter *scatter2 = gwy_scatter_new_from_data(xy, data, 1, 5);
    assert_scatter_xy(scatter2, xy, 5, 0.0);
    assert_scatter_data(scatter2, data, 5, 1, 0.0);
    g_assert_finalize_object(scatter2);
}

void
test_scatter_data_changed(void)
{
    GwyScatter *scatter = gwy_scatter_new(1);
    guint item_changed = 0;
    g_signal_connect_swapped(scatter, "data-changed", G_CALLBACK(record_signal), &item_changed);
    gwy_scatter_data_changed(scatter);
    g_assert_cmpuint(item_changed, ==, 1);
    gwy_scatter_data_changed(scatter);
    g_assert_cmpuint(item_changed, ==, 2);
    gwy_scatter_xy_changed(scatter);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_finalize_object(scatter);
}

void
test_scatter_xy_changed(void)
{
    GwyScatter *scatter = gwy_scatter_new(2);
    guint item_changed = 0;
    g_signal_connect_swapped(scatter, "xy-changed", G_CALLBACK(record_signal), &item_changed);
    gwy_scatter_xy_changed(scatter);
    g_assert_cmpuint(item_changed, ==, 1);
    gwy_scatter_xy_changed(scatter);
    g_assert_cmpuint(item_changed, ==, 2);
    gwy_scatter_data_changed(scatter);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_finalize_object(scatter);
}

static GwyScatter*
create_scatter_for_serialisation(void)
{
    const GwyXY xy[5] = {
        { 1, 2 },
        { G_PI, G_PI },
        { -1.3, 0.0 },
        { 10.5, 11.5 },
        { 1e-6, 1e-7 },
    };
    const gdouble data[2*5] = {
        7.5, G_SQRT2, -1.0, 0.0, G_PI,
        -2.5, 1e9, 0.125, -5.0, G_LN2,
    };

    GwyScatter *scatter = gwy_scatter_new_from_data(xy, data, 2, 5);
    return scatter;
}

static void
set_scatter_units_for_serialisation(GwyScatter *scatter)
{
    gwy_unit_set_from_string(gwy_scatter_get_unit_xy(scatter), "m");
    // Set only the second unit to check handling of unset units.
    gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 1), "s");
}

static void
set_scatter_labels_for_serialisation(GwyScatter *scatter)
{
    gwy_scatter_set_data_label(scatter, 0, "Unknown");
    gwy_scatter_set_data_label(scatter, 1, "Time");
}

void
test_scatter_serialization(void)
{
    GwyScatter *scatter = create_scatter_for_serialisation();
    serialize_object_and_back(G_OBJECT(scatter), scatter_assert_equal, FALSE, NULL);

    set_scatter_units_for_serialisation(scatter);
    serialize_object_and_back(G_OBJECT(scatter), scatter_assert_equal, FALSE, NULL);

    set_scatter_labels_for_serialisation(scatter);
    serialize_object_and_back(G_OBJECT(scatter), scatter_assert_equal, FALSE, NULL);

    g_assert_finalize_object(scatter);
}

void
test_scatter_copy(void)
{
    GwyScatter *scatter = create_scatter_for_serialisation();
    serializable_test_copy(GWY_SERIALIZABLE(scatter), scatter_assert_equal);

    set_scatter_units_for_serialisation(scatter);
    serializable_test_copy(GWY_SERIALIZABLE(scatter), scatter_assert_equal);

    set_scatter_labels_for_serialisation(scatter);
    serializable_test_copy(GWY_SERIALIZABLE(scatter), scatter_assert_equal);

    g_assert_finalize_object(scatter);
}

void
test_scatter_assign(void)
{
    GwyScatter *scatter = create_scatter_for_serialisation();
    serializable_test_assign(GWY_SERIALIZABLE(scatter), NULL, scatter_assert_equal);

    set_scatter_units_for_serialisation(scatter);
    serializable_test_assign(GWY_SERIALIZABLE(scatter), NULL, scatter_assert_equal);

    set_scatter_labels_for_serialisation(scatter);
    serializable_test_assign(GWY_SERIALIZABLE(scatter), NULL, scatter_assert_equal);

    const GwyXYZ xyz[1] = { { 7, 8, 9 } };
    GwyScatter *another = gwy_scatter_new_from_xyz(xyz, 1);
    gwy_unit_set_from_string(gwy_scatter_get_unit_xy(another), "A");
    gwy_unit_set_from_string(gwy_scatter_get_unit_data(another, 0), "kg");
    serializable_test_assign(GWY_SERIALIZABLE(scatter), GWY_SERIALIZABLE(another), scatter_assert_equal);
    g_assert_finalize_object(another);

    g_assert_finalize_object(scatter);
}

void
test_scatter_data_labels(void)
{
    GwyScatter *scatter = gwy_scatter_new(3);

    for (gint i = 0; i < 3; i++)
        g_assert_null(gwy_scatter_get_data_label(scatter, i));

    gwy_scatter_set_data_label(scatter, 1, "Labelled data");
    for (gint i = 0; i < 3; i++)
        g_assert_nonnull(gwy_scatter_get_data_label(scatter, i));

    gwy_scatter_clear_data_labels(scatter);
    for (gint i = 0; i < 3; i++)
        g_assert_null(gwy_scatter_get_data_label(scatter, i));

    g_assert_finalize_object(scatter);
}

void
test_scatter_find_data(void)
{
    GwyScatter *scatter = gwy_scatter_new(5);

    // The silly case.
    g_assert_cmpint(gwy_scatter_find_data_by_label(scatter, NULL), ==, 0);

    gwy_scatter_set_data_label(scatter, 3, "Hold");
    gwy_scatter_set_data_label(scatter, 2, "Hold");
    gwy_scatter_set_data_label(scatter, 4, "Retract");
    g_assert_cmpint(gwy_scatter_find_data_by_label(scatter, NULL), ==, -1);
    g_assert_cmpint(gwy_scatter_find_data_by_label(scatter, "Definitely not a data name"), ==, -1);
    g_assert_cmpint(gwy_scatter_find_data_by_label(scatter, "Hold"), ==, 2);
    g_assert_cmpint(gwy_scatter_find_data_by_label(scatter, "Retract"), ==, 4);

    g_assert_finalize_object(scatter);
}

void
test_scatter_from_field(void)
{
    const gdouble data[2*3] = { G_PI, -3.0, 0.0, 1.5, G_E, 1e6 };
    gdouble dx = 2.0, dy = 0.5;
    GwyField *field = gwy_field_new(3, 2, 3*dx, 2*dy, FALSE);
    gwy_assign(gwy_field_get_data(field), data, 2*3);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");
    gwy_unit_set_from_string(gwy_field_get_unit_z(field), "A");
    gwy_field_set_xoffset(field, -1.0);
    gwy_field_set_yoffset(field, -3.0);

    GwyScatter *scatter = gwy_scatter_new(0);
    gwy_scatter_set_from_field(scatter, field);

    GwyXY expected_xy[6] = {
        { -1.0 + 0.5*dx, -3.0 + 0.5*dy },
        { -1.0 + 1.5*dx, -3.0 + 0.5*dy },
        { -1.0 + 2.5*dx, -3.0 + 0.5*dy },
        { -1.0 + 0.5*dx, -3.0 + 1.5*dy },
        { -1.0 + 1.5*dx, -3.0 + 1.5*dy },
        { -1.0 + 2.5*dx, -3.0 + 1.5*dy },
    };

    assert_scatter_xy(scatter, expected_xy, 2*3, 0.0);
    assert_scatter_data(scatter, data, 2*3, 1, 0.0);

    g_assert_finalize_object(scatter);
    g_assert_finalize_object(field);
}

void
test_scatter_pack_normal(void)
{
    enum { n = 3, nval = 3 };
    const GwyXY xy[n] = { { 0.1, G_PI }, { -1.5, G_E }, { 0.0, G_LN2 } };
    const gdouble data[nval*n] = { 7.1, 7.2, 7.7, -2.5, 2.0, -2.1, 6.5, 1e9, -1e9 };

    GwyScatter *scatter = gwy_scatter_new_from_data(xy, data, nval, n);

    gdouble *packed_all = gwy_scatter_pack(scatter, -1);
    const gdouble expected_all[(nval + 2)*n] = {
        0.1,  G_PI,  7.1, -2.5, 6.5,
        -1.5, G_E,   7.2, 2.0,  1e9,
        0.0,  G_LN2, 7.7, -2.1, -1e9
    };
    for (guint i = 0; i < G_N_ELEMENTS(expected_all); i++)
        g_assert_cmpfloat(packed_all[i], ==, expected_all[i]);
    g_free(packed_all);

    gdouble *packed_1 = gwy_scatter_pack(scatter, 1);
    const gdouble expected_1[3*n] = {
        0.1, G_PI, -2.5,
        -1.5, G_E, 2.0,
        0.0, G_LN2, -2.1,
    };
    for (guint i = 0; i < G_N_ELEMENTS(expected_1); i++)
        g_assert_cmpfloat(packed_1[i], ==, expected_1[i]);
    g_free(packed_1);

    g_assert_finalize_object(scatter);
}

void
test_scatter_pack_novalues(void)
{
    enum { n = 3 };
    const GwyXY xy[n] = { { 0.1, G_PI }, { -1.5, G_E }, { 0.0, G_LN2 } };

    GwyScatter *scatter = gwy_scatter_new_from_data(xy, NULL, 0, n);

    gdouble *packed_all = gwy_scatter_pack(scatter, -1);
    const gdouble expected_all[2*n] = {
        0.1, G_PI,
        -1.5, G_E,
        0.0, G_LN2,
    };
    for (guint i = 0; i < G_N_ELEMENTS(expected_all); i++)
        g_assert_cmpfloat(packed_all[i], ==, expected_all[i]);
    g_free(packed_all);

    g_assert_finalize_object(scatter);
}

void
test_scatter_pack_set_from(void)
{
    enum { n = 3, nval = 3 };
    const gdouble packed_all[(nval + 2)*n] = {
        0.1,  G_PI,  7.1, -2.5, 6.5,
        -1.5, G_E,   7.2, 2.0,  1e9,
        0.0,  G_LN2, 7.7, -2.1, -1e9
    };

    GwyScatter *scatter = gwy_scatter_new(1);
    gwy_scatter_set_from_packed(scatter, packed_all, nval, n);

    const GwyXY expected_xy[n] = { { 0.1, G_PI }, { -1.5, G_E }, { 0.0, G_LN2 } };
    const gdouble expected_data[nval*n] = { 7.1, 7.2, 7.7, -2.5, 2.0, -2.1, 6.5, 1e9, -1e9 };

    const GwyXY *xy = gwy_scatter_get_xy_const(scatter);
    for (guint i = 0; i < n; i++) {
        g_assert_cmpfloat(xy[i].x, ==, expected_xy[i].x);
        g_assert_cmpfloat(xy[i].y, ==, expected_xy[i].y);
    }

    for (guint m = 0; m < nval; m++) {
        const gdouble *data = gwy_scatter_get_data_const(scatter, m);
        for (guint i = 0; i < n; i++)
            g_assert_cmpfloat(data[i], ==, expected_data[n*m + i]);
    }

    g_assert_finalize_object(scatter);
}

static GwyScatter*
make_small_scatter_for_filtering(guint32 keepbits)
{
    enum { filter_small_nval = 4, filter_small_n = 7 };
    static const gdouble filter_small_packed_data[filter_small_n][filter_small_nval + 2] = {
        { 0.0,  1.0,  1.1, 1.2e6, 3e-9,  G_PI,        },
        { 1.5,  0.3,  1.2, 1.7e6, -2e-9, G_E,         },
        { 0.1,  0.22, 0.9, 2e6,   -5e-9, G_SQRT2,     },
        { -0.1, 1.5,  1.3, 1.9e6, -1e-9, GWY_SQRT3,   },
        { 1.4,  -0.6, 1.0, 2.1e6, 1e-9,  GWY_SQRT_PI, },
        { -1.3, 1.4,  1.4, 1.6e6, 4e-9,  G_LN2,       },
        { 2.0,  -0.1, 1.0, 1.4e6, -3e-9, G_LN10,      },
    };

    GwyScatter *scatter = gwy_scatter_new(4);
    gwy_scatter_set_data_label(scatter, 1, "Height");
    gwy_scatter_set_data_label(scatter, 2, "Current");
    gwy_unit_set_from_string(gwy_scatter_get_unit_xy(scatter), "m");
    gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 0), "s");
    gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 1), "m");
    gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 2), "A");

    gdouble *pdata = g_new(gdouble, filter_small_n*(filter_small_nval + 2));
    guint count = 0;
    for (guint i = 0; i < filter_small_n; i++) {
        if (keepbits & (1u << i)) {
            gdouble *dest = pdata + count*(filter_small_nval + 2);
            gwy_assign(dest, filter_small_packed_data[i], filter_small_nval + 2);
            count++;
        }
    }
    gwy_scatter_set_from_packed(scatter, pdata, filter_small_nval, count);
    g_free(pdata);

    return scatter;
}

void
test_scatter_subset_new_small_empty(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    g_assert_cmpuint(gwy_scatter_get_n_points(scatter), <, 32);
    guint32 keep = 0;
    GwyScatter *filtered = gwy_scatter_new_subset(scatter, &keep);

    GwyScatter *expected = gwy_scatter_new_alike(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_subset_new_small_full(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    g_assert_cmpuint(gwy_scatter_get_n_points(scatter), <, 32);
    guint32 keep = G_MAXUINT32;
    GwyScatter *filtered = gwy_scatter_new_subset(scatter, &keep);

    GwyScatter *expected = gwy_scatter_copy(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_subset_new_small_general(void)
{
    guint n = g_test_slow() ? 100 : 20;

    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT);
    g_assert_cmpuint(gwy_scatter_get_n_points(scatter), <, 32);

    for (guint i = 0; i < n; i++) {
        guint32 keep = g_test_rand_int();
        GwyScatter *filtered = gwy_scatter_new_subset(scatter, &keep);
        GwyScatter *expected = make_small_scatter_for_filtering(keep);
        scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(expected);
    }

    g_assert_finalize_object(scatter);
}

void
test_scatter_subset_inplace_small_empty(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    g_assert_cmpuint(gwy_scatter_get_n_points(scatter), <, 32);
    guint32 keep = 0;
    GwyScatter *filtered = gwy_scatter_copy(scatter);
    gwy_scatter_reduce_to_subset(filtered, &keep);

    GwyScatter *expected = gwy_scatter_new_alike(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_subset_inplace_small_full(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    g_assert_cmpuint(gwy_scatter_get_n_points(scatter), <, 32);
    guint32 keep = G_MAXUINT32;
    GwyScatter *filtered = gwy_scatter_copy(scatter);
    gwy_scatter_reduce_to_subset(filtered, &keep);

    GwyScatter *expected = gwy_scatter_copy(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_subset_inplace_small_general(void)
{
    guint n = g_test_slow() ? 100 : 20;

    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT);
    g_assert_cmpuint(gwy_scatter_get_n_points(scatter), <, 32);

    for (guint i = 0; i < n; i++) {
        guint32 keep = g_test_rand_int();
        GwyScatter *filtered = gwy_scatter_copy(scatter);
        gwy_scatter_reduce_to_subset(filtered, &keep);
        GwyScatter *expected = make_small_scatter_for_filtering(keep);
        scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(expected);
    }

    g_assert_finalize_object(scatter);
}

static gboolean
filter_func_none(G_GNUC_UNUSED const GwyXY *xy,
                 G_GNUC_UNUSED const gdouble *values,
                 G_GNUC_UNUSED gpointer user_data)
{
    return FALSE;
}

static gboolean
filter_func_all(G_GNUC_UNUSED const GwyXY *xy,
                G_GNUC_UNUSED const gdouble *values,
                G_GNUC_UNUSED gpointer user_data)
{
    return TRUE;
}

static gboolean
filter_func_matchxy(const GwyXY *xy,
                    G_GNUC_UNUSED const gdouble *values,
                    gpointer user_data)
{
    GwyScatter *reference = (GwyScatter*)user_data;
    const GwyXY *refxy = gwy_scatter_get_xy(reference);
    gsize n = gwy_scatter_get_n_points(reference);
    for (gsize k = 0; k < n; k++) {
        if (xy->x == refxy[k].x && xy->y == refxy[k].y)
            return TRUE;
    }
    return FALSE;
}

void
test_scatter_filter_new_small_empty(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    GwyScatter *filtered = gwy_scatter_new_filtered(scatter, filter_func_none, NULL);

    GwyScatter *expected = gwy_scatter_new_alike(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_filter_new_small_full(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    GwyScatter *filtered = gwy_scatter_new_filtered(scatter, filter_func_all, NULL);

    GwyScatter *expected = gwy_scatter_copy(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_filter_new_small_general(void)
{
    guint n = g_test_slow() ? 100 : 20;

    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT);

    for (guint i = 0; i < n; i++) {
        guint32 keep = g_test_rand_int();
        GwyScatter *expected = make_small_scatter_for_filtering(keep);
        GwyScatter *filtered = gwy_scatter_new_filtered(scatter, filter_func_matchxy, expected);
        scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(expected);
    }

    g_assert_finalize_object(scatter);
}

void
test_scatter_filter_inplace_small_empty(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    GwyScatter *filtered = gwy_scatter_copy(scatter);
    gwy_scatter_filter(filtered, filter_func_none, NULL);

    GwyScatter *expected = gwy_scatter_new_alike(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_filter_inplace_small_full(void)
{
    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT32);
    GwyScatter *filtered = gwy_scatter_copy(scatter);
    gwy_scatter_filter(filtered, filter_func_all, NULL);

    GwyScatter *expected = gwy_scatter_copy(scatter);
    scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));

    g_assert_finalize_object(filtered);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(scatter);
}

void
test_scatter_filter_inplace_small_general(void)
{
    guint n = g_test_slow() ? 100 : 20;

    GwyScatter *scatter = make_small_scatter_for_filtering(G_MAXUINT);

    for (guint i = 0; i < n; i++) {
        guint32 keep = g_test_rand_int();
        GwyScatter *expected = make_small_scatter_for_filtering(keep);
        GwyScatter *filtered = gwy_scatter_copy(scatter);
        gwy_scatter_filter(filtered, filter_func_matchxy, expected);
        scatter_assert_equal(G_OBJECT(filtered), G_OBJECT(expected));
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(expected);
    }

    g_assert_finalize_object(scatter);
}

void
test_scatter_new_part(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gint nvalues = g_test_rand_int_range(0, 5);
        gsize npoints = g_test_rand_int_range(0, 200);
        GwyXY *xy = g_new(GwyXY, npoints);
        gdouble *data = g_new(gdouble, npoints*nvalues);

        gdouble xmin = g_test_rand_double_range(-12.0, 12.0);
        gdouble ymin = g_test_rand_double_range(-12.0, 12.0);
        gdouble xmax = g_test_rand_double_range(-12.0, 12.0);
        gdouble ymax = g_test_rand_double_range(-12.0, 12.0);
        GWY_ORDER(gdouble, xmin, xmax);
        GWY_ORDER(gdouble, ymin, ymax);

        gsize inside_count = 0;
        for (gsize k = 0; k < npoints; k++) {
            xy[k].x = g_test_rand_double_range(-10.0, 10.0);
            xy[k].y = g_test_rand_double_range(-10.0, 10.0);
            if (xy[k].x >= xmin && xy[k].x <= xmax && xy[k].y >= ymin && xy[k].y <= ymax)
                inside_count++;

            for (gint m = 0; m < nvalues; m++)
                data[m*npoints + k] = g_test_rand_double();
        }
        GwyScatter *scatter = gwy_scatter_new_from_data(xy, data, nvalues, npoints);

        GwyScatter *part = gwy_scatter_new_part(scatter, xmin, xmax, ymin, ymax);
        gwy_scatter_invalidate(part);

        // Check:
        // 1. All remaining points are within given bounds.
        // 2. There is the expected number of points.
        // Aside from breaking uniqueness, this ensures the part was constructed correctly.
        if (gwy_scatter_get_n_points(part)) {
            gdouble pxmin, pxmax, pymin, pymax;
            gwy_scatter_xrange(part, &pxmin, &pxmax);
            gwy_scatter_yrange(part, &pymin, &pymax);
            g_assert_cmpfloat(pxmin, >=, xmin);
            g_assert_cmpfloat(pxmax, <=, xmax);
            g_assert_cmpfloat(pymin, >=, ymin);
            g_assert_cmpfloat(pymax, <=, ymax);
        }
        g_assert_cmpuint(gwy_scatter_get_n_points(part), ==, inside_count);

        g_assert_finalize_object(part);
        g_assert_finalize_object(scatter);
        g_free(xy);
        g_free(data);
    }
}

void
test_scatter_insert(void)
{
    enum { npoints = 3, nvalues = 2 };
    static const GwyXY xy1[npoints] = { { 1.0, -2.0 }, { G_PI, G_E }, { 1e30, -4e30 } };
    static const gdouble data1[nvalues*npoints] = { 1.5, 4.2, 0.6, 8.8, -1e4, 1e7 };

    for (gint i = 0; i < (nvalues + 1)*(nvalues + 1); i++) {
        gint insert_pos = i % (nvalues + 1), insert_nvalues = i/(nvalues + 1);

        GwyScatter *scatter = gwy_scatter_new_sized(nvalues, npoints);
        gwy_assign(gwy_scatter_get_xy(scatter), xy1, npoints);
        gwy_assign(gwy_scatter_get_data(scatter, 0), data1, nvalues*npoints);
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 0), "m");
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 1), "A");
        gwy_scatter_set_data_label(scatter, 0, "Topography");
        gwy_scatter_set_data_label(scatter, 1, "Current");
        gwy_scatter_insert_values(scatter, insert_pos, insert_nvalues);

        // Indices where we expect the existing data, units and labels to be moved to.
        gint expected_positions[nvalues];
        for (gint pos = 0; pos < nvalues; pos++) {
            if (pos < insert_pos)
                expected_positions[pos] = pos;
            else
                expected_positions[pos] = pos + insert_nvalues;
        }

        g_assert_cmpuint(gwy_scatter_get_n_points(scatter), ==, npoints);
        g_assert_cmpint(gwy_scatter_get_n_values(scatter), ==, nvalues + insert_nvalues);
        assert_scatter_xy(scatter, xy1, npoints, 0.0);

        g_assert_true(gwy_unit_equal_string(gwy_scatter_get_unit_data(scatter, expected_positions[0]), "m"));
        g_assert_true(gwy_unit_equal_string(gwy_scatter_get_unit_data(scatter, expected_positions[1]), "A"));
        for (gint j = 0; j < insert_nvalues; j++)
            g_assert_true(gwy_unit_is_empty(gwy_scatter_get_unit_data(scatter, insert_pos + j)));
        g_assert_cmpstr(gwy_scatter_get_data_label(scatter, expected_positions[0]), ==, "Topography");
        g_assert_cmpstr(gwy_scatter_get_data_label(scatter, expected_positions[1]), ==, "Current");

        for (gint j = 0; j < nvalues; j++) {
            const gdouble *data = gwy_scatter_get_data(scatter, expected_positions[j]);
            const gdouble *expected_data = data1 + npoints*j;
            for (gsize k = 0; k < npoints; k++)
                g_assert_cmpfloat(data[k], ==, expected_data[k]);
        }
        g_assert_finalize_object(scatter);
    }
}

void
test_scatter_remove(void)
{
    enum { npoints = 2, nvalues = 4 };
    static const GwyXY xy1[npoints] = { { 1.0, -2.0 }, { G_PI, G_E } };
    static const gdouble data1[nvalues*npoints] = { 1.5, 4.2, 5.5, 3.0, -1e4, 1e7, -1e6, 2e8 };

    for (gint i = 0; i < (nvalues + 1)*(nvalues + 1); i++) {
        // There are only (nvalues + 1)*nvalues/2 valid combinations. Enumerating them properly is too messy, so just
        // go through all (nvalues + 1)² and discard the invalid ones.
        gint remove_pos = i % (nvalues + 1), remove_nvalues = i/(nvalues + 1);
        if (remove_pos + remove_nvalues > nvalues)
            continue;

        GwyScatter *scatter = gwy_scatter_new_sized(nvalues, npoints);
        gwy_assign(gwy_scatter_get_xy(scatter), xy1, npoints);
        gwy_assign(gwy_scatter_get_data(scatter, 0), data1, nvalues*npoints);
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 0), "m");
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 1), "A");
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 2), "s");
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter, 3), "Hz");
        gwy_scatter_set_data_label(scatter, 0, "Topography");
        gwy_scatter_set_data_label(scatter, 1, "Current");
        gwy_scatter_set_data_label(scatter, 2, "Time");
        gwy_scatter_set_data_label(scatter, 3, "Frequency");
        gwy_scatter_remove_values(scatter, remove_pos, remove_nvalues);

        // Indices where we expect the existing data, units and labels to be moved to.
        gint expected_positions[nvalues];
        for (gint pos = 0; pos < nvalues; pos++) {
            if (pos < remove_pos)
                expected_positions[pos] = pos;
            else if (pos >= remove_pos + remove_nvalues)
                expected_positions[pos] = pos - remove_nvalues;
            else
                expected_positions[pos] = -1;
        }

        g_assert_cmpuint(gwy_scatter_get_n_points(scatter), ==, npoints);
        g_assert_cmpint(gwy_scatter_get_n_values(scatter), ==, nvalues - remove_nvalues);
        assert_scatter_xy(scatter, xy1, npoints, 0.0);

        if (expected_positions[0] >= 0) {
            g_assert_true(gwy_unit_equal_string(gwy_scatter_get_unit_data(scatter, expected_positions[0]), "m"));
            g_assert_cmpstr(gwy_scatter_get_data_label(scatter, expected_positions[0]), ==, "Topography");
        }
        if (expected_positions[1] >= 0) {
            g_assert_true(gwy_unit_equal_string(gwy_scatter_get_unit_data(scatter, expected_positions[1]), "A"));
            g_assert_cmpstr(gwy_scatter_get_data_label(scatter, expected_positions[1]), ==, "Current");
        }
        if (expected_positions[2] >= 0) {
            g_assert_true(gwy_unit_equal_string(gwy_scatter_get_unit_data(scatter, expected_positions[2]), "s"));
            g_assert_cmpstr(gwy_scatter_get_data_label(scatter, expected_positions[2]), ==, "Time");
        }
        if (expected_positions[3] >= 0) {
            g_assert_true(gwy_unit_equal_string(gwy_scatter_get_unit_data(scatter, expected_positions[3]), "Hz"));
            g_assert_cmpstr(gwy_scatter_get_data_label(scatter, expected_positions[3]), ==, "Frequency");
        }

        for (gint j = 0; j < nvalues; j++) {
            if (expected_positions[j] < 0)
                continue;
            const gdouble *data = gwy_scatter_get_data(scatter, expected_positions[j]);
            const gdouble *expected_data = data1 + npoints*j;
            for (gsize k = 0; k < npoints; k++)
                g_assert_cmpfloat(data[k], ==, expected_data[k]);
        }
        g_assert_finalize_object(scatter);
    }
}

void
test_scatter_stack(void)
{
    enum { npoints = 2, nvalues1 = 1, nvalues2 = 3, nvalues3 = 0 };
    static const GwyXY xy1[npoints] = { { 1.0, -2.0 }, { G_PI, G_E } };
    static const gdouble data1[nvalues1*npoints] = { 1.5, 5.5 };
    static const gdouble data2[nvalues2*npoints] = { -300.0, -400.0, 1e6, 1e7, G_SQRT2, G_LN10 };

    for (gint i = 0; i < 3*3*3*3; i++) {
        gint nstack = i/27, iorder = i % 27;
        // There are only 3! valid orders. Enumerating permutation is messy, so just go through all combinations with
        // repetitions and discard the invalid cases.
        gint order[3] = { iorder/9, (iorder/3) % 3, iorder % 3 };
        if (order[0] == order[1] || order[1] == order[2] || order[2] == order[0])
            continue;

        GwyScatter *scatter1 = gwy_scatter_new_sized(nvalues1, npoints);
        GwyScatter *scatter2 = gwy_scatter_new_sized(nvalues2, npoints);
        GwyScatter *scatter3 = gwy_scatter_new_sized(nvalues3, npoints);
        GwyScatter *orig_scatters[3] = { scatter1, scatter2, scatter3 };
        for (gint j = 0; j < 3; j++)
            gwy_assign(gwy_scatter_get_xy(orig_scatters[j]), xy1, npoints);
        gwy_assign(gwy_scatter_get_data(scatter1, 0), data1, nvalues1*npoints);
        gwy_assign(gwy_scatter_get_data(scatter2, 0), data2, nvalues2*npoints);
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter1, 0), "m");
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter2, 0), "A");
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter2, 1), "s");
        gwy_unit_set_from_string(gwy_scatter_get_unit_data(scatter2, 1), "Hz");
        // Leave labels in one of the operands unset to see what happens.
        gwy_scatter_set_data_label(scatter2, 0, "Current");
        gwy_scatter_set_data_label(scatter2, 1, "Time");
        gwy_scatter_set_data_label(scatter2, 2, "Frequency");

        GwyScatter *scatters[3] = { orig_scatters[order[0]], orig_scatters[order[1]], orig_scatters[order[2]] };
        // Keep the original pre-stacked first argument in the array for reference.
        GwyScatter *stacked = gwy_scatter_copy(scatters[0]);

        gwy_scatter_stack(stacked, scatters + 1, nstack);

        gint expected_nvalues = 0;
        gboolean expecting_labels = FALSE;
        for (gint j = 0; j <= nstack; j++) {
            expected_nvalues += gwy_scatter_get_n_values(scatters[j]);
            if (gwy_scatter_get_n_values(scatters[j]) && gwy_scatter_get_data_label(scatters[j], 0))
                expecting_labels = TRUE;
        }

        g_assert_cmpuint(gwy_scatter_get_n_points(stacked), ==, npoints);
        g_assert_cmpint(gwy_scatter_get_n_values(stacked), ==, expected_nvalues);
        assert_scatter_xy(stacked, xy1, npoints, 0.0);

        gint pos = 0;
        for (gint j = 0; j <= nstack; j++) {
            GwyScatter *operand = scatters[j];
            gint nvalues = gwy_scatter_get_n_values(operand);
            for (gint m = 0; m < nvalues; m++) {
                g_assert_true(gwy_unit_equal(gwy_scatter_get_unit_data(stacked, pos),
                                             gwy_scatter_get_unit_data(scatters[j], m)));

                const gchar *orig_label = gwy_scatter_get_data_label(scatters[j], m);
                if (orig_label) {
                    const gchar *label = gwy_scatter_get_data_label(stacked, pos);
                    g_assert_cmpstr(label, ==, orig_label);
                }
                else if (expecting_labels)
                    g_assert_nonnull(gwy_scatter_get_data_label(stacked, pos));
                else
                    g_assert_null(gwy_scatter_get_data_label(stacked, pos));

                const gdouble *data = gwy_scatter_get_data(stacked, pos);
                const gdouble *expected_data = gwy_scatter_get_data(scatters[j], m);
                for (gsize k = 0; k < npoints; k++)
                    g_assert_cmpfloat(data[k], ==, expected_data[k]);

                pos++;
            }
        }

        g_assert_finalize_object(stacked);
        for (gint j = 0; j < 3; j++)
            g_assert_finalize_object(scatters[j]);
    }
}

void
test_scatter_sort_by_value_ascending(void)
{
    enum { npoints = 5, nvalues = 3 };
    static const GwyXY xy[npoints] = { { 1.0, -2.0 }, { G_PI, G_E }, { 0.0, 0.1 }, { 1e6, 1e9 }, { 4.4, -7.1 } };
    static const gdouble data[npoints*nvalues] = {
        7.5, 1.2, -0.3, 5.5, 2.9,
        1e6, -1e6, 1e9, -1e9, 0.0,
        4.0, 3.0, 2.0, 1.0, 0.0,
    };
    static const guint expected_order0[npoints] = { 2, 1, 4, 3, 0 };
    static const guint expected_order1[npoints] = { 3, 1, 4, 0, 2 };
    static const guint expected_order2[npoints] = { 4, 3, 2, 1, 0 };
    static const guint *expected_orders[nvalues] = { expected_order0, expected_order1, expected_order2 };

    GwyScatter *scatter = gwy_scatter_new_sized(nvalues, npoints);
    gwy_assign(gwy_scatter_get_xy(scatter), xy, npoints);
    gwy_assign(gwy_scatter_get_data(scatter, 0), data, nvalues*npoints);

    for (guint i = 0; i < nvalues; i++) {
        gwy_scatter_sort_by_value(scatter, i, TRUE);
        const GwyXY *sorted_xy = gwy_scatter_get_xy_const(scatter);
        const guint *order = expected_orders[i];
        for (guint j = 0; j < npoints; j++) {
            g_assert_cmpfloat(sorted_xy[j].x, ==, xy[order[j]].x);
            g_assert_cmpfloat(sorted_xy[j].y, ==, xy[order[j]].y);
        }
        for (guint m = 0; m < nvalues; m++) {
            const gdouble *sorted_data = gwy_scatter_get_data_const(scatter, m);
            const gdouble *orig_data = data + m*npoints;
            for (guint j = 0; j < npoints; j++)
                g_assert_cmpfloat(sorted_data[j], ==, orig_data[order[j]]);
        }
    }

    g_assert_finalize_object(scatter);
}

void
test_scatter_sort_by_value_descending(void)
{
    enum { npoints = 5, nvalues = 3 };
    static const GwyXY xy[npoints] = { { 1.0, -2.0 }, { G_PI, G_E }, { 0.0, 0.1 }, { 1e6, 1e9 }, { 4.4, -7.1 } };
    static const gdouble data[npoints*nvalues] = {
        7.5, 1.2, -0.3, 5.5, 2.9,
        1e6, -1e6, 1e9, -1e9, 0.0,
        4.0, 3.0, 2.0, 1.0, 0.0,
    };
    static const guint expected_order0[npoints] = { 0, 3, 4, 1, 2 };
    static const guint expected_order1[npoints] = { 2, 0, 4, 1, 3 };
    static const guint expected_order2[npoints] = { 0, 1, 2, 3, 4 };
    static const guint *expected_orders[nvalues] = { expected_order0, expected_order1, expected_order2 };

    GwyScatter *scatter = gwy_scatter_new_sized(nvalues, npoints);
    gwy_assign(gwy_scatter_get_xy(scatter), xy, npoints);
    gwy_assign(gwy_scatter_get_data(scatter, 0), data, nvalues*npoints);

    for (guint i = 0; i < nvalues; i++) {
        gwy_scatter_sort_by_value(scatter, i, FALSE);
        const GwyXY *sorted_xy = gwy_scatter_get_xy_const(scatter);
        const guint *order = expected_orders[i];
        for (guint j = 0; j < npoints; j++) {
            g_assert_cmpfloat(sorted_xy[j].x, ==, xy[order[j]].x);
            g_assert_cmpfloat(sorted_xy[j].y, ==, xy[order[j]].y);
        }
        for (guint m = 0; m < nvalues; m++) {
            const gdouble *sorted_data = gwy_scatter_get_data_const(scatter, m);
            const gdouble *orig_data = data + m*npoints;
            for (guint j = 0; j < npoints; j++)
                g_assert_cmpfloat(sorted_data[j], ==, orig_data[order[j]]);
        }
    }

    g_assert_finalize_object(scatter);
}

static GwyScatter*
make_random_scatter(guint maxnvalues, guint maxnpoints)
{
    gint nvalues = g_test_rand_int_range(1, maxnvalues+1);
    gint npoints = g_test_rand_int_range(1, maxnpoints+1);
    GwyScatter *scatter = gwy_scatter_new_sized(nvalues, npoints);

    GwyXY *xy = gwy_scatter_get_xy(scatter);
    for (guint i = 0; i < npoints; i++) {
        xy[i].x = g_test_rand_double_range(-1, 1);
        xy[i].y = g_test_rand_double_range(-1, 1);
    }
    for (guint j = 0; j < nvalues; j++) {
        gdouble *v = gwy_scatter_get_data(scatter, j);
        for (guint i = 0; i < npoints; i++)
            v[i] = g_test_rand_double_range(-1, 1);
    }

    return scatter;
}

void
test_scatter_min_max(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyScatter *scatter = make_random_scatter(5, 100);
        guint nvalues = gwy_scatter_get_n_values(scatter);
        guint ival = g_test_rand_int_range(0, nvalues);
        guint ipt = g_test_rand_int_range(0, gwy_scatter_get_n_points(scatter));

        gdouble expected_min = g_test_rand_double_range(-100, -10);
        gwy_scatter_get_data(scatter, ival)[ipt] = expected_min;
        for (guint j = 0; j < nvalues; j++) {
            gdouble min, max;
            gwy_scatter_min_max(scatter, j, &min, &max);
            if (j == ival)
                g_assert_cmpfloat(min, ==, expected_min);
            else
                g_assert_cmpfloat(min, >=, -1.0);
            g_assert_cmpfloat(max, <=, 1.0);
        }

        gdouble expected_max = g_test_rand_double_range(10, 100);
        gwy_scatter_get_data(scatter, ival)[ipt] = expected_max;
        for (guint j = 0; j < nvalues; j++) {
            gdouble min, max;
            gwy_scatter_min_max(scatter, j, &min, &max);
            if (j == ival)
                g_assert_cmpfloat(max, ==, expected_max);
            else
                g_assert_cmpfloat(max, <=, 1.0);
            g_assert_cmpfloat(min, >=, -1.0);
        }
    }
}

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