/*
 *  $Id: corrlen.c 30146 2026-06-25 12:51:18Z yeti-dn $
 *  Copyright (C) 2020-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 <gtk/gtk.h>
#include <gwy.h>
#include "libgwyddion/omp.h"
#include "libgwyapp/sanity.h"

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

enum {
    PARAM_ORIENTATION,
    PARAM_MASKING,
    PARAM_LEVEL,
    PARAM_MODEL,
    PARAM_INSTANT_UPDATE,
    PARAM_REPORT_STYLE,
    PARAM_OUTPUT,
    PARAM_TARGET_GRAPH,
    PARAM_HOLD_SELECTION,
    PARAM_OPTIONS_VISIBLE,

    WIDGET_RESULTS_CORRLEN,
};

enum {
    ACF_CURVE_RAW       = 0,
    ACF_CURVE_CORRECTED = 1,
    ACF_CURVE_FIT       = 2,
};

typedef enum {
    CORRLEN_MODEL_GAUSS = 0,
    CORRLEN_MODEL_EXP   = 1,
} CorrlenModelType;

typedef enum {
    CORRLEN_OUTPUT_ACF_RAW  = 1,
    CORRLEN_OUTPUT_ACF_CORR = 2,
    CORRLEN_OUTPUT_ACF_BOTH = 3,
} CorrlenOutputType;

#define GWY_TYPE_TOOL_CORR_LEN            (gwy_tool_corr_len_get_type())
#define GWY_TOOL_CORR_LEN(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_CORR_LEN, GwyToolCorrLen))
#define GWY_IS_TOOL_CORR_LEN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_CORR_LEN))
#define GWY_TOOL_CORR_LEN_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_CORR_LEN, GwyToolCorrLenClass))

typedef struct _GwyToolCorrLen      GwyToolCorrLen;
typedef struct _GwyToolCorrLenClass GwyToolCorrLenClass;

typedef struct {
    GwyMaskingType masking;
    GwyResultsReportType report_style;
    gboolean instant_update;
    gint level;
    GwyOrientation orientation;
} ToolArgs;

typedef struct {
    const gdouble *acf;
    gdouble dx;
    gint degree;
    gint N;
} CorrectedACFModelData;

struct _GwyToolCorrLen {
    GwyPlainTool parent_instance;

    GwyParams *params;
    GwyResults *results;
    GwyGraphModel *gmodel_acf;
    GwyGraphModel *gmodel_psdf;

    GwyRectSelectionLabels *rlabels;
    GwyParamTable *table_options;
    GwyParamTable *table_results;
    GtkWidget *update;

    gint isel[4];
    gint isel_prev[4];
    gdouble rsel[4];
};

struct _GwyToolCorrLenClass {
    GwyPlainToolClass parent_class;
};

static gboolean     module_register                    (void);
static GwyParamDef* define_module_params               (void);
static GType        gwy_tool_corr_len_get_type         (void)                      G_GNUC_CONST;
static void         gwy_tool_corr_len_finalize         (GObject *object);
static void         gwy_tool_corr_len_init_dialog      (GwyToolCorrLen *tool);
static void         gwy_tool_corr_len_data_switched    (GwyTool *gwytool,
                                                        GwyDataView *data_view);
static void         gwy_tool_corr_len_data_changed     (GwyPlainTool *plain_tool);
static void         gwy_tool_corr_len_mask_changed     (GwyPlainTool *plain_tool);
static void         gwy_tool_corr_len_response         (GwyTool *tool,
                                                        gint response_id);
static void         gwy_tool_corr_len_selection_changed(GwyPlainTool *plain_tool,
                                                        gint hint);
static void         update_selected_rectangle          (GwyToolCorrLen *tool);
static void         param_changed                      (GwyToolCorrLen *tool,
                                                        gint id);
static void         update_sensitivity                 (GwyToolCorrLen *tool);
static void         update_labels                      (GwyToolCorrLen *tool);
static void         update_target_graphs               (GwyToolCorrLen *tool);
static void         gwy_tool_corr_len_apply            (GwyToolCorrLen *tool);
static gboolean     calculate                          (GwyToolCorrLen *tool);
static void         update_units                       (GwyToolCorrLen *tool);
static GwyResults*  create_results                     (void);

static const GwyEnum models[] = {
    { N_("Gaussian"),    CORRLEN_MODEL_GAUSS, },
    { N_("Exponential"), CORRLEN_MODEL_EXP,   },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Correlation length tool."),
    "Yeti <yeti@gwyddion.net>",
    "3.1",
    "David Nečas (Yeti)",
    "2020",
};

GWY_MODULE_QUERY2(module_info, corrlen)

G_DEFINE_TYPE(GwyToolCorrLen, gwy_tool_corr_len, GWY_TYPE_PLAIN_TOOL)

static gboolean
module_register(void)
{
    gwy_tool_func_register(GWY_TYPE_TOOL_CORR_LEN);

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum levellings[] = {
        { gwy_NC("line-leveling", "None"), 0, },
        { N_("Offset"),             1, },
        { N_("Tilt"),               2, },
        { N_("Bow"),                3, },
        { N_("Cubic"),              4, },
    };
    static const GwyEnum outputs[] = {
        { N_("Raw ACF"),       CORRLEN_OUTPUT_ACF_RAW,  },
        { N_("Corrected ACF"), CORRLEN_OUTPUT_ACF_CORR, },
        { N_("Both ACF"),      CORRLEN_OUTPUT_ACF_BOTH, },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "corrlen");
    gwy_param_def_add_enum(paramdef, PARAM_ORIENTATION, "orientation", NULL,
                           GWY_TYPE_ORIENTATION, GWY_ORIENTATION_HORIZONTAL);
    gwy_param_def_add_enum(paramdef, PARAM_MASKING, "masking", NULL, GWY_TYPE_MASKING_TYPE, GWY_MASK_IGNORE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_LEVEL, "level", _("Line leveling"),
                              levellings, G_N_ELEMENTS(levellings), 1);
    gwy_param_def_add_gwyenum(paramdef, PARAM_MODEL, "model", _("ACF model"),
                              models, G_N_ELEMENTS(models), CORRLEN_MODEL_GAUSS);
    gwy_param_def_add_instant_updates(paramdef, PARAM_INSTANT_UPDATE, "instant_update", NULL, TRUE);
    gwy_param_def_add_report_type(paramdef, PARAM_REPORT_STYLE, "report_style", _("Save Statistical Quantities"),
                                  GWY_RESULTS_EXPORT_PARAMETERS, GWY_RESULTS_REPORT_COLON);
    gwy_param_def_add_gwyenum(paramdef, PARAM_OUTPUT, "output", _("Output _type"),
                              outputs, G_N_ELEMENTS(outputs), CORRLEN_OUTPUT_ACF_CORR);
    gwy_param_def_add_target_graph(paramdef, PARAM_TARGET_GRAPH, NULL, NULL);
    gwy_param_def_add_hold_selection(paramdef, PARAM_HOLD_SELECTION, "hold_selection", NULL);
    gwy_param_def_add_boolean(paramdef, PARAM_OPTIONS_VISIBLE, "options_visible", NULL, FALSE);

    return paramdef;
}

static void
gwy_tool_corr_len_class_init(GwyToolCorrLenClass *klass)
{
    GwyPlainToolClass *ptool_class = GWY_PLAIN_TOOL_CLASS(klass);
    GwyToolClass *tool_class = GWY_TOOL_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = gwy_tool_corr_len_finalize;

    tool_class->icon_name = GWY_ICON_CORRELATION_LENGTH;
    tool_class->title = _("Correlation Length");
    tool_class->tooltip = _("Correlation Length");
    tool_class->prefix = "/module/corrlen";
    tool_class->default_width = 640;
    tool_class->default_height = 400;
    tool_class->data_switched = gwy_tool_corr_len_data_switched;
    tool_class->response = gwy_tool_corr_len_response;

    ptool_class->data_changed = gwy_tool_corr_len_data_changed;
    ptool_class->mask_changed = gwy_tool_corr_len_mask_changed;
    ptool_class->selection_changed = gwy_tool_corr_len_selection_changed;
}

static void
gwy_tool_corr_len_finalize(GObject *object)
{
    GwyToolCorrLen *tool = GWY_TOOL_CORR_LEN(object);

    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);
    g_clear_object(&tool->results);
    g_clear_object(&tool->gmodel_acf);
    g_clear_object(&tool->gmodel_psdf);

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

static void
gwy_tool_corr_len_init(GwyToolCorrLen *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    plain_tool->lazy_updates = TRUE;
    plain_tool->unit_style = GWY_UNIT_FORMAT_VFMARKUP;

    tool->params = gwy_params_new_from_settings(define_module_params());
    tool->results = create_results();

    gwy_plain_tool_connect_selection(plain_tool, GWY_TYPE_LAYER_RECTANGLE, "rectangle");
    memset(tool->isel_prev, 0xff, 4*sizeof(gint));
    gwy_plain_tool_enable_selection_holding(plain_tool);

    gwy_tool_corr_len_init_dialog(tool);
}

static void
gwy_tool_corr_len_rect_updated(GwyToolCorrLen *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    gwy_rect_selection_labels_select(tool->rlabels, plain_tool->selection, plain_tool->field);
}

static void
gwy_tool_corr_len_init_dialog(GwyToolCorrLen *tool)
{
    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GtkWidget *hbox, *vbox, *graph, *options;
    GwyParamTable *table;

    tool->gmodel_acf = gwy_graph_model_new();
    tool->gmodel_psdf = gwy_graph_model_new();
    update_units(tool);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), hbox, TRUE, TRUE, 0);

    /* Selection info */
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_hexpand(vbox, FALSE);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, TRUE, 0);

    tool->rlabels = gwy_rect_selection_labels_new(TRUE, G_CALLBACK(gwy_tool_corr_len_rect_updated), tool);
    gtk_box_pack_start(GTK_BOX(vbox), gwy_rect_selection_labels_get_table(tool->rlabels), FALSE, FALSE, 0);

    /* Options */
    options = gwy_create_expander_with_param(_("<b>Options</b>"), tool->params, PARAM_OPTIONS_VISIBLE);
    gtk_box_pack_start(GTK_BOX(vbox), options, FALSE, FALSE, 0);

    table = tool->table_options = gwy_param_table_new(tool->params);
    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_combo(table, PARAM_MODEL);
    gwy_param_table_append_combo(table, PARAM_MASKING);
    gwy_param_table_append_combo(table, PARAM_LEVEL);
    gwy_param_table_append_message(table, -1, _("The degree must be ≥ any prior line leveling."));
    gwy_param_table_append_separator(table);
    gwy_param_table_append_combo(table, PARAM_ORIENTATION);
    gwy_param_table_append_checkbox(table, PARAM_INSTANT_UPDATE);
    gwy_param_table_append_hold_selection(table, PARAM_HOLD_SELECTION);
    gwy_plain_tool_add_param_table(plain_tool, table);
    gtk_container_add(GTK_CONTAINER(options), gwy_param_table_widget(table));

    /* Results */
    table = tool->table_results = gwy_param_table_new(tool->params);
    gwy_param_table_append_header(table, -1, _("Correlation Length T"));
    gwy_param_table_append_results(table, WIDGET_RESULTS_CORRLEN, tool->results,
                                   "acf_1e", "acf_1e_corr", "T_acf", "T_psdf", "acf_0",
                                   NULL);
    gwy_param_table_append_header(table, -1, _("Relation to Image Size"));
    gwy_param_table_append_results(table, WIDGET_RESULTS_CORRLEN, tool->results,
                                   "alpha", "L_T",
                                   NULL);
    gwy_param_table_append_header(table, -1, _("Mean Square Roughness"));
    gwy_param_table_append_results(table, WIDGET_RESULTS_CORRLEN, tool->results,
                                   "sigma", "sigma_corr", "sigma_acf", "sigma_psdf",
                                   NULL);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_report(table, PARAM_REPORT_STYLE);
    gwy_param_table_report_set_results(table, PARAM_REPORT_STYLE, tool->results);
    gwy_param_table_append_header(table, -1, _("Output"));
    gwy_param_table_append_combo(table, PARAM_OUTPUT);
    gwy_param_table_append_target_graph(table, PARAM_TARGET_GRAPH, tool->gmodel_acf);
    gwy_plain_tool_add_param_table(plain_tool, table);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), FALSE, FALSE, 0);

    /* Graphs */
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 0);

    graph = gwy_graph_new(tool->gmodel_acf);
    gwy_graph_enable_user_input(GWY_GRAPH(graph), FALSE);
    gtk_box_pack_start(GTK_BOX(vbox), graph, TRUE, TRUE, 2);

    graph = gwy_graph_new(tool->gmodel_psdf);
    gwy_graph_enable_user_input(GWY_GRAPH(graph), FALSE);
    gtk_box_pack_start(GTK_BOX(vbox), graph, TRUE, TRUE, 2);

    tool->update = gwy_add_button_to_dialog(dialog, _("_Update"), GWY_ICON_GTK_EXECUTE, GWY_RESPONSE_UPDATE);
    gwy_plain_tool_add_clear_button(plain_tool);
    gwy_tool_add_hide_button(GWY_TOOL(tool), TRUE);
    gwy_plain_tool_add_apply_button(plain_tool);
    gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_APPLY, FALSE);
    gwy_help_add_to_tool_dialog(dialog, GWY_TOOL(tool), GWY_HELP_DEFAULT);

    update_sensitivity(tool);

    g_signal_connect_swapped(tool->table_options, "param-changed", G_CALLBACK(param_changed), tool);
    g_signal_connect_swapped(tool->table_results, "param-changed", G_CALLBACK(param_changed), tool);

    gtk_widget_show_all(gtk_dialog_get_content_area(dialog));
}

