/*
 *  $Id: volume_cluster.c 26744 2024-10-18 15:03:29Z yeti-dn $
 *  Copyright (C) 2014-2026 David Necas (Yeti), Petr Klapetek, Daniil Bratashov, Evgeniy Ryabov.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net, dn2010@gmail.com, k1u2r3ka@mail.ru.
 *
 *  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 <glib/gi18n-lib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "libgwyddion/omp.h"

#define RUN_MODES (GWY_RUN_INTERACTIVE)

#define DECLARE_METHOD(name) \
    static void define_params_##name(GwyParamDef *pardef); \
    static void append_gui_##name(ModuleGUI *gui); \
    static gboolean cluster_##name(ModuleArgs *args, GtkWindow *wait_window);

#define METHOD_FUNCS(name) \
    define_params_##name, append_gui_##name, cluster_##name

enum {
    PREVIEW_SIZE = 360,
};

enum {
    PARAM_SEED,
    PARAM_RANDOMIZE,
    PARAM_OUTLIERS,
    PARAM_BACKGROUND,
    PARAM_TRANSFORM,
    PARAM_SCALING,
    PARAM_METHOD,
    PARAM_OUTPUT_MAP,
    PARAM_OUTPUT_GRAPH,
    PARAM_OUTPUT_DIFFERENCE,
    PARAM_OUTPUT_OUTLIERS,
    PARAM_OUTPUT_DATA,
    PARAM_OUTPUT_ORIGINAL_CENTERS,
    PARAM_SHOW,
    PARAM_SHOW_CURVE,
    PARAM_XPOS,
    PARAM_YPOS,
    PARAM_KMEANS_K,
    PARAM_KMEANS_ITERATIONS,
    PARAM_KMEANS_LOG_EPSILON,
    PARAM_KMEANS_REMOVE_OUTLIERS,
    PARAM_KMEANS_OUTLIERS_THRESHOLD,
    PARAM_KMEDIANS_K,
    PARAM_KMEDIANS_ITERATIONS,
    PARAM_KMEDIANS_LOG_EPSILON,
    PARAM_KMEDIANS_REMOVE_OUTLIERS,
    PARAM_KMEDIANS_OUTLIERS_THRESHOLD,
    PARAM_DBSCAN_RANGE,
    PARAM_DBSCAN_NEIGHBORS,
    LABEL_RESULT,
};

typedef enum {
    SHOW_DATA  = 0,
    SHOW_RESULT = 1,
} ClusterShow;

typedef enum {
    OUTLIERS_NONE    = 0,
    OUTLIERS_MEAN3   = 1,
    OUTLIERS_MAD     = 2,
    //OUTLIERS_IQR     = 3,
} ClusterOutliers;

typedef enum {
    BACKGROUND_NONE     = 0,
    BACKGROUND_MEAN     = 1,
    BACKGROUND_SLOPE    = 2,
    BACKGROUND_PARABOLA = 3,
    BACKGROUND_MC       = 4,
} ClusterBackground;

typedef enum {
    TRANSFORM_NONE   = 0,
    TRANSFORM_LOG    = 1,
    TRANSFORM_1DER   = 2,
    TRANSFORM_2DER   = 3,
} ClusterTransform;

typedef enum {
    SCALING_NONE      = 0,
    SCALING_RANGE     = 1,
    SCALING_VARIANCE  = 2,
    SCALING_PARETO    = 3,
    SCALING_VAST      = 4,
    SCALING_AREA      = 5,
} ClusterScaling;

typedef enum {
    METHOD_KMEANS      = 0,
    METHOD_KMEDIANS    = 1,
    METHOD_DBSCAN      = 2,
    METHOD_NTYPES      = 3,
} ClusterMethodType;

typedef struct {
    GwyParams *params;
    GwyBrick *brick;
    GwyBrick *prepbrick;
    GwyField *result; // FIXME: This is the classification, and should be a Nield. One day.
    GwyField *scale;
    GwyField *errormap;
    GwyLine *calibration;
    GwyField *preview;
    GwyNield *mask;
    GwyGraphModel *gmodel;
    gdouble value;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyField *showfield;
    GwyParamTable *table_show;
    GwyParamTable *table_preprocess;
    GwyParamTable *table_method;
    GwyParamTable *table_methodparam[METHOD_NTYPES];
    GwyParamTable *table_output;
    GtkWidget *method_vbox;
    GtkWidget *method_widget;
    GwyGraphModel *gmodel;
    GtkWidget *dataview;
    GwySelection *image_selection;
    ClusterMethodType method_type;
    gboolean computed;
    GwyGradient *gradient_data;
    GwyGradient *gradient_result;
} ModuleGUI;

typedef void     (*DefineParamsFunc) (GwyParamDef *paramdef);
typedef void     (*AppendGUIFunc)    (ModuleGUI *gui);
typedef gboolean (*ClusterFunc)      (ModuleArgs *args,
                                      GtkWindow *wait_window);

typedef struct {
    const gchar *name;
    DefineParamsFunc define_params;
    AppendGUIFunc append_gui;
    ClusterFunc cluster;
} ClusterMethod;

static gboolean         module_register        (void);
static GwyParamDef*     define_module_params   (void);
static void             cluster                (GwyFile *data,
                                                GwyRunModeFlags mode);
static gboolean         execute                (ModuleArgs *args,
                                                GtkWindow *wait_window);
static gboolean         preprocess             (ModuleArgs *args,
                                                GtkWindow *wait_window);
static gboolean         outliers               (ModuleArgs *args,
                                                GtkWindow *wait_window);
static gboolean         background             (ModuleArgs *args,
                                                GtkWindow *wait_window);
static gboolean         transform              (ModuleArgs *args,
                                                GtkWindow *wait_window);
static gboolean         scale                  (ModuleArgs *args,
                                                GtkWindow *wait_window);
static GwyDialogOutcome run_gui                (ModuleArgs *args,
                                                GwyFile *data,
                                                gint id);
static void             update_display         (ModuleGUI *gui);
static void             param_changed          (ModuleGUI *gui,
                                                gint id);
static void             point_selection_changed(ModuleGUI *gui,
                                                gint id,
                                                GwySelection *selection);
static void             preview                (gpointer user_data);
static void             extract_graph_curve    (ModuleArgs *args,
                                                GwyGraphCurveModel *gcmodel);
static void             setup_gmodel           (ModuleArgs *args,
                                                GwyGraphModel *gmodel);
static GwyGraphModel*   create_graph           (GwyBrick *brick,
                                                GwyLine *calibration,
                                                const gdouble *centers,
                                                gint nclusters,
                                                gint *npix);
static void             sanitise_params        (ModuleArgs *args);

DECLARE_METHOD(kmeans);
DECLARE_METHOD(kmedians);
DECLARE_METHOD(dbscan);

static const ClusterMethod methods[] = {
    { N_("K-means"),   METHOD_FUNCS(kmeans),   },
    { N_("K-medians"), METHOD_FUNCS(kmedians), },
    { N_("DBSCAN"),    METHOD_FUNCS(dbscan),   },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Splits data into clusters"),
    "Petr Klapetek <klapetek@gwyddion.net> & Daniil Bratashov <dn2010@gmail.com> & Evgeniy Ryabov <k1u2r3ka@mail.ru>",
    "1.1",
    "Petr Klapetek & David Nečas (Yeti) & Daniil Bratashov & Evgeniy Ryabov",
    "2025",
};

GWY_MODULE_QUERY2(module_info, volume_cluster)

static gboolean
module_register(void)
{
    gwy_volume_func_register("volume_cluster",
                             &cluster,
                             N_("/_Statistics/_Clustering..."),
                             NULL,
                             RUN_MODES,
                             GWY_MENU_FLAG_VOLUME,
                             N_("Split data into clusters"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    gint i;
    GwyEnum *types = NULL;

    static const GwyEnum displays[] = {
        { N_("Preprocessed _data"),   SHOW_DATA,   },
        { N_("Clustering _result"),   SHOW_RESULT, },
    };
    static const GwyEnum outliers[] = {
        { N_("None"),      OUTLIERS_NONE,   },
        { N_("3 σ"),       OUTLIERS_MEAN3,  },
        { N_("3 MAD"),     OUTLIERS_MAD,    },
//        { N_("IQR"),       OUTLIERS_IQR,    },
    };
    static const GwyEnum backgrounds[] = {
        { N_("None"),       BACKGROUND_NONE,      },
        { N_("Mean"),       BACKGROUND_MEAN,      },
        { N_("Linear"),     BACKGROUND_SLOPE,     },
        { N_("Quadratic"),  BACKGROUND_PARABOLA,  },
        { N_("Mean curve"), BACKGROUND_MC,        },
    };
    static const GwyEnum transforms[] = {
        { N_("None"),       TRANSFORM_NONE,   },
        { N_("Logarithm"),  TRANSFORM_LOG     },
        { N_("Gradient"),   TRANSFORM_1DER,   },
        { N_("Curvature"),  TRANSFORM_2DER,   },
    };
     static const GwyEnum scalings[] = {
        { N_("None"),       SCALING_NONE,      },
        { N_("Min/max"),    SCALING_RANGE,     },
        { N_("Variance"),   SCALING_VARIANCE,  },
        { N_("Pareto"),     SCALING_PARETO,    },
        { N_("VAST"),       SCALING_VAST,      },
        { N_("Area"),       SCALING_AREA,      },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    types = gwy_enum_fill_from_struct(NULL, G_N_ELEMENTS(methods), methods, sizeof(ClusterMethod),
                                      G_STRUCT_OFFSET(ClusterMethod, name), -1);

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_volume_func_current());
    gwy_param_def_add_gwyenum(paramdef, PARAM_METHOD, "method", _("_Method"),
                              types, G_N_ELEMENTS(methods), METHOD_KMEANS);

    gwy_param_def_add_int(paramdef, PARAM_XPOS, "xpos", _("_X"), -1, G_MAXINT, -1);
    gwy_param_def_add_int(paramdef, PARAM_YPOS, "ypos", _("_Y"), -1, G_MAXINT, -1);
    gwy_param_def_add_seed(paramdef, PARAM_SEED, "seed", NULL);
    gwy_param_def_add_randomize(paramdef, PARAM_RANDOMIZE, PARAM_SEED, "randomize", NULL, TRUE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_OUTLIERS, "outliers", _("_Outliers criterion"),
                              outliers, G_N_ELEMENTS(outliers), OUTLIERS_NONE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_BACKGROUND, "background", _("_Background removal"),
                              backgrounds, G_N_ELEMENTS(backgrounds), BACKGROUND_NONE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_TRANSFORM, "transformation", _("_Transformation"),
                              transforms, G_N_ELEMENTS(transforms), TRANSFORM_NONE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_SCALING, "scaling", _("_Scaling"),
                              scalings, G_N_ELEMENTS(scalings), SCALING_NONE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_SHOW, "show_type", C_("verb", "_Display"),
                              displays, G_N_ELEMENTS(displays), SHOW_DATA);
    gwy_param_def_add_boolean(paramdef, PARAM_OUTPUT_MAP, "output_map", _("_Cluster map"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_OUTPUT_GRAPH, "output_graph", _("Cluster centers _graph"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_OUTPUT_DIFFERENCE, "output_difference", _("_Error map"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_OUTPUT_OUTLIERS, "output_outliers", _("Outliers _mask"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_OUTPUT_DATA, "output_data", _("Preprocessed _data"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_OUTPUT_ORIGINAL_CENTERS, "output_original",
                              _("Output _non-processed centers"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_SHOW_CURVE, "show_curve", _("Sho_w data with result"), FALSE);


    for (i = 0; i < G_N_ELEMENTS(methods); i++)
        methods[i].define_params(paramdef);

    return paramdef;
}


static void
cluster(GwyFile *data, GwyRunModeFlags mode)
{
    ModuleArgs args;
    GwyDialogOutcome outcome;
    GwyBrick *brick;
    gint id, newid;
    gchar *description;

    g_return_if_fail(mode & RUN_MODES);
    g_return_if_fail(g_type_from_name("GwyLayerPoint"));

    gwy_clear1(args);
    gwy_data_browser_get_current(GWY_APP_BRICK, &args.brick,
                                 GWY_APP_BRICK_ID, &id,
                                 0);
    g_return_if_fail(GWY_IS_BRICK(args.brick));

    brick = args.brick;
    args.calibration = gwy_brick_get_zcalibration(brick);
    if (args.calibration && gwy_brick_get_zres(brick) != gwy_line_get_res(args.calibration))
        args.calibration = NULL;
    args.params = gwy_params_new_from_settings(define_module_params());
    sanitise_params(&args);
    args.prepbrick = gwy_brick_copy(brick);

    args.preview = gwy_field_copy(gwy_dict_get_object(GWY_DICT(data), gwy_file_key_volume_picture(id)));
    args.mask = gwy_field_new_nield_alike(args.preview);

    outcome = run_gui(&args, data, id);
    gwy_params_save_to_settings(args.params);
    if (outcome == GWY_DIALOG_CANCEL)
        goto end;

    if (outcome != GWY_DIALOG_HAVE_RESULT)
        goto end;

    if (!args.result) {
        goto end;
    }

    description = gwy_file_get_display_title(data, GWY_FILE_VOLUME, id);
    newid = gwy_file_add_image(data, args.result);
    gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE); //was result
    gwy_file_pass_title(data, GWY_FILE_IMAGE, newid, g_strdup_printf(_("%s: cluster map"), description));
    gwy_file_set_palette(data, GWY_FILE_IMAGE, newid, "Clusters");
    gwy_dict_set_enum(GWY_DICT(data), gwy_file_key_image_color_mapping(newid), GWY_COLOR_MAPPING_FIXED);
    gwy_dict_set_double(GWY_DICT(data), gwy_file_key_image_range_min(newid), 0); //-0.1
    gwy_dict_set_double(GWY_DICT(data), gwy_file_key_image_range_max(newid),
                        gwy_field_max(args.result));
    gwy_log_add_full(data, GWY_FILE_IMAGE, -1, newid, "volume::volume_cluster", NULL);

    if (gwy_params_get_boolean(args.params, PARAM_OUTPUT_OUTLIERS))
        gwy_log_add_full(data, GWY_FILE_IMAGE, -1, newid, "volume::volume_cluster", NULL);

    if (gwy_params_get_boolean(args.params, PARAM_OUTPUT_DIFFERENCE) && args.errormap) {
        newid = gwy_file_add_image(data, args.errormap);
        gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE); //was result
        gwy_file_pass_title(data, GWY_FILE_IMAGE, newid, g_strdup_printf(_("%s: error map"), description));
        gwy_file_sync_items(data, GWY_FILE_VOLUME, id,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_PALETTE, FALSE);
        gwy_log_add_full(data, GWY_FILE_IMAGE, -1, newid, "volume::volume_cluster", NULL);
    }

    if (gwy_params_get_boolean(args.params, PARAM_OUTPUT_GRAPH) && args.gmodel) {
        if (gwy_params_get_boolean(args.params, PARAM_OUTPUT_ORIGINAL_CENTERS)) {
            //use cluster map to build cluster centers entirely from scratch from original data
            GwyGraphCurveModel *gcmodel;
            gint nc = gwy_graph_model_get_n_curves(args.gmodel);
            gint n, c, col, row, lev, npix;
            const gdouble *xdata;
            gint xres = gwy_brick_get_xres(brick);
            gint yres = gwy_brick_get_yres(brick);
            const gdouble *labeldata = gwy_field_get_data(args.result);
            const gint *maskdata = gwy_nield_get_data_const(args.mask);
            const gdouble *bdata = gwy_brick_get_data_const(args.brick);
            for (c = 1; c <= nc; c++) {
                gcmodel = gwy_graph_model_get_curve(args.gmodel, c-1);
                n = gwy_graph_curve_model_get_n_data(gcmodel);
                xdata = gwy_graph_curve_model_get_xdata(gcmodel);
                gdouble *newydata = g_new0(gdouble, n);
                npix = 0;
                for (row = 0; row < yres; row++) {
                    for (col = 0; col < xres; col++) {
                        if (labeldata[col + xres*row] != c || maskdata[col + xres*row] > 0)
                            continue;
                        for (lev = 0; lev < n; lev++)
                            newydata[lev] += bdata[lev*xres*yres + row*xres + col];
                        npix++;
                    }
                }
                for (lev = 0; lev < n; lev++)
                    newydata[lev] /= npix;
                gwy_graph_curve_model_set_data(gcmodel, xdata, newydata, n);
                g_free(newydata);
            }
        }
        g_object_set(args.gmodel,
                     "title", g_strdup_printf(_("%s: cluster centers"), description),
                     NULL);
        newid = gwy_file_add_graph(data, args.gmodel);
        gwy_file_set_visible(data, GWY_FILE_GRAPH, newid, TRUE);
        gwy_log_add_full(data, GWY_FILE_GRAPH, -1, newid, "volume::volume_cluster", NULL);
    }

    if (gwy_params_get_boolean(args.params, PARAM_OUTPUT_DATA)) {
        GwyBrick *outbrick = gwy_brick_new_alike(args.prepbrick, FALSE);

        gwy_brick_transpose(args.prepbrick, outbrick, GWY_BRICK_TRANSPOSE_ZXY, FALSE, FALSE, FALSE);

        newid = gwy_file_add_volume(data, outbrick, NULL);
        gwy_file_set_visible(data, GWY_FILE_VOLUME, newid, TRUE);
        gwy_file_set_title(data, GWY_FILE_VOLUME, newid, g_strdup_printf(_("Pre-processed %s"), description), TRUE);
        gwy_file_sync_items(data, GWY_FILE_VOLUME, id,
                            data, GWY_FILE_VOLUME, newid,
                            GWY_FILE_ITEM_PALETTE, FALSE);
        gwy_log_add(data, GWY_FILE_VOLUME, id, newid);
        g_object_unref(outbrick);
    }

    g_free(description);

end:
    g_clear_object(&args.result);
    g_object_unref(args.params);
    g_object_unref(args.prepbrick);
    g_object_unref(args.preview);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GtkWidget *hbox;
    GwyGraph *graph;
    GwyParamTable *table;
    GwyDialog *dialog;
    ModuleGUI gui;
    GwyDialogOutcome outcome;
    const gchar *name = NULL;
    gint i;

    gwy_clear1(gui);
    gui.args = args;

    gui.showfield = gwy_field_copy(args->preview);
    gui.computed = FALSE;

    name = gwy_file_get_palette(data, GWY_FILE_VOLUME, id);
    gui.gradient_data = gwy_gradients_get_gradient(name);
    gui.gradient_result = gwy_gradients_get_gradient("Clusters");

    gui.gmodel = gwy_graph_model_new();
    setup_gmodel(args, gui.gmodel);

    gui.dialog = gwy_dialog_new(_("Cluster Data"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gwy_dialog_add_content(dialog, hbox, FALSE, FALSE, 4);

    gui.dataview = gwy_create_preview(gui.showfield, args->mask, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.dataview), data, GWY_FILE_VOLUME, id, GWY_FILE_ITEM_PALETTE);
    gtk_box_pack_start(GTK_BOX(hbox), gui.dataview, FALSE, FALSE, 0);
    gui.image_selection = gwy_create_preview_vector_layer(GWY_DATA_VIEW(gui.dataview),
                                                          GWY_TYPE_LAYER_POINT, 1, TRUE);

    graph = GWY_GRAPH(gwy_graph_new(gui.gmodel));
    gwy_graph_enable_user_input(graph, FALSE);
    gtk_widget_set_size_request(GTK_WIDGET(graph), PREVIEW_SIZE, PREVIEW_SIZE);
    gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(graph), TRUE, TRUE, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 24);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), hbox, TRUE, TRUE, 4);

    gui.table_show = table = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("View"));
    gwy_param_table_append_radio(table, PARAM_SHOW);
    gwy_param_table_append_checkbox(table, PARAM_SHOW_CURVE);
    gwy_param_table_append_header(table, -1, _("Initialization"));
    gwy_param_table_append_seed(table, PARAM_SEED);
    gwy_param_table_append_checkbox(table, PARAM_RANDOMIZE);

    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), FALSE, FALSE, 0);

    gui.table_preprocess = table = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Pre-processing"));
    gwy_param_table_append_combo(table, PARAM_OUTLIERS);
    gwy_param_table_append_combo(table, PARAM_BACKGROUND);
    gwy_param_table_append_combo(table, PARAM_TRANSFORM);
    gwy_param_table_append_combo(table, PARAM_SCALING);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), FALSE, FALSE, 0);

    gui.method_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
    gtk_box_pack_start(GTK_BOX(hbox), gui.method_vbox, FALSE, FALSE, 0);

    gui.table_method = table = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Clustering"));
    gwy_param_table_append_combo(table, PARAM_METHOD);
    gtk_box_pack_start(GTK_BOX(gui.method_vbox), gwy_param_table_widget(table), FALSE, FALSE, 0);

   for (i = 0; i < G_N_ELEMENTS(methods); i++) {
        const ClusterMethod *method = methods + i;

        gui.table_methodparam[i] = table = gwy_param_table_new(args->params);
        g_object_ref_sink(table);
        method->append_gui(&gui);
    }
    table = gui.table_methodparam[gui.method_type];
    gui.method_widget = gwy_param_table_widget(table);
    gtk_box_pack_start(GTK_BOX(gui.method_vbox), gui.method_widget, FALSE, FALSE, 0);

    gui.table_output = table = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Output"));
    gwy_param_table_append_checkbox(table, PARAM_OUTPUT_MAP);
    gwy_param_table_append_checkbox(table, PARAM_OUTPUT_GRAPH);
    gwy_param_table_append_checkbox(table, PARAM_OUTPUT_DIFFERENCE);
    gwy_param_table_append_checkbox(table, PARAM_OUTPUT_DATA);
    gwy_param_table_append_checkbox(table, PARAM_OUTPUT_ORIGINAL_CENTERS);
    gwy_param_table_append_checkbox(table, PARAM_OUTPUT_OUTLIERS);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_info(table, LABEL_RESULT, _("Result:"));
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), FALSE, FALSE, 0);

    g_signal_connect_swapped(gui.table_show, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_preprocess, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_output, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_method, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.image_selection, "changed", G_CALLBACK(point_selection_changed), &gui);
    for (i = 0; i < G_N_ELEMENTS(methods); i++) {
        g_signal_connect_swapped(gui.table_methodparam[i], "param-changed", G_CALLBACK(param_changed), &gui);
    }

    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_UPON_REQUEST, preview, &gui, NULL);

    gwy_param_table_info_set_valuestr(gui.table_output, LABEL_RESULT, _("no clusters"));

    preprocess(args, GTK_WINDOW(dialog));
    update_display(&gui);
    gtk_dialog_set_response_sensitive(GTK_DIALOG(dialog), GTK_RESPONSE_OK, FALSE);

    outcome = gwy_dialog_run(dialog);

    for (i = 0; i < G_N_ELEMENTS(methods); i++)
        g_object_unref(gui.table_methodparam[i]);

    g_object_unref(gui.gmodel);

    return outcome;
}

static void
update_display(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    ClusterShow show = gwy_params_get_enum(args->params, PARAM_SHOW);
    GwyDataView *dataview = GWY_DATA_VIEW(gui->dataview);
    gint i, nc;
    GwyGraphCurveModel *gcmodel;
    static const GwyRGBA black = { 0.0, 0.0, 0.0, 1.0 };

    gwy_graph_model_remove_all_curves(gui->gmodel);
    if (show == SHOW_DATA) {
        gwy_data_view_set_gradient(dataview, gui->gradient_data);
        gwy_field_assign(gui->showfield, args->preview);
        gwy_data_view_set_color_mapping(dataview, GWY_COLOR_MAPPING_FULL);

        gcmodel = gwy_graph_curve_model_new();
        extract_graph_curve(args, gcmodel);
        g_object_set(gcmodel, "mode", GWY_GRAPH_CURVE_LINE, NULL);
        gwy_graph_model_add_curve(gui->gmodel, gcmodel);
        g_object_unref(gcmodel);
    }
    else {
        //show clustering map and cluster centers
        gwy_data_view_set_gradient(dataview, gui->gradient_result);

        if (!gui->computed || !args->result) {
            gwy_field_fill(gui->showfield, 0);
            gwy_data_view_set_fixed_color_range(dataview, 0.0/*-0.1*/, 10.0);
        }
        else {
            gwy_field_assign(gui->showfield, args->result);
            gwy_data_view_set_fixed_color_range(dataview, 0.0/*-0.1*/, gwy_field_max(args->result));
        }
        gwy_data_view_set_color_mapping(dataview, GWY_COLOR_MAPPING_FIXED);

        if (gui->computed && args->gmodel) {
            nc = gwy_graph_model_get_n_curves(args->gmodel);
            for (i = 0; i < nc; i++) {
                gcmodel = gwy_graph_model_get_curve(args->gmodel, i);
                gwy_graph_model_add_curve(gui->gmodel, gcmodel);
            }

            if (gwy_params_get_boolean(args->params, PARAM_SHOW_CURVE)) {
                gcmodel = gwy_graph_curve_model_new();
                extract_graph_curve(args, gcmodel);
                g_object_set(gcmodel,
                             "mode", GWY_GRAPH_CURVE_LINE,
                             "line-width", 3.0,
                             "line-style", GWY_LINE_DASH,
                             "color", &black,
                             NULL);
                gwy_graph_model_add_curve(gui->gmodel, gcmodel);
                g_object_unref(gcmodel);
            }
        }
    }
    gwy_field_data_changed(gui->showfield);
}

