/*
 *  $Id: sfunctions.c 30029 2026-06-09 13:46:07Z 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 <gtk/gtk.h>
#include <gwy.h>

enum {
    PARAM_OUTPUT_TYPE,
    PARAM_MASKING,
    PARAM_ORIENTATION,
    PARAM_WINDOWING,
    PARAM_RESOLUTION,
    PARAM_FIXERS,
    PARAM_INSTANT_UPDATE,
    PARAM_TARGET_GRAPH,
    PARAM_HOLD_SELECTION,
    PARAM_OPTIONS_VISIBLE,
};

typedef enum {
    GWY_SF_HEIGHT_DIST            = 0,
    GWY_SF_HEIGHT_CDIST           = 1,
    GWY_SF_ANGLE_DIST             = 2,
    GWY_SF_ANGLE_CDIST            = 3,
    GWY_SF_ACF                    = 4,
    GWY_SF_HHCF                   = 5,
    GWY_SF_PSDF                   = 6,
    GWY_SF_MINKOWSKI_VOLUME       = 7,
    GWY_SF_MINKOWSKI_BOUNDARY     = 8,
    GWY_SF_MINKOWSKI_CONNECTIVITY = 9,
    GWY_SF_RPSDF                  = 10,
    GWY_SF_RACF                   = 11,
    GWY_SF_RANGE                  = 12,
    GWY_SF_ASG                    = 13,
    GWY_SF_ANGSPEC                = 14,
    GWY_SF_IDF                    = 15,
    GWY_SF_RIDF                   = 16,
    GWY_SF_IPSDF                  = 17,
    GWY_SF_NFUNCTIONS
} GwySFOutputType;

#define GWY_TYPE_TOOL_SFUNCTIONS            (gwy_tool_sfunctions_get_type())
#define GWY_TOOL_SFUNCTIONS(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_SFUNCTIONS, GwyToolSFunctions))
#define GWY_IS_TOOL_SFUNCTIONS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_SFUNCTIONS))
#define GWY_TOOL_SFUNCTIONS_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_SFUNCTIONS, GwyToolSFunctionsClass))

typedef struct _GwyToolSFunctions      GwyToolSFunctions;
typedef struct _GwyToolSFunctionsClass GwyToolSFunctionsClass;

struct _GwyToolSFunctions {
    GwyPlainTool parent_instance;

    GwyParams *params;

    GwyRectSelectionLabels *rlabels;

    GwyLine *line;
    gint isel[4];
    gint isel_prev[4];

    GwyField *cached_flipped_field;
    GwyNield *cached_flipped_mask; /* Always INCLUDE, possibly flipped. */

    GwyGraphModel *gmodel;

    GwyParamTable *table_quantity;
    GwyParamTable *table_options;
    GtkWidget *update;
};

struct _GwyToolSFunctionsClass {
    GwyPlainToolClass parent_class;
};

static gboolean     module_register                      (void);
static GwyParamDef* define_module_params                 (void);
static GType        gwy_tool_sfunctions_get_type         (void)                       G_GNUC_CONST;
static void         gwy_tool_sfunctions_finalize         (GObject *object);
static void         gwy_tool_sfunctions_init_dialog      (GwyToolSFunctions *tool);
static void         gwy_tool_sfunctions_data_switched    (GwyTool *gwytool,
                                                          GwyDataView *data_view);
static void         gwy_tool_sfunctions_response         (GwyTool *tool,
                                                          gint response_id);
static void         gwy_tool_sfunctions_data_changed     (GwyPlainTool *plain_tool);
static void         gwy_tool_sfunctions_mask_changed     (GwyPlainTool *plain_tool);
static void         gwy_tool_sfunctions_selection_changed(GwyPlainTool *plain_tool,
                                                          gint hint);
static void         update_selected_rectangle            (GwyToolSFunctions *tool);
static void         update_sensitivity                   (GwyToolSFunctions *tool);
static void         update_curve                         (GwyToolSFunctions *tool);
static void         param_changed                        (GwyToolSFunctions *tool,
                                                          gint id);
