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

#include "config.h"
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"
#include "libgwyapp/sanity.h"

#define RUN_MODES (GWY_RUN_INTERACTIVE)

enum {
    PARAM_ZOOM,
    PARAM_EDIT_MODE,
    PARAM_DISPLAY,
    PARAM_OUTPUT,
    PARAM_SNAP,

    BUTTON_UNDO,
    BUTTON_CLEAR,
    BUTTON_FILL,
};

enum {
    RESPONSE_UNDO = GWY_RESPONSE_USER,
    RESPONSE_FILL,
};

typedef enum {
    UPDATE_NOTHING  = 0,
    UPDATE_ZOOMED   = 1 << 0,
    UPDATE_FILTERED = 1 << 1,
} UpdateWhat;

typedef enum {
    FFT_ELLIPSE_ADD   = 0,
    FFT_RECTANGLE_ADD = 1,
    FFT_ELLIPSE_SUB   = 2,
    FFT_RECTANGLE_SUB = 3,
} EditType;

typedef enum {
    PREVIEW_FFT      = 0,
    PREVIEW_IMAGE    = 1,
    PREVIEW_FILTERED = 2,
    PREVIEW_DIFF     = 3,
} PreviewType;

typedef enum {
    OUTPUT_FFT   = 0,
    OUTPUT_IMAGE = 1,
    OUTPUT_DIFF  = 2,
} OutputType;

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyNield *mask;
    GwyField *result;
    GwyField *diff;
    GwyField *filtered_modulus;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GwyField *modulus;
    GwyNield *prev_mask;
    gboolean has_undo;

    GwyFile *data;
    gint id;

    GtkWidget *dialog;
    GwyParamTable *table;

    gulong rect_signal;
    gulong ellipse_signal;

    GtkWidget *view;
    UpdateWhat update;
} ModuleGUI;

static gboolean         module_register        (void);
static void             module_main            (GwyFile *data,
                                                GwyRunModeFlags mode);
static void             execute                (ModuleArgs *args);
static GwyDialogOutcome run_gui                (ModuleArgs *args,
                                                GwyFile *data,
                                                gint id);
static void             dialog_response        (ModuleGUI *gui,
                                                gint response);
static void             preview                (gpointer user_data);
static void             param_changed          (ModuleGUI *gui,
                                                gint id);
static void             update_sensitivity     (ModuleGUI *gui);
static void             selection_finished     (GwySelection *selection,
                                                ModuleGUI *gui);
static void             update_snap            (ModuleGUI *gui);
static void             switch_layer           (ModuleGUI *gui);
static void             calculate_zoomed_fields(ModuleGUI *gui);
static GwyField*        cut_field_to_zoom      (GwyField *field,
                                                gint zoom);
static GwyNield*        cut_nield_to_zoom      (GwyNield *field,
                                                gint zoom);
static void             calculate_modulus      (GwyField *field,
                                                GwyField *fftmod);
static gint             add_output_field       (GwyFile *data,
                                                GwyField *field,
                                                gint oldid,
                                                const gchar *title);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("2D FFT Filtering"),
    "Chris Anderson <sidewinder.asu@gmail.com>",
    "2.1",
    "Chris Anderson, Molecular Imaging Corp.",
    "2005",
};

GWY_MODULE_QUERY2(module_info, fft_filter_2d)