static void
point_selection_changed(ModuleGUI *gui,
                        G_GNUC_UNUSED gint id,
                        GwySelection *selection)
{
    ModuleArgs *args = gui->args;
    GwyBrick *brick = args->brick;
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick);
    gdouble xy[2];

    if (!gwy_selection_get_object(selection, 0, xy))
        return;

    gwy_params_set_int(args->params, PARAM_XPOS, CLAMP(gwy_brick_rtoi(brick, xy[0]), 0, xres-1));
    gwy_params_set_int(args->params, PARAM_YPOS, CLAMP(gwy_brick_rtoj(brick, xy[1]), 0, yres-1));

    update_display(gui);
}

static void
switch_method_type(ModuleGUI *gui)
{
    ClusterMethodType type = gwy_params_get_enum(gui->args->params, PARAM_METHOD);
    GwyParamTable *table;

    if (gui->method_widget) {
        gtk_widget_destroy(gui->method_widget);
        gui->method_widget = NULL;
    }

    gui->method_type = type;

    table = gui->table_methodparam[gui->method_type];
    gui->method_widget = gwy_param_table_widget(table);
    gtk_widget_show_all(gui->method_widget);

    gtk_box_pack_start(GTK_BOX(gui->method_vbox), gui->method_widget, FALSE, FALSE, 0);
}