static void
gwy_tool_corr_len_data_switched(GwyTool *gwytool,
                                GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolCorrLen *tool = GWY_TOOL_CORR_LEN(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);

    GWY_TOOL_CLASS(gwy_tool_corr_len_parent_class)->data_switched(gwytool, data_view);

    if (ignore || plain_tool->init_failed)
        return;

    gwy_param_table_set_sensitive(tool->table_results, PARAM_REPORT_STYLE, FALSE);
    if (data_view) {
        gwy_object_set_or_reset(plain_tool->layer, GWY_TYPE_LAYER_RECTANGLE,
                                "editable", TRUE,
                                "focus", -1,
                                NULL);
        gwy_selection_set_max_objects(plain_tool->selection, 1);
        gwy_plain_tool_hold_selection(plain_tool, gwy_params_get_flags(tool->params, PARAM_HOLD_SELECTION));
    }
    update_units(tool);
    update_labels(tool);
    update_target_graphs(tool);
}

static void
update_units(GwyToolCorrLen *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyField *field = plain_tool->field;
    GwyUnit *xyunit, *zunit, *unit = gwy_unit_new(NULL);

    if (!field) {
        g_object_set(tool->gmodel_psdf, "unit-x", unit, "unit-y", unit, NULL);
        g_object_set(tool->gmodel_acf, "unit-x", unit, "unit-y", unit, NULL);
        gwy_results_set_unit(tool->results, "x", unit);
        gwy_results_set_unit(tool->results, "y", unit);
        gwy_results_set_unit(tool->results, "z", unit);
        g_object_unref(unit);
        return;
    }

    xyunit = gwy_field_get_unit_xy(field);
    zunit = gwy_field_get_unit_z(field);

    gwy_results_set_unit(tool->results, "x", xyunit);
    gwy_results_set_unit(tool->results, "y", xyunit);
    gwy_results_set_unit(tool->results, "z", zunit);

    g_object_set(tool->gmodel_acf, "unit-x", xyunit, NULL);
    gwy_unit_power(unit, zunit, 2);
    g_object_set(tool->gmodel_acf, "unit-y", unit, NULL);
    gwy_unit_power(unit, xyunit, -1);
    g_object_set(tool->gmodel_psdf, "unit-x", unit, NULL);
    gwy_unit_power_multiply(unit, xyunit, 1, zunit, 2);
    g_object_set(tool->gmodel_psdf, "unit-y", unit, NULL);

    g_object_unref(unit);
}