static gboolean
module_register(void)
{
    gwy_image_func_register("fft_filter_2d",
                            module_main,
                            N_("/_Correct Data/_2D FFT Filtering..."),
                            GWY_ICON_FFT_FILTER_2D,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Two-dimensional FFT filtering"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum displays[] = {
        { N_("_FFT mask editor"),  PREVIEW_FFT,      },
        { N_("Original _image"),   PREVIEW_IMAGE,    },
        { N_("Fi_ltered image"),   PREVIEW_FILTERED, },
        { N_("Image _difference"), PREVIEW_DIFF,     },
    };
    static const GwyEnum outputs[] = {
        { N_("Filtered i_mage"),       1u << OUTPUT_IMAGE, },
        { N_("Ima_ge difference"),     1u << OUTPUT_DIFF,  },
        { N_("Filtered FFT mo_dulus"), 1u << OUTPUT_FFT,   },
    };
    static const GwyEnum modes[] = {
        { N_("Add an ellipse to the FFT mask"),         FFT_ELLIPSE_ADD, },
        { N_("Add a rectangle to the FFT mask"),        FFT_RECTANGLE_ADD,    },
        { N_("Subtract an ellipse from the FFT mask"),  FFT_ELLIPSE_SUB, },
        { N_("Subtract a rectangle from the FFT mask"), FFT_RECTANGLE_SUB,    },
    };
    static GwyEnum zooms[5];
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    for (guint i = 0; i < G_N_ELEMENTS(zooms); i++) {
        zooms[i].value = 1u << i;
        zooms[i].name = g_strdup_printf("%u×", zooms[i].value);
    }

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_image_func_current());
    gwy_param_def_add_gwyenum(paramdef, PARAM_ZOOM, "zoom", _("Zoom"), zooms, G_N_ELEMENTS(zooms), 1);
    gwy_param_def_add_gwyenum(paramdef, PARAM_EDIT_MODE, "edit_mode", _("Edit mode"),
                              modes, G_N_ELEMENTS(modes), FFT_ELLIPSE_ADD);
    gwy_param_def_add_gwyenum(paramdef, PARAM_DISPLAY, NULL, _("Display"),
                              displays, G_N_ELEMENTS(displays), PREVIEW_FFT);
    gwy_param_def_add_gwyflags(paramdef, PARAM_OUTPUT, "output", _("Output"),
                               outputs, G_N_ELEMENTS(outputs), 1 << OUTPUT_IMAGE);
    gwy_param_def_add_boolean(paramdef, PARAM_SNAP, "snap", _("_Snap to origin"), TRUE);
    return paramdef;
}

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

    ModuleArgs args;
    gwy_clear1(args);

    gint id;
    gwy_data_browser_get_current(GWY_APP_FIELD, &args.field,
                                 GWY_APP_FIELD_ID, &id,
                                 0);

    g_return_if_fail(GWY_IS_FIELD(args.field));
    args.result = gwy_field_new_alike(args.field, FALSE);
    args.diff = gwy_field_new_alike(args.field, FALSE);
    args.filtered_modulus = gwy_field_new_alike(args.field, FALSE);
    args.mask = gwy_field_new_nield_alike(args.field);
    args.params = gwy_params_new_from_settings(define_module_params());

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

    OutputType outputs = gwy_params_get_flags(args.params, PARAM_OUTPUT);
    if (outputs & OUTPUT_IMAGE) {
        gint newid = add_output_field(data, args.result, id, _("Filtered Data"));
        gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_RANGE, FALSE);
    }
    if (outputs & OUTPUT_DIFF) {
        gint newid = add_output_field(data, args.diff, id, _("Difference"));
        gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_COLOR_MAPPING, FALSE);
    }
    /* FIXME: Show the mask? */
    if (outputs & OUTPUT_FFT) {
        add_output_field(data, args.filtered_modulus, id, _("Filtered Modulus"));
    }

