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

#include "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#include <stdlib.h>
#include <gtk/gtk.h>
#include <gwy.h>

#include "../preview.h"

/* Lower symmetric part indexing */
/* i MUST be greater or equal than j */
#define SLi(a, i, j) a[(i)*((i) + 1)/2 + (j)]

#define RUN_MODES GWY_RUN_INTERACTIVE

enum {
    PARAM_FITFUNC,
    PARAM_XPOS,
    PARAM_YPOS,
    PARAM_PLOT_FULL,
    PARAM_AUTO_ESTIMATE,
    PARAM_ESTIMATE,
    PARAM_OUTPUT_TYPE,
    PARAM_REPORT_STYLE,
    PARAM_RANGE_FROM,
    PARAM_RANGE_TO,

    MESSAGE_ERROR,
    INFO_RSS,
};

typedef enum {
    FIT_INITIALISED,
    FIT_ESTIMATED,
    FIT_FITTED,
    FIT_USER_MODIFIED,
    FIT_ESTIMATE_FAILED,
    FIT_FIT_FAILED,
    FIT_VOLUME_FITTED,
} FitState;

typedef enum {
    OUTPUT_VALUES,
    OUTPUT_ERRORS,
    OUTPUT_RESIDUA,
    /* XXX: We used to have also correlation matrix output, but that is kind of insane number of images. Add it to the
     * flags when really needed. */
} OutputFlags;

typedef struct {
    GwyParams *params;
    GwyBrick *brick;
    GwyNLFitPreset *fitfunc;
    GwyLine *xdata;
    GwyLine *ydata;
    GwyUnit *zunit;
    GwyLine *calibration;
    gdouble zmin;
    gdouble zmax;
    gdouble *param;
    gboolean *fixed;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GwyFile *parent_data;
    GtkWidget *dialog;
    GtkWidget *dataview;
    GwyGraphModel *gmodel;
    GwySelection *iselection;
    GwySelection *selection;
    GwyParamTable *table_above;
    GwyParamTable *table_below;
    GtkWidget *revert;
    GtkWidget *fit_params;
    GtkWidget *correl_table;
    GtkWidget *function;
    GwyResults *results;
    GwyNLFitter *fitter;
    gdouble *correl;
    gdouble rss;
    FitState state;
    gboolean changing_selection;
} ModuleGUI;

static gboolean         module_register                 (void);
static GwyParamDef*     define_module_params            (void);
static void             module_main                     (GwyFile *data,
                                                         GwyRunModeFlags mode);
static GwyDialogOutcome run_gui                         (ModuleArgs *args,
                                                         GwyFile *container,
                                                         gint id);
static GtkWidget*       create_param_tab                (ModuleGUI *gui);
static GtkWidget*       create_correl_tab               (ModuleGUI *gui);
static GtkWidget*       create_function_tab             (ModuleGUI *gui);
static gboolean         switch_curve                    (ModuleGUI *gui);
static void             fit_param_table_resize          (ModuleGUI *gui);
static void             fit_correl_table_resize         (ModuleGUI *gui);
static void             execute_brick_and_create_outputs(ModuleArgs *args,
                                                         GwyFile *data,
                                                         gint id);
static void             execute                         (ModuleGUI *gui);
static void             estimate                        (ModuleGUI *gui);
static void             param_changed                   (ModuleGUI *gui,
                                                         gint id);
static void             param_value_activate            (ModuleGUI *gui,
                                                         guint i);
static void             preview                         (gpointer user_data);
static void             dialog_response                 (ModuleGUI *gui,
                                                         gint response);
static void             revert_parameters               (ModuleGUI *gui);
static void             point_selection_changed         (ModuleGUI *gui,
                                                         gint id,
                                                         GwySelection *selection);
static void             guess_parameters                (ModuleGUI *gui);
static void             plot_curve                      (ModuleGUI *gui);
static void             update_fit_state                (ModuleGUI *gui,
                                                         FitState state);
static gint             extract_data                    (ModuleArgs *args,
                                                         gboolean for_plotting);
static void             create_results                  (ModuleGUI *gui);
static void             fill_results                    (ModuleGUI *gui);
static void             sanitise_params                 (ModuleArgs *args);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Evaluate volume force-distance data"),
    "Petr Klapetek <klapetek@gwyddion.net>, David Nečas <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti) & Petr Klapetek",
    "2013",
};

GWY_MODULE_QUERY2(module_info, volume_fdfit)