static void
update_selected_rectangle(GwyToolCorrLen *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwySelection *selection = plain_tool->selection;
    GwyField *field = plain_tool->field;
    gint n = selection ? gwy_selection_get_data(selection, NULL) : 0;

    gwy_rect_selection_labels_fill(tool->rlabels, n == 1 ? selection : NULL, field, NULL, tool->isel);
}

static void
param_changed(GwyToolCorrLen *tool, gint id)
{
    GwyParams *params = tool->params;
    gboolean do_update = (id != PARAM_INSTANT_UPDATE && id != PARAM_MASKING
                          && id != PARAM_REPORT_STYLE && id != PARAM_OPTIONS_VISIBLE
                          && id != PARAM_TARGET_GRAPH && id != PARAM_OUTPUT);

    if (id == PARAM_INSTANT_UPDATE)
        do_update = do_update || gwy_params_get_boolean(params, PARAM_INSTANT_UPDATE);
    if (id == PARAM_MASKING) {
        do_update = do_update || (GWY_PLAIN_TOOL(tool)->field && GWY_PLAIN_TOOL(tool)->mask_field);
    }
    if (id < 0 || id == PARAM_INSTANT_UPDATE)
        update_sensitivity(tool);
    if (do_update)
        update_labels(tool);
}

static void
update_sensitivity(GwyToolCorrLen *tool)
{
    gtk_widget_set_sensitive(tool->update, !gwy_params_get_boolean(tool->params, PARAM_INSTANT_UPDATE));
}