end:
    g_object_unref(args.params);
    g_clear_object(&args.result);
    g_clear_object(&args.diff);
    g_clear_object(&args.mask);
    g_clear_object(&args.filtered_modulus);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    static const GwyEnum mode_icons[] = {
        { GWY_ICON_MASK_CIRCLE_INCLUSIVE, FFT_ELLIPSE_ADD,   },
        { GWY_ICON_MASK_RECT_INCLUSIVE,   FFT_RECTANGLE_ADD, },
        { GWY_ICON_MASK_CIRCLE_EXCLUSIVE, FFT_ELLIPSE_SUB,   },
        { GWY_ICON_MASK_RECT_EXCLUSIVE,   FFT_RECTANGLE_SUB, },
    };
    static const GwyRGBA mask_color = { 0.56, 0.39, 0.07, 0.5 };

    ModuleGUI gui;
    gwy_clear1(gui);
    gui.args = args;
    gui.update = UPDATE_FILTERED;
    gui.data = data;
    gui.id = id;

    gui.modulus = gwy_field_new_alike(args->field, FALSE);
    calculate_modulus(args->field, gui.modulus);

    gui.dialog = gwy_dialog_new(_("2D FFT Filtering"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    gui.view = gwy_create_preview(args->field, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.view), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_REAL_SQUARE);
    calculate_zoomed_fields(&gui);
    gwy_data_view_set_mask_color(GWY_DATA_VIEW(gui.view), &mask_color);
    GtkWidget *hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(gui.view), FALSE);

    GwyParamTable *table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Edit Filter"));
    gwy_param_table_append_radio_buttons(table, PARAM_EDIT_MODE, mode_icons);
    gwy_param_table_append_checkbox(table, PARAM_SNAP);
    gwy_param_table_append_button(table, BUTTON_UNDO, -1, RESPONSE_UNDO, _("_Undo"));
    gwy_param_table_button_set_icon(table, BUTTON_UNDO, GWY_ICON_GTK_UNDO);
    gwy_param_table_append_button(table, BUTTON_CLEAR, BUTTON_UNDO, GWY_RESPONSE_CLEAR, _("_Clear"));
    gwy_param_table_button_set_icon(table, BUTTON_CLEAR, GWY_ICON_GTK_CLEAR);
    gwy_param_table_append_button(table, BUTTON_FILL, BUTTON_CLEAR, RESPONSE_FILL, _("_Fill"));
    gwy_param_table_button_set_icon(table, BUTTON_FILL, GWY_ICON_MASK);
    gwy_param_table_append_header(table, -1, _("Preview"));
    gwy_param_table_append_radio(table, PARAM_DISPLAY);
    gwy_param_table_append_radio_row(table, PARAM_ZOOM);
    gwy_param_table_append_header(table, -1, _("Output"));
    gwy_param_table_append_checkboxes(table, PARAM_OUTPUT);
    /* FIXME: There used to be a bunch of tooltips. We need param table support.
    gtk_widget_set_tooltip_text(button, _("Undo the last change to the filter mask"));
    gtk_widget_set_tooltip_text(button, _("Clear the entire filter mask"));
    gtk_widget_set_tooltip_text(button, _("Fill the entire filter mask"));
    gtk_widget_set_tooltip_text(button, _("Force shapes to center around the origin"));
    */

    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), FALSE, FALSE, 0);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);
    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(dialog, "response", G_CALLBACK(dialog_response), &gui);

    GwyDialogOutcome outcome = gwy_dialog_run(dialog);

    g_clear_object(&gui.prev_mask);
    g_object_unref(gui.modulus);

    return outcome;
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    if (id < 0 || id == PARAM_DISPLAY)
        update_sensitivity(gui);
    if (id < 0 || id == PARAM_DISPLAY || id == PARAM_EDIT_MODE)
        switch_layer(gui);
    if (id < 0 || id == PARAM_ZOOM)
        gui->update |= UPDATE_ZOOMED;
    if (id < 0 || id == PARAM_SNAP)
        update_snap(gui);

    if (id < 0 || id == PARAM_DISPLAY || id == PARAM_ZOOM)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));

    /* TODO: Disable OK button when there is no output. */
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;
    PreviewType display = gwy_params_get_enum(args->params, PARAM_DISPLAY);

    if (display == PREVIEW_FFT && (gui->update & UPDATE_ZOOMED)) {
        calculate_zoomed_fields(gui);
        gui->update &= ~UPDATE_ZOOMED;
    }

    if ((display == PREVIEW_FILTERED || display == PREVIEW_DIFF) && (gui->update & UPDATE_FILTERED)) {
        execute(args);
        gwy_field_data_changed(args->result);
        gwy_field_data_changed(args->diff);
        gui->update &= ~UPDATE_FILTERED;
    }
}

static void
update_sensitivity(ModuleGUI *gui)
{
    PreviewType display = gwy_params_get_enum(gui->args->params, PARAM_DISPLAY);
    GwyParamTable *table = gui->table;
    gboolean is_editor = (display == PREVIEW_FFT);
    gboolean has_undo = !!gui->prev_mask;

    gwy_param_table_set_sensitive(table, PARAM_ZOOM, is_editor);
    gwy_param_table_set_sensitive(table, PARAM_EDIT_MODE, is_editor);
    gwy_param_table_set_sensitive(table, PARAM_SNAP, is_editor);
    gwy_param_table_set_sensitive(table, BUTTON_UNDO, is_editor && has_undo);
    gwy_param_table_set_sensitive(table, BUTTON_CLEAR, is_editor);
    gwy_param_table_set_sensitive(table, BUTTON_FILL, is_editor);
}