static void         update_target_graphs                 (GwyToolSFunctions *tool);
static void         gwy_tool_sfunctions_apply            (GwyToolSFunctions *tool);
static void         make_angular_spectrum                (GwyField *field,
                                                          GwyNield *mask,
                                                          GwyMaskingType masking,
                                                          gint col,
                                                          gint row,
                                                          gint w,
                                                          gint h,
                                                          gint lineres,
                                                          GwyWindowingType windowing,
                                                          gint level,
                                                          GwyLine *target);
static void         gwy_field_area_range                 (GwyField *dfield,
                                                          GwyLine *dline,
                                                          gint col,
                                                          gint row,
                                                          gint width,
                                                          gint height,
                                                          GwyOrientation direction,
                                                          gint lineres);
static gboolean     sfunction_supports_masking           (GwySFOutputType type);
static gboolean     sfunction_makes_new_line             (GwySFOutputType type);
static gboolean     sfunction_has_native_sampling        (GwySFOutputType type);
static gboolean     sfunction_has_direction              (GwySFOutputType type);
static gboolean     sfunction_is_only_row_wise           (GwySFOutputType type);
static gboolean     sfunction_has_windowing              (GwySFOutputType type);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Statistical function tool, calculates one-dimensional statistical functions (height distribution, "
       "correlations, PSDF, Minkowski functionals) of selected part of data."),
    "Petr Klapetek <klapetek@gwyddion.net>",
    "4.0",
    "David Nečas (Yeti) & Petr Klapetek",
    "2004",
};

static const GwyEnum quantities[] =  {
    { N_("Height distribution"),         GWY_SF_HEIGHT_DIST,                     },
    { N_("Cum. height distribution"),    GWY_SF_HEIGHT_CDIST,                    },
    { N_("Distribution of angles"),      GWY_SF_ANGLE_DIST,                     },
    { N_("Cum. distribution of angles"), GWY_SF_ANGLE_CDIST,                    },
    { N_("ACF"),                         GWY_SF_ACF,                    },
    { N_("HHCF"),                        GWY_SF_HHCF,                   },
    { N_("PSDF"),                        GWY_SF_PSDF,                   },
    { N_("Radial PSDF"),                 GWY_SF_RPSDF,                  },
    { N_("Agnuarly averaged PSDF"),      GWY_SF_IPSDF,                  },
    { N_("Angular spectrum"),            GWY_SF_ANGSPEC,                },
    { N_("Radial ACF"),                  GWY_SF_RACF,                   },
    { N_("Minkowski volume"),            GWY_SF_MINKOWSKI_VOLUME,       },
    { N_("Minkowski boundary"),          GWY_SF_MINKOWSKI_BOUNDARY,     },
    { N_("Minkowski connectivity"),      GWY_SF_MINKOWSKI_CONNECTIVITY, },
    { N_("Range"),                       GWY_SF_RANGE,                  },
    { N_("Area scale graph"),            GWY_SF_ASG,                    },
    { N_("IDF"),                         GWY_SF_IDF,                    },
    { N_("Radial IDF"),                  GWY_SF_RIDF,                   },
};

GWY_MODULE_QUERY2(module_info, sfunctions)

