/*
 *  $Id: classify.c 30111 2026-06-22 11:02:00Z yeti-dn $
 *  Copyright (C) 2003-2026 David Necas (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 "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES GWY_RUN_INTERACTIVE

#define CDEBUG 0

enum {
    NARGS = 4,
    NCRITS = 5,
};

typedef enum {
    CLASSIFY_QUANTITY_VALUE = 0,
    CLASSIFY_QUANTITY_LOG   = 1,
    CLASSIFY_QUANTITY_SLOPE = 2,
    CLASSIFY_QUANTITY_NTYPES
} ClassifyQuantityType;

typedef enum {
    CLASSIFY_DISPLAY_MASK_A   = 0,
    CLASSIFY_DISPLAY_MASK_B   = 1,
    CLASSIFY_DISPLAY_RESULT_A = 2,
    CLASSIFY_DISPLAY_RESULT_B = 3,
    CLASSIFY_DISPLAY_NTYPES
} ClassifyDisplayType;

typedef enum {
    CLASSIFY_SCALE_1X  = 0,
    CLASSIFY_SCALE_2X  = 1,
    CLASSIFY_SCALE_4X  = 2,
    CLASSIFY_SCALE_8X  = 3,
    CLASSIFY_SCALE_16X = 4,
    CLASSIFY_SCALE_32X = 5,
    CLASSIFY_SCALE_NTYPES
} ClassifyScaleType;

typedef struct {
    guint err;
    GwyAppDataId objects[NARGS];
    GwyAppDataId show;
    gint maska;
    gint maskb;
    gint id[2*NCRITS];
    ClassifyQuantityType quantity[2*NCRITS];
    ClassifyScaleType scale[2*NCRITS];
    ClassifyDisplayType display;
    GwyNield *result_a;
    GwyNield *result_b;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *show;
    GtkWidget *dialog;
    GtkWidget *display;
    GtkWidget *view;
    GtkWidget *data[NARGS];
    GSList *maska;
    GSList *maskb;
} ModuleGUI;

#define MAXRULES 100
#define MAXBRANCHES 10
#define PURCRIT 1e-2

typedef struct {
    int nrules;
    int rule_parameter[MAXRULES];   //which parameter (dfield) to use for decision
    double rule_threshold[MAXRULES]; //threshold for decision
    int rule_goto_high[MAXRULES];    //points to either result (-1, -2) or next rule
    int rule_goto_low[MAXRULES];     //points to either result (-1, -2) or next rule
} CTree;

typedef struct {
    CTree ct[100];
    int verbose;
} Classifier;

static gboolean         module_register             (void);
static void             module_main                 (GwyFile *data,
                                                     GwyRunModeFlags mode);
static void             load_args                   (GwyDict *settings,
                                                     ModuleArgs *args);
static void             save_args                   (GwyDict *settings,
                                                     ModuleArgs *args);
static GwyDialogOutcome run_gui                     (GwyFile *data,
                                                     gint id,
                                                     ModuleArgs *args);
static void             data_chosen                 (GwyDataChooser *chooser,
                                                     ModuleGUI *gui);
static void             show_chosen                 (GwyDataChooser *chooser,
                                                     ModuleGUI *gui);
static void             maska_selected              (ModuleGUI *gui);
static void             maskb_selected              (ModuleGUI *gui);
static void             preview                     (gpointer user_data);
static GtkWidget*       quantity_selector_new       (ModuleGUI *gui,
                                                     gint i);
static GtkWidget*       scale_selector_new          (ModuleGUI *gui,
                                                     gint i);
static GtkWidget*       display_selector_new        (ModuleGUI *gui);
static void             id_selected                 (ModuleGUI *gui,
                                                     GtkAdjustment *adj);
static void             update_criterion_sensitivity(GtkAdjustment *adj);
static void             update_view                 (GtkComboBox *combo,
                                                     ModuleGUI *gui);
static void             run_classification          (ModuleGUI *gui);
static void             classifier_train_full       (Classifier *cl,
                                                     GwyField **cldata,
                                                     gint ndata,
                                                     GwyNield *mask_a,
                                                     GwyNield *mask_b);
static void             classifier_run              (Classifier *cl,
                                                     GwyField **cldata,
                                                     gint ndata,
                                                     GwyNield *result_a,
                                                     GwyNield *result_b);
static void             ctree_run                   (CTree *ct,
                                                     GwyField **cldata,
                                                     gint ndata,
                                                     GwyNield *result_a,
                                                     GwyNield *result_b);

static GwyAppDataId object_ids[NARGS];

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Classify data sets using multiple data fields."),
    "Petr Klapetek <klapetek@gwyddion.net>",
    "1.2",
    "Petr Klapetek",
    "2020",
};

GWY_MODULE_QUERY2(module_info, classify)

static gboolean
module_register(void)
{
    guint i;

    for (i = 0; i < NARGS; i++) {
        object_ids[i].datano = GWY_FILE_ID_NONE;
        object_ids[i].id = -1;
    }
    gwy_image_func_register("classify",
                            module_main,
                            N_("/M_ultidata/_Classify..."),
                            NULL,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Classify data sets"));

    return TRUE;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    ModuleArgs args;
    GwyDict *settings;
    gint datano, id;

    g_return_if_fail(mode & RUN_MODES);
    gwy_clear1(args);

    gwy_data_browser_get_current(GWY_APP_FIELD_ID, &id,
                                 GWY_APP_FILE_ID, &datano,
                                 0);

    args.objects[0].datano = datano;
    args.objects[0].id = id;

    settings = gwy_app_settings_get();
    load_args(settings, &args);

    GwyDialogOutcome outcome = run_gui(data, id, &args);
    if (outcome == GWY_DIALOG_PROCEED) {
        GQuark quark = gwy_file_key_image_mask(args.show.id);
        gwy_app_undo_qcheckpointv(GWY_DICT(data), 1, &quark);

        if (args.result_b && args.display == CLASSIFY_DISPLAY_RESULT_B)
            gwy_file_set_image_mask(data, args.show.id, args.result_b);
        else if (args.result_a)
            gwy_file_set_image_mask(data, args.show.id, args.result_a);
    }

    save_args(settings, &args);
}

static GwyDialogOutcome
run_gui(GwyFile *data,
        gint id,
        ModuleArgs *args)
{
    GtkWidget *hbox, *vbox, *vbox2, *grid, *chooser, *label, *button, *combo, *spin;
    ModuleGUI gui;
    GwyField *dfield;
    guint i, row;
    GwyRGBA color;
    gchar *s;

    gui.args = args;

    gui.dialog = gwy_dialog_new(_("Classify"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), hbox,
                       FALSE, FALSE, 4);

    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
    /* Ensure no wild changes of the dialog size due to non-square data. */
    gtk_widget_set_size_request(vbox, PREVIEW_SIZE, PREVIEW_SIZE);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 4);

    dfield = gwy_field_new(PREVIEW_SIZE, PREVIEW_SIZE, 1.0, 1.0, TRUE);
    gui.view = gwy_create_preview(dfield, NULL, PREVIEW_SIZE);
    /* XXX: This should go away with GwyParams. */
    gwy_rgba_get_from_dict(&color, gwy_app_settings_get(), "/mask");
    gwy_data_view_set_mask_color(GWY_DATA_VIEW(gui.view), &color);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.view), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_MASK_COLOR | GWY_FILE_ITEM_REAL_SQUARE);
    gtk_box_pack_start(GTK_BOX(vbox), gui.view, FALSE, FALSE, 0);

    vbox2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
    gtk_box_pack_start(GTK_BOX(hbox), vbox2, TRUE, TRUE, 4);

    grid = gtk_grid_new();
    gtk_grid_set_row_spacing(GTK_GRID(grid), 2);
    gtk_grid_set_column_spacing(GTK_GRID(grid), 6);
    gtk_container_set_border_width(GTK_CONTAINER(grid), 4);
    gtk_box_pack_start(GTK_BOX(vbox2), grid, TRUE, TRUE, 4);
    row = 0;

    label = gtk_label_new(_("Id"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);

    label = gtk_label_new(_("Data"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 1, row, 1, 1);

    label = gtk_label_new(_("Mask A"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 2, row, 1, 1);

    label = gtk_label_new(_("Mask B"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 3, row, 1, 1);
    row++;

    gui.maska = NULL;
    gui.maskb = NULL;
    for (i = 0; i < NARGS; i++) {
        /* VALUE is 0 */
        s = g_strdup_printf("%d:", i+1);
        label = gtk_label_new(s);
        g_free(s);
        gtk_label_set_xalign(GTK_LABEL(label), 0.0);
        gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);

        chooser = gwy_data_chooser_new(GWY_FILE_IMAGE);
        gwy_data_chooser_set_active_id(GWY_DATA_CHOOSER(chooser), args->objects + i);
        g_signal_connect(chooser, "changed", G_CALLBACK(data_chosen), &gui);
        g_object_set_data(G_OBJECT(chooser), "index", GUINT_TO_POINTER(i));
        gtk_grid_attach(GTK_GRID(grid), chooser, 1, row, 1, 1);
        gtk_label_set_mnemonic_widget(GTK_LABEL(label), chooser);
        gui.data[i] = chooser;

        button = gtk_radio_button_new(gui.maska);
        gui.maska = gtk_radio_button_get_group(GTK_RADIO_BUTTON(button));
        gwy_radio_button_set_value(button, i);
        s = g_strdup_printf(_("Mask A is in data d%d"), i+1);
        gtk_widget_set_tooltip_text(button, s);
        g_free(s);
        gtk_grid_attach(GTK_GRID(grid), button, 2, row, 1, 1);
        g_signal_connect_swapped(button, "clicked", G_CALLBACK(maska_selected), &gui);

        button = gtk_radio_button_new(gui.maskb);
        gui.maskb = gtk_radio_button_get_group(GTK_RADIO_BUTTON(button));
        gwy_radio_button_set_value(button, i);
        s = g_strdup_printf(_("Mask B is in data d%d"), i+1);
        gtk_widget_set_tooltip_text(button, s);
        g_free(s);
        gtk_grid_attach(GTK_GRID(grid), button, 3, row, 1, 1);
        g_signal_connect_swapped(button, "clicked", G_CALLBACK(maskb_selected), &gui);
        row++;
    }
    row++;

    grid = gtk_grid_new();
    gtk_grid_set_row_spacing(GTK_GRID(grid), 2);
    gtk_grid_set_column_spacing(GTK_GRID(grid), 6);
    gtk_container_set_border_width(GTK_CONTAINER(grid), 4);
    gtk_box_pack_start(GTK_BOX(vbox2), grid, TRUE, TRUE, 4);
    row = 0;

    label = gtk_label_new(_("Id"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);

    label = gtk_label_new(_("Criterion"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 1, row, 1, 1);

    label = gtk_label_new(_("Scale"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 2, row, 1, 1);

    label = gtk_label_new(_("Id"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 4, row, 1, 1);

    label = gtk_label_new(_("Criterion"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 5, row, 1, 1);

    label = gtk_label_new(_("Scale"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 6, row, 1, 1);
    row++;

    for (i = 0; i < NCRITS; i++) {
        GtkAdjustment *adjustment = gtk_adjustment_new(args->id[2*i], 0, NARGS, 1, 10, 0);
        g_object_set_data(G_OBJECT(adjustment), "index", GUINT_TO_POINTER(2*i));
        spin = gtk_spin_button_new(GTK_ADJUSTMENT(adjustment), 1, 2);
        gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 0);
        g_signal_connect_swapped(adjustment, "value-changed", G_CALLBACK(id_selected), &gui);
        gtk_grid_attach(GTK_GRID(grid), spin, 0, row, 1, 1);

        combo = quantity_selector_new(&gui, 2*i);
        g_object_set_data(G_OBJECT(adjustment), "quantity", combo);
        gtk_grid_attach(GTK_GRID(grid), combo, 1, row, 1, 1);

        combo = scale_selector_new(&gui, 2*i);
        g_object_set_data(G_OBJECT(adjustment), "scale", combo);
        gtk_grid_attach(GTK_GRID(grid), combo, 2, row, 1, 1);
        update_criterion_sensitivity(GTK_ADJUSTMENT(adjustment));

        adjustment = gtk_adjustment_new(args->id[2*i + 1], 0, NARGS, 1, 10, 0);
        g_object_set_data(G_OBJECT(adjustment), "index", GUINT_TO_POINTER(2*i + 1));
        spin = gtk_spin_button_new(GTK_ADJUSTMENT(adjustment), 1, 2);
        gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 0);
        g_signal_connect_swapped(adjustment, "value-changed", G_CALLBACK(id_selected), &gui);
        gtk_grid_attach(GTK_GRID(grid), spin, 4, row, 1, 1);

        combo = quantity_selector_new(&gui, 2*i + 1);
        g_object_set_data(G_OBJECT(adjustment), "quantity", combo);
        gtk_grid_attach(GTK_GRID(grid), combo, 5, row, 1, 1);

        combo = scale_selector_new(&gui, 2*i + 1);
        g_object_set_data(G_OBJECT(adjustment), "scale", combo);
        gtk_grid_attach(GTK_GRID(grid), combo, 6, row, 1, 1);
        update_criterion_sensitivity(GTK_ADJUSTMENT(adjustment));
        row++;
    }

    grid = gtk_grid_new();
    gtk_grid_set_row_spacing(GTK_GRID(grid), 2);
    gtk_grid_set_column_spacing(GTK_GRID(grid), 6);
    gtk_container_set_border_width(GTK_CONTAINER(grid), 4);
    gtk_box_pack_start(GTK_BOX(vbox2), grid, TRUE, TRUE, 4);
    row = 0;

    label = gtk_label_new(_("Preview:"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);

    gui.show = gwy_data_chooser_new(GWY_FILE_IMAGE);
    gwy_data_chooser_set_active_id(GWY_DATA_CHOOSER(gui.show), &(args->show));
    g_signal_connect(gui.show, "changed", G_CALLBACK(show_chosen), &gui);
    gtk_grid_attach(GTK_GRID(grid), gui.show, 1, row, 1, 1);
    gtk_label_set_mnemonic_widget(GTK_LABEL(label), gui.show);
    row++;

    label = gtk_label_new(_("Display mask:"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);

    gui.display = display_selector_new(&gui);
    gtk_grid_attach(GTK_GRID(grid), gui.display, 1, row, 1, 1);
    row++;

    gtk_widget_show_all(gui.dialog);
    gwy_radio_buttons_set_current(gui.maska, args->maska);
    gwy_radio_buttons_set_current(gui.maskb, args->maskb);
    update_view(GTK_COMBO_BOX(gui.display), &gui);

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

    return gwy_dialog_run(dialog);
}

static void
id_selected(ModuleGUI *gui, GtkAdjustment *adj)
{
    guint i;

    i = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(adj), "index"));
    gui->args->id[i] = gwy_adjustment_get_int(adj);
    update_criterion_sensitivity(adj);
}

static void
update_criterion_sensitivity(GtkAdjustment *adj)
{
    GObject *obj = G_OBJECT(adj);
    GtkWidget *widget;
    gboolean sens;

    sens = gwy_adjustment_get_int(adj);
    widget = GTK_WIDGET(g_object_get_data(obj, "quantity"));
    gtk_widget_set_sensitive(widget, sens);
    widget = GTK_WIDGET(g_object_get_data(obj, "scale"));
    gtk_widget_set_sensitive(widget, sens);
}

static void
update_int(GtkComboBox *combo, gint *intval)
{
    *intval = gwy_enum_combo_box_get_active(combo);
}

static GtkWidget*
quantity_selector_new(ModuleGUI *gui, gint i)
{
    static const GwyEnum quantity_types[] = {
        { N_("Value"), CLASSIFY_QUANTITY_VALUE, },
        { N_("LoG"),   CLASSIFY_QUANTITY_LOG,   },
        { N_("Slope"), CLASSIFY_QUANTITY_SLOPE, },
    };
    GtkWidget *combo;

    combo = gwy_enum_combo_box_new(quantity_types, G_N_ELEMENTS(quantity_types),
                                   G_CALLBACK(update_int), gui->args->quantity + i,
                                   gui->args->quantity[i], TRUE);
    return combo;
}

static GtkWidget*
display_selector_new(ModuleGUI *gui)
{
    static const GwyEnum display_types[] = {
        { N_("Mask A"),   CLASSIFY_DISPLAY_MASK_A   },
        { N_("Mask B"),   CLASSIFY_DISPLAY_MASK_B   },
        { N_("Result A"), CLASSIFY_DISPLAY_RESULT_A },
        { N_("Result B"), CLASSIFY_DISPLAY_RESULT_B },
    };
    GtkWidget *combo;

    combo = gwy_enum_combo_box_new(display_types, G_N_ELEMENTS(display_types),
                                   G_CALLBACK(update_view),
                                   gui, gui->args->display, TRUE);
    return combo;
}

static GtkWidget*
scale_selector_new(ModuleGUI *gui, gint i)
{
    static GwyEnum scale_types[] = {
        { NULL, CLASSIFY_SCALE_1X,  },
        { NULL, CLASSIFY_SCALE_2X,  },
        { NULL, CLASSIFY_SCALE_4X,  },
        { NULL, CLASSIFY_SCALE_8X,  },
        { NULL, CLASSIFY_SCALE_16X, },
        { NULL, CLASSIFY_SCALE_32X, },
    };
    GtkWidget *combo;
    ClassifyScaleType *scale = gui->args->scale + i;

    if (!scale_types[0].name) {
        scale_types[CLASSIFY_SCALE_1X].name = g_strdup_printf("%u %s", 1, _("px"));
        scale_types[CLASSIFY_SCALE_2X].name = g_strdup_printf("%u %s", 2, _("px"));
        scale_types[CLASSIFY_SCALE_4X].name = g_strdup_printf("%u %s", 4, _("px"));
        scale_types[CLASSIFY_SCALE_8X].name = g_strdup_printf("%u %s", 8, _("px"));
        scale_types[CLASSIFY_SCALE_16X].name = g_strdup_printf("%u %s", 16, _("px"));
        scale_types[CLASSIFY_SCALE_32X].name = g_strdup_printf("%u %s", 32, _("px"));
    }

    combo = gwy_enum_combo_box_new(scale_types, G_N_ELEMENTS(scale_types),
                                   G_CALLBACK(update_int), scale,
                                   *scale, TRUE);
    return combo;
}

static void
update_view(G_GNUC_UNUSED GtkComboBox *combo, ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    GwyField *result;
    GwyNield *mask = NULL;

    args->display = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(gui->display));

    gwy_data_chooser_get_active_id(GWY_DATA_CHOOSER(gui->show), &(args->show));

    result = gwy_file_get_image(gwy_data_browser_get_file(args->show.datano), args->show.id);
    gwy_data_view_set_field(GWY_DATA_VIEW(gui->view), result);

    /* XXX: Why is all of this here when we do not do anything with the data and quarks? */
    if (args->display == CLASSIFY_DISPLAY_MASK_A) {
       if (CDEBUG > 1)
           printf("getting mask A for %d\n", args->maska);
       //data = gwy_data_browser_get_file(args->objects[args->maska % NARGS].datano);
       //quark = gwy_file_key_image_mask(args->objects[args->maska % NARGS].id);
       //if (!gwy_dict_gis_object(data, quark, &mask)) printf("There is no mask A in channel %d\n", args->maska);
    }
    else if (args->display == CLASSIFY_DISPLAY_MASK_B) {
       //data = gwy_data_browser_get_file(args->objects[args->maskb % NARGS].datano);
       //quark = gwy_file_key_image_mask(args->objects[args->maskb % NARGS].id);
       //if (!gwy_dict_gis_object(data, quark, &mask)) printf("There is no mask B in channel %d\n", args->maska);
    }
    else if (args->display == CLASSIFY_DISPLAY_RESULT_A) {
        mask = args->result_a;
    }
    else if (args->display == CLASSIFY_DISPLAY_RESULT_B) {
        mask = args->result_b;
    }

    gwy_data_view_set_mask(GWY_DATA_VIEW(gui->view), mask);
    gwy_set_data_preview_size(GWY_DATA_VIEW(gui->view), PREVIEW_SIZE);
}

static void
data_chosen(GwyDataChooser *chooser,
            ModuleGUI *gui)
{
    ModuleArgs *args;
    guint i;

    args = gui->args;
    i = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(chooser), "index"));
    gwy_data_chooser_get_active_id(chooser, args->objects + i);
    update_view(GTK_COMBO_BOX(gui->display), gui);
}

static void
show_chosen(G_GNUC_UNUSED GwyDataChooser *chooser,
            ModuleGUI *gui)
{
    update_view(GTK_COMBO_BOX(gui->display), gui);
}

static void
maska_selected(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    args->maska = gwy_radio_buttons_get_current(gui->maska);
    update_view(GTK_COMBO_BOX(gui->display), gui);
}

static void
maskb_selected(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    args->maskb = gwy_radio_buttons_get_current(gui->maskb);
    update_view(GTK_COMBO_BOX(gui->display), gui);
}

/*
static const gchar*
check_fields(ModuleArgs *args)
{
    guint first = 0, i;
    GwyFile *data;
    GQuark quark;
    GwyField *dfirst, *dfield;
    GwyDataMismatchFlags diff;

    data = gwy_data_browser_get_file(args->objects[first].datano);
    g_return_val_if_fail(data, NULL);
    quark = gwy_file_key_image(args->objects[first].id);
    dfirst = GWY_FIELD(gwy_dict_get_object(data, quark));
    for (i = first+1; i < NARGS; i++) {

        data = gwy_data_browser_get_file(args->objects[i].datano);
        g_return_val_if_fail(data, NULL);
        quark = gwy_file_key_image(args->objects[i].id);
        dfield = GWY_FIELD(gwy_dict_get_object(data, quark));

        diff = gwy_field_is_incompatible
                                            (dfirst, dfield,
                                             GWY_DATA_MISMATCH_RES
                                             | GWY_DATA_MISMATCH_REAL
                                             | GWY_DATA_MISMATCH_LATERAL);
        if (diff) {
            if (diff & GWY_DATA_MISMATCH_RES)
                return _("Pixel dimensions differ");
            if (diff & GWY_DATA_MISMATCH_LATERAL)
                return _("Lateral dimensions are different physical "
                         "quantities");
            if (diff & GWY_DATA_MISMATCH_REAL)
                return _("Physical dimensions differ");
        }
    }

    return NULL;
}
*/

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    run_classification(gui);
    update_view(GTK_COMBO_BOX(gui->display), gui);
}

static gdouble
fit_local_plane_by_pos(gint n,
                       const gint *xp, const gint *yp, const gdouble *z,
                       gdouble *bx, gdouble *by)
{
    gdouble m[12], b[4];
    gint i;

    gwy_clear(m, 6);
    gwy_clear(b, 4);
    for (i = 0; i < n; i++) {
        m[1] += xp[i];
        m[2] += xp[i]*xp[i];
        m[3] += yp[i];
        m[4] += xp[i]*yp[i];
        m[5] += yp[i]*yp[i];
        b[0] += z[i];
        b[1] += xp[i]*z[i];
        b[2] += yp[i]*z[i];
        b[3] += z[i]*z[i];
    }
    m[0] = n;
    gwy_assign(m + 6, m, 6);
    if (gwy_math_choleski_decompose(3, m))
        gwy_math_choleski_solve(3, m, b);
    else
        b[0] = b[1] = b[2] = 0.0;

    *bx = b[1];
    *by = b[2];
    return (b[3] - (b[0]*b[0]*m[6+0] + b[1]*b[1]*m[6+2] + b[2]*b[2]*m[6+5])
            - 2.0*(b[0]*b[1]*m[6+1] + b[0]*b[2]*m[6+3] + b[1]*b[2]*m[6+4]));
}

static void
inclination_filter(GwyField *dfield)
{
    GwyField *show = gwy_field_new_alike(dfield, FALSE);
    static const gdouble r = 2.5;
    gint xres, yres, i, j, size;
    gdouble qx, qy;
    gint *xp, *yp;
    gdouble *d, *z;

    xres = gwy_field_get_xres(dfield);
    yres = gwy_field_get_yres(dfield);
    d = gwy_field_get_data(show);
    qx = gwy_field_get_dx(dfield);
    qy = gwy_field_get_dx(dfield);

    size = gwy_circular_area_size(r);
    z = g_new(gdouble, size);
    xp = g_new(gint, 2*size);
    yp = xp + size;
    for (i = 0; i < yres; i++) {
        for (j = 0; j < xres; j++) {
            gint n = gwy_field_circular_area_extract(dfield, j, i, r, z, xp, yp);
            gdouble bx, by;
            fit_local_plane_by_pos(n, xp, yp, z, &bx, &by);
            bx /= qx;
            by /= qy;
            d[i*xres + j] = atan(hypot(bx, by));
        }
    }
    g_free(xp);
    g_free(z);

    gwy_field_copy_data(show, dfield);
    g_clear_object(&show);
}

static void
run_classification(ModuleGUI *gui)
{
    GwyNield *mask_a = NULL, *mask_b = NULL;
    GwyField *dfield;
    GwyField **cldata;
    ModuleArgs *args;
    GwyFile *data;
    GtkWidget *dialog;
    gint i, n, ncriteria;
    Classifier cl;

    args = gui->args;

    ncriteria = 0;
    for (i = 0; i < 2*NCRITS; i++) {
        if (args->id[i] > 0) {
            if (CDEBUG>1) printf("data %d using quantity %d on scale %d\n", args->id[i], args->quantity[i], args->scale[i]);
            ncriteria++;
        }
    }

    if (ncriteria == 0) {
        dialog = gtk_message_dialog_new(GTK_WINDOW(gui->dialog),
                                        GTK_DIALOG_DESTROY_WITH_PARENT,
                                        GTK_MESSAGE_ERROR,
                                        GTK_BUTTONS_OK,
                                        _("No data are selected for any criterion (all IDs are 0)."));
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);

        if (CDEBUG>1) printf("Error: there are no valid criteria to apply\n");
        return;
    }

    if (CDEBUG>1) printf("getting mask A for %d\n", args->maska);
    data = gwy_data_browser_get_file(args->objects[args->maska % NARGS].datano);

    if (!(mask_a = gwy_file_get_image_mask(data, args->objects[args->maska % NARGS].id))) {
        dialog = gtk_message_dialog_new(GTK_WINDOW(gui->dialog),
                                        GTK_DIALOG_DESTROY_WITH_PARENT,
                                        GTK_MESSAGE_ERROR,
                                        GTK_BUTTONS_OK,
                                        _("Image A has no mask."));
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);

        if (CDEBUG>1) printf("Error: There is no mask A in channel %d\n", args->maska);
        return;
    }

    if (CDEBUG>1) printf("getting mask B for %d\n", args->maskb);
    data = gwy_data_browser_get_file(args->objects[args->maskb % NARGS].datano);
    if (!(mask_b = gwy_file_get_image_mask(data, args->objects[args->maskb % NARGS].id))) {
        dialog = gtk_message_dialog_new(GTK_WINDOW(gui->dialog),
                                        GTK_DIALOG_DESTROY_WITH_PARENT,
                                        GTK_MESSAGE_ERROR,
                                        GTK_BUTTONS_OK,
                                        _("Image B has no mask."));
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);

        if (CDEBUG>1) printf("Error: There is no mask B in channel %d\n", args->maskb);
        return;
    }

    if (CDEBUG) printf("filling the data for %d criteria\n", ncriteria);
    //create the data sets - one field for each valid criterion, either allocated and filled or pointer
    cldata = (GwyField **)g_malloc(ncriteria*sizeof(GwyField *));
    n = 0;
    for (i=0; i<2*NCRITS; i++) {
        if (args->id[i]==0) continue;

        data = gwy_data_browser_get_file(args->objects[args->id[i]-1].datano);
        dfield = gwy_file_get_image(data, args->objects[args->id[i]-1].id);

        cldata[n] = gwy_field_copy(dfield);
        gwy_field_filter_gaussian(cldata[n], pow(2, args->scale[i]));

        if (args->quantity[i] == 1)
            gwy_field_laplacian_of_gaussians(cldata[n], cldata[n]);
        else if (args->quantity[i] == 2)
            inclination_filter(cldata[n]);

        n++;
    }

    //train the classifier using the data set, mask_a and mask_b
    cl.verbose = 1;
    gwy_app_wait_start(GTK_WINDOW(gui->dialog), _("Training classifier..."));
    classifier_train_full(&cl, cldata, ncriteria, mask_a, mask_b);
    gwy_app_wait_finish();

    //run the classification, creating result_a and result_b
    if (!args->result_a)
        args->result_a = gwy_nield_copy(mask_b);
    if (!args->result_b)
        args->result_b = gwy_nield_copy(mask_a);

    classifier_run(&cl, cldata, ncriteria, args->result_a, args->result_b);

    update_view(GTK_COMBO_BOX(gui->display), gui);

    if (CDEBUG) printf("freeing the data\n");
    for (i=0; i<ncriteria; i++) g_clear_object(cldata + i);
    g_free(cldata);
}