static gboolean
module_register(void)
{
    gwy_volume_func_register("volume_fdfit",
                             module_main,
                             N_("/SPM M_odes/_Evaluate FD Data..."),
                             GWY_ICON_VOLUME_FD,
                             RUN_MODES,
                             GWY_MENU_FLAG_VOLUME,
                             N_("Evaluate force-distance volume data"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum output_types[] = {
        { N_("values"),  1 << OUTPUT_VALUES,  },
        { N_("errors"),  1 << OUTPUT_ERRORS,  },
        { N_("residua"), 1 << OUTPUT_RESIDUA, },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_volume_func_current());
    gwy_param_def_add_resource(paramdef, PARAM_FITFUNC, "fitfunc", _("_Function"),
                               gwy_fd_curve_presets(), "DMT: spherical");
    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_boolean(paramdef, PARAM_PLOT_FULL, "plot_full", _("Plot full range"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_AUTO_ESTIMATE, "auto_estimate", _("Instant esti_mate"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_ESTIMATE, "estimate", _("Run _estimate at each point"), TRUE);
    gwy_param_def_add_gwyflags(paramdef, PARAM_OUTPUT_TYPE, "output_type", _("Output"),
                               output_types, G_N_ELEMENTS(output_types), (1 << OUTPUT_VALUES));
    gwy_param_def_add_report_type(paramdef, PARAM_REPORT_STYLE, "report_style", NULL,
                                  GWY_RESULTS_EXPORT_PARAMETERS, GWY_RESULTS_REPORT_COLON | GWY_RESULTS_REPORT_MACHINE);
    /* Range is GUI-only. */
    gwy_param_def_add_double(paramdef, PARAM_RANGE_FROM, NULL, _("Range"), -G_MAXDOUBLE, G_MAXDOUBLE, 0.0);
    gwy_param_def_add_double(paramdef, PARAM_RANGE_TO, NULL, NULL, -G_MAXDOUBLE, G_MAXDOUBLE, 0.0);

    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    g_return_if_fail(mode & RUN_MODES);

    ModuleArgs args;
    gwy_clear1(args);

    gint id;
    GwyBrick *brick;
    gwy_data_browser_get_current(GWY_APP_BRICK, &brick,
                                 GWY_APP_BRICK_ID, &id,
                                 0);
    g_return_if_fail(GWY_IS_BRICK(brick));

    args.params = gwy_params_new_from_settings(define_module_params());
    args.brick = brick;

    GwyLine *calibration;
    calibration = gwy_brick_get_zcalibration(brick);
    if (calibration && (gwy_brick_get_zres(brick) != gwy_line_get_res(calibration)))
        calibration = NULL;
    args.calibration = calibration;
    args.zunit = (calibration ? gwy_line_get_unit_y(calibration) : gwy_brick_get_unit_z(brick));
    args.zmin = (calibration ? gwy_line_min(calibration) : gwy_brick_get_zoffset(brick));
    args.zmax = (calibration ? gwy_line_max(calibration) : args.zmin + gwy_brick_get_zreal(brick));

    args.xdata = gwy_line_new(1, 1.0, FALSE);
    args.ydata = gwy_line_new(1, 1.0, FALSE);
    sanitise_params(&args);

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

    /* NB: This changes args.params internally. */
    execute_brick_and_create_outputs(&args, data, id);

end:
    g_object_unref(args.params);
    g_object_unref(args.xdata);
    g_object_unref(args.ydata);
    g_free(args.param);
    g_free(args.fixed);
}

static void
append_position(GwyParamTable *table, gint id, gint res)
{
    gwy_param_table_append_slider(table, id);
    gwy_param_table_slider_restrict_range(table, id, 0, res-1);
    gwy_param_table_set_unitstr(table, id, _("px"));
    gwy_param_table_slider_set_mapping(table, id, GWY_SCALE_MAPPING_LINEAR);
    gwy_param_table_slider_add_alt(table, id);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    ModuleGUI gui;
    GwyBrick *brick = args->brick;

    gwy_clear1(gui);
    gui.args = args;
    gui.parent_data = data;

    GwyField *field = gwy_dict_get_object(GWY_DICT(data), gwy_file_key_volume_picture(id));

    gui.dialog = gwy_dialog_new(_("FD Fit of Volume Data"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_COPY, GWY_STOCK_SAVE, GWY_STOCK_FIT, GWY_STOCK_ESTIMATE,
                                 GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_container_set_border_width(GTK_CONTAINER(hbox), 4);
    gwy_dialog_add_content(dialog, hbox, TRUE, TRUE, 0);

    gui.dataview = gwy_create_preview(field, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.dataview), data, GWY_FILE_VOLUME, id, GWY_FILE_ITEM_PALETTE);
    gui.iselection = gwy_create_preview_vector_layer(GWY_DATA_VIEW(gui.dataview), GWY_TYPE_LAYER_POINT, 1, TRUE);
    gtk_box_pack_start(GTK_BOX(hbox), gui.dataview, FALSE, FALSE, 0);

    gui.gmodel = gwy_graph_model_new();
    g_object_set(gui.gmodel,
                 "label-visible", FALSE,
                 "unit-x", args->zunit,
                 "unit-y", gwy_brick_get_unit_w(brick),
                 NULL);

    GwyGraphCurveModel *gcmodel = gwy_graph_curve_model_new();
    g_object_set(gcmodel,
                 "mode", GWY_GRAPH_CURVE_LINE,
                 "color", gwy_graph_get_preset_color(0),
                 "description", _("Data"),
                 NULL);
    gwy_graph_model_add_curve(gui.gmodel, gcmodel);
    g_object_unref(gcmodel);

    /* XXX: We need to have a full-range curve for gwy_create_graph_xrange_with_params(). */
    gboolean plot_full = gwy_params_get_boolean(args->params, PARAM_PLOT_FULL);
    gwy_params_set_boolean(args->params, PARAM_PLOT_FULL, TRUE);
    switch_curve(&gui);
    gwy_params_set_boolean(args->params, PARAM_PLOT_FULL, plot_full);

    GtkWidget *graph = gwy_graph_new(gui.gmodel);
    gtk_widget_set_size_request(graph, 400, 300);
    gwy_graph_enable_user_input(GWY_GRAPH(graph), FALSE);
    gtk_box_pack_start(GTK_BOX(hbox), graph, TRUE, TRUE, 0);

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, TRUE, 0);

    gui.table_above = gwy_param_table_new(args->params);
    GwyParamTable *table = GWY_PARAM_TABLE(gui.table_above);
    append_position(table, PARAM_XPOS, gwy_brick_get_xres(brick));
    gwy_param_table_alt_set_brick_pixel_x(table, PARAM_XPOS, brick);
    append_position(table, PARAM_YPOS, gwy_brick_get_yres(brick));
    gwy_param_table_alt_set_brick_pixel_y(table, PARAM_YPOS, brick);
    gwy_param_table_append_combo(table, PARAM_FITFUNC);
    gtk_box_pack_start(GTK_BOX(vbox), gwy_param_table_widget(table), FALSE, TRUE, 0);

    GtkWidget *notebook = gtk_notebook_new();
    gtk_notebook_append_page(GTK_NOTEBOOK(notebook), create_param_tab(&gui), gtk_label_new(_("Parameters")));
    gtk_notebook_append_page(GTK_NOTEBOOK(notebook), create_correl_tab(&gui), gtk_label_new(_("Correlation Matrix")));
    gtk_notebook_append_page(GTK_NOTEBOOK(notebook), create_function_tab(&gui), gtk_label_new(_("Function")));
    gtk_box_pack_start(GTK_BOX(vbox), notebook, FALSE, TRUE, 0);

    gui.table_below = gwy_param_table_new(args->params);
    table = GWY_PARAM_TABLE(gui.table_below);
    gwy_create_graph_xrange_with_params(table, PARAM_RANGE_FROM, PARAM_RANGE_TO, GWY_GRAPH(graph), NULL);
    gwy_param_table_append_info(table, INFO_RSS, _("Mean square difference"));
    gwy_param_table_append_message(table, MESSAGE_ERROR, NULL);
    gwy_param_table_append_checkbox(table, PARAM_PLOT_FULL);
    gwy_param_table_append_checkbox(table, PARAM_AUTO_ESTIMATE);
    gwy_param_table_append_checkbox(table, PARAM_ESTIMATE);
    gwy_param_table_append_checkbox_row(table, PARAM_OUTPUT_TYPE);
    gtk_box_pack_start(GTK_BOX(vbox), gwy_param_table_widget(table), FALSE, TRUE, 0);

    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);
    g_signal_connect_swapped(gui.table_above, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_below, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.iselection, "changed", G_CALLBACK(point_selection_changed), &gui);
    g_signal_connect_swapped(dialog, "response", G_CALLBACK(dialog_response), &gui);

    GwyDialogOutcome outcome = gwy_dialog_run(dialog);

    g_free(gui.correl);
    g_object_unref(gui.gmodel);
    if (gui.fitter)
        gwy_nl_fitter_free(gui.fitter);
    g_clear_object(&gui.results);

    return outcome;
}

static GtkWidget*
create_param_tab(ModuleGUI *gui)
{
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), 4);

    gui->fit_params = gwy_fit_table_new();
    GwyFitTable *table = GWY_FIT_TABLE(gui->fit_params);
    gwy_fit_table_set_values_editable(table, TRUE);
    gwy_fit_table_set_has_checkboxes(table, TRUE);
    gwy_fit_table_set_header(table, _("Fix"), 0, 1);
    gwy_fit_table_set_header(table, _("Parameter"), 1, 3);
    gwy_fit_table_set_header(table, _("Error"), 6, 2);
    gtk_box_pack_start(GTK_BOX(vbox), gui->fit_params, FALSE, TRUE, 0);
    g_signal_connect_swapped(gui->fit_params, "activated", G_CALLBACK(param_value_activate), gui);

    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);

    gui->revert = gtk_button_new_with_mnemonic(_("Revert to _Previous Values"));
    gtk_box_pack_start(GTK_BOX(hbox), gui->revert, TRUE, TRUE, 0);
    g_signal_connect_swapped(gui->revert, "clicked", G_CALLBACK(revert_parameters), gui);

    return vbox;
}