G_DEFINE_TYPE(GwyToolSFunctions, gwy_tool_sfunctions, GWY_TYPE_PLAIN_TOOL)

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

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "sfunctions");
    gwy_param_def_add_gwyenum(paramdef, PARAM_OUTPUT_TYPE, "output_type", _("_Quantity"),
                              quantities, G_N_ELEMENTS(quantities), GWY_SF_HEIGHT_DIST);
    gwy_param_def_add_enum(paramdef, PARAM_MASKING, "masking", NULL, GWY_TYPE_MASKING_TYPE, GWY_MASK_IGNORE);
    gwy_param_def_add_enum(paramdef, PARAM_ORIENTATION, "orientation", NULL, GWY_TYPE_ORIENTATION,
                           GWY_ORIENTATION_HORIZONTAL);
    gwy_param_def_add_enum(paramdef, PARAM_WINDOWING, "windowing", NULL, GWY_TYPE_WINDOWING_TYPE,
                           GWY_WINDOWING_BLACKMANN);
    gwy_param_def_add_int(paramdef, PARAM_RESOLUTION, "resolution", _("_Fixed resolution"), 4, 16384, 120);
    gwy_param_def_add_boolean(paramdef, PARAM_FIXERS, "fixres", _("_Fixed resolution"), FALSE);
    gwy_param_def_add_instant_updates(paramdef, PARAM_INSTANT_UPDATE, "instant_update", NULL, TRUE);
    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_sfunctions_class_init(GwyToolSFunctionsClass *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_sfunctions_finalize;

    tool_class->icon_name = GWY_ICON_GRAPH_HALFGAUSS;
    tool_class->title = _("Statistical Functions");
    tool_class->tooltip = _("Calculate 1D statistical functions");
    tool_class->prefix = "/module/sfunctions";
    tool_class->default_width = 640;
    tool_class->default_height = 400;
    tool_class->data_switched = gwy_tool_sfunctions_data_switched;
    tool_class->response = gwy_tool_sfunctions_response;

    ptool_class->data_changed = gwy_tool_sfunctions_data_changed;
    ptool_class->mask_changed = gwy_tool_sfunctions_mask_changed;
    ptool_class->selection_changed = gwy_tool_sfunctions_selection_changed;
}

static void
gwy_tool_sfunctions_finalize(GObject *object)
{
    GwyToolSFunctions *tool = GWY_TOOL_SFUNCTIONS(object);

    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);
    g_clear_object(&tool->line);
    g_clear_object(&tool->gmodel);
    g_clear_object(&tool->cached_flipped_field);
    g_clear_object(&tool->cached_flipped_mask);

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

static void
gwy_tool_sfunctions_init(GwyToolSFunctions *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

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

    tool->params = gwy_params_new_from_settings(define_module_params());
    tool->line = gwy_line_new(4, 1.0, FALSE);

    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_sfunctions_init_dialog(tool);
}

static void
gwy_tool_sfunctions_rect_updated(GwyToolSFunctions *tool)
{
    GwyPlainTool *plain_tool;

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

static void
gwy_tool_sfunctions_init_dialog(GwyToolSFunctions *tool)
{
    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GtkWidget *hbox, *vbox, *options, *graph;
    GwyParamTable *table;

    tool->gmodel = gwy_graph_model_new();

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

    /* Left pane */
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
    gtk_widget_set_hexpand(vbox, FALSE);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, TRUE, 0);

    /* Selection info */
    tool->rlabels = gwy_rect_selection_labels_new(TRUE, G_CALLBACK(gwy_tool_sfunctions_rect_updated), tool);
    gtk_box_pack_start(GTK_BOX(vbox), gwy_rect_selection_labels_get_table(tool->rlabels), FALSE, FALSE, 0);

    /* Output type */
    table = tool->table_quantity = gwy_param_table_new(tool->params);
    gwy_param_table_append_combo(table, PARAM_OUTPUT_TYPE);
    gwy_plain_tool_add_param_table(GWY_PLAIN_TOOL(tool), table);
    gtk_box_pack_start(GTK_BOX(vbox), gwy_param_table_widget(table), 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_checkbox(table, PARAM_INSTANT_UPDATE);
    gwy_param_table_append_slider(table, PARAM_RESOLUTION);
    gwy_param_table_add_enabler(table, PARAM_FIXERS, PARAM_RESOLUTION);
    gwy_param_table_append_combo(table, PARAM_ORIENTATION);
    gwy_param_table_append_combo(table, PARAM_MASKING);
    gwy_param_table_append_combo(table, PARAM_WINDOWING);
    gwy_param_table_append_target_graph(table, PARAM_TARGET_GRAPH, tool->gmodel);
    gwy_param_table_append_hold_selection(table, PARAM_HOLD_SELECTION);
    gwy_plain_tool_add_param_table(GWY_PLAIN_TOOL(tool), table);
    gtk_container_add(GTK_CONTAINER(options), gwy_param_table_widget(table));

    /* Graph */
    graph = gwy_graph_new(tool->gmodel);
    gwy_graph_enable_user_input(GWY_GRAPH(graph), FALSE);
    gtk_box_pack_start(GTK_BOX(hbox), graph, TRUE, TRUE, 2);

    tool->update = gwy_stock_add_button_to_dialog(dialog, GWY_STOCK_UPDATE);
    gwy_plain_tool_add_clear_button(GWY_PLAIN_TOOL(tool));
    gwy_tool_add_hide_button(GWY_TOOL(tool), FALSE);
    gwy_plain_tool_add_apply_button(GWY_PLAIN_TOOL(tool));
    gtk_dialog_set_default_response(dialog, GTK_RESPONSE_APPLY);
    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_quantity, "param-changed", G_CALLBACK(param_changed), tool);
    g_signal_connect_swapped(tool->table_options, "param-changed", G_CALLBACK(param_changed), tool);

    gtk_widget_show_all(gtk_dialog_get_content_area(dialog));
}