static void
gwy_tool_corr_len_data_changed(GwyPlainTool *plain_tool)
{
    GwyToolCorrLen *tool = GWY_TOOL_CORR_LEN(plain_tool);

    update_selected_rectangle(tool);
    update_units(tool);
    update_labels(tool);
    update_target_graphs(tool);
}

static void
gwy_tool_corr_len_mask_changed(GwyPlainTool *plain_tool)
{
    GwyToolCorrLen *tool = GWY_TOOL_CORR_LEN(plain_tool);

    if (gwy_params_get_enum(tool->params, PARAM_MASKING) != GWY_MASK_IGNORE)
        update_labels(tool);
}

static void
gwy_tool_corr_len_response(GwyTool *tool,
                           gint response_id)
{
    GWY_TOOL_CLASS(gwy_tool_corr_len_parent_class)->response(tool, response_id);

    if (response_id == GTK_RESPONSE_APPLY)
        gwy_tool_corr_len_apply(GWY_TOOL_CORR_LEN(tool));
    else if (response_id == GWY_RESPONSE_UPDATE)
        update_labels(GWY_TOOL_CORR_LEN(tool));
}

static void
gwy_tool_corr_len_selection_changed(GwyPlainTool *plain_tool,
                                    gint hint)
{
    GwyToolCorrLen *tool = GWY_TOOL_CORR_LEN(plain_tool);

    g_return_if_fail(hint <= 0);
    update_selected_rectangle(tool);
    if (gwy_params_get_boolean(tool->params, PARAM_INSTANT_UPDATE)) {
        if (memcmp(tool->isel, tool->isel_prev, 4*sizeof(gint)) != 0)
            update_labels(tool);
    }
    else
        gwy_param_table_set_sensitive(tool->table_results, PARAM_REPORT_STYLE, FALSE);
}

static void
update_target_graphs(GwyToolCorrLen *tool)
{
    gwy_param_table_data_id_refilter(tool->table_results, PARAM_TARGET_GRAPH);
}

static void
gwy_tool_corr_len_apply(GwyToolCorrLen *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyParams *params = tool->params;
    CorrlenOutputType output_type = gwy_params_get_enum(params, PARAM_OUTPUT);
    GwyGraphModel *gmodel, *outgmodel, *acfgmodel = tool->gmodel_acf;

    outgmodel = gwy_graph_model_new_alike(acfgmodel);
    if (output_type == CORRLEN_OUTPUT_ACF_RAW || output_type == CORRLEN_OUTPUT_ACF_BOTH)
        gwy_graph_model_add_curve(outgmodel, gwy_graph_model_get_curve(acfgmodel, ACF_CURVE_RAW));
    if (output_type == CORRLEN_OUTPUT_ACF_CORR || output_type == CORRLEN_OUTPUT_ACF_BOTH)
        gwy_graph_model_add_curve(outgmodel, gwy_graph_model_get_curve(acfgmodel, ACF_CURVE_CORRECTED));

    if ((gmodel = gwy_params_get_graph(params, PARAM_TARGET_GRAPH)))
        gwy_graph_model_append_curves(gmodel, outgmodel, 1);
    else {
        gint newid = gwy_file_add_graph(GWY_FILE(plain_tool->container), outgmodel);
        gwy_file_set_visible(GWY_FILE(plain_tool->container), GWY_FILE_GRAPH, newid, TRUE);
    }

    g_object_unref(outgmodel);
}

static void
update_labels(GwyToolCorrLen *tool)
{
    static const gint results_ids[] = { WIDGET_RESULTS_CORRLEN };
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    guint i;

    gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY, FALSE);
    if (!plain_tool->field) {
        for (i = 0; i < G_N_ELEMENTS(results_ids); i++)
            gwy_param_table_results_clear(tool->table_results, results_ids[i]);
        gwy_graph_model_remove_all_curves(tool->gmodel_acf);
        gwy_graph_model_remove_all_curves(tool->gmodel_psdf);
        return;
    }

    if (plain_tool->pending_updates & GWY_PLAIN_TOOL_CHANGED_SELECTION)
        update_selected_rectangle(tool);
    plain_tool->pending_updates = 0;

    if (calculate(tool)) {
        for (i = 0; i < G_N_ELEMENTS(results_ids); i++)
            gwy_param_table_results_fill(tool->table_results, results_ids[i]);
        gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY, TRUE);
    }
}