static GtkWidget*
create_correl_tab(ModuleGUI *gui)
{
    GtkWidget *scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_container_set_border_width(GTK_CONTAINER(scwin), 4);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER);

    gui->correl_table = gwy_correl_table_new();
    gtk_container_set_border_width(GTK_CONTAINER(gui->correl_table), 4);
    gtk_container_add(GTK_CONTAINER(scwin), gui->correl_table);

    return scwin;
}

static GtkWidget*
create_function_tab(ModuleGUI *gui)
{
    GtkWidget *scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_container_set_border_width(GTK_CONTAINER(scwin), 8);

    gui->function = gtk_label_new(NULL);
    gtk_label_set_xalign(GTK_LABEL(gui->function), 0.0);
    gtk_container_add(GTK_CONTAINER(scwin), gui->function);

    return scwin;
}

static void
fit_param_table_resize(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    guint nparams = gwy_nlfit_preset_get_n_params(args->fitfunc);
    GwyFitTable *table = GWY_FIT_TABLE(gui->fit_params);
    gwy_fit_table_resize(table, nparams);

    GwyUnit *xunit = args->zunit, *yunit = gwy_brick_get_unit_w(args->brick);

    args->param = g_renew(gdouble, args->param, nparams);
    args->fixed = g_renew(gboolean, args->fixed, nparams);
    for (guint i = 0; i < nparams; i++) {
        gwy_fit_table_set_checked(table, i, FALSE);
        gwy_fit_table_set_name(table, i, gwy_nlfit_preset_get_param_name(args->fitfunc, i));
        GwyUnit *unit = gwy_nlfit_preset_get_param_units(args->fitfunc, i, xunit, yunit);
        gwy_fit_table_set_unit(table, i, unit);
        g_object_unref(unit);
    }

    gwy_fit_table_clear_errors(table);
}