static void
param_changed(ModuleGUI *gui, gint id)
{
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    const ClusterMethodType type = gwy_params_get_enum(params, PARAM_METHOD);

    if (id < 0 || id == PARAM_METHOD) {
        if (type != gui->method_type) {
            switch_method_type(gui);
            id = -1;
        }
    }
    if (!(id == PARAM_SHOW || id == PARAM_SHOW_CURVE || id == PARAM_OUTPUT_ORIGINAL_CENTERS
          || id == PARAM_OUTPUT_MAP || id == PARAM_OUTPUT_GRAPH || id == PARAM_OUTPUT_DIFFERENCE
          || id == PARAM_OUTPUT_DATA || id == PARAM_OUTPUT_OUTLIERS)) {
       gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
       gui->computed = FALSE;
       gtk_dialog_set_response_sensitive(GTK_DIALOG(gui->dialog), GTK_RESPONSE_OK, FALSE);
    }

    if (id < 0 || id == PARAM_OUTLIERS || id == PARAM_KMEANS_REMOVE_OUTLIERS) {
       ClusterOutliers outliers = gwy_params_get_enum(params, PARAM_OUTLIERS);
       gboolean kmeans_remove_outliers = gwy_params_get_boolean(params, PARAM_KMEANS_REMOVE_OUTLIERS);

       if (kmeans_remove_outliers)
           gwy_param_table_set_sensitive(gui->table_methodparam[METHOD_KMEANS],
                                         PARAM_KMEANS_OUTLIERS_THRESHOLD, TRUE);
       else
           gwy_param_table_set_sensitive(gui->table_methodparam[METHOD_KMEANS],
                                         PARAM_KMEANS_OUTLIERS_THRESHOLD, FALSE);

       if (outliers != OUTLIERS_NONE || (type == METHOD_KMEANS && kmeans_remove_outliers))
           gwy_param_table_set_sensitive(gui->table_output, PARAM_OUTPUT_OUTLIERS, TRUE);
       else
           gwy_param_table_set_sensitive(gui->table_output, PARAM_OUTPUT_OUTLIERS, FALSE);
    }

    if (id < 0 || id == PARAM_OUTLIERS || id == PARAM_BACKGROUND || id == PARAM_SCALING || id == PARAM_TRANSFORM) {
        gwy_param_table_info_set_valuestr(gui->table_output, LABEL_RESULT, _("no clusters"));
        preprocess(args, GTK_WINDOW(gui->dialog));
    }

    update_display(gui);
}