static gdouble
find_decay_point(GwyLine *line, gdouble q)
{
    const gdouble *d = gwy_line_get_data(line);
    guint res = gwy_line_get_res(line);
    gdouble max = d[0];
    gdouble threshold = q*max;
    guint i;
    gdouble v0, v1, t;

    for (i = 1; i < res; i++) {
        if (d[i] <= threshold) {
            if (d[i] == threshold)
                return gwy_line_itor(line, i);

            v0 = d[i-1] - threshold;
            v1 = d[i] - threshold;
            t = v0/(v0 - v1);
            return gwy_line_itor(line, i-1 + t);
        }
    }

    return -1.0;
}

static void
fit_psdf(GwyLine *psdf, const gchar *presetname, gdouble T_estim,
         gdouble *sigma, gdouble *T, GwyLine *fit)
{
    GwyNLFitPreset *preset = gwy_inventory_get_item(gwy_nlfit_presets(), presetname);
    guint nfit, i, res = gwy_line_get_res(psdf);
    const gdouble *ydata = gwy_line_get_data(psdf);
    gdouble *xdata, *fitdata, *xdata_all;
    gdouble t = 0.0, s = gwy_line_sum(psdf);
    GwyNLFitter *fitter;
    gdouble params[2], errors[2];
    gboolean ok;

    for (nfit = 0; nfit < res; nfit++) {
        t += ydata[nfit];
        if (t > 0.999*s)
            break;
    }
    xdata = xdata_all = gwy_math_linspace(NULL, res, 0.0, gwy_line_get_dx(psdf));

    /* Try to skip the smallest frequencies.  Unfortunately, we cannot do that for tiny data. */
    for (i = 0; i < 4; i++) {
        if (nfit > (4 << i)) {
            xdata++;
            ydata++;
            nfit--;
        }
    }

    params[0] = sqrt(s*gwy_line_get_dx(psdf));
    params[1] = T_estim;
    fitter = gwy_nlfit_preset_fit(preset, NULL, nfit, xdata, ydata, params, errors, NULL);
    *sigma = (gwy_nl_fitter_succeeded(fitter) ? params[0] : -1.0);
    *T = (gwy_nl_fitter_succeeded(fitter) ? params[1] : -1.0);

    fitdata = gwy_line_get_data(fit);
    for (i = 0; i < res; i++)
        fitdata[i] = gwy_nlfit_preset_get_value(preset, xdata_all[i], params, &ok);

    gwy_nl_fitter_free(fitter);
    g_free(xdata_all);
}

/* Calculate v*A (since we do not distinguish row and colum vectors this can also be seen as A^T*v) */
static inline void
matrix_multiply_right(const gdouble *v, const gdouble *A,
                      gdouble *result,
                      guint n)
{
    guint i, j;

    for (i = 0; i < n; i++) {
        gdouble s = 0.0;
        for (j = 0; j < n; j++)
            s += v[j]*A[j*n + i];
        result[i] = s;
    }
}

static void
acf_extrapolate_correction(GwyLine *acf, GwyLine *corrected_acf)
{
    const gdouble *a = gwy_line_get_data(acf);
    const gdouble *c = gwy_line_get_data(corrected_acf);
    gint ares = gwy_line_get_res(acf);
    gint cres = gwy_line_get_res(corrected_acf);
    gdouble diff = c[cres-1] - a[cres-1];
    GwyLine *extended = gwy_line_new_alike(acf, FALSE);
    gdouble *e = gwy_line_get_data(extended);
    gint i;

    gwy_assign(e, c, cres);

    for (i = cres; i < ares; i++)
        e[i] = a[i] + diff;

    gwy_line_assign(corrected_acf, extended);
    g_object_unref(extended);
}

static GwyLine*
acf_selfcorrect(GwyLine *acf, gint degree, gint N, gint K)
{
    GwyLine *acf_corr = gwy_line_copy(acf);
    gdouble *x, *A, *ATA, *rhs;
    gdouble qnN;
    guint m, j, k, ATA_size;
    gint n;

    gwy_line_crop(acf_corr, 0, K);
    x = gwy_line_get_data(acf_corr);

    /**
     * Construct the matrix. It is
     *
     * δ_{m,j} - 2n/(N-m) + 2n²/N² (N+m)/(N-m) j + 2n²/N (m-j)/(N-m) χ(j < m)
     **/
    A = g_new(gdouble, K*K);
    n = degree + 1;
    for (m = 0; m < K; m++) {
        for (j = 0; j < K; j++) {
            qnN = 2.0*n/(N-m);
            k = m*K + j;

            /* First two terms (diagonal and first order). */
            A[k] = (m == j) - qnN;
            /* Third term (simple second order). */
            A[k] += qnN*n/N*(N + m)/N*j;
            /* The last term (messy second order). */
            if (j < m)
                A[k] += qnN*n/N*(m - j);
        }
    }

    /* Construct A^T T and A^T b (the rhs for A^T A x = A^T b). It is symmetrical so only construct the lower
     * triangular part. */
    ATA_size = K*(K+1)/2;
    ATA = g_new(gdouble, ATA_size);
    for (m = 0; m < K; m++) {
        for (j = 0; j <= m; j++) {
            gdouble s = 0.0;
            for (k = 0; k < K; k++)
                s += A[m*K + k]*A[k*K + j];
            SLi(ATA, m, j) = s;
        }
    }

    rhs = g_new(gdouble, K);
    matrix_multiply_right(x, A, rhs, K);
    g_free(A);

    gwy_math_choleski_decompose(K, ATA);
    gwy_math_choleski_solve(K, ATA, rhs);
    gwy_assign(x, rhs, K);

    g_free(ATA);
    g_free(rhs);

    return acf_corr;
}