static void
fit_correl_table_resize(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    guint nparams = gwy_nlfit_preset_get_n_params(args->fitfunc);
    gui->correl = g_renew(gdouble, gui->correl, nparams*(nparams + 1)/2);

    GwyCorrelTable *table = GWY_CORREL_TABLE(gui->correl_table);
    gwy_correl_table_resize(table, nparams);
    for (guint i = 0; i < nparams; i++) {
        const gchar *name = gwy_nlfit_preset_get_param_name(args->fitfunc, i);
        gwy_correl_table_set_name(table, i, name);
    }
}

static gboolean
switch_curve(ModuleGUI *gui)
{
    GwyGraphModel *gmodel = gui->gmodel;
    while (gwy_graph_model_get_n_curves(gmodel) > 1)
        gwy_graph_model_remove_curve(gmodel, gwy_graph_model_get_n_curves(gmodel)-1);

    GwyGraphCurveModel *gcmodel = gwy_graph_model_get_curve(gmodel, 0);
    gboolean want_replot = FALSE;
    ModuleArgs *args = gui->args;
    if (extract_data(args, TRUE)) {
        gwy_graph_curve_model_set_data(gcmodel,
                                       gwy_line_get_data(args->xdata),
                                       gwy_line_get_data(args->ydata),
                                       gwy_line_get_res(args->xdata));
        want_replot = TRUE;
    }
    else
        gwy_graph_curve_model_set_data(gcmodel, NULL, NULL, 0);

    return want_replot;
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    if (gui->changing_selection)
        return;

    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    gboolean want_estimate = FALSE, want_replot = FALSE;
    gboolean auto_estimate = gwy_params_get_boolean(params, PARAM_AUTO_ESTIMATE);

    if (id < 0 || id == PARAM_XPOS || id == PARAM_YPOS) {
        want_replot = switch_curve(gui);
        update_fit_state(gui, FIT_INITIALISED);
    }

    if (id < 0 || id == PARAM_FITFUNC) {
        args->fitfunc = gwy_inventory_get_item(gwy_fd_curve_presets(), gwy_params_get_string(params, PARAM_FITFUNC));
        gtk_label_set_markup(GTK_LABEL(gui->function), gwy_nlfit_preset_get_formula(args->fitfunc));

        fit_param_table_resize(gui);
        fit_correl_table_resize(gui);
        create_results(gui);
        want_replot = TRUE;
        want_estimate = want_estimate || auto_estimate;
        guess_parameters(gui);
    }

    if (id < 0 || id == PARAM_AUTO_ESTIMATE) {
        if (auto_estimate && !(gui->state == FIT_FITTED || gui->state == FIT_ESTIMATED))
            want_estimate = TRUE;
    }

    if (id < 0 || id == PARAM_RANGE_FROM || id == PARAM_RANGE_TO) {
        want_replot = TRUE;
        want_estimate = want_estimate || auto_estimate;
    }

    if (id < 0 || id == PARAM_PLOT_FULL) {
        want_replot = TRUE;
        want_estimate = want_estimate || auto_estimate;
    }

    if (id < 0 || id == PARAM_OUTPUT_TYPE) {
        gtk_dialog_set_response_sensitive(GTK_DIALOG(gui->dialog), GTK_RESPONSE_OK,
                                          gwy_params_get_flags(params, PARAM_OUTPUT_TYPE));
    }

    if (want_estimate) {
        estimate(gui);
    }
    else if (want_replot) {
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
    }
}