static void
update_graph_colors(GwyGraphModel *gmodel, GwyField *clusters, GwyGradient *gradient)
{
    gint nc = gwy_graph_model_get_n_curves(gmodel);
    gdouble cfrom = 0; //our range is always from 0 (unclassified points)
    gdouble cto = gwy_field_max(clusters);
    GwyRGBA color;
    gdouble val;

    // printf("nc %d, cfrom %g cto %g\n", nc, cfrom, cto);

    for (gint i = 1; i <= nc; i++) {
        GwyGraphCurveModel *gcmodel = gwy_graph_model_get_curve(gmodel, i - 1);
        val = (i-cfrom)/(cto - cfrom);
        if (val > 1) {
            printf("gradient out of range: %d of nc %d, cfrom %g cto %g, value %g\n", i, nc, cfrom, cto, val);
            val = 1;
        }
        gwy_gradient_get_color(gradient, val, &color);
        g_object_set(gcmodel,
                     "color", &color,
                     NULL);
    }
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;

    if (execute(args, GTK_WINDOW(gui->dialog))) {
        if (args->result) {
            update_graph_colors(args->gmodel, args->result, gui->gradient_result);
            gwy_field_data_changed(args->result);
            gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
            gui->computed = TRUE;
            gtk_dialog_set_response_sensitive(GTK_DIALOG(gui->dialog), GTK_RESPONSE_OK, TRUE);

            gchar *message = g_strdup_printf(_("%.0f clusters, error: %g"),
                                             gwy_field_max(args->result),
                                             gwy_field_mean(args->errormap));
            gwy_param_table_info_set_valuestr(gui->table_output, LABEL_RESULT, message);
            g_free(message);
        }
    }
    else {
        gwy_param_table_info_set_valuestr(gui->table_output, LABEL_RESULT, _("no clusters"));
    }
    update_display(gui);
}

static gboolean
preprocess(ModuleArgs *args, GtkWindow *wait_window)
{
    gwy_brick_transpose(args->brick, args->prepbrick, GWY_BRICK_TRANSPOSE_YZX, FALSE, FALSE, FALSE);
    gwy_nield_clear(args->mask);
    gwy_nield_data_changed(args->mask);

    if (!outliers(args, wait_window))
        return FALSE;

    if (!background(args, wait_window))
        return FALSE;

    if (!transform(args, wait_window))
        return FALSE;

    if (!scale(args, wait_window))
        return FALSE;

    return TRUE;
}

static gboolean
execute(ModuleArgs *args, GtkWindow *wait_window)
{
    ClusterMethodType method_type = gwy_params_get_enum(args->params, PARAM_METHOD);
    const ClusterMethod *method = methods + method_type;

    preprocess(args, wait_window); //should not be needed if data were already preprocessed

    if (!method->cluster(args, wait_window))
        return FALSE;

    return TRUE;
}

//note that preprocessing brick is swapped
static void
extract_graph_curve(ModuleArgs *args, GwyGraphCurveModel *gcmodel)
{
    GwyBrick *brick = args->prepbrick;
    GwyLine *line;
    gint xres = gwy_brick_get_xres(brick);
    gint xpos = gwy_params_get_int(args->params, PARAM_XPOS);
    gint ypos = gwy_params_get_int(args->params, PARAM_YPOS);

    line = gwy_line_new(1, 1.0, FALSE);
    gwy_brick_extract_line(brick, line, 0, xpos, ypos, xres, xpos, ypos, FALSE);

    gwy_graph_curve_model_set_data_from_line(gcmodel, line, 0, 0);

    g_object_unref(line);
}

static void
setup_gmodel(ModuleArgs *args, GwyGraphModel *gmodel)
{
    GwyBrick *brick = args->brick;
    GwyLine *calibration = gwy_brick_get_zcalibration(brick);
    GwyUnit *xunit = (calibration ? gwy_line_get_unit_y(calibration) : gwy_brick_get_unit_z(brick));

    g_object_set(gmodel,
                 "label-visible", FALSE,
                 "unit-x", xunit,
                 "unit-y", gwy_brick_get_unit_w(brick),
                 "axis-label-bottom", "z",
                 "axis-label-left", "w",
                 NULL);
}

static inline void
clamp_int_param(GwyParams *params, gint id, gint min, gint max, gint default_value)
{
    gint p = gwy_params_get_int(params, id);

    if (p < min || p > max)
        gwy_params_set_int(params, id, default_value);
}

static void
sanitise_params(ModuleArgs *args)
{
    GwyParams *params = args->params;
    GwyBrick *brick = args->brick;
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick);

    clamp_int_param(params, PARAM_XPOS, 0, xres-1, xres/2);
    clamp_int_param(params, PARAM_YPOS, 0, yres-1, yres/2);
}

/***********************************************************************************************************
 *
 * Pre-processing
 *
 * ********************************************************************************************************/

static void
brick_extract_x_line(GwyBrick *brick, GwyLine *line, gint row, gint lev)
{
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gdouble *bdata = gwy_brick_get_data(brick);
    gdouble *ldata = gwy_line_get_data(line);
    gint col;

    for (col = 0; col < xres; col++)
        ldata[col] = bdata[col + xres*row + xres*yres*lev];
}
static void
brick_insert_x_line(GwyBrick *brick, GwyLine *line, gint row, gint lev)
{
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gdouble *bdata = gwy_brick_get_data(brick);
    gdouble *ldata = gwy_line_get_data(line);
    gint col;

    for (col = 0; col < xres; col++)
        bdata[col + xres*row + xres*yres*lev] = ldata[col];
}

static gdouble
line_dist(GwyLine *la, GwyLine *lb)
{
    gint i, res = gwy_line_get_res(la);
    gdouble *dla = gwy_line_get_data(la);
    gdouble *dlb = gwy_line_get_data(lb);
    gdouble sum = 0;

    for (i = 0; i < res; i++)
        sum += (dla[i]-dlb[i])*(dla[i]-dlb[i]);

    return sqrt(sum/res);
}

//note that brick is transposed and spectra are now in x direction
static gboolean
outliers(ModuleArgs *args, G_GNUC_UNUSED GtkWindow *wait_window)
{
    GwyBrick* brick = args->prepbrick;
    ClusterOutliers outliers = gwy_params_get_enum(args->params, PARAM_OUTLIERS);
    gint col, row, lev;
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gint zres = gwy_brick_get_zres(brick);
    gdouble xrange = gwy_brick_get_xreal(brick);
    gdouble *bdata = gwy_brick_get_data(brick);
    GwyLine *xline, *xavline;
    GwyField *distances, *amad;
    gdouble *xavdata;
    gdouble dist, median, mad;

    if (outliers == OUTLIERS_NONE)
        return TRUE;

    xline = gwy_line_new(xres, xrange, FALSE);
    xavline = gwy_line_new(xres, xrange, TRUE);
    distances = gwy_field_new(yres, zres, yres, zres, TRUE);
    xavdata = gwy_line_get_data(xavline);

    for (lev = 0; lev < zres; lev++) {
        for (row = 0; row < yres; row++) {
            for (col = 0; col < xres; col++) {
                xavdata[col] += bdata[col + xres*row + xres*yres*lev];
            }
        }
    }
    gwy_line_multiply(xavline, 1.0/(yres*zres));

    for (lev = 0; lev < zres; lev++) {
        for (row = 0; row < yres; row++) {
            brick_extract_x_line(brick, xline, row, lev);
            dist = line_dist(xline, xavline);
            gwy_field_set_val(distances, row, lev, dist);
        }
    }

    if (outliers == OUTLIERS_MEAN3) {
        gwy_field_mark_outliers(distances, args->mask, 3);
        gwy_nield_data_changed(args->mask);
    }
    else if (outliers == OUTLIERS_MAD) {
        median = gwy_field_median(distances);
        amad = gwy_field_copy(distances);
        gwy_field_add(amad, -median);
        gwy_field_abs(amad);
        mad = gwy_field_median(amad);
        gwy_nield_mark_range(args->mask, distances, median + 3*mad, median - 3*mad);
        gwy_nield_data_changed(args->mask);
        g_object_unref(amad);
    }

    g_object_unref(distances);
    g_object_unref(xline);
    g_object_unref(xavline);
    return TRUE;
}

//note that brick is transposed and spectra are now in x direction
static gboolean
background(ModuleArgs *args, G_GNUC_UNUSED GtkWindow *wait_window)
{
    GwyBrick* brick = args->prepbrick;
    ClusterBackground background = gwy_params_get_enum(args->params, PARAM_BACKGROUND);
    gint col, row, lev;
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gint zres = gwy_brick_get_zres(brick);
    gdouble xrange = gwy_brick_get_xreal(brick);
    gdouble *bdata = gwy_brick_get_data(brick);
    GwyLine *xline = gwy_line_new(xres, xrange, FALSE);
    GwyLine *xtline = gwy_line_new(xres, xrange, TRUE);
    gdouble *xtldata = gwy_line_get_data(xtline);
    gdouble av, bv, coeffs[3];

    if (background == BACKGROUND_MEAN) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                gwy_line_add(xline, -gwy_line_mean(xline));
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    else if (background == BACKGROUND_SLOPE) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                gwy_line_fit_line(xline, &av, &bv);
                gwy_line_line_level(xline, av, bv);
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    else if (background == BACKGROUND_PARABOLA) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                gwy_line_fit_poly(xline, 3, coeffs);
                gwy_line_subtract_poly(xline, 3, coeffs);
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    else if (background == BACKGROUND_MC) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                for (col = 0; col < xres; col++) {
                    xtldata[col] += bdata[col + xres*row + xres*yres*lev];
                }
            }
        }
        gwy_line_multiply(xtline, 1.0/(yres*zres));
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                gwy_line_subtract_lines(xline, xline, xtline);
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }

    g_object_unref(xline);
    g_object_unref(xtline);
    return TRUE;
}