//typedef struct
//{
//    int nrules;
//    int rule_parameter[MAXRULES];
//    double rule_threshold[MAXRULES];
//    int rule_goto_high[MAXRULES];
//    int rule_godo_low[MAXRULES];
//} CTree;

//mask_a: user defined mask a
//mask_b: user defined mask b
//result_a: computed mask a
//result_b: computed mask b
//selection: masked data will be evaluated only, or NULL for evaluating whole images (still based on mask_a, mask_b only).
//a_purity: how much the A is really filled with As
//b_purity: how much the B is really filled with Bs
static gdouble
get_score(GwyNield *mask_a, GwyNield *mask_b, GwyNield *result_a, GwyNield *result_b,
          GwyNield *selection, gdouble *a_purity, gdouble *b_purity, gdouble *sumsa, gdouble *sumsb)
{
    gint i;
    gint n = gwy_nield_get_xres(mask_a)*gwy_nield_get_yres(mask_a);
    gint *ma, *mb, *ra, *rb, *s;
    gdouble sumainb, sumbina, nma, sumbinb, sumaina, nmb, paina, painb, pbina, pbinb;
    gboolean selall = 0;
    gdouble ginia, ginib;

    ma = gwy_nield_get_data(mask_a);
    mb = gwy_nield_get_data(mask_b);
    ra = gwy_nield_get_data(result_a);
    rb = gwy_nield_get_data(result_b);

    if (!selection)
        selall = 1;
    else
        s = gwy_nield_get_data(selection);

    sumaina = sumbina = nma = 0;
    sumainb = sumbinb = nmb = 0;
    for (i = 0; i < n; i++) {
        if (selall || s[i]) {
           sumaina += ma[i]*ra[i];
           sumbina += mb[i]*ra[i];
           nma += ma[i]*ra[i] + mb[i]*ra[i];
           sumainb += ma[i]*rb[i];
           sumbinb += mb[i]*rb[i];
           nmb += ma[i]*rb[i] + mb[i]*rb[i];
        }
    }
    if (sumaina + sumbina > 0) {
       paina = sumaina/(sumaina + sumbina);
       pbina = sumbina/(sumaina + sumbina);
    }
    else
        paina = pbina = 0;

    if (sumainb + sumbinb > 0) {
       painb = sumainb/(sumainb + sumbinb);
       pbinb = sumbinb/(sumainb + sumbinb);
    }
    else
        painb = pbinb = 0;

    ginia = paina*(1-paina) + pbina*(1-pbina);
    ginib = painb*(1-painb) + pbinb*(1-pbinb);

    *sumsa = sumaina + sumainb;
    *sumsb = sumbina + sumbinb;
    *a_purity = ginia;
    *b_purity = ginib;

    if (CDEBUG>1) printf(" pura %g purb %g score %g  sumaina %g  sumainb %g  sumbina %g  sumbinb %g  nma %g nmb %g\n", ginia, ginib, nma*ginia/(nma+nmb) + nmb*ginib/(nma+nmb), sumaina, sumainb, sumbina, sumbinb, nma, nmb);

    return nma*ginia/(nma+nmb) + nmb*ginib/(nma+nmb);
}