static void
gwy_tool_sfunctions_data_switched(GwyTool *gwytool, GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolSFunctions *tool = GWY_TOOL_SFUNCTIONS(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);

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

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

    g_clear_object(&tool->cached_flipped_field);
    g_clear_object(&tool->cached_flipped_mask);

    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_curve(tool);
    update_target_graphs(tool);
}

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

    if (response_id == GTK_RESPONSE_APPLY)
        gwy_tool_sfunctions_apply(GWY_TOOL_SFUNCTIONS(tool));
    else if (response_id == GWY_RESPONSE_UPDATE)
        update_curve(GWY_TOOL_SFUNCTIONS(tool));
}

static void
update_selected_rectangle(GwyToolSFunctions *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
gwy_tool_sfunctions_data_changed(GwyPlainTool *plain_tool)
{
    GwyToolSFunctions *tool = GWY_TOOL_SFUNCTIONS(plain_tool);

    g_clear_object(&tool->cached_flipped_field);
    update_selected_rectangle(tool);
    update_curve(tool);
    update_target_graphs(tool);
}

static void
gwy_tool_sfunctions_mask_changed(GwyPlainTool *plain_tool)
{
    GwyToolSFunctions *tool = GWY_TOOL_SFUNCTIONS(plain_tool);

    g_clear_object(&tool->cached_flipped_mask);
    if (sfunction_supports_masking(gwy_params_get_enum(tool->params, PARAM_OUTPUT_TYPE)))
        update_curve(tool);
}

static void
gwy_tool_sfunctions_selection_changed(GwyPlainTool *plain_tool, gint hint)
{
    GwyToolSFunctions *tool = GWY_TOOL_SFUNCTIONS(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_curve(tool);
    }
}

static void
update_sensitivity(GwyToolSFunctions *tool)
{
    GwyParams *params = tool->params;
    GwyParamTable *table = tool->table_options;
    GwySFOutputType output_type = gwy_params_get_enum(params, PARAM_OUTPUT_TYPE);

    gtk_widget_set_sensitive(tool->update, !gwy_params_get_boolean(params, PARAM_INSTANT_UPDATE));
    gwy_param_table_set_sensitive(table, PARAM_RESOLUTION, !sfunction_has_native_sampling(output_type));
    gwy_param_table_set_sensitive(table, PARAM_ORIENTATION, sfunction_has_direction(output_type));
    gwy_param_table_set_sensitive(table, PARAM_WINDOWING, sfunction_has_windowing(output_type));
    gwy_param_table_set_sensitive(table, PARAM_MASKING, sfunction_supports_masking(output_type));
}

static void
update_curve(GwyToolSFunctions *tool)
{
    static const gchar* xlabels[GWY_SF_NFUNCTIONS] = {
        "z", "z", "tan β", "tan β", "τ", "τ", "k", "z", "z", "z", "k", "τ", "τ", "τ", "α", "τ", "τ", "k",
    };
    static const gchar* ylabels[GWY_SF_NFUNCTIONS] = {
        "ρ", "D", "ρ", "D", "G", "H", "W<sub>1</sub>", "V", "S", "χ", "W<sub>r</sub>", "G<sub>r</sub>",
        "R", "A<sub>excess</sub>", "W<sub>a</sub>", "I", "I<sub>r</sub>", "W<sub>avg</sub>",
    };

    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyParams *params = tool->params;
    GwyField *dfield = plain_tool->field;
    GwyNield *mask = plain_tool->mask_field, *mask_to_use = NULL;
    GwyField *field_to_use, *levelled;
    GwyOrientation orientation = gwy_params_get_enum(params, PARAM_ORIENTATION);
    GwyWindowingType windowing = gwy_params_get_enum(params, PARAM_WINDOWING);
    GwyMaskingType masking = gwy_params_get_masking(params, PARAM_MASKING, &mask);
    GwySFOutputType output_type = gwy_params_get_enum(params, PARAM_OUTPUT_TYPE);
    gboolean fixres = gwy_params_get_boolean(params, PARAM_FIXERS);
    gint lineres = fixres ? gwy_params_get_int(params, PARAM_RESOLUTION) : -1;
    GwyGraphModel *gmodel = tool->gmodel;
    GwyGraphCurveModel *gcmodel;
    gint n, nsel, col, row, w, h;
    const gchar *funcname;
    gchar *title, *dirname;

    if (!dfield) {
        gwy_graph_model_remove_all_curves(gmodel);
        gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY, FALSE);
        return;
    }

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

    gwy_assign(tool->isel_prev, tool->isel, 4);
    n = gwy_graph_model_get_n_curves(gmodel);
    col = tool->isel[0];
    row = tool->isel[1];
    w = tool->isel[2]+1 - tool->isel[0];
    h = tool->isel[3]+1 - tool->isel[1];
    nsel = (dfield && w >= 4 && h >= 4) ? 1 : 0;
    gwy_debug("%d x %d at (%d, %d)", w, h, col, row);

    gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY, nsel > 0);

    if (nsel == 0 && n == 0)
        return;

    if (nsel == 0 && n > 0) {
        gwy_graph_model_remove_all_curves(gmodel);
        return;
    }

    /* Create transformed/inverted mask as necesary and remember it.  Keep mask_to_use as NULL if we do not want
     * masking.  */
    if (sfunction_supports_masking(output_type) && mask) {
        if (!tool->cached_flipped_mask) {
            if (sfunction_is_only_row_wise(output_type) && orientation == GWY_ORIENTATION_VERTICAL) {
                tool->cached_flipped_mask = gwy_nield_new_alike(mask);
                gwy_nield_transpose(mask, tool->cached_flipped_mask, FALSE);
            }
            else
                tool->cached_flipped_mask = g_object_ref(mask);
        }
        mask_to_use = tool->cached_flipped_mask;
    }

    field_to_use = dfield;
    if (sfunction_is_only_row_wise(output_type) && orientation == GWY_ORIENTATION_VERTICAL) {
        if (!tool->cached_flipped_field) {
            tool->cached_flipped_field = gwy_field_new_alike(dfield, FALSE);
            gwy_field_transpose(dfield, tool->cached_flipped_field, FALSE);
        }
        field_to_use = tool->cached_flipped_field;
        GWY_SWAP(gint, col, row);
        GWY_SWAP(gint, w, h);
    }

    if (sfunction_makes_new_line(output_type))
        g_clear_object(&tool->line);

    if (output_type == GWY_SF_HEIGHT_DIST || output_type == GWY_SF_HEIGHT_CDIST) {
        gwy_field_area_height_dist(field_to_use, mask_to_use, masking, tool->line, col, row, w, h, lineres);
        if (output_type == GWY_SF_HEIGHT_CDIST)
            gwy_line_cumulate(tool->line, TRUE);
    }
    else if (output_type == GWY_SF_ANGLE_DIST || output_type == GWY_SF_ANGLE_CDIST) {
        gwy_field_area_angle_dist(field_to_use, mask_to_use, masking, tool->line, col, row, w, h, orientation, lineres);
        if (output_type == GWY_SF_ANGLE_CDIST)
            gwy_line_cumulate(tool->line, TRUE);
    }
    else if (output_type == GWY_SF_ACF) {
        tool->line = gwy_field_area_row_acf(field_to_use, mask_to_use, masking, col, row, w, h, 1, NULL);
    }
    else if (output_type == GWY_SF_HHCF) {
        tool->line = gwy_field_area_row_hhcf(field_to_use, mask_to_use, masking, col, row, w, h, 1, NULL);
    }
    else if (output_type == GWY_SF_PSDF) {
        tool->line = gwy_field_area_row_psdf(field_to_use, mask_to_use, masking, col, row, w, h, windowing, 1);
    }
    else if (output_type == GWY_SF_MINKOWSKI_VOLUME
             || output_type == GWY_SF_MINKOWSKI_BOUNDARY
             || output_type == GWY_SF_MINKOWSKI_CONNECTIVITY) {
        GwyMinkowskiFunctional functype;
        if (output_type == GWY_SF_MINKOWSKI_VOLUME)
            functype = GWY_MINKOWSKI_VOLUME;
        else if (output_type == GWY_SF_MINKOWSKI_BOUNDARY)
            functype = GWY_MINKOWSKI_BOUNDARY;
        else if (output_type == GWY_SF_MINKOWSKI_CONNECTIVITY)
            functype = GWY_MINKOWSKI_CONNECTIVITY;
        tool->line = gwy_field_area_minkowski(field_to_use, mask_to_use, masking, col, row, w, h, functype, lineres,
                                              G_MAXDOUBLE, -G_MAXDOUBLE);
    }
    else if (output_type == GWY_SF_RPSDF)
        gwy_field_area_rpsdf(field_to_use, tool->line, col, row, w, h, windowing, lineres);
    else if (output_type == GWY_SF_IPSDF)
        gwy_field_area_ipsdf(field_to_use, tool->line, col, row, w, h, windowing, lineres);
    else if (output_type == GWY_SF_ANGSPEC)
        make_angular_spectrum(field_to_use, mask_to_use, masking, col, row, w, h, lineres, windowing, 1, tool->line);
    else if (output_type == GWY_SF_RACF) {
        levelled = gwy_field_area_extract(field_to_use, col, row, w, h);
        gwy_field_add(levelled, -gwy_field_mean(levelled));
        gwy_field_racf(levelled, tool->line, lineres);
        g_object_unref(levelled);
    }
    else if (output_type == GWY_SF_RANGE)
        gwy_field_area_range(field_to_use, tool->line, col, row, w, h, orientation, lineres);
    else if (output_type == GWY_SF_ASG) {
        tool->line = gwy_field_area_row_asg(field_to_use, mask_to_use, masking, col, row, w, h, 1);
    }
    else if (output_type == GWY_SF_IDF) {
        GwyLine *tmp = gwy_field_area_row_hhcf(field_to_use, mask_to_use, masking, col, row, w, h, 1, NULL);
        gwy_line_filter_slope(tmp, tool->line);
        gwy_line_assign(tmp, tool->line);
        gwy_line_filter_slope(tmp, tool->line);
        g_object_unref(tmp);
    }
    else if (output_type == GWY_SF_RIDF) {
        GwyLine *tmp = gwy_line_new(1, 1.0, FALSE);
        gwy_field_area_racf(field_to_use, tmp, col, row, w, h, lineres);
        gwy_line_filter_slope(tmp, tool->line);
        gwy_line_assign(tmp, tool->line);
        gwy_line_filter_slope(tmp, tool->line);
        g_object_unref(tmp);
    }
    else {
        g_return_if_reached();
    }

    if (nsel > 0 && n == 0) {
        gcmodel = gwy_graph_curve_model_new();
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_set(gcmodel, "mode", GWY_GRAPH_CURVE_LINE, NULL);
        g_object_unref(gcmodel);
    }
    else {
        gcmodel = gwy_graph_model_get_curve(gmodel, 0);
        if (gwy_graph_model_get_n_curves(gmodel) > 1)
            gwy_graph_model_remove_curve(gmodel, 1);
    }

    gwy_graph_curve_model_set_data_from_line(gcmodel, tool->line, 0, 0);
    funcname = _(gwy_enum_to_string(output_type, quantities, G_N_ELEMENTS(quantities)));
    if (sfunction_has_direction(output_type)) {
        /* This is not completely language agnostic, but probably good enough for all LTR. */
        dirname = g_utf8_strdown(gwy_enum_to_string(orientation, gwy_orientation_get_enum(), -1), -1);
        title = g_strdup_printf("%s (%s)", funcname, gwy_C(dirname));
        g_free(dirname);
    }
    else
        title = g_strdup(funcname);
    g_object_set(gcmodel, "description", title, NULL);

    g_object_set(gmodel,
                 "title", title,
                 "axis-label-bottom", xlabels[output_type],
                 "axis-label-left", ylabels[output_type],
                 NULL);
    g_free(title);

    gwy_graph_model_set_units_from_line(gmodel, tool->line);
    update_target_graphs(tool);
}

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

    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 (do_update)
            g_clear_object(&tool->cached_flipped_mask);
    }
    if (id < 0 || id == PARAM_ORIENTATION)
        g_clear_object(&tool->cached_flipped_mask);

    if (id < 0 || id == PARAM_INSTANT_UPDATE || id == PARAM_OUTPUT_TYPE)
        update_sensitivity(tool);
    if (do_update)
        update_curve(tool);
    if (id < 0 || id == PARAM_OUTPUT_TYPE)
        update_target_graphs(tool);
}