//note that brick is transposed and spectra are now in x direction
static gboolean
transform(ModuleArgs *args, G_GNUC_UNUSED GtkWindow *wait_window)
{
    GwyBrick* brick = args->prepbrick;
    ClusterTransform transform = gwy_params_get_enum(args->params, PARAM_TRANSFORM);
    gint row, lev, i;
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gint zres = gwy_brick_get_zres(brick);
    gdouble xrange = gwy_brick_get_xreal(brick);
    gdouble *bdata = gwy_brick_get_data(brick);
    GwyLine *xline = gwy_line_new(xres, xrange, FALSE);
    GwyLine *xtline = gwy_line_new(xres, xrange, FALSE);
    gdouble *xldata = gwy_line_get_data(xline);
    gdouble *xtldata = gwy_line_get_data(xtline);
    gdouble mult;

    if (transform == TRANSFORM_LOG) {
        for (i = 0; i < (xres*yres*zres); i++)
            bdata[i] = log(fabs(bdata[i]));
    }
    else if (transform == TRANSFORM_1DER) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                for (i = 0; i < xres; i++)
                    xtldata[i] = gwy_line_der(xline, i);
                brick_insert_x_line(brick, xtline, row, lev);
            }
        }
    }
    else if (transform == TRANSFORM_2DER) {
        mult = xres*xres/xrange/xrange;
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                xtldata[0] = 0;
                xtldata[xres-1] = 0;
                for (i = 1; i < (xres - 1); i++)
                    xtldata[i] = xldata[i-1] - 2*xldata[i] + xldata[i+1];
                gwy_line_multiply(xtline, mult);
                brick_insert_x_line(brick, xtline, row, lev);
            }
        }
    }

    g_object_unref(xline);
    g_object_unref(xtline);
    return TRUE;
}

//note that brick is transposed and spectra are now in x direction
static gboolean
scale(ModuleArgs *args, G_GNUC_UNUSED GtkWindow *wait_window)
{
    GwyBrick* brick = args->prepbrick;
    ClusterScaling scaling = gwy_params_get_enum(args->params, PARAM_SCALING);
    gint row, lev;
    gdouble min, max, rms, avg, sum;
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gint zres = gwy_brick_get_zres(brick);
    gdouble xrange = gwy_brick_get_xreal(brick);
    GwyLine *xline = gwy_line_new(xres, xrange, FALSE);

    if (scaling == SCALING_RANGE) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                gwy_line_min_max(xline, &min, &max);
                gwy_line_add(xline, -min);
                gwy_line_multiply(xline, 1.0/(max-min));
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    else if (scaling == SCALING_VARIANCE) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                rms = gwy_line_rms(xline);
                gwy_line_add(xline, -gwy_line_mean(xline));
                if (rms > 0)
                    gwy_line_multiply(xline, 1.0/rms);
                else
                    gwy_line_fill(xline, 0);
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    else if (scaling == SCALING_PARETO) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                rms = gwy_line_rms(xline);
                gwy_line_add(xline, -gwy_line_mean(xline));
                if (rms > 0)
                    gwy_line_multiply(xline, 1.0/sqrt(rms));
                else
                    gwy_line_fill(xline, 0);
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    else if (scaling == SCALING_VAST) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                rms = gwy_line_rms(xline);
                avg = gwy_line_mean(xline);
                gwy_line_add(xline, -avg);
                if (rms > 0)
                    gwy_line_multiply(xline, avg/(rms*rms));
                else
                    gwy_line_fill(xline, 0);
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    else if (scaling == SCALING_AREA) {
        for (lev = 0; lev < zres; lev++) {
            for (row = 0; row < yres; row++) {
                brick_extract_x_line(brick, xline, row, lev);
                gwy_line_min_max(xline, &min, &max);
                gwy_line_add(xline, -min);
                sum = gwy_line_sum(xline); //what if we have calibration?
                sum *= xrange;
                if (sum != 0)
                    gwy_line_multiply(xline, 1/sum);
                else
                    gwy_line_fill(xline, 0);
                brick_insert_x_line(brick, xline, row, lev);
            }
        }
    }
    g_object_unref(xline);
    return TRUE;
}

static GwyGraphModel*
create_graph(GwyBrick *brick, GwyLine *calibration, const gdouble *centers, gint nclusters, gint *npix)
{
    gint xres = gwy_brick_get_xres(brick);
    gdouble xreal = gwy_brick_get_xreal(brick);
    GwyGraphModel *gmodel = gwy_graph_model_new();
    gdouble xoffset = gwy_brick_get_xoffset(brick);
    GwyGraphCurveModel *gcmodel;
    gdouble *xdata, *ydata;
    GwyUnit *siunit;
    gint c;

    xdata = g_new(gdouble, xres);
    ydata = g_new(gdouble, xres);
    if (calibration) {
        gwy_assign(xdata, gwy_line_get_data(calibration), xres);
        siunit = gwy_line_get_unit_y(calibration);
    }
    else {
        gwy_math_linspace(xdata, xres, xoffset, xreal/xres);
        siunit = gwy_brick_get_unit_x(brick);
    }

    for (c = 0; c < nclusters; c++) {
        if (!npix[c])
            continue;
        gwy_assign(ydata, centers + c*xres, xres);
        gcmodel = gwy_graph_curve_model_new();
        gwy_graph_curve_model_set_data(gcmodel, xdata, ydata, xres);
        gchar *s = g_strdup_printf(_("Cluster center %d"), c+1);
        g_object_set(gcmodel,
                     "mode", GWY_GRAPH_CURVE_LINE,
                     "line-width", 2.0,
                     "description", s,
                     NULL);
        g_free(s);
        gwy_graph_curve_model_enforce_order(gcmodel);
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);
    }
    g_free(xdata);
    g_free(ydata);

    g_object_set(gmodel,
                 "unit-x", siunit,
                 "unit-y", gwy_brick_get_unit_w(brick),
                 "axis-label-bottom", "x",
                 "axis-label-left", "y",
                 NULL);

    return gmodel;
}

static void
create_cluster_centers(GwyBrick *brick, GwyField *labelmap, gdouble *centers, gint nclusters, gint *npix)
{
    gint i, j, k, ic;
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gint zres = gwy_brick_get_zres(brick);
    gdouble *data = gwy_brick_get_data(brick);
    gdouble *labeldata = gwy_field_get_data(labelmap);
    gint *nc = g_new0(gint, nclusters);

    for (k = 0; k < zres; k++) {
        for (j = 0; j < yres; j++) {
            ic = (gint)(labeldata[k*yres + j]);
            if ((ic < 0) || (ic >= nclusters))
                continue;
            nc[ic]++;
            for (i = 0; i < xres; i++)
                centers[ic*xres + i] += data[k*xres*yres + j*xres + i];
        }
    }
    for (ic = 0; ic < nclusters; ic++) {
        npix[ic] = nc[ic];
        if (nc[ic] > 0) {
            for (i = 0; i < xres; i++)
                centers[ic*xres + i] /= nc[ic];
        }
    }

    g_free(nc);
}

static void
create_error_map(GwyBrick *brick, GwyField *errormap, GwyField *labelmap,
                 const gdouble *centers, gint nclusters)
{
    gint i, j, k, ic;
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gint zres = gwy_brick_get_zres(brick);
    gdouble *data = gwy_brick_get_data(brick);
    gdouble *errordata = gwy_field_get_data(errormap);
    gdouble *labeldata = gwy_field_get_data(labelmap);
    gdouble d, dist;

    //create error map
    for (k = 0; k < zres; k++) {
        for (j = 0; j < yres; j++) {
            dist = 0.0;
            ic = (gint)(labeldata[k*yres + j]);
            if ((ic >= 0) && (ic < nclusters)) {
                for (i = 0; i < xres; i++) {
                    d = data[k*xres*yres + j*xres + i] - centers[ic*xres + i];
                    dist += d*d;
                }
                errordata[k*yres + j] = sqrt(dist);
            } else
                errordata[k*yres + j] = 0;
        }
    }
    gwy_unit_assign(gwy_field_get_unit_z(errormap), gwy_brick_get_unit_w(brick));
}


static void
init_cluster_centers(GwyBrick *brick, GwyNield *mask, gdouble *centers, gint nclusters, gint seed)
{
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick), zres = gwy_brick_get_zres(brick);
    gint i, j, k, c, ca, startj, startk;
    gdouble *probdata = g_new0(gdouble, yres*zres);
    gint *maskdata = gwy_nield_get_data(mask);
    const gdouble *data = gwy_brick_get_data_const(brick);
    gdouble dist, mindist, max;
    GRand *rand = g_rand_new();

    //k-means++ initialisation
    //pick first center randomly
    g_rand_set_seed(rand, seed);

    do {
        startj = g_rand_int_range(rand, 0, yres);
        startk = g_rand_int_range(rand, 0, zres);
    } while (maskdata[startk*yres + startj] != 0);

    for (i = 0; i < xres; i++)
        centers[i] = data[startk*xres*yres + startj*xres + i];

    //printf("first %d %d\n", startj, startk);

    for (c = 1; c < nclusters; c++) {
        //calculate distance-based probabilities
        max = 0;
        for (k = 0; k < zres; k++) {
            for (j = 0; j < yres; j++) {
                if (maskdata[k*yres + j])
                    continue;

                //find distance to closest existing cluster
                mindist = G_MAXDOUBLE;
                for (ca = 0; ca < c; ca++) {
                    dist = 0;
                    for (i = 0; i < xres; i++) { //distance to closest
                        gdouble d = (data[k*xres*yres + j*xres + i] - centers[ca*xres + i]);
                        dist += d*d;
                    }
                    if (dist < mindist)
                        mindist = dist;
                }
                probdata[k*yres + j] = mindist;
                if (mindist > max) {
                    max = mindist;
                }
            }
        }
        //pick next randomly preferring large distance
        do {
            do {
                startj = g_rand_int_range(rand, 0, yres);
                startk = g_rand_int_range(rand, 0, zres);
            } while (maskdata[startk*yres + startj] != 0);
        } while (g_rand_double_range(rand, 0, max) > probdata[startk*yres + startj]);
        //printf("next %d %d prob %g out of %g\n", startj, startk, probdata[startk*yres + startj], max);

        //assign data to start cluster
        for (i = 0; i < xres; i++)
            centers[c*xres + i] = data[startk*xres*yres + startj*xres + i];
    }

    g_free(probdata);
    g_rand_free(rand);
}