static void
print_ct(CTree *ct)
{
    int n;

    printf("Printing tree, it has %d rules\n", ct->nrules);
    for (n=0; n<ct->nrules; n++)
    {
       printf("Node %d: ------------------------\n", n);
       printf("if quantity %d is bigger than %g:\n", ct->rule_parameter[n], ct->rule_threshold[n]);
       printf("     goto %d\n", ct->rule_goto_high[n]);
       printf("else goto %d\n", ct->rule_goto_low[n]);
       printf("--------------------------------\n");
    }

}

//find the best splitting criterion and threshold value
//returns index of the best criterion (field in cldata) for splitting,
//threshold to split most efficiently,
//purity of the found set A,
//purity of the found set B
//last crit is the criterion that was used last time, to be skipped
static gint
get_next_split(GwyField **cldata, gint ndata,
               GwyNield *mask_a, GwyNield *mask_b, GwyNield *selection, gdouble *threshold,
               gdouble *a_purity, gdouble *b_purity, gdouble *gini,
               gdouble *sumsa, gdouble *sumsb,
               GwyNield *result_a, GwyNield *result_b,
               gint lastcrit)
{
   gint n, bestcrit;
   gdouble bestscore, bestth, bestgini, bestthreshold, min, max, step, th, score, suma, sumb;
   gdouble apur, bpur, bestapur, bestbpur, bestapurity, bestbpurity, bestsuma, bestsumb, bestsumsa, bestsumsb;
   CTree ct;

   if (CDEBUG > 1) printf("Called get next split\n");

   //go thorugh criteria (fields in cldata) one by one and all possible thresholds, searching for lowest gini impurity
   bestgini = 1;
   bestcrit = 0;
   bestthreshold = 0;
   bestapurity = 0;
   bestbpurity = 0;
   bestsumsa = 0;
   bestsumsb = 0;
   for (n = 0; n < ndata; n++) {
       if (n == lastcrit)
           continue; //skip what was used for splitting last time

       ct.rule_parameter[0] = n;
       ct.rule_goto_high[0] = -1;
       ct.rule_goto_low[0] = -2;

       gint xres = gwy_field_get_xres(cldata[ct.rule_parameter[0]]);
       gint yres = gwy_field_get_yres(cldata[ct.rule_parameter[0]]);
       gwy_field_area_min_max(cldata[ct.rule_parameter[0]], selection, GWY_MASK_INCLUDE, 0, 0, xres, yres, &min, &max);
       if (CDEBUG>1) printf("criterion %d min %g max %g\n", ct.rule_parameter[0], min, max);
       step = (max-min)/100;

       bestscore = 1;
       bestth = 0;
       bestapur = 0;
       bestbpur = 0;
       bestsuma = 0;
       bestsumb = 0;
       for (th = min; th < max; th += step) {
           ct.rule_threshold[0] = th;
           ctree_run(&ct, cldata, ndata, result_a, result_b);
           if (CDEBUG > 1) printf("threshold %g ", th);
           score = get_score(mask_a, mask_b, result_a, result_b, selection, &apur, &bpur, &suma, &sumb);
           //  printf("n %d  th %g  score %g  apur %g  bpur %g  sums %g\n", n, th, score, apur, bpur, sum);
           if (score < bestscore) {
               bestscore = score;
               bestth = th;
               bestapur = apur;
               bestbpur = bpur;
               bestsuma = suma;
               bestsumb = sumb;
           }
       }
       if (CDEBUG) printf("best threshold for quantity n: %d  gini %g threshold %g  purities %g %g  sum %g %g\n", n, bestscore, bestth, bestapur, bestbpur, bestsuma, bestsumb);

       if (bestscore<bestgini) {
           bestgini = bestscore;
           bestcrit = n;
           bestthreshold = bestth;
           bestapurity = bestapur;
           bestbpurity = bestbpur;
           bestsumsa = bestsuma;
           bestsumsb = bestsumb;
       }
   }
   if (CDEBUG) printf("Get branch result: criterion %d gini %g threshold %g  purities %g %g  sums %g %g\n", bestcrit, bestgini, bestthreshold, bestapurity, bestbpurity, bestsumsa, bestsumsb);

   //fill the results with mask of a and b
   ct.rule_parameter[0] = bestcrit;
   ct.rule_threshold[0] = bestthreshold;
   ctree_run(&ct, cldata, ndata, result_a, result_b);

   *threshold = bestthreshold;
   *gini = bestgini;
   *a_purity = bestapurity;
   *b_purity = bestbpurity;
   *sumsa = bestsumsa;
   *sumsb = bestsumsb;
   return bestcrit;
}