static void
update_target_graphs(GwyToolSFunctions *tool)
{
    gwy_param_table_data_id_refilter(tool->table_options, PARAM_TARGET_GRAPH);
}

static void
gwy_tool_sfunctions_apply(GwyToolSFunctions *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyParams *params = tool->params;
    GwyGraphModel *gmodel;

    g_return_if_fail(plain_tool->selection);

    if ((gmodel = gwy_params_get_graph(params, PARAM_TARGET_GRAPH))) {
        gwy_graph_model_append_curves(gmodel, tool->gmodel, 1);
        return;
    }

    gmodel = gwy_graph_model_copy(tool->gmodel);
    gint newid = gwy_file_add_graph(GWY_FILE(plain_tool->container), gmodel);
    gwy_file_set_visible(GWY_FILE(plain_tool->container), GWY_FILE_GRAPH, newid, TRUE);

    g_object_unref(gmodel);
}

static void
make_angular_spectrum(GwyField *field,
                      GwyNield *mask, GwyMaskingType masking,
                      gint col, gint row, gint w, gint h,
                      gint lineres,
                      GwyWindowingType windowing, gint level,
                      GwyLine *target)
{
    GwyField *psdf = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    GwyLine *tmpline;

    gwy_field_area_psdf_2d(field, mask, masking, psdf, col, row, w, h, windowing, level);
    tmpline = gwy_field_psdf_to_angular_spectrum(psdf, lineres);
    g_object_unref(psdf);
    gwy_line_assign(target, tmpline);
    g_object_unref(tmpline);

    /* Transform to degrees. */
    gwy_line_multiply(target, G_PI/180.0);
    gwy_line_set_real(target, 360.0);
    gwy_line_set_offset(target, -180.0/gwy_line_get_res(target));
    gwy_unit_set_from_string(gwy_line_get_unit_x(target), "deg");
}