//resort clusters to start from the biggest one, swap the labels in field and curves
static void
resort_clusters_by_size(GwyField *clusters, gdouble *centers, gint nclusters, gint res, gint *npix, gint *nnpix)
{
    gint c, m;
    gint n = gwy_field_get_xres(clusters)*gwy_field_get_yres(clusters);
    gdouble *labeldata = gwy_field_get_data(clusters);
    gdouble *nextpos = g_new(gdouble, nclusters);
    gint *shift = g_new0(gint, nclusters);
    gint *scdone = g_new0(gint, nclusters);
    gint *smdone = g_new0(gint, nclusters);
    gdouble *nextcenters = g_new(gdouble, nclusters*res);

    for (c = 0; c < nclusters; c++)
        nextpos[c] = npix[c];

    gwy_math_sort(nextpos, nclusters);

    for (c = 0; c < nclusters; c++) {
        nnpix[c] = nextpos[nclusters - c - 1];
        shift[c] = -1;
    }

    for (c = 0; c < nclusters; c++) {
        for (m = 0; m < nclusters; m++) {
      //      printf("c %d  m %d  %d=%d  scdone %d smdone %d\n", c, m, nnpix[c], npix[m], scdone[c], smdone[m]);
            if (nnpix[c] == npix[m] && scdone[c] == 0 && smdone[m] == 0) {
      //          printf("assigning shift[%d] = %d  scdone[%d] smdone[%d] = 1\n", m, c, c, m);
                shift[m] = c;
                scdone[c] = 1;
                smdone[m] = 1;
            }
        }
    }
    //shift[i] is the new value to be entered for cluster i

    //for (c = 0; c < nclusters; c++)
    //    printf("old %d new %d  shift %d scdone %d smdone %d\n", npix[c], nnpix[c], shift[c], scdone[c], smdone[c]);

    //swap labels in place
    for (m = 0; m < n; m++) {
        if (labeldata[m] >= 0)
            labeldata[m] = shift[(gint)labeldata[m]];
    }
    //swap curves
    for (c = 0; c < nclusters; c++) {
        for (m = 0; m < res; m++)
            nextcenters[shift[c]*res + m] = centers[c*res + m];
    }
    for (m = 0; m < nclusters*res; m++)
        centers[m] = nextcenters[m];

    g_free(nextpos);
    g_free(shift);
    g_free(scdone);
    g_free(smdone);
    g_free(nextcenters);
}




/************************************************************************************************************
 *
 * K-means
 *
 ************************************************************************************************************/
static void
define_params_kmeans(GwyParamDef *paramdef)
{
    gwy_param_def_add_int(paramdef, PARAM_KMEANS_K, "kmeans/k", _("Number of clusters"), 2, 100.0, 10);
    gwy_param_def_add_int(paramdef, PARAM_KMEANS_ITERATIONS,
                          "kmeans/iterations", _("_Max. iterations"), 1, 100000, 100);
    gwy_param_def_add_double(paramdef, PARAM_KMEANS_LOG_EPSILON, NULL, _("Convergence _precision digits"),
                             1.0, 20.0, 12.0);
    gwy_param_def_add_boolean(paramdef, PARAM_KMEANS_REMOVE_OUTLIERS, "kmeans/remove_outliers",
                              _("_Remove outliers"), FALSE);
    gwy_param_def_add_double(paramdef, PARAM_KMEANS_OUTLIERS_THRESHOLD, "kmeans/outliers_threshold",
                             _("Outliers _threshold"), 1.0, 10.0, 3.0);
}

static void
append_gui_kmeans(ModuleGUI *gui)
{
    GwyParamTable *table = gui->table_methodparam[METHOD_KMEANS];

    gwy_param_table_append_slider(table, PARAM_KMEANS_K);
    gwy_param_table_append_slider(table, PARAM_KMEANS_LOG_EPSILON);
    gwy_param_table_slider_set_mapping(table, PARAM_KMEANS_LOG_EPSILON, GWY_SCALE_MAPPING_LINEAR);
    gwy_param_table_append_slider(table, PARAM_KMEANS_ITERATIONS);
    gwy_param_table_slider_set_mapping(table, PARAM_KMEANS_ITERATIONS, GWY_SCALE_MAPPING_LOG);
    gwy_param_table_append_checkbox(table, PARAM_KMEANS_REMOVE_OUTLIERS);
    gwy_param_table_append_slider(table, PARAM_KMEANS_OUTLIERS_THRESHOLD);
}

static gboolean
cluster_kmeans(ModuleArgs *args, GtkWindow *wait_window)
{
    GwyParams *params = args->params;
    GwyBrick *brick = args->prepbrick;
    gint nclusters = gwy_params_get_int(params, PARAM_KMEANS_K);
    gint max_iterations = gwy_params_get_int(params, PARAM_KMEANS_ITERATIONS);
    gdouble epsilon = -log10(gwy_params_get_double(params, PARAM_KMEDIANS_LOG_EPSILON));
    gboolean remove_outliers = gwy_params_get_boolean(params, PARAM_KMEANS_REMOVE_OUTLIERS);
    gdouble outliers_threshold = gwy_params_get_double(params, PARAM_KMEANS_OUTLIERS_THRESHOLD);
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick), zres = gwy_brick_get_zres(brick);
    GwyField *clusters = NULL, *errormap = NULL;
    GwyNield *mask = args->mask;
    const gdouble *data;
    gdouble min, dist;
    gboolean ok = FALSE, cancelled = FALSE, converged = FALSE;
    gint i, j, k, c, iterations = 0;

    /* NB: brick == args->prepbrick is transposed wrt args->brick. Try to not mix them in a confusing manner… */
    clusters = gwy_brick_new_field_like_xy_plane(args->brick, FALSE);
    gwy_field_fill(clusters, -1); //unassigned
    errormap = gwy_field_new_alike(clusters, TRUE);

    gwy_app_wait_start(wait_window, _("Initializing..."));

    data = gwy_brick_get_data_const(brick);

    gdouble *centers = g_new0(gdouble, nclusters*xres);
    gdouble *oldcenters = g_new(gdouble, nclusters*xres);
    gdouble *sum = g_new(gdouble, xres*nclusters);
    gdouble *variance = g_new(gdouble, nclusters);
    gint *npix = g_new(gint, nclusters);
    gdouble *labeldata = gwy_field_get_data(clusters);
    gint *maskdata = gwy_nield_get_data(mask);

    //kmeans++ initialisation
    init_cluster_centers(brick, mask, centers, nclusters, gwy_params_get_int(params, PARAM_SEED));

    if (!gwy_app_wait_set_message(_("K-means iteration...")))
        cancelled = TRUE;

    /* FIXME: There is lots of repetitive and/or OpenMP parallelisable code below. */
    while (!converged && !cancelled) {
        if (!gwy_app_wait_set_fraction((gdouble)iterations/max_iterations)) {
            cancelled = TRUE;
            break;
        }

        /* pixels belong to cluster with min distance */
        for (k = 0; k < zres; k++) {
            for (j = 0; j < yres; j++) {
                if (maskdata[k*yres + j])
                    continue;
                labeldata[k*yres + j] = 0;
                min = G_MAXDOUBLE;
                for (c = 0; c < nclusters; c++) {
                    dist = 0;
                    for (i = 0; i < xres; i++) {
                        oldcenters[c*xres + i] = centers[c*xres + i];
                        gdouble d = (data[k*xres*yres + j*xres + i] - centers[c*xres + i]);
                        dist += d*d;
                    }
                    if (dist < min) {
                        min = dist;
                        labeldata[k*yres + j] = c;
                    }
                }
            }
        }
        /* new center coordinates as average of pixels */
        for (c = 0; c < nclusters; c++) {
            npix[c] = 0;
            gwy_clear(sum + c*xres, xres);
        }
        for (k = 0; k < zres; k++) {
            for (j = 0; j < yres; j++) {
                if (maskdata[k*yres + j])
                    continue;
                c = (gint)(labeldata[k*yres + j]);
                npix[c] += 1;
                for (i = 0; i < xres; i++)
                    sum[c*xres + i] += data[k*xres*yres + j*xres + i];
            }
        }
        for (c = 0; c < nclusters; c++)
            for (i = 0; i < xres; i++)
                centers[c*xres + i] = (npix[c] > 0) ? sum[c*xres + i]/(gdouble)(npix[c]) : 0.0;

        converged = TRUE;
        for (c = 0; c < nclusters; c++) {
            for (i = 0; i < xres; i++)
                if (fabs(oldcenters[c*xres + i] - centers[c*xres + i]) > epsilon) {
                    converged = FALSE;
                    break;
                }
        }
        if (iterations == max_iterations) {
            converged = TRUE;
            break;
        }
        iterations++;
    }
    if (cancelled) {
        gwy_app_wait_finish();
        goto fail;
    }

    /* second try, outliers are not counted now */
    if (remove_outliers) {
        converged = FALSE;
        iterations = 0;
        while (!converged && !cancelled) {
            if (!gwy_app_wait_set_fraction((gdouble)iterations/max_iterations)) {
                cancelled = TRUE;
                break;
            }

            /* pixels belong to cluster with min distance */
            for (k = 0; k < zres; k++)
                for (j = 0; j < yres; j++) {
                    if (maskdata[k*yres + j])
                        continue;
                    labeldata[k*yres + j] = 0;
                    min = G_MAXDOUBLE;
                    for (c = 0; c < nclusters; c++) {
                        dist = 0;
                        for (i = 0; i < xres; i++) {
                            gdouble d = data[k*xres*yres + j*xres + i] - centers[c*xres + i];
                            oldcenters[c*xres + i] = centers[c*xres + i];
                            dist += d*d;
                        }
                        if (dist < min) {
                            min = dist;
                            labeldata[k*yres + j] = c;
                        }
                    }
                }
            /* variance calculation */
            for (c = 0; c < nclusters; c++) {
                npix[c] = 0;
                variance[c] = 0.0;
            }

            for (k = 0; k < zres; k++) {
                for (j = 0; j < yres; j++) {
                    if (maskdata[k*yres + j])
                        continue;
                    c = (gint)(labeldata[k*yres + j]);
                    npix[c] += 1;
                    dist = 0;
                    for (i = 0; i < xres; i++) {
                        gdouble d = data[k*xres*yres + j*xres + i] - centers[c*xres + i];
                        dist += d*d;
                    }
                    variance[c] += dist;
                }
            }

            for (c = 0; c < nclusters; c++) {
                if (npix[c] > 0) {
                    variance[c] /= npix[c];
                    variance[c] = sqrt(variance[c]);
                }
            }
            /* new center coordinates as average of pixels */
            for (c = 0; c < nclusters; c++) {
                npix[c] = 0;
                for (i = 0; i < xres; i++) {
                    sum[c*xres + i] = 0;
                }
            }
            for (k = 0; k < zres; k++) {
                for (j = 0; j < yres; j++) {
                    if (maskdata[k*yres + j])
                        continue;
                    c = (gint)(labeldata[k*yres + j]);
                    dist = 0;
                    for (i = 0; i < xres; i++) {
                        dist += ((data[k*xres*yres + j*xres + i] - centers[c*xres + i])
                                 * (data[k*xres*yres + j*xres + i] - centers[c*xres + i]));
                    }
                    if (sqrt(dist) < outliers_threshold * variance[c]) {
                        npix[c] += 1;
                        for (i = 0; i < xres; i++)
                            sum[c*xres + i] += data[k*xres*yres + j*xres + i];
                    } else {
                        maskdata[k*yres + j] = 1;
                    }
                }
            }
            for (c = 0; c < nclusters; c++)
                for (i = 0; i < xres; i++) {
                    centers[c*xres + i] = (npix[c] > 0) ? sum[c*xres + i]/(gdouble)(npix[c]) : 0.0;
                }
            converged = TRUE;
            for (c = 0; c < nclusters; c++) {
                for (i = 0; i < xres; i++)
                    if (fabs(oldcenters[c*xres + i] - centers[c*xres + i]) > epsilon) {
                        converged = FALSE;
                        break;
                    }
            }
            if (iterations == max_iterations) {
                converged = TRUE;
                break;
            }
            iterations++;
        }
    }

    gwy_app_wait_finish();
    if (cancelled) {
        goto fail;
    }
    create_error_map(brick, errormap, clusters, centers, nclusters);

    gint *nnpix = g_new0(gint, nclusters);
    resort_clusters_by_size(clusters, centers, nclusters, xres, npix, nnpix);

    gwy_field_add(clusters, 1); //shift clusters to start at 1

    args->result = g_object_ref(clusters);
    args->errormap = g_object_ref(errormap);
    args->gmodel = create_graph(brick, gwy_brick_get_zcalibration(args->brick), centers, nclusters, nnpix);
    ok = TRUE;

    g_free(nnpix);