#if 0
static void
print_dfield(GwyField *df, gint index)
{
    FILE *fw;
    int i, j;
    char filename[100];

    if (!df) return;

    printf("printing index %d\n", index);

    sprintf(filename, "sel_%d_%dx%d.txt", index, gwy_field_get_xres(df), gwy_field_get_yres(df));

    fw = fopen(filename, "w");

    if (df) {
       for (j=0; j<gwy_field_get_yres(df); j++)
       {
           for (i=0; i<gwy_field_get_xres(df); i++)
           {
              fprintf(fw, "%g ", gwy_field_get_val(df, i, j));
           }
           fprintf(fw, "\n");
       }
    }

    fclose(fw);
}
#endif

static gint
process_branch(CTree *ct, GwyField **cldata, GwyNield *mask_a, GwyNield *mask_b,
               gint ndata, gint *n, GwyNield *selection, gint lastcrit)
{
    GwyNield *result_a = gwy_field_new_nield_alike(cldata[0]);
    GwyNield *result_b = gwy_field_new_nield_alike(cldata[0]);
    GwyNield *sel_a = gwy_field_new_nield_alike(cldata[0]);
    GwyNield *sel_b = gwy_field_new_nield_alike(cldata[0]);
    gdouble apur, bpur, gini, threshold, sumsa, sumsb, retval;
    gint thisn = *n;
    gint nextn, ret;

    if (CDEBUG) printf("Processing branch %d\n", thisn);

    //if (CDEBUG>1) print_dfield(selection, thisn);

    if (ndata==1) //special case when only one criterion exists, so we can't swap them
        ct->rule_parameter[thisn] = get_next_split(cldata, ndata,
                                                   mask_a, mask_b, selection, &threshold,
                                                   &apur, &bpur, &gini, &sumsa, &sumsb, result_a, result_b, -1);
    else //normal case, last criterion is not used for next split
        ct->rule_parameter[thisn] = get_next_split(cldata, ndata,
                                                   mask_a, mask_b, selection, &threshold,
                                                   &apur, &bpur, &gini, &sumsa, &sumsb, result_a, result_b, lastcrit);

    //if (CDEBUG>1) print_dfield(result_a, 100+thisn);
    //if (CDEBUG>1) print_dfield(result_b, 200+thisn);

    ct->rule_threshold[thisn] = threshold;
    if (CDEBUG) printf("(%d) sugggested rule for split: crit %d  threshold %g, purities %g %g  sums %g %g\n", thisn, ct->rule_parameter[thisn], ct->rule_threshold[thisn], apur, bpur, sumsa, sumsb);

    if (sumsa == 0 || sumsb == 0) { //one of branches has no members, so report this to what it had called and don't create new rule.
        if (sumsa>=sumsb) retval = -1;
        else retval = -2;
        if (CDEBUG) printf("Error: one branch does not have members, stop further branching and return %g\n", retval);
    }
    else //setup new rule
    {
        if (CDEBUG) printf("Rule accepted and will be further developed\n");
        ct->nrules++;
        retval = 0;

        if (apur>PURCRIT || *n>MAXBRANCHES) //make this adjustable!
        {
            ct->rule_goto_high[thisn] = -1;
            if (CDEBUG) printf("(%d) step high: we are done (purity %g), response is -1\n", thisn, apur);
        }
        else {
            *n += 1;
            nextn = *n;
            ct->rule_goto_high[thisn] = nextn;
            if (CDEBUG) printf("(%d) step high: go to next branch at index %d\n", thisn, ct->rule_goto_high[thisn]);

            //create actual selection, combining the previous selection with last result_a
            gwy_nield_copy_data(result_a, sel_a);
            if (selection)
                gwy_nield_intersect(sel_a, selection);

            if (CDEBUG) printf("(%d) selection for next process %d has %d points\n", thisn, nextn, gwy_nield_count(sel_a));
            if (CDEBUG) printf("now will process branch A with number %d\n", nextn);
            if ((ret = process_branch(ct, cldata, mask_a, mask_b, ndata, n, sel_a, ct->rule_parameter[thisn]))!=0)
            {
                if (CDEBUG) printf("Branch could not be further developed, goto_high in this branch %d will be %d\n", thisn, ret);
                ct->rule_goto_high[thisn] = ret;
                *n -= 1;
            }
        }

        if (bpur>PURCRIT || *n>MAXBRANCHES) //make this adjustable!
        {
            ct->rule_goto_low[thisn] = -2;
            if (CDEBUG) printf("(%d) step low: we are done (purity %g), response is -2\n", thisn, apur);
        }
        else {
            *n += 1;
            nextn = *n;
            ct->rule_goto_low[thisn] = nextn;
            if (CDEBUG) printf("(%d) step low: go to next branch at index %d\n", thisn, ct->rule_goto_low[thisn]);

            //create actual selection, combining the previous selection with last result_a
            gwy_nield_copy_data(result_b, sel_b);
            if (selection)
               gwy_nield_intersect(sel_b, selection);

            if (CDEBUG) printf("(%d) selection for next process %d has %d points\n", thisn, nextn, gwy_nield_count(sel_b));
            if (CDEBUG) printf("now will process branch B with number %d\n", nextn);

            if ((ret = process_branch(ct, cldata, mask_a, mask_b, ndata, n, sel_b, ct->rule_parameter[thisn]))!=0) //we could not branch further, stop it
            {
                if (CDEBUG) printf("Branch could not be further developed, goto_high in this branch %d will be %d\n", thisn, ret);
                ct->rule_goto_low[thisn] = ret;
                *n -= 1;
            }

        }
    }

    if (CDEBUG) printf("End of processing branch %d\n", thisn);

    g_clear_object(&result_a);
    g_clear_object(&result_b);
    g_clear_object(&sel_a);
    g_clear_object(&sel_b);

    return retval;
}