static void
param_value_activate(ModuleGUI *gui, guint i)
{
    gwy_fit_table_clear_error(GWY_FIT_TABLE(gui->fit_params), i);
    gwy_correl_table_clear(GWY_CORREL_TABLE(gui->correl_table));
    update_fit_state(gui, FIT_USER_MODIFIED);
    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
revert_parameters(ModuleGUI *gui)
{
    gwy_fit_table_revert_values(GWY_FIT_TABLE(gui->fit_params));
    gwy_fit_table_clear_errors(GWY_FIT_TABLE(gui->fit_params));
    gwy_correl_table_clear(GWY_CORREL_TABLE(gui->correl_table));
    update_fit_state(gui, FIT_USER_MODIFIED);
    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
update_rss_label(ModuleGUI *gui)
{
    GwyValueFormat *vf = gwy_unit_get_format(gwy_brick_get_unit_w(gui->args->brick),
                                             GWY_UNIT_FORMAT_VFMARKUP, gui->rss, NULL);
    gchar *s = g_strdup_printf("%.*f", vf->precision, gui->rss/vf->magnitude);
    gwy_param_table_info_set_valuestr(gui->table_below, INFO_RSS, s);
    g_free(s);
    gwy_param_table_set_unitstr(gui->table_below, INFO_RSS, vf->units);
    gwy_value_format_free(vf);
}

static void
guess_parameters(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;

    guint nparams = gwy_nlfit_preset_get_n_params(args->fitfunc);
    gdouble param[nparams];

    guint n = extract_data(args, TRUE);
    gdouble *xd = gwy_line_get_data(args->xdata);
    gdouble *yd = gwy_line_get_data(args->ydata);

    gboolean ok = TRUE;
    gwy_nlfit_preset_guess(args->fitfunc, n, xd, yd, param, &ok);
    GwyFitTable *table = GWY_FIT_TABLE(gui->fit_params);
    for (guint i = 0; i < nparams; i++)
        gwy_fit_table_set_value(table, i, param[i]);

    update_fit_state(gui, FIT_INITIALISED);
}

static void
plot_curve(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;

    guint nparams = gwy_nlfit_preset_get_n_params(args->fitfunc);
    gdouble param[nparams];
    gwy_fit_table_gather(GWY_FIT_TABLE(gui->fit_params), param, NULL, NULL);

    guint n = extract_data(args, TRUE);
    gdouble *xd = gwy_line_get_data(args->xdata);
    gdouble *yd = gwy_line_get_data(args->ydata);
    gboolean ok = TRUE;
    gdouble rss = 0.0;
    for (guint i = 0; i < n; i++) {
        gdouble v = gwy_nlfit_preset_get_value(args->fitfunc, xd[i], param, &ok);
        rss += (yd[i] - v)*(yd[i] - v);
        yd[i] = v;
    }
    gui->rss = sqrt(rss/n);

    GwyGraphCurveModel *gcmodel;
    if (gwy_graph_model_get_n_curves(gui->gmodel) < 2) {
        const GwyRGBA *color = gwy_graph_get_preset_color(1);
        gcmodel = gwy_graph_curve_model_new();
        g_object_set(gcmodel,
                     "mode", GWY_GRAPH_CURVE_LINE,
                     "color", color,
                     "line-style", GWY_LINE_DASH_DENSE,
                     "line-width", 2.0,
                     NULL);
        gwy_graph_model_add_curve(gui->gmodel, gcmodel);
        g_object_unref(gcmodel);
    }
    else
        gcmodel = gwy_graph_model_get_curve(gui->gmodel, 1);
    gwy_graph_curve_model_set_data(gcmodel, xd, yd, n);

    const gchar *desc = _("Model");
    if (gui->state == FIT_FITTED || gui->state == FIT_FIT_FAILED)
        desc = _("Fit");
    else if (gui->state == FIT_ESTIMATED || gui->state == FIT_ESTIMATE_FAILED)
        desc = _("Estimate");

    g_object_set(gcmodel, "description", desc, NULL);

    update_rss_label(gui);
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;

    /* TODO: Plot calculated function. That's what we do here. */
    plot_curve(gui);
}

static void
dialog_response(ModuleGUI *gui, gint response)
{
    gwy_fit_table_gather(GWY_FIT_TABLE(gui->fit_params), gui->args->param, NULL, gui->args->fixed);

    if (response == GWY_RESPONSE_COPY || response == GWY_RESPONSE_SAVE) {
        GwyResultsExportStyle report_style = gwy_params_get_report_type(gui->args->params, PARAM_REPORT_STYLE);
        gchar *report = gwy_results_create_report(gui->results, report_style);

        if (response == GWY_RESPONSE_SAVE)
            gwy_save_auxiliary_data(_("Save Fit Report"), GTK_WINDOW(gui->dialog), report, -1, FALSE);
        else {
            GdkDisplay *display = gtk_widget_get_display(gui->dialog);
            GtkClipboard *clipboard = gtk_clipboard_get_for_display(display, GDK_SELECTION_CLIPBOARD);
            gtk_clipboard_set_text(clipboard, report, -1);
        }
        g_free(report);
    }
    else if (response == GWY_RESPONSE_EXECUTE) {
        estimate(gui);
    }
    else if (response == GWY_RESPONSE_REFINE) {
        execute(gui);
    }
}

static void
point_selection_changed(ModuleGUI *gui,
                        gint id,
                        GwySelection *selection)
{
    /* What should we do here?  Hope we always get another update with a specific id afterwards. */
    gdouble xy[2];
    if (id < 0 || !gwy_selection_get_object(selection, id, xy))
        return;

    GwyBrick *brick = gui->args->brick;
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick);
    gui->changing_selection = TRUE;
    gwy_param_table_set_double(gui->table_above, PARAM_XPOS, CLAMP(gwy_brick_rtoi(brick, xy[1]), 0, yres-1));
    gwy_param_table_set_double(gui->table_above, PARAM_YPOS, CLAMP(gwy_brick_rtoj(brick, xy[0]), 0, xres-1));
    gui->changing_selection = FALSE;
    gwy_param_table_param_changed(gui->table_above, PARAM_XPOS);
}

static void
create_output_field(GwyField *field, GwyNield *mask, GwyFile *data, gchar *title)
{
    /* Do this before adding anything to the data browser to avoid uninitialised values. */
    if (mask)
        gwy_field_laplace_solve(field, mask, GWY_LAPLACE_MASKED, 1.0);
    gint newid = gwy_file_add_image(data, field);
    gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
    gwy_file_set_title(data, GWY_FILE_IMAGE, newid, title, TRUE);
    g_free(title);
    if (mask)
        gwy_file_pass_image_mask(data, newid, gwy_nield_copy(mask));
    gwy_log_add_full(data, GWY_FILE_IMAGE, -1, newid, "volume::volume_fdfit", NULL);
}

static void
execute_brick_and_create_outputs(ModuleArgs *args, GwyFile *data, gint id)
{
    GwyNLFitPreset *fitfunc = args->fitfunc;
    GwyBrick *brick = args->brick;
    guint nparams = gwy_nlfit_preset_get_n_params(fitfunc);
    guint nfree = 0;
    for (guint i = 0; i < nparams; i++)
        nfree += !args->fixed[i];

    if (!nfree)
        return;

    if (nfree >= gwy_brick_get_zres(brick)) {
        GtkWidget *dialog = gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
                                                   _("It is necessary to select more data points "
                                                     "than free fit parameters."));
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);
        return;
    }

    GtkWindow *wait_window = gwy_data_browser_get_window_for_data(data, GWY_FILE_VOLUME, id);
    gwy_app_wait_start(wait_window, _("Running computation..."));

    GwyField *resid_field = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    gwy_brick_extract_xy_plane(brick, resid_field, 0);
    GwyField *value_fields[nfree], *error_fields[nfree];
    for (guint k = 0, m = 0; k < nparams; k++) {
        if (args->fixed[k])
            continue;

        value_fields[m] = gwy_field_new_alike(resid_field, FALSE);
        error_fields[m] = gwy_field_new_alike(resid_field, FALSE);
        GwyUnit *unit = gwy_nlfit_preset_get_param_units(fitfunc, k, args->zunit, gwy_brick_get_unit_w(brick));
        gwy_unit_assign(gwy_field_get_unit_z(value_fields[m]), unit);
        gwy_unit_assign(gwy_field_get_unit_z(error_fields[m]), unit);
        g_object_unref(unit);
        m++;
    }
    GwyNield *mask = gwy_field_new_nield_alike(resid_field);
    gwy_nield_fill(mask, 1);

    gboolean always_estimate = gwy_params_get_boolean(args->params, PARAM_ESTIMATE);
    gint xres = gwy_brick_get_xres(brick), yres = gwy_brick_get_yres(brick);
    GwyNLFitter *fitter = NULL;
    for (gint i = 0; i < yres; i++) {
        gwy_params_set_int(args->params, PARAM_YPOS, i);
        for (gint j = 0; j < xres; j++) {
            gwy_params_set_int(args->params, PARAM_XPOS, j);
            extract_data(args, FALSE);
            guint npts = gwy_line_get_res(args->xdata);
            const gdouble *xdata = gwy_line_get_data_const(args->xdata);
            const gdouble *ydata = gwy_line_get_data_const(args->ydata);

            gdouble param[nparams], error[nparams];
            gwy_assign(param, args->param, nparams);
            if (always_estimate) {
                gboolean is_fitted;
                gwy_nlfit_preset_guess(fitfunc, npts, xdata, ydata, param, &is_fitted);
                if (!is_fitted)
                    continue;
            }

            fitter = gwy_nlfit_preset_fit(fitfunc, fitter, npts, xdata, ydata, param, error, args->fixed);
            if (!gwy_nl_fitter_succeeded(fitter))
                continue;

            for (guint k = 0, m = 0; k < nparams; k++) {
                if (args->fixed[k])
                    continue;
                gwy_field_set_val(value_fields[m], j, i, param[k]);
                gwy_field_set_val(error_fields[m], j, i, error[k]);
                m++;
            }
            gwy_field_set_val(resid_field, j, i, sqrt(gwy_nl_fitter_get_dispersion(fitter)));
            gwy_nield_set_val(mask, j, i, 0);
        }
        if (!gwy_app_wait_set_fraction((gdouble)i/xres)) {
            gwy_app_wait_finish();
            goto fail;
        }
    }

    gwy_app_wait_finish();

    if (!gwy_nield_count(mask))
        g_clear_object(&mask);

    OutputFlags output_flags = gwy_params_get_flags(args->params, PARAM_OUTPUT_TYPE);
    for (guint k = 0, m = 0; k < nparams; k++) {
        if (args->fixed[k])
            continue;

        const gchar *parname = gwy_nlfit_preset_get_param_name(fitfunc, k);

        if (output_flags & (1 << OUTPUT_VALUES))
            create_output_field(value_fields[m], mask, data, g_strdup_printf(_("Result %s"), parname));
        if (output_flags & (1 << OUTPUT_ERRORS))
            create_output_field(error_fields[m], mask, data, g_strdup_printf(_("Error %s"), parname));
        m++;
    }
    if (output_flags & (1 << OUTPUT_RESIDUA))
        create_output_field(resid_field, mask, data, g_strdup(_("Mean square difference")));