fail:
    g_clear_object(&errormap);
    g_clear_object(&clusters);
    g_free(npix);
    g_free(sum);
    g_free(variance);
    g_free(oldcenters);
    g_free(centers);

    return ok;
}

/************************************************************************************************************
 *
 * K-medians
 *
 ************************************************************************************************************/
static void
define_params_kmedians(GwyParamDef *paramdef)
{
    gwy_param_def_add_int(paramdef, PARAM_KMEDIANS_K, "kmedians/k", _("Number of clusters"), 2, 100.0, 10);
    gwy_param_def_add_int(paramdef, PARAM_KMEDIANS_ITERATIONS,
                          "kmedians/iterations", _("_Max. iterations"), 1, 100000, 100);
    gwy_param_def_add_double(paramdef, PARAM_KMEDIANS_LOG_EPSILON, NULL, _("Convergence _precision digits"),
                             1.0, 20.0, 12.0);
}

static void
append_gui_kmedians(ModuleGUI *gui)
{
    GwyParamTable *table = gui->table_methodparam[METHOD_KMEDIANS];

    gwy_param_table_append_slider(table, PARAM_KMEDIANS_K);
    gwy_param_table_append_slider(table, PARAM_KMEDIANS_LOG_EPSILON);
    gwy_param_table_slider_set_mapping(table, PARAM_KMEDIANS_LOG_EPSILON, GWY_SCALE_MAPPING_LINEAR);
    gwy_param_table_append_slider(table, PARAM_KMEDIANS_ITERATIONS);
    gwy_param_table_slider_set_mapping(table, PARAM_KMEDIANS_ITERATIONS, GWY_SCALE_MAPPING_LOG);
}

static gboolean
cluster_kmedians(ModuleArgs *args, GtkWindow *wait_window)
{
    GwyParams *params = args->params;
    GwyBrick *brick = args->prepbrick;
    gint nclusters = gwy_params_get_int(params, PARAM_KMEDIANS_K);
    gint max_iterations = gwy_params_get_int(params, PARAM_KMEDIANS_ITERATIONS);
    gdouble epsilon = -log10(gwy_params_get_double(params, PARAM_KMEDIANS_LOG_EPSILON));
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick), zres = gwy_brick_get_zres(brick);
    GwyField *clusters = NULL, *errormap = NULL;
    GwyNield *mask = args->mask;
    const gdouble *data;
    gdouble min, dist, d;
    gboolean ok = FALSE, cancelled = FALSE, converged = FALSE;
    gint i, j, k, c, iterations = 0;

    clusters = gwy_field_new(yres, zres,
                             gwy_brick_get_xreal(args->brick), gwy_brick_get_yreal(args->brick),
                             FALSE);
    gwy_field_fill(clusters, 0); //unassigned
    gwy_field_set_xoffset(clusters, gwy_brick_get_xoffset(args->brick));
    gwy_field_set_yoffset(clusters, gwy_brick_get_yoffset(args->brick));
    gwy_unit_assign(gwy_field_get_unit_xy(clusters), gwy_brick_get_unit_x(args->brick));

    errormap = gwy_field_new_alike(clusters, TRUE);

    gwy_app_wait_start(wait_window, _("Initializing..."));

    data = gwy_brick_get_data_const(brick);

    gdouble *centers = g_new0(gdouble, nclusters*xres);
    gdouble *oldcenters = g_new(gdouble, nclusters*xres);
    gdouble *plane = g_new(gdouble, yres*zres*nclusters);
    gint *npix = g_new(gint, nclusters);
    gdouble *labeldata = gwy_field_get_data(clusters);
    gint *maskdata = gwy_nield_get_data(mask);

    //kmeans++ initialisation
    init_cluster_centers(brick, mask, centers, nclusters, gwy_params_get_int(params, PARAM_SEED));

    if (!gwy_app_wait_set_message(_("K-medians iteration...")))
        cancelled = TRUE;

    while (!converged && !cancelled) {
        if (!gwy_app_wait_set_fraction((gdouble)iterations/max_iterations)) {
            cancelled = TRUE;
            break;
        }

        /* pixels belong to cluster with min distance */
        for (k = 0; k < zres; k++)
            for (j = 0; j < yres; j++) {
                if (maskdata[k*yres + j] != 0)
                    continue;
                labeldata[k*yres + j] = 0;
                min = G_MAXDOUBLE;
                for (c = 0; c < nclusters; c++) {
                    dist = 0;
                    for (i = 0; i < xres; i++) {
                        d = data[k*xres*yres + j*xres + i] - centers[c*xres + i];
                        oldcenters[c*xres + i] = centers[c*xres + i];
                        dist += d*d;
                    }
                    if (dist < min) {
                        min = dist;
                        labeldata[k*yres + j] = c;
                    }
                }
            }
        /* We're calculating median per one coordinate of all pixels that belongs to same cluster and use it as this
         * coordinate position for cluster center */
        for (i = 0; i < xres; i++) {
            gwy_clear(npix, nclusters);
            for (k = 0; k < zres; k++) {
                for (j = 0; j < yres; j++) {
                    if (maskdata[k*yres + j] != 0)
                        continue;
                    c = (gint)(labeldata[k*yres + j]);
                    npix[c]++;
                    plane[c*yres*zres + npix[c] - 1] = data[k*xres*yres + j*xres + i];
                }
            }
            for (c = 0; c < nclusters; c++) {
                gwy_math_sort(plane + c*yres*zres, npix[c]);
                centers[c*xres + i] = plane[c*yres*zres + npix[c]/2];
            }
        }

        converged = TRUE;
        for (c = 0; c < nclusters; c++) {
            for (i = 0; i < xres; i++)
                if (fabs(oldcenters[c*xres + i] - centers[c*xres + i]) > epsilon) {
                    converged = FALSE;
                    break;
                }
        }
        if (iterations == max_iterations) {
            converged = TRUE;
        }
        iterations++;
    }
    gwy_app_wait_finish();
    if (cancelled) {
        goto fail;
    }
    create_error_map(brick, errormap, clusters, centers, nclusters);

    gint *nnpix = g_new0(gint, nclusters);
    resort_clusters_by_size(clusters, centers, nclusters, xres, npix, nnpix);

    gwy_field_add(clusters, 1); //shift clusters to start at 1

    args->result = g_object_ref(clusters);
    args->errormap = g_object_ref(errormap);
    args->gmodel = create_graph(brick, gwy_brick_get_zcalibration(args->brick), centers, nclusters, nnpix);
    ok = TRUE;

    g_free(nnpix);

fail:
    g_clear_object(&errormap);
    g_clear_object(&clusters);
    g_free(npix);
    g_free(oldcenters);
    g_free(centers);

    return ok;
}

/************************************************************************************************************
 *
 * DBSCAN
 *
 ************************************************************************************************************/
static void
define_params_dbscan(GwyParamDef *paramdef)
{
    gwy_param_def_add_int(paramdef, PARAM_DBSCAN_NEIGHBORS, "dbscan/n", _("Min. neighbors"), 1, 100.0, 10);
    gwy_param_def_add_percentage(paramdef, PARAM_DBSCAN_RANGE, "dbscan/range", _("_Range factor"), 0.1);
}