static gdouble
corrected_gaussian_fitfunc(guint i,
                           const gdouble *param,
                           gpointer user_data,
                           gboolean *success)
{
    const CorrectedACFModelData *model = (const CorrectedACFModelData*)user_data;
    gdouble sigma = param[0], T = fabs(param[1]);
    gdouble L = model->N * model->dx, alpha = T/L;
    gdouble tau = i*model->dx, x = tau/T;
    gint n = model->degree + 1;
    gdouble na = n*alpha;
    gdouble G = exp(-x*x);

    *success = (T > 0.0);
    return sigma*sigma*((G + na*na) - GWY_SQRT_PI*na + GWY_SQRT_PI*na*alpha*x*(n*erf(x) - 1)) - model->acf[i];
}

static gdouble
corrected_exponential_fitfunc(guint i,
                              const gdouble *param,
                              gpointer user_data,
                              gboolean *success)
{
    const CorrectedACFModelData *model = (const CorrectedACFModelData*)user_data;
    gdouble sigma = param[0], T = fabs(param[1]);
    gdouble L = model->N * model->dx, alpha = T/L;
    gdouble tau = i*model->dx, x = tau/T;
    gint n = model->degree + 1;
    gdouble na = n*alpha;
    gdouble E = exp(-x);

    *success = (T > 0.0);
    return sigma*sigma*((E + 2*na*na) - 2*na + 2*na*(n-1)*alpha*x) - model->acf[i];
}

static gboolean
fit_acf_with_bias_corrected_model(GwyNLFitIdxFunc func, GwyLine *acf, gint degree, gint N, gint K,
                                  gdouble *sigma, gdouble *T, GwyLine *fit)
{
    GwyNLFitter *fitter;
    CorrectedACFModelData model;
    gdouble param[2], rss;
    gboolean success;
    gdouble *f, *zeros;
    gint i, res;

    g_return_val_if_fail(GWY_IS_LINE(acf), FALSE);
    /* Allow -1 to get uncorrected fits using the same function. */
    g_return_val_if_fail(degree >= -1, FALSE);
    g_return_val_if_fail(N > 0, FALSE);
    g_return_val_if_fail(K > 0, FALSE);
    g_return_val_if_fail(K <= N, FALSE);
    if (!sigma && !T)
        return TRUE;

    model.acf = gwy_line_get_data(acf);
    model.dx = gwy_line_get_dx(acf);
    model.degree = degree;
    model.N = N;

    fitter = gwy_nl_fitter_new_idx(func, NULL);
    param[0] = sqrt(model.acf[0]);
    param[1] = 0.5*K*model.dx;
    rss = gwy_nl_fitter_fit_idx(fitter, K, G_N_ELEMENTS(param), param, &model);
    gwy_nl_fitter_free(fitter);

    if (rss < 0.0)
        return FALSE;

    if (sigma)
        *sigma = fabs(param[0]);
    if (T)
        *T = fabs(param[1]);

    res = gwy_line_get_res(acf);
    model.acf = zeros = g_new0(gdouble, res);
    gwy_line_assign(fit, acf);
    f = gwy_line_get_data(fit);
    for (i = 0; i < res; i++)
        f[i] = func(i, param, &model, &success);

    g_free(zeros);

    return TRUE;
}

static GwyGraphCurveModel*
make_gcmodel(GwyLine *line, const gchar *desc, gint icolor)
{
    GwyGraphCurveModel *gcmodel = gwy_graph_curve_model_new();

    g_object_set(gcmodel,
                 "mode", GWY_GRAPH_CURVE_LINE,
                 "description", desc,
                 "color", gwy_graph_get_preset_color(icolor),
                 NULL);
    gwy_graph_curve_model_set_data_from_line(gcmodel, line, 0, 0);

    return gcmodel;
}

static void
fill_result_if_positive(GwyResults *results, const gchar *name, gdouble value)
{
    if (value >= 0.0)
        gwy_results_fill_values(results, name, value, NULL);
    else
        gwy_results_set_na(results, name, NULL);
}