static void
gwy_line_range_transform(GwyLine *dline, GwyLine *target,
                         gdouble *mindata, gdouble *maxdata)
{
    gint res = dline->res, tres = target->res;
    gint i, j;

    g_return_if_fail(tres < res);
    gwy_assign(mindata, gwy_line_get_data_const(dline), res);
    gwy_assign(maxdata, gwy_line_get_data_const(dline), res);

    gdouble *tdata = gwy_line_get_data(target);
    for (i = 1; i < tres; i++) {
        gdouble r = 0.0;
        for (j = 0; j < res-i; j++) {
            if (mindata[j+1] < mindata[j])
                mindata[j] = mindata[j+1];
            if (maxdata[j+1] > maxdata[j])
                maxdata[j] = maxdata[j+1];
            r += maxdata[j] - mindata[j];
        }
        tdata[i] += r/(res - i);
    }
}

static void
gwy_field_area_range(GwyField *dfield,
                     GwyLine *dline,
                     gint col, gint row, gint width, gint height,
                     GwyOrientation direction,
                     gint lineres)
{
    GwyLine *buf = gwy_line_new(1, 1.0, FALSE);
    gint res, thickness, i;
    gdouble *mindata, *maxdata;
    gdouble h;

    gwy_field_copy_units_to_line(dfield, dline);
    if (direction == GWY_ORIENTATION_HORIZONTAL) {
        res = width-1;
        thickness = height;
        h = gwy_field_get_dx(dfield);
    }
    else if (direction == GWY_ORIENTATION_VERTICAL) {
        res = height-1;
        thickness = width;
        h = gwy_field_get_dy(dfield);
    }
    else {
        g_return_if_reached();
    }

    mindata = g_new(gdouble, res+1);
    maxdata = g_new(gdouble, res+1);
    if (lineres > 0)
        res = MIN(lineres, res);

    gwy_line_resize(dline, res);
    gwy_line_clear(dline);
    gwy_line_set_offset(dline, 0.0);
    gwy_line_set_real(dline, res*h);
    for (i = 0; i < thickness; i++) {
        if (direction == GWY_ORIENTATION_HORIZONTAL)
            gwy_field_get_row_part(dfield, buf, row+i, col, col+width);
        else
            gwy_field_get_column_part(dfield, buf, col+i, row, row+height);

        gwy_line_range_transform(buf, dline, mindata, maxdata);
    }
    gwy_line_multiply(dline, 1.0/thickness);

    g_free(maxdata);
    g_free(mindata);
    g_object_unref(buf);
}