fail:
    for (guint k = 0; k < nfree; k++) {
        g_object_unref(value_fields[k]);
        g_object_unref(error_fields[k]);
    }
    g_object_unref(resid_field);
    g_clear_object(&mask);
}

static void
execute(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    guint nparams = gwy_nlfit_preset_get_n_params(args->fitfunc);
    gdouble error[nparams];
    guint nfree = nparams - gwy_fit_table_gather(GWY_FIT_TABLE(gui->fit_params), args->param, NULL, args->fixed);

    if (!nfree)
        return;

    GwyFitTable *table = GWY_FIT_TABLE(gui->fit_params);
    gwy_fit_table_clear_errors(table);

    GwyCorrelTable *ctable = GWY_CORREL_TABLE(gui->correl_table);
    gwy_correl_table_clear(ctable);

    guint npoints;
    npoints = extract_data(args, FALSE);
    if (npoints <= nfree) {
        gwy_param_table_set_label(gui->table_below, MESSAGE_ERROR, _("Not enough data points."));
        return;
    }
    gwy_param_table_set_label(gui->table_below, MESSAGE_ERROR, NULL);

    if (gui->fitter)
        gwy_nl_fitter_free(gui->fitter);

    GwyNLFitter *fitter = gwy_nlfit_preset_fit(args->fitfunc, NULL,
                                               gwy_line_get_res(args->xdata),
                                               gwy_line_get_data_const(args->xdata),
                                               gwy_line_get_data_const(args->ydata),
                                               args->param, error, args->fixed);
    gui->fitter = fitter;
    gboolean is_fitted = gwy_nl_fitter_succeeded(fitter);

    if (is_fitted) {
        for (guint i = 0; i < nparams; i++) {
            if (args->fixed[i])
                args->param[i] = gwy_fit_table_get_value(table, i);
            else
                gwy_fit_table_set_value(table, i, args->param[i]);

            gwy_fit_table_set_error(table, i, args->fixed[i] ? 0.0 : gwy_nl_fitter_get_sigma(fitter, i));
            gwy_correl_table_set_fixed(ctable, i, args->fixed[i]);
        }

        for (guint i = 0; i < nparams; i++) {
            for (guint j = 0; j <= i; j++) {
                gdouble c = gwy_nl_fitter_get_correlations(fitter, i, j);
                gwy_correl_table_set_correl(ctable, i, j, c);
                SLi(gui->correl, i, j) = c;
            }
        }
    }
    else {
        gwy_fit_table_gather(GWY_FIT_TABLE(gui->fit_params), args->param, NULL, NULL);
    }

    plot_curve(gui);
    update_fit_state(gui, is_fitted ? FIT_FITTED : FIT_FIT_FAILED);
    if (is_fitted)
        fill_results(gui);
}