static void
switch_layer(ModuleGUI *gui)
{
    GwyParams *params = gui->args->params;
    PreviewType display = gwy_params_get_enum(params, PARAM_DISPLAY);
    EditType mode = gwy_params_get_enum(params, PARAM_EDIT_MODE);
    GwyDataView *dataview = GWY_DATA_VIEW(gui->view);

    if (display != PREVIEW_FFT) {
        gwy_data_view_set_interactive_layer(dataview, NULL);
        gwy_data_view_set_mask(dataview, NULL);
        if (display == PREVIEW_IMAGE) {
            gwy_setup_data_view(GWY_DATA_VIEW(gui->view), gui->data, GWY_FILE_IMAGE, gui->id,
                                GWY_FILE_ITEM_RANGE);
        }
        else {
            /* Cannot really show the filtered image with original's fixed range. */
            gwy_data_view_set_color_mapping(dataview, GWY_COLOR_MAPPING_FULL);
        }
        gwy_set_data_preview_size(dataview, PREVIEW_SIZE);
        return;
    }

    calculate_zoomed_fields(gui);
    gwy_data_view_set_gradient(dataview, gwy_gradients_get_gradient("DFit"));
    gwy_data_view_set_color_mapping(dataview, GWY_COLOR_MAPPING_AUTO);

    GwyVectorLayer *vec_layer = gwy_data_view_get_interactive_layer(dataview);
    GType layer_type = 0;
    if (mode == FFT_RECTANGLE_ADD || mode == FFT_RECTANGLE_SUB)
        layer_type = GWY_TYPE_LAYER_RECTANGLE;
    else if (mode == FFT_ELLIPSE_ADD || mode == FFT_ELLIPSE_SUB)
        layer_type = GWY_TYPE_LAYER_ELLIPSE;
    else {
        g_return_if_reached();
    }

    if (!vec_layer || !g_type_is_a(G_OBJECT_TYPE(vec_layer), layer_type)) {
        vec_layer = g_object_new(layer_type, NULL);
        gwy_data_view_set_interactive_layer(dataview, vec_layer);
        update_snap(gui);

        GwySelection *selection = gwy_vector_layer_ensure_selection(vec_layer);
        g_signal_connect(selection, "finished", G_CALLBACK(selection_finished), gui);
    }
}

static void
update_snap(ModuleGUI *gui)
{
    GwyVectorLayer *vec_layer = gwy_data_view_get_interactive_layer(GWY_DATA_VIEW(gui->view));
    if (vec_layer) {
        gboolean snap = gwy_params_get_boolean(gui->args->params, PARAM_SNAP);
        g_object_set(vec_layer,
                     "snap-to-center", snap,
                     "draw-reflection", !snap,
                     NULL);
    }
}

static void
record_undo(ModuleGUI *gui)
{
    if (!gui->prev_mask)
        gui->prev_mask = gwy_nield_copy(gui->args->mask);
    else
        gwy_nield_copy_data(gui->args->mask, gui->prev_mask);
    gui->has_undo = TRUE;
}