static gboolean
calculate(GwyToolCorrLen *tool)
{
    static const GwyEnum psdf_fitfunc_names[] = {
        { "Gaussian (PSDF)",    CORRLEN_MODEL_GAUSS, },
        { "Exponential (PSDF)", CORRLEN_MODEL_EXP,   },
    };

    GwyParams *params = tool->params;
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyField *field = plain_tool->field;
    GwyNield *mask = plain_tool->mask_field;
    GwyMaskingType masking = gwy_params_get_masking(params, PARAM_MASKING, &mask);
    GwyOrientation orientation = gwy_params_get_enum(params, PARAM_ORIENTATION);
    CorrlenModelType model = gwy_params_get_enum(params, PARAM_MODEL);
    gint level = gwy_params_get_int(params, PARAM_LEVEL);
    GwyField *field_to_use = NULL;
    GwyNield *mask_to_use = NULL;
    GwyGraphModel *gmodel;
    GwyGraphCurveModel *gcmodel;
    GwyResults *results = tool->results;
    GwyLine *psdf, *acf, *fit, *acf_corr;
    gdouble L, T_estim, acf_1e, acf_0, T_corr, sigma, sigma_corr;
    gdouble T_acf, sigma_acf, T_psdf, sigma_psdf;
    gint col, row, w, h, fxres, fyres, ncut, acfres, K;
    const gchar *model_name;
    gboolean ok;

    gwy_param_table_set_sensitive(tool->table_results, PARAM_REPORT_STYLE, FALSE);
    gwy_assign(tool->isel_prev, tool->isel, 4);
    col = tool->isel[0];
    row = tool->isel[1];
    w = tool->isel[2]+1 - tool->isel[0];
    h = tool->isel[3]+1 - tool->isel[1];
    gwy_debug("%d x %d at (%d, %d)", w, h, col, row);

    if (w < 4 || h < 4)
        return FALSE;

    gdouble xoff = gwy_field_get_xoffset(field), yoff = gwy_field_get_yoffset(field);

    if (orientation == GWY_ORIENTATION_VERTICAL) {
        field_to_use = gwy_field_new(1, 1, 1, 1, FALSE);
        gwy_field_area_transpose(field, col, row, w, h, field_to_use, FALSE);
        if (mask && masking != GWY_MASK_IGNORE) {
            mask_to_use = gwy_nield_new(1, 1);
            gwy_nield_area_transpose(mask, col, row, w, h, mask_to_use, FALSE);
        }
    }
    else {
        field_to_use = gwy_field_area_extract(field, col, row, w, h);
        if (mask && masking != GWY_MASK_IGNORE)
            mask_to_use = gwy_nield_area_extract(mask, col, row, w, h);
    }
    fxres = gwy_field_get_xres(field_to_use);
    fyres = gwy_field_get_yres(field_to_use);
    L = w*gwy_field_get_dx(field_to_use);

    /* level=0 means really do nothing with individual lines */
    if (level)
        gwy_field_row_level_poly(field_to_use, mask_to_use, masking, level-1, NULL);
    gwy_field_add(field_to_use, -gwy_field_mean(field));

    /************************************************************************
     *
     * ACF-based calculations.
     *
     ************************************************************************/
    acf = gwy_field_area_row_acf(field_to_use, mask_to_use, masking, 0, 0, fxres, fyres, 0, NULL);
    sigma = sqrt(gwy_line_get_val(acf, 0));
    acfres = gwy_line_get_res(acf);
    acf_1e = find_decay_point(acf, exp(-1.0));
    acf_0 = find_decay_point(acf, 0.0);
    if (acf_0 < 0.0)
        acf_0 = gwy_line_get_real(acf);
    ncut = 5*gwy_line_rtoi(acf, acf_0);
    ncut = MIN(acfres, ncut);
    K = gwy_line_rtoi(acf, 1.15*acf_0);
    K = MIN(acfres, K);

    fit = gwy_line_new_alike(acf, FALSE);
    ok = FALSE;
    /* Fit models take the degree, which is not our ‘level’ parameter! */
    if (model == CORRLEN_MODEL_GAUSS) {
        ok = fit_acf_with_bias_corrected_model(corrected_gaussian_fitfunc, acf, level-1, acfres, K,
                                               &sigma_acf, &T_acf, fit);
    }
    else if (model == CORRLEN_MODEL_EXP) {
        ok = fit_acf_with_bias_corrected_model(corrected_exponential_fitfunc, acf, level-1, acfres, K,
                                               &sigma_acf, &T_acf, fit);
    }

    if (!ok) {
        sigma_acf = T_acf = -1.0;
    }

    /* acf_selfcorrect() takes the degree, which is not our ‘level’ parameter! */
    acf_corr = acf_selfcorrect(acf, level-1, acfres, K);
    sigma_corr = sqrt(gwy_line_get_val(acf_corr, 0));
    gwy_line_crop(acf, 0, ncut);
    gwy_line_crop(fit, 0, ncut);
    acf_extrapolate_correction(acf, acf_corr);

    T_corr = find_decay_point(acf_corr, exp(-1.0));

    gmodel = tool->gmodel_acf;
    gwy_graph_model_remove_all_curves(gmodel);
    g_object_set(gmodel,
                 "axis-label-left", "G",
                 "axis-label-bottom", "τ",
                 NULL);

    gcmodel = make_gcmodel(acf, _("ACF"), 0);
    gwy_graph_model_add_curve(gmodel, gcmodel);
    g_object_unref(gcmodel);

    gcmodel = make_gcmodel(acf_corr, _("Corrected ACF"), 3);
    gwy_graph_model_add_curve(gmodel, gcmodel);
    g_object_unref(gcmodel);

    if (T_corr > 0.0) {
        gcmodel = make_gcmodel(fit, _("Biased fit"), 1);
        g_object_set(gcmodel, "line-style", GWY_LINE_DASH, NULL);
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);
    }

    g_object_unref(acf);
    g_object_unref(acf_corr);
    g_object_unref(fit);

    /************************************************************************
     *
     * PSDF-based calculations.
     *
     ************************************************************************/
    model_name = gwy_enum_to_string(model, psdf_fitfunc_names, G_N_ELEMENTS(psdf_fitfunc_names));
    g_assert(model_name && *model_name);
    psdf = gwy_field_area_row_psdf(field_to_use, mask_to_use, masking, 0, 0, fxres, fyres, GWY_WINDOWING_HANN, 0);
    fit = gwy_line_new_alike(psdf, FALSE);
    T_estim = (acf_1e > 0.0 ? acf_1e : 0.05*L);
    fit_psdf(psdf, model_name, T_estim, &sigma_psdf, &T_psdf, fit);

    gmodel = tool->gmodel_psdf;
    gwy_graph_model_remove_all_curves(gmodel);
    g_object_set(gmodel,
                 "axis-label-left", "W<sub>1</sub>",
                 "axis-label-bottom", "k",
                 NULL);

    gcmodel = make_gcmodel(psdf, _("PSDF"), 0);
    gwy_graph_model_add_curve(gmodel, gcmodel);
    if (T_psdf > 0.0) {
        gcmodel = make_gcmodel(fit, _("fit"), 1);
        g_object_set(gcmodel, "line-style", GWY_LINE_DASH, NULL);
        gwy_graph_model_add_curve(gmodel, gcmodel);
    }
    g_object_unref(gcmodel);

    g_object_unref(fit);
    g_object_unref(psdf);

    /************************************************************************
     *
     * Results.
     *
     ************************************************************************/
    gwy_results_fill_format(results, "isel", "w", w, "h", h, "x", col, "y", row, NULL);
    gwy_results_fill_format(results, "realsel",
                            "w", fabs(tool->rsel[2] - tool->rsel[0]),
                            "h", fabs(tool->rsel[3] - tool->rsel[1]),
                            "x", MIN(tool->rsel[0], tool->rsel[2]) + xoff,
                            "y", MIN(tool->rsel[1], tool->rsel[3]) + yoff,
                            NULL);
    gwy_results_fill_values(results,
                            "masking", !!mask,
                            "levelling", level-1,
                            "model", gwy_enum_to_string(model, models, G_N_ELEMENTS(models)),
                            NULL);

    if (acf_1e > 0.0) {
        gwy_results_fill_values(results, "acf_1e", acf_1e, NULL);
        if (T_corr > 0.0) {
            gwy_results_fill_values(results,
                                    "acf_1e_corr", T_corr,
                                    "alpha", T_corr/L,
                                    "L_T", L/T_corr,
                                    NULL);
        }
    }
    else
        gwy_results_set_na(results, "acf_1e", "acf_1e_corr", "alpha", "L_T", NULL);

    gwy_results_fill_values(results, "sigma", sigma, NULL);
    fill_result_if_positive(results, "acf_0", acf_0);
    fill_result_if_positive(results, "T_psdf", T_psdf);
    fill_result_if_positive(results, "sigma_psdf", sigma_psdf);
    fill_result_if_positive(results, "T_acf", T_acf);
    fill_result_if_positive(results, "sigma_acf", sigma_acf);
    fill_result_if_positive(results, "sigma_corr", sigma_corr);

    gwy_results_fill_filename(results, "file", GWY_FILE(plain_tool->container));
    gwy_results_fill_data_name(results, "image", GWY_FILE(plain_tool->container), GWY_FILE_IMAGE, plain_tool->id);

    gwy_param_table_set_sensitive(tool->table_results, PARAM_REPORT_STYLE, TRUE);

    g_clear_object(&mask_to_use);
    g_object_unref(field_to_use);

    update_target_graphs(tool);

    return TRUE;
}