static gboolean
sfunction_supports_masking(GwySFOutputType type)
{
    return (type == GWY_SF_HEIGHT_DIST || type == GWY_SF_HEIGHT_CDIST
            || type == GWY_SF_ANGLE_DIST || type == GWY_SF_ANGLE_CDIST
            || type == GWY_SF_ACF || type == GWY_SF_HHCF || type == GWY_SF_ASG
            || type == GWY_SF_PSDF || type == GWY_SF_ANGSPEC || type == GWY_SF_IDF
            || type == GWY_SF_MINKOWSKI_VOLUME
            || type == GWY_SF_MINKOWSKI_BOUNDARY
            || type == GWY_SF_MINKOWSKI_CONNECTIVITY);
}

static gboolean
sfunction_makes_new_line(GwySFOutputType type)
{
    return (type == GWY_SF_ACF || type == GWY_SF_HHCF || type == GWY_SF_ASG || type == GWY_SF_PSDF
            || type == GWY_SF_MINKOWSKI_VOLUME
            || type == GWY_SF_MINKOWSKI_BOUNDARY
            || type == GWY_SF_MINKOWSKI_CONNECTIVITY);
}

static gboolean
sfunction_has_native_sampling(GwySFOutputType type)
{
    return (type == GWY_SF_ACF || type == GWY_SF_HHCF || type == GWY_SF_ASG || type == GWY_SF_PSDF
            || type == GWY_SF_RANGE || type == GWY_SF_IDF);
}

static gboolean
sfunction_has_direction(GwySFOutputType type)
{
    return (type == GWY_SF_ANGLE_DIST || type == GWY_SF_ANGLE_CDIST
            || type == GWY_SF_ACF || type == GWY_SF_HHCF
            || type == GWY_SF_ASG || type == GWY_SF_PSDF || type == GWY_SF_IDF);
}

static gboolean
sfunction_is_only_row_wise(GwySFOutputType type)
{
    return (type == GWY_SF_ACF || type == GWY_SF_HHCF || type == GWY_SF_ASG || type == GWY_SF_PSDF
            || type == GWY_SF_IDF);
}

static gboolean
sfunction_has_windowing(GwySFOutputType type)
{
    return (type == GWY_SF_PSDF || type == GWY_SF_RPSDF || type == GWY_SF_IPSDF || type == GWY_SF_ANGSPEC);
}

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