static void
selection_finished(GwySelection *selection, ModuleGUI *gui)
{
    gdouble sel[4];
    if (!gwy_selection_get_object(selection, 0, sel))
        return;

    ModuleArgs *args = gui->args;
    GwyDataView *dataview = GWY_DATA_VIEW(gui->view);
    GwyField *modulus = gui->modulus;
    GwyNield *mask = args->mask;
    GwyField *zoomed = gwy_data_view_get_field(dataview);
    EditType mode = gwy_params_get_enum(args->params, PARAM_EDIT_MODE);
    gboolean snap = gwy_params_get_boolean(args->params, PARAM_SNAP);

    gint width = gwy_field_get_xres(modulus);
    gint height = gwy_field_get_yres(modulus);
    gint zwidth = gwy_field_get_xres(zoomed);
    gint zheight = gwy_field_get_yres(zoomed);

    gint isel[4] = {
        gwy_field_rtoj(zoomed, sel[0]) + (width - zwidth)/2,
        gwy_field_rtoi(zoomed, sel[1]) + (height - zheight)/2,
        gwy_field_rtoj(zoomed, sel[2]) + (width - zwidth)/2,
        gwy_field_rtoi(zoomed, sel[3]) + (height - zheight)/2,
    };
    if (!snap) {
        isel[2]++;
        isel[3]++;
    }

    /* Because of the offset (see below), must make sure selection is not along left or top edge. */
    isel[0] = MAX(isel[0], 1);
    isel[1] = MAX(isel[1], 1);

    /* For the mirrored selection to look "correct" as far as the FFT goes, it must be shifted one pixel to the right,
     * and one pixel down for even-sized images.
     * XXX: It looks a bit weird when the selection and mask are displayed together. */
    gint mirror[4];
    mirror[0] = (-isel[2] + width) + (1 - width % 2);
    mirror[1] = (-isel[3] + height) + (1 - height % 2);
    mirror[2] = (-isel[0] + width) + (1 - width % 2);
    mirror[3] = (-isel[1] + height) + (1 - height % 2);

    g_printerr("area [%d..%d] × [%d..%d]\n", isel[0], isel[2], isel[1], isel[3]);
    g_printerr("mirror [%d..%d] × [%d..%d]\n", mirror[0], mirror[2], mirror[1], mirror[3]);

    /* Change coordinates to sizes. */
    isel[2] -= isel[0];
    isel[3] -= isel[1];
    mirror[2] -= mirror[0];
    mirror[3] -= mirror[1];

    gdouble value = 1;
    if (mode == FFT_RECTANGLE_SUB || mode == FFT_ELLIPSE_SUB)
        value = 0;

    /* Apply change to mask and queue recomputation. */
    record_undo(gui);
    if (mode == FFT_ELLIPSE_ADD || mode == FFT_ELLIPSE_SUB) {
        gwy_nield_elliptic_area_fill(mask, isel[0], isel[1], isel[2], isel[3], value);
        gwy_nield_elliptic_area_fill(mask, mirror[0], mirror[1], mirror[2], mirror[3], value);
    }
    else {
        gwy_nield_area_fill(mask, NULL, GWY_MASK_IGNORE, isel[0], isel[1], isel[2], isel[3], value);
        gwy_nield_area_fill(mask, NULL, GWY_MASK_IGNORE, mirror[0], mirror[1], mirror[2], mirror[3], value);
    }
    gwy_selection_clear(selection);

    gui->update |= UPDATE_ZOOMED | UPDATE_FILTERED;
    update_sensitivity(gui);
    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
dialog_response(ModuleGUI *gui, gint response)
{
    ModuleArgs *args = gui->args;

    if (response == RESPONSE_UNDO) {
        if (gui->has_undo) {
            gwy_nield_copy_data(gui->prev_mask, args->mask);
            gwy_nield_data_changed(args->mask);
            gui->has_undo = FALSE;
            if (gwy_params_get_enum(gui->args->params, PARAM_DISPLAY) == PREVIEW_FFT)
                calculate_zoomed_fields(gui);
            gui->update |= UPDATE_ZOOMED | UPDATE_FILTERED;
            update_sensitivity(gui);
            gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
        }
    }
    else if (response == GWY_RESPONSE_CLEAR || response == RESPONSE_FILL) {
        record_undo(gui);
        /* Compute the results trivially here. No need to queue FFT. */
        if (response == GWY_RESPONSE_CLEAR) {
            gwy_nield_clear(args->mask);
            gwy_field_assign(args->result, args->field);
            gwy_field_clear(args->diff);
        }
        else {
            gwy_nield_fill(args->mask, 1);
            gwy_field_clear(args->result);
            gwy_field_assign(args->diff, args->field);
        }
        gui->update |= UPDATE_ZOOMED;
        update_sensitivity(gui);
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
    }
}

static void
execute(ModuleArgs *args)
{
    /* Run the forward FFT. */
    GwyField *r_in = args->result;
    GwyField *r_out = gwy_field_new_alike(r_in, FALSE);
    GwyField *i_out = gwy_field_new_alike(r_in, FALSE);
    gint xres = gwy_field_get_xres(r_in), yres = gwy_field_get_yres(r_in);
    gwy_field_assign(r_in, args->field);

    gwy_field_fft_2d_raw(r_in, NULL, r_out, i_out, GWY_TRANSFORM_DIRECTION_FORWARD);

    /* Remember the filtered modulus in case someone wants it as an output. */
    gwy_field_hypot_of_fields(args->filtered_modulus, r_out, i_out);
    gwy_field_fft_postprocess(args->filtered_modulus, TRUE);
    gwy_field_area_fill(args->filtered_modulus, args->mask, GWY_MASK_EXCLUDE, 0, 0, xres, yres, 0.0);

    /* Apply mask to the fft. Filling unmasked with zero is a slightly involved way of saying ‘clear pixels which are
     * not marked‘. */
    gwy_field_fft_2d_center(r_out);
    gwy_field_fft_2d_center(i_out);

    gwy_field_area_fill(r_out, args->mask, GWY_MASK_EXCLUDE, 0, 0, xres, yres, 0.0);
    gwy_field_area_fill(i_out, args->mask, GWY_MASK_EXCLUDE, 0, 0, xres, yres, 0.0);

    /* Run the inverse FFT */
    GwyField *i_in = gwy_field_new_alike(r_in, TRUE);
    gwy_field_fft_2d_decenter(r_out);
    gwy_field_fft_2d_decenter(i_out);
    gwy_field_fft_2d_raw(r_out, i_out, r_in, i_in, GWY_TRANSFORM_DIRECTION_BACKWARD);

    gwy_field_subtract_fields(args->diff, args->field, args->result);

    /* Finalize */
    g_object_unref(i_out);
    g_object_unref(r_out);
    g_object_unref(i_in);
}

static void
calculate_zoomed_fields(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    guint zoom = gwy_params_get_enum(args->params, PARAM_ZOOM);
    GwyDataView *dataview = GWY_DATA_VIEW(gui->view);

    GwyField *zoomed = cut_field_to_zoom(gui->modulus, zoom);
    gwy_field_data_changed(zoomed);
    gwy_data_view_set_field(dataview, zoomed);
    g_object_unref(zoomed);

    GwyNield* zoomedmask = cut_nield_to_zoom(args->mask, zoom);
    gwy_nield_data_changed(zoomedmask);
    gwy_data_view_set_mask(dataview, zoomedmask);
    g_object_unref(zoomedmask);

    gwy_set_data_preview_size(dataview, PREVIEW_SIZE);
}

static gboolean
calc_cut_to_zoom(gint xres, gint yres, gint zoom, gint *pwidth, gint *pheight)
{
    gint width = (xres/zoom) | 1;
    gint height = (yres/zoom) | 1;
    if (width < 17)
        width = MAX(width, MIN(17, xres));
    if (height < 17)
        height = MAX(height, MIN(17, yres));

    *pwidth = width;
    *pheight = height;
    return width >= xres && height >= yres;
}

static GwyField*
cut_field_to_zoom(GwyField *field, gint zoom)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    guint width, height;

    if (calc_cut_to_zoom(xres, yres, zoom, &width, &height))
        return g_object_ref(field);

    GwyField *zoomed = gwy_field_area_extract(field, (xres - width + 1)/2, (yres - height + 1)/2, width, height);
    gwy_field_set_xoffset(zoomed, -0.5*gwy_field_get_xreal(zoomed));
    gwy_field_set_yoffset(zoomed, -0.5*gwy_field_get_yreal(zoomed));

    return zoomed;
}