static GwyResults*
create_results(void)
{
    GwyResults *results = gwy_results_new();

    /* TODO: Add informational fields like model type and line leveling type. */

    gwy_results_add_header(results, N_("Correlation Length"));
    gwy_results_add_value_str(results, "file", N_("File"));
    gwy_results_add_value_str(results, "image", N_("Image"));
    gwy_results_add_format(results, "isel", N_("Selected area"), TRUE,
                           N_("%{w}i × %{h}i at (%{x}i, %{y}i)"),
                           "unit-str", N_("px"), "translate-unit", TRUE,
                           NULL);
    gwy_results_add_format(results, "realsel", "", TRUE,
                           N_("%{w}v × %{h}v at (%{x}v, %{y}v)"),
                           "power-x", 1,
                           NULL);
    gwy_results_add_value_yesno(results, "masking", N_("Mask in use"));
    gwy_results_add_value_int(results, "levelling", N_("Line leveling"));
    gwy_results_add_value_str(results, "model", N_("ACF model"));
    gwy_results_add_separator(results);

    gwy_results_add_header(results, N_("Correlation Length T"));
    gwy_results_add_value_x(results, "acf_1e", N_("Raw ACF decay to 1/e"));
    gwy_results_add_value_x(results, "acf_1e_corr", N_("Corrected ACF decay to 1/e"));
    gwy_results_add_value_x(results, "acf_0", N_("ACF zero crossing"));
    gwy_results_add_value_z(results, "T_acf", N_("ACF fit with bias"));
    gwy_results_add_value_x(results, "T_psdf", N_("PSDF fit"));
    gwy_results_bind_formats(results, "acf_1e", "acf_1e_corr", "acf_0", "T_acf", "T_psdf", NULL);
    gwy_results_add_separator(results);

    gwy_results_add_header(results, N_("Relation to Image Size"));
    gwy_results_add_value_plain(results, "alpha", N_("Ratio α = T/L"));
    gwy_results_add_value_plain(results, "L_T", N_("Image size measured in T"));
    gwy_results_add_separator(results);

    gwy_results_add_header(results, N_("Mean Square Roughness"));
    gwy_results_add_value_z(results, "sigma", N_("Raw ACF at zero"));
    gwy_results_add_value_z(results, "sigma_corr", N_("Corrected ACF at zero"));
    gwy_results_add_value_z(results, "sigma_acf", N_("ACF fit with bias"));
    gwy_results_add_value_z(results, "sigma_psdf", N_("PSDF fit"));
    gwy_results_bind_formats(results, "sigma", "sigma_corr", "sigma_psdf", "sigma_acf", NULL);

    return results;
}

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