static void
append_gui_dbscan(ModuleGUI *gui)
{
    GwyParamTable *table = gui->table_methodparam[METHOD_DBSCAN];

    gwy_param_table_append_slider(table, PARAM_DBSCAN_RANGE);
    gwy_param_table_append_slider(table, PARAM_DBSCAN_NEIGHBORS);
}

static gint
get_neighbors(GwyBrick *brick, gdouble *labeldata, gint jm, gint km,
              gdouble range, gint *nblistj, gint *nblistk)
{
    gint i, j, k, nnbs;
    gint xres = gwy_brick_get_xres(brick);
    gint yres = gwy_brick_get_yres(brick);
    gint zres = gwy_brick_get_zres(brick);
    gdouble dist, sumdist, sqrange = range*range;
    gdouble *data = gwy_brick_get_data(brick);

    nnbs = 0;
    for (k = 0; k < zres; k++) {
       for (j = 0; j < yres; j++) {
          if (((j == jm) && (k == km)) || labeldata[k*yres + j] == -3) //it is me or masked point
              continue;

          sumdist = 0;
          for (i = 0; i < xres; i++) {
              dist = data[k*xres*yres + j*xres + i] - data[km*xres*yres + jm*xres + i];
              sumdist += dist*dist;
          }
          if ((sumdist < sqrange) && (nnbs < (yres*zres))) {
              nblistj[nnbs] = j;
              nblistk[nnbs] = k;
              nnbs++;
          }
       }
    }
    return nnbs;
}

static gboolean
cluster_dbscan(ModuleArgs *args, GtkWindow *wait_window)
{
    GwyParams *params = args->params;
    GwyBrick *brick = args->prepbrick;
    gdouble range = gwy_params_get_double(params, PARAM_DBSCAN_RANGE);
    gint minpts = gwy_params_get_int(params, PARAM_DBSCAN_NEIGHBORS);
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick), zres = gwy_brick_get_zres(brick);
    GwyField *clusters = NULL, *errormap = NULL;
    GwyNield *mask = args->mask;
    gboolean ok = FALSE, cancelled = FALSE;
    gchar message[100];
    gint i, j, k, si, m, nseed, npos, ic, found, nclusters, ndone, nvalid;

    clusters = gwy_field_new(yres, zres, gwy_brick_get_xreal(args->brick), gwy_brick_get_yreal(args->brick), TRUE);
    gwy_field_set_xoffset(clusters, gwy_brick_get_xoffset(args->brick));
    gwy_field_set_yoffset(clusters, gwy_brick_get_yoffset(args->brick));
    gwy_unit_assign(gwy_field_get_unit_xy(clusters), gwy_brick_get_unit_x(args->brick));

    gwy_app_wait_start(wait_window, _("Initializing..."));

    const gdouble *data = gwy_brick_get_data_const(brick);
    npos = yres*zres;

    gwy_field_fill(clusters, -1);             //unassigned
    gdouble *labeldata = gwy_field_get_data(clusters);

    gint *nblistj = g_new(gint, npos*sizeof(gint));    //neighbors list
    gint *nblistk = g_new(gint, npos*sizeof(gint));
    gint *seedlistj = g_new(gint, npos*sizeof(gint));  //dbscan dynamic seed list
    gint *seedlistk = g_new(gint, npos*sizeof(gint));
    gdouble *mc = g_new0(gdouble, xres*sizeof(gdouble));  //mean curve

    errormap = gwy_field_new_alike(clusters, TRUE);
    gdouble *mcdist = gwy_field_get_data(errormap);
    gint *maskdata = gwy_nield_get_data(mask);

    //get some normalisation for range: a mean curve and variance of all the curves from it
    //mark points that are under mask, not to be used for clustering
    nvalid = 0;
    for (k = 0; k < zres; k++) {
        for (j = 0; j < yres; j++) {
            if (maskdata[k*yres + j] != 0)
                continue;
            for (i = 0; i < xres; i++) {
                mc[i] += data[k*xres*yres + j*xres + i];
            }
            nvalid++;
        }
    }
    for (i = 0; i < xres; i++) {
        mc[i] /= nvalid;
    }
    for (k = 0; k < zres; k++) {
        for (j = 0; j < yres; j++) {
            if (maskdata[k*yres + j] != 0)
                labeldata[k*yres + j] = -3; //ignore this point while clustering
            else {
                for (i = 0; i < xres; i++) {
                    mcdist[k*yres + j] +=
                        (data[k*xres*yres + j*xres + i] - mc[i])*(data[k*xres*yres + j*xres + i] - mc[i]);
                }
            }
        }
    }
    for (k = 0; k < zres; k++) {
        for (j = 0; j < yres; j++) {
            mcdist[k*yres + j] = sqrt(mcdist[k*yres + j]);
        }
    }

    gwy_field_data_changed(errormap);
    range = 10*range*gwy_field_area_rms(errormap, mask, GWY_MASK_EXCLUDE, 0, 0, yres, zres);

    //perform clustering
    if (!(gwy_app_wait_set_message(_("Clustering...")) && gwy_app_wait_set_fraction(0.0))) {
        cancelled = TRUE;
        goto fail;
    }

    ic = -1;
    ndone = 0;
    for (k = 0; k < zres; k++) {
        for (j = 0; j < yres; j++) {
            if (labeldata[j + yres*k] != -1)
                continue;

            gint nnbs = get_neighbors(brick, labeldata, j, k, range, nblistj, nblistk);
            //printf("curve %d, %d has %d neighbors:\n", j, k, nnbs);
            //for (i=0; i<nnbs; i++)
            //    printf("%d %d %d\n", i, nblistj[i], nblistk[i]);

            if (nnbs < minpts) {
                //printf("point %d %d is noise\n", i, j);
                labeldata[k*yres + j] = -2; //noise
                continue;
            }

            ic++;
            labeldata[k*yres + j] = ic;
            ndone++;
            //printf("point %d %d is cluster %d\n", i, j, ic);

            nseed = nnbs;
            for (i = 0; i < nnbs; i++) {
               seedlistj[i] = nblistj[i];
               seedlistk[i] = nblistk[i];
            }

            //printf("seed list has %d points\n", nseed);
            si = 0;
            while (si < nseed) {
                //printf("going to seed point %d of %d (pos %d %d)\n", si, nseed, seedlistj[si], seedlistk[si]);
                if (labeldata[(seedlistj[si] + yres*seedlistk[si])] == -2) {//was noise, now will be in cluster
                    //printf("was a noise, but should be our cluster\n");
                    labeldata[(seedlistj[si] + yres*seedlistk[si])] = ic;
                }

                if (labeldata[(seedlistj[si] + yres*seedlistk[si])] != -1) {//was already processed
                    //printf("this was already processed\n");
                    si++;
                    continue;
                }

                labeldata[(seedlistj[si] + yres*seedlistk[si])] = ic;
                ndone++;
                //printf("point %d %d is cluster %d\n", seedlistj[si], seedlistk[si], ic);

                nnbs = get_neighbors(brick, labeldata, seedlistj[si], seedlistk[si],
                                     range, nblistj, nblistk);

                if (nnbs >= minpts) {
                    if ((nnbs+nseed) < nvalid) {
                        //printf("adding up to %d seed points\n", nnbs);
                        for (i = 0; i < nnbs; i++) {
                            found = 0;
                            for (m = 0; m < nseed; m++) {
                                if ((seedlistj[m] == nblistj[i]) && (seedlistk[m] == nblistk[i])) {
                                    //printf("seed candidate %d %d is already in the list\n", nblistj[i], nblistk[i]);
                                    found = 1;
                                }
                            }
                            if (!found) {
                               seedlistj[nseed] = nblistj[i];
                               seedlistk[nseed] = nblistk[i];
                               nseed++;
                            }
                        }
                    }
                }
                //printf("seed point %d done\n", si);
                snprintf(message, sizeof(message), "Clustering: started from %2.1f%% positions, found %d clusters\n",
                         100.0*k/zres, ic+1);
                if (!(gwy_app_wait_set_message(message) && gwy_app_wait_set_fraction((gdouble)ndone/nvalid))) {
                    cancelled = TRUE;
                    break;
               }
               si++;
          }
          if (cancelled)
              break;
       }
       if (cancelled)
           break;

       snprintf(message, sizeof(message), "Clustering: started from %2.1f%% positions, found %d clusters\n",
                100.0*k/zres, ic+1);
       if (!(gwy_app_wait_set_message(message) && gwy_app_wait_set_fraction((gdouble)ndone/nvalid))) {
           cancelled = TRUE;
           break;
       }
    }
    gwy_app_wait_finish();
    if (cancelled || (ic <= 0)) {
        //printf("cancelled or ic %d <= 0\n", ic);
        goto fail;
    }

    for (k = 0; k < yres*zres; k++) {
        if (labeldata[k] == -3) //this was mask
            labeldata[k] = -1; //now unassigned

        if (labeldata[k] == -2) //this was noise
            labeldata[k] = -0.5; //now still noise
    }

    //create cluster centers
    //printf("%d clusters in total\n", ic);
    nclusters = ic + 1;
    gdouble *centers = g_new0(gdouble, nclusters*xres);
    gint *npix = g_new0(gint, nclusters);
    create_cluster_centers(brick, clusters, centers, nclusters, npix);
    create_error_map(brick, errormap, clusters, centers, nclusters);

    gint *nnpix = g_new0(gint, nclusters);
    resort_clusters_by_size(clusters, centers, nclusters, xres, npix, nnpix);

    gwy_field_add(clusters, 1); //shift clusters to start at 1

    args->result = g_object_ref(clusters);
    args->errormap = g_object_ref(errormap);
    args->gmodel = create_graph(brick, gwy_brick_get_zcalibration(args->brick), centers, nclusters, nnpix);
    ok = TRUE;

    g_free(centers);
    g_free(npix);

fail:
    g_clear_object(&errormap);
    g_clear_object(&clusters);
    g_free(nblistj);
    g_free(nblistk);
    g_free(seedlistj);
    g_free(seedlistk);
    g_free(mc);

    return ok;
}

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