static GwyNield*
cut_nield_to_zoom(GwyNield *nield, gint zoom)
{
    gint xres = gwy_nield_get_xres(nield), yres = gwy_nield_get_yres(nield);
    guint width, height;

    if (calc_cut_to_zoom(xres, yres, zoom, &width, &height))
        return g_object_ref(nield);
    return gwy_nield_area_extract(nield, (xres - width + 1)/2, (yres - height + 1)/2, width, height);
}

/* This is the modulus we show in GUI (with windowing, etc.) */
static void
calculate_modulus(GwyField *field, GwyField *fftmod)
{
    gwy_field_psdf_2d(field, fftmod, GWY_WINDOWING_LANCZOS, 1);
    gint n = gwy_field_get_xres(fftmod)*gwy_field_get_yres(fftmod);
    gdouble *d = gwy_field_get_data(fftmod);
    for (gint i = 0; i < n; i++)
        d[i] = sqrt(fmax(d[i], 0.0));
}

static gint
add_output_field(GwyFile *data, GwyField *field,
                 gint oldid, const gchar *title)
{
    gint newid = gwy_file_add_image(data, field);
    gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
    gwy_file_sync_items(data, GWY_FILE_IMAGE, oldid,
                        data, GWY_FILE_IMAGE, newid,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_MASK_COLOR
                        | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_REAL_SQUARE,
                        FALSE);
    gwy_file_set_title(data, GWY_FILE_IMAGE, newid, title, TRUE);
    gwy_log_add(data, GWY_FILE_IMAGE, oldid, newid);

    return newid;
}

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