static void
train_tree(CTree *ct, GwyField **cldata,
           gint ndata, GwyNield *mask_a, GwyNield *mask_b,
           GwyNield *selection)
{
    gint n = 0;

    process_branch(ct, cldata, mask_a, mask_b, ndata, &n, selection, -1);
    if (CDEBUG) print_ct(ct);
}

//setup whole forest
static void
classifier_train_full(Classifier *cl, GwyField **cldata,
                      gint ndata, GwyNield *mask_a, GwyNield *mask_b)
{
    CTree *ct;
    if (CDEBUG) printf("Classifier train started on %d data sets\n", ndata);

    ct = cl->ct + 0;
    ct->nrules = 0;
    train_tree(ct, cldata, ndata, mask_a, mask_b, NULL);
}

//run single tree on single point in the image
static gint
run_ct(CTree *ct, GwyField **cldata, G_GNUC_UNUSED gint ndata, gint xpos, gint ypos)
{
    gint n = 0;

    for (gint i = 0; i < 1000; i++) {
        //printf("rp at %d %d  n %d param %d\n", xpos, ypos, n, ct->rule_parameter[n]);
        // XXX: If you like it very slow, do things like this!
        if (gwy_field_get_val(cldata[ct->rule_parameter[n]], xpos, ypos) > ct->rule_threshold[n]) {
            if (ct->rule_goto_high[n] < 0)
                return ct->rule_goto_high[n];
            n = ct->rule_goto_high[n];
        }
        else {
            if (ct->rule_goto_low[n] < 0)
                return ct->rule_goto_low[n];
            n = ct->rule_goto_low[n];
        }
        //printf("next n: %d\n", n);
    }
    //printf("Error: CT run did not finish after 1000 iterations\n");
    return -3;
}