static void
update_fit_state(ModuleGUI *gui, FitState state)
{
    if (gui->state == state)
        return;

    gui->state = state;
    gtk_dialog_set_response_sensitive(GTK_DIALOG(gui->dialog), GWY_RESPONSE_SAVE, state == FIT_FITTED);
    gtk_dialog_set_response_sensitive(GTK_DIALOG(gui->dialog), GWY_RESPONSE_COPY, state == FIT_FITTED);

    if (state != FIT_FITTED) {
        gwy_fit_table_clear_errors(GWY_FIT_TABLE(gui->fit_params));
        gwy_correl_table_clear(GWY_CORREL_TABLE(gui->correl_table));
        gwy_param_table_set_label(gui->table_below, MESSAGE_ERROR, _("Fit failed."));
    }
}

static void
estimate(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    GwyNLFitPreset *fitfunc = args->fitfunc;
    guint nparams = gwy_nlfit_preset_get_n_params(fitfunc);
    gwy_fit_table_gather(GWY_FIT_TABLE(gui->fit_params), args->param, NULL, args->fixed);

    if (!extract_data(args, FALSE))
        return;

    gboolean ok;
    gwy_nlfit_preset_guess(fitfunc,
                           gwy_line_get_res(args->xdata),
                           gwy_line_get_data_const(args->xdata),
                           gwy_line_get_data_const(args->ydata),
                           args->param, &ok);

    GwyFitTable *table = GWY_FIT_TABLE(gui->fit_params);
    for (guint i = 0; i < nparams; i++) {
        gwy_debug("[%u] %g", i, args->param[i]);
        if (args->fixed[i])
            args->param[i] = gwy_fit_table_get_value(table, i);
        else
            gwy_fit_table_set_value(table, i, args->param[i]);
    }
    gwy_fit_table_clear_errors(table);
    update_fit_state(gui, ok ? FIT_ESTIMATED : FIT_ESTIMATE_FAILED);
    plot_curve(gui);
}