//run a single tree on whole image
static void
ctree_run(CTree *ct, GwyField **cldata,
          gint ndata, GwyNield *result_a, GwyNield *result_b)
{
    gint i, j, result;
    gint xres = gwy_field_get_xres(cldata[0]);
    gint yres = gwy_field_get_yres(cldata[0]);

    for (i = 0; i < xres; i++) {
        for (j = 0; j < yres; j++) {
            result = run_ct(ct, cldata, ndata, i, j);
            if (result == -1) {
                gwy_nield_set_val(result_a, i, j, 1);
                gwy_nield_set_val(result_b, i, j, 0);
            }
            if (result == -2) {
                gwy_nield_set_val(result_a, i, j, 0);
                gwy_nield_set_val(result_b, i, j, 1);
            }
        }
    }

}

//run the forest on whole image
static void
classifier_run(Classifier *cl, GwyField **cldata,
               gint ndata, GwyNield *result_a, GwyNield *result_b)
{
    ctree_run(cl->ct + 0, cldata, ndata, result_a, result_b);  //now just run the first tree
}

static const gchar mask_a_key[]   = "/module/classify/mask_a";
static const gchar mask_b_key[]   = "/module/classify/mask_b";
static const gchar display_key[]  = "/module/classify/display";

static void
load_args(GwyDict *settings, ModuleArgs *args)
{
    guint i;
    gchar key[50];

    args->maska = 1;
    args->maskb = 2;
    args->display = 0;
    gwy_dict_gis_int32_by_name(settings, mask_a_key, &args->maska);
    gwy_dict_gis_int32_by_name(settings, mask_b_key, &args->maskb);
    gwy_dict_gis_enum_by_name(settings, display_key, &args->display);

    gwy_clear(args->id, 2*NCRITS);
    gwy_clear(args->scale, 2*NCRITS);
    gwy_clear(args->quantity, 2*NCRITS);
    for (i = 0; i < 2*NCRITS; i++) {
        g_snprintf(key, sizeof(key), "/module/classify/id%u", i);
        gwy_dict_gis_int32_by_name(settings, key, &(args->id[i]));

        g_snprintf(key, sizeof(key), "/module/classify/quantity%u", i);
        gwy_dict_gis_enum_by_name(settings, key, &(args->quantity[i]));

        g_snprintf(key, sizeof(key), "/module/classify/scale%u", i);
        gwy_dict_gis_enum_by_name(settings, key, &(args->scale[i]));
     }

    args->show = object_ids[0]; //this should be done better, saving the last selection

    for (i = 1; i < NARGS; i++) {
        args->objects[i] = object_ids[i];
        // Init to d1 instead of ‘none’ when we lose the fields.
        if (!gwy_app_data_id_verify_channel(args->objects + i))
            args->objects[i] = args->objects[0];
    }
}

static void
save_args(GwyDict *settings, ModuleArgs *args)
{
    guint i;
    gchar key[50];

    gwy_assign(object_ids, args->objects, NARGS);

    gwy_dict_set_int32_by_name(settings, mask_a_key, args->maska);
    gwy_dict_set_int32_by_name(settings, mask_b_key, args->maskb);
    gwy_dict_set_enum_by_name(settings, display_key, args->display);

    for (i = 0; i < 2*NCRITS; i++) {
        g_snprintf(key, sizeof(key), "/module/classify/id%u", i);
        gwy_dict_set_int32_by_name(settings, key, args->id[i]);

        g_snprintf(key, sizeof(key), "/module/classify/quantity%u", i);
        gwy_dict_set_enum_by_name(settings, key, args->quantity[i]);

        g_snprintf(key, sizeof(key), "/module/classify/scale%u", i);
        gwy_dict_set_enum_by_name(settings, key, args->scale[i]);
    }
}

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