/* Extract relevant part of data. */
static gint
extract_data(ModuleArgs *args, gboolean for_plotting)
{
    GwyBrick *brick = args->brick;
    GwyParams *params = args->params;
    gint xpos = gwy_params_get_int(params, PARAM_XPOS), ypos = gwy_params_get_int(params, PARAM_YPOS);
    gdouble from = gwy_params_get_double(params, PARAM_RANGE_FROM);
    gdouble to = gwy_params_get_double(params, PARAM_RANGE_TO);
    gint zres = gwy_brick_get_zres(brick);
    if (for_plotting && gwy_params_get_boolean(params, PARAM_PLOT_FULL)) {
        from = -G_MAXDOUBLE;
        to = G_MAXDOUBLE;
    }

    gwy_brick_extract_line(brick, args->ydata, xpos, ypos, 0, xpos, ypos, zres, FALSE);
    gdouble *xd, *yd = gwy_line_get_data(args->ydata);

    if (args->calibration) {
        gwy_line_assign(args->xdata, args->calibration);
        xd = gwy_line_get_data(args->xdata);
    }
    else {
        gwy_line_resize(args->xdata, zres);
        xd = gwy_line_get_data(args->xdata);
        gwy_math_linspace(xd, zres, args->zmin, (args->zmax - args->zmin)/zres);
    }

    guint n = 0;
    gboolean skip_zero = FALSE;
    for (guint i = 0; i < zres; i++) {
        if (xd[i] >= from && xd[i] <= to && !(skip_zero && xd[i] == 0.0)) {
            xd[n] = xd[i];
            yd[n] = yd[i];
            n++;
        }
    }
    if (!n)
        return 0;

    if (n < zres) {
        gwy_line_crop(args->xdata, 0, n);
        gwy_line_crop(args->ydata, 0, n);
    }

    return n;
}

static void
create_results(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    GwyNLFitPreset *fitfunc = args->fitfunc;

    g_clear_object(&gui->results);
    GwyResults *results = gui->results = gwy_results_new();
    gwy_results_add_header(results, N_("Fit Results"));
    gwy_results_add_value_str(results, "file", N_("File"));
    gwy_results_add_value_str(results, "channel", N_("Curve"));
    gwy_results_add_format(results, "npts", N_("Number of points"), TRUE,
    /* TRANSLATORS: %{n}i and %{total}i are ids, do NOT translate them. */
                           N_("%{n}i of %{ntotal}i"), NULL);
    gwy_results_add_value_str(results, "func", N_("Fitted function"));
    gwy_results_add_value_z(results, "rss", N_("Mean square difference"));

    gwy_results_add_separator(results);
    gwy_results_add_header(results, N_("Parameters"));
    guint nparams = gwy_nlfit_preset_get_n_params(fitfunc);
    const gchar *names[nparams];
    for (guint i = 0; i < nparams; i++) {
        const gchar *name = names[i] = gwy_nlfit_preset_get_param_name(fitfunc, i);
        gint power_xy = gwy_nlfit_preset_get_param_power_x(fitfunc, i);
        gint power_z = gwy_nlfit_preset_get_param_power_y(fitfunc, i);
        gwy_results_add_value(results, name, "",
                              "symbol", name, "is-fitting-param", TRUE,
                              "power-x", power_xy, "power-z", power_z,
                              NULL);
    }

    gwy_results_add_separator(results);
    gwy_results_add_correlation_matrixv(results, "correl", N_("Correlation Matrix"), nparams, names);
}

static void
fill_results(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    GwyNLFitPreset *fitfunc = args->fitfunc;
    guint nparams = gwy_nlfit_preset_get_n_params(args->fitfunc);
    gdouble param[nparams], error[nparams];
    gboolean fixed[nparams];
    gwy_fit_table_gather(GWY_FIT_TABLE(gui->fit_params), param, error, fixed);

    GwyResults *results = gui->results;
    GwyGraphModel *gmodel = gui->gmodel;
    GwyUnit *xunit, *yunit;
    g_object_get(gmodel,
                 "unit-x", &xunit,
                 "unit-y", &yunit,
                 NULL);
    gwy_results_set_unit(results, "x", xunit);
    gwy_results_set_unit(results, "y", yunit);
    g_object_unref(xunit);
    g_object_unref(yunit);

    GwyGraphCurveModel *gcmodel_data = gwy_graph_model_get_curve(gmodel, 0);
    GwyGraphCurveModel *gcmodel_fit = gwy_graph_model_get_curve(gmodel, 1);
    guint ntotal = gwy_graph_curve_model_get_n_data(gcmodel_data);
    guint n = gwy_graph_curve_model_get_n_data(gcmodel_fit);
    gchar *desc;
    g_object_get(gcmodel_data, "description", &desc, NULL);

    gwy_results_fill_filename(results, "file", gui->parent_data);
    gwy_results_fill_values(results,
                            "func", gwy_resource_get_name(GWY_RESOURCE(fitfunc)),
                            "channel", desc,
                            "rss", sqrt(gwy_nl_fitter_get_dispersion(gui->fitter)),
                            NULL);
    g_free(desc);
    gwy_results_fill_format(results, "npts",
                            "n", n,
                            "ntotal", ntotal,
                            NULL);

    for (guint i = 0; i < nparams; i++) {
        const gchar *name = gwy_nlfit_preset_get_param_name(fitfunc, i);

        if (fixed[i])
            gwy_results_fill_values(results, name, param[i], NULL);
        else
            gwy_results_fill_values_with_errors(results, name, param[i], error[i], NULL);
    }

    gwy_results_fill_correlation_matrix(results, "correl", fixed, gui->correl);
}

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;
    gint xres = gwy_brick_get_xres(args->brick);
    gint yres = gwy_brick_get_yres(args->brick);

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

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