/*
 *  $Id: correct_affine.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2013-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 <stdlib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES (GWY_RUN_INTERACTIVE)

enum {
    LATTICE_USER_DEFINED = -1,
    LATTICE_HOPG = 0,
};

enum {
    SENS_USER_LATTICE = 1,
    SENS_DIFFERENT_LENGTHS = 2,
    SENS_VALID_LATTICE = 4,
};

enum {
    INVALID_A1 = 1,
    INVALID_A2 = 2,
    INVALID_PHI = 4,
    INVALID_SEL = 8,
};

enum {
    PARAM_A1,
    PARAM_A2,
    PARAM_PHI,
    PARAM_DIFFERENT_LENGTHS,
    PARAM_INTERPOLATION,
    PARAM_DISTRIBUTE,
    PARAM_FIX_HACF,
    PARAM_PREVIEW,
    PARAM_PRESET,
    PARAM_SCALING,
    PARAM_ZOOM,
    PARAM_ACF_IMAGE,

    WIDGET_LATTICE,
};

typedef enum {
    PREVIEW_DATA,
    PREVIEW_ACF,
    PREVIEW_CORRECTED,
} ImageMode;

typedef struct {
    gdouble a1;
    gdouble a2;
    gdouble phi;
} LatticePreset;

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *result;
    GwySelection *selection;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GtkWidget *view;
    GwyParamTable *table_lattice;
    GwyParamTable *table_options;
    GwyVectorLayer *vlayer;
    GwyValueFormat *vf;
    GwyValueFormat *vfphi;
    GwyField *acf;
    GwyField *acf_zoomed;
    GwyLine *hacf_orig;
    GwyLine *hacf_fixed;
    GwyField *acf_calculated_for;
    GwyFile *data;
    gint id;
    /* Measured */
    GtkWidget *a1_x;
    GtkWidget *a1_y;
    GtkWidget *a1_len;
    GtkWidget *a1_phi;
    GtkWidget *a2_x;
    GtkWidget *a2_y;
    GtkWidget *a2_len;
    GtkWidget *a2_phi;
    GtkWidget *phi;
    GtkWidget *preset;
    gdouble xy[4];
    /* Correct (wanted) */
    guint invalid_corr;
} ModuleGUI;

static gboolean         module_register          (void);
static GwyParamDef*     define_module_params     (void);
static void             module_main              (GwyFile *data,
                                                  GwyRunModeFlags mode);
static GwyDialogOutcome run_gui                  (ModuleArgs *args,
                                                  GwyFile *data,
                                                  gint id);
static GtkWidget*       create_lattice_table     (gpointer user_data);
static void             param_changed            (ModuleGUI *gui,
                                                  gint id);
static void             correction_may_be_invalid(ModuleGUI *gui);
static void             preview                  (gpointer user_data);
static void             switch_preview           (ModuleGUI *gui);
static void             dialog_response          (GtkDialog *dialog,
                                                  gint response,
                                                  ModuleGUI *gui);
static void             a1_changed_manually      (GtkEntry *entry,
                                                  ModuleGUI *gui);
static void             a2_changed_manually      (GtkEntry *entry,
                                                  ModuleGUI *gui);
static gboolean         acffield_filter          (GwyFile *data,
                                                  gint id,
                                                  gpointer user_data);
static void             set_acf_middle_row       (ModuleGUI *gui);
static void             calculate_zoomed_fields  (ModuleGUI *gui);
static GwyField*        cut_field_to_zoom        (GwyField *field,
                                                  gint zoom);
static void             init_selection           (ModuleGUI *gui);
static void             calculate_acf_field      (ModuleGUI *gui);
static void             refine                   (ModuleGUI *gui);
static void             estimate                 (ModuleGUI *gui);
static void             selection_changed        (ModuleGUI *gui);
static void             do_correction            (ModuleGUI *gui);
static void             fill_correct_vectors     (ModuleArgs *args,
                                                  gdouble *a1a2);
static GwyField*        create_corrected_field   (GwyField *field,
                                                  const gdouble *a1a2,
                                                  gdouble *a1a2_corr,
                                                  GwyInterpolationType interp,
                                                  GwyAffineScalingType scaling);
static void             sanitise_params          (ModuleArgs *args);

static const LatticePreset lattice_presets[] = {
    { 2.46e-10, 2.46e-10, G_PI/3.0 },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Corrects affine distortion of images by matching image Bravais lattice to the true one."),
    "Yeti <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti)",
    "2013",
};

GWY_MODULE_QUERY2(module_info, correct_affine)

static gboolean
module_register(void)
{
    gwy_image_func_register("correct_affine",
                            module_main,
                            N_("/_Distortion/_Affine..."),
                            GWY_ICON_CORRECT_AFFINE,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Correct affine distortion"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum previews[] = {
        { N_("_Data"),           PREVIEW_DATA,      },
        { N_("2D _ACF"),         PREVIEW_ACF,       },
        { N_("Correc_ted data"), PREVIEW_CORRECTED, },
    };
    static const GwyEnum scalings[] = {
        { N_("Exactly as specified"), GWY_AFFINE_SCALING_AS_GIVEN,      },
        { N_("Preserve area"),        GWY_AFFINE_SCALING_PRESERVE_AREA, },
        { N_("Preserve X scale"),     GWY_AFFINE_SCALING_PRESERVE_X,    },
    };
    static const GwyEnum presets[] = {
        { N_("User defined"), LATTICE_USER_DEFINED, },
        { N_("HOPG"),         0,                    },
    };
    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());
    /* These are the correct vectors. The measured vectors are not parameters. */
    gwy_param_def_add_double(paramdef, PARAM_A1, "a1", "a<sub>1</sub>", 0.0, G_MAXDOUBLE, 1.0);
    gwy_param_def_add_double(paramdef, PARAM_A2, "a2", "a<sub>2</sub>", 0.0, G_MAXDOUBLE, 1.0);
    gwy_param_def_add_double(paramdef, PARAM_PHI, "phi", "φ", -G_MAXDOUBLE, G_MAXDOUBLE, 0.5*G_PI);
    gwy_param_def_add_boolean(paramdef, PARAM_DIFFERENT_LENGTHS, "different-lengths", _("_Different lengths"), FALSE);
    gwy_param_def_add_enum(paramdef, PARAM_INTERPOLATION, "interpolation", NULL, GWY_TYPE_INTERPOLATION_TYPE,
                           GWY_INTERPOLATION_LINEAR);
    gwy_param_def_add_boolean(paramdef, PARAM_DISTRIBUTE, "distribute", _("_Apply to all compatible images"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_FIX_HACF, "fix_hacf", _("Interpolate _horizontal ACF"), FALSE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_PREVIEW, NULL, _("Display"),
                              previews, G_N_ELEMENTS(previews), PREVIEW_DATA);
    gwy_param_def_add_gwyenum(paramdef, PARAM_SCALING, "scaling", _("_Scaling"),
                              scalings, G_N_ELEMENTS(scalings), GWY_AFFINE_SCALING_AS_GIVEN);
    gwy_param_def_add_gwyenum(paramdef, PARAM_PRESET, "preset", _("_Latttice type"),
                              presets, G_N_ELEMENTS(presets), LATTICE_USER_DEFINED);
    gwy_param_def_add_gwyenum(paramdef, PARAM_ZOOM, "zoom", _("ACF zoom"), zooms, G_N_ELEMENTS(zooms), 1);
    gwy_param_def_add_image_id(paramdef, PARAM_ACF_IMAGE, NULL, _("Image for _ACF"));
    return paramdef;
}

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

    ModuleArgs args;
    gwy_clear1(args);
    gint id, datano;

    args.params = gwy_params_new_from_settings(define_module_params());
    gwy_data_browser_get_current(GWY_APP_FIELD, &args.field,
                                 GWY_APP_FIELD_ID, &id,
                                 GWY_APP_FILE_ID, &datano,
                                 0);
    g_return_if_fail(args.field);
    /* Init the image we use for ACF to the current image. If someone wants to use a different image she will
     * remember it. Leaving a forgotten random image there is confusing. */
    gwy_params_set_image_id(args.params, PARAM_ACF_IMAGE, (GwyAppDataId){ datano, id });
    sanitise_params(&args);

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

    gdouble a1a2_corr[4], a1a2[4];
    gwy_selection_get_object(args.selection, 0, a1a2);

    gint *all_channels;
    if (gwy_params_get_boolean(args.params, PARAM_DISTRIBUTE))
        all_channels = gwy_file_get_ids(data, GWY_FILE_IMAGE);
    else {
        all_channels = g_new(gint, 2);
        all_channels[0] = id;
        all_channels[1] = -1;
    }

    for (guint i = 0; all_channels[i] != -1; i++) {
        const guint compat_flags = GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_LATERAL;
        gint oldid = all_channels[i];
        GwyField *field = gwy_file_get_image(data, oldid);
        if (gwy_field_is_incompatible(field, field, compat_flags))
            continue;

        fill_correct_vectors(&args, a1a2_corr);

        GwyField *corrected;
        if (oldid == id && outcome == GWY_DIALOG_HAVE_RESULT)
            corrected = g_object_ref(args.result);
        else {
            corrected = create_corrected_field(field, a1a2, a1a2_corr,
                                               gwy_params_get_enum(args.params, PARAM_INTERPOLATION),
                                               gwy_params_get_enum(args.params, PARAM_SCALING));
        }

        gint corrid = gwy_file_add_image(data, corrected);
        gwy_file_set_visible(data, GWY_FILE_IMAGE, corrid, oldid == id);
        gwy_file_sync_items(data, GWY_FILE_IMAGE, oldid,
                            data, GWY_FILE_IMAGE, corrid,
                            GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_PALETTE, FALSE);
        g_object_unref(corrected);

        /* FIXME: We might want an option for that. */
        GwySelection *selection = gwy_selection_lattice_new();
        gwy_selection_set_data(selection, 1, a1a2_corr);
        gwy_dict_pass_object(GWY_DICT(data), gwy_file_key_image_selection(corrid, "lattice"), selection);

        gchar *s = gwy_file_get_display_title(data, GWY_FILE_IMAGE, oldid);
        gchar *t = g_strconcat(s, " ", _("Corrected"), NULL);
        gwy_file_pass_title(data, GWY_FILE_IMAGE, corrid, t);
        g_free(s);

        gwy_log_add(data, GWY_FILE_IMAGE, oldid, corrid);
    }
    g_free(all_channels);

end:
    g_object_unref(args.params);
    g_clear_object(&args.selection);
    g_clear_object(&args.result);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    ModuleGUI gui;
    gwy_clear1(gui);
    gui.args = args;
    gui.data = data;
    gui.id = id;
    gui.vf = gwy_field_get_value_format_xy(args->field, GWY_UNIT_FORMAT_MARKUP, NULL);
    gui.vf->precision += 2;
    gui.vfphi = gwy_value_format_new(G_PI/180.0, 2, _("deg"));
    calculate_acf_field(&gui);
    calculate_zoomed_fields(&gui);

    gui.dialog = gwy_dialog_new(_("Affine Correction"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_RESET, GWY_STOCK_ESTIMATE, GWY_STOCK_REFINE,
                                 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_COLOR_MAPPING | GWY_FILE_ITEM_RANGE
                        | GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE);
    gtk_widget_set_halign(gui.view, GTK_ALIGN_START);
    gtk_widget_set_valign(gui.view, GTK_ALIGN_START);
    args->selection = gwy_create_preview_vector_layer(GWY_DATA_VIEW(gui.view), GWY_TYPE_LAYER_LATTICE, 1, TRUE);
    g_object_ref(args->selection);
    gui.vlayer = gwy_data_view_get_interactive_layer(GWY_DATA_VIEW(gui.view));
    g_signal_connect_swapped(args->selection, "changed", G_CALLBACK(selection_changed), &gui);

    GtkWidget *hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(gui.view), FALSE);

    GwyParamTable *table;
    table = gui.table_lattice = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Lattice Vectors"));
    gwy_param_table_append_foreign(table, WIDGET_LATTICE, create_lattice_table, &gui, NULL);
    /* TRANSLATORS: Correct is an adjective here. */
    gwy_param_table_append_header(table, -1, _("Correct Lattice"));
    gwy_param_table_append_combo(table, PARAM_PRESET);
    gwy_param_table_append_entry(table, PARAM_A1);
    gwy_param_table_entry_set_value_format(table, PARAM_A1, gui.vf);
    gwy_param_table_append_entry(table, PARAM_A2);
    gwy_param_table_append_checkbox(table, PARAM_DIFFERENT_LENGTHS);
    gwy_param_table_entry_set_value_format(table, PARAM_A2, gui.vf);
    gwy_param_table_append_entry(table, PARAM_PHI);
    gwy_param_table_entry_set_value_format(table, PARAM_PHI, gui.vfphi);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

    table = gui.table_options = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Preview"));
    gwy_param_table_append_radio(table, PARAM_PREVIEW);
    gwy_param_table_append_radio_row(table, PARAM_ZOOM);
    gwy_param_table_append_header(table, -1, _("ACF Options"));
    gwy_param_table_append_image_id(table, PARAM_ACF_IMAGE);
    gwy_param_table_data_id_set_filter(table, PARAM_ACF_IMAGE, acffield_filter, &gui, NULL);
    gwy_param_table_append_checkbox(table, PARAM_FIX_HACF);
    gwy_param_table_append_header(table, -1, _("Output"));
    gwy_param_table_append_combo(table, PARAM_INTERPOLATION);
    gwy_param_table_append_combo(table, PARAM_SCALING);
    gwy_param_table_append_checkbox(table, PARAM_DISTRIBUTE);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

    GObject *selection;
    GQuark selection_key = gwy_file_key_image_selection(id, "lattice");
    if (gwy_dict_gis_object(GWY_DICT(data), selection_key, &selection)
        && gwy_selection_get_data(GWY_SELECTION(selection), NULL) == 1)
        gwy_selection_assign(args->selection, GWY_SELECTION(selection));
    else
        estimate(&gui);

    g_signal_connect_swapped(gui.table_lattice, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_options, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_after(dialog, "response", G_CALLBACK(dialog_response), &gui);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);

    GwyDialogOutcome outcome = gwy_dialog_run(dialog);

    gwy_dict_pass_object(GWY_DICT(data), selection_key, gwy_selection_copy(args->selection));
    gwy_value_format_free(gui.vf);
    gwy_value_format_free(gui.vfphi);
    g_object_unref(gui.acf);
    g_object_unref(gui.acf_zoomed);
    g_object_unref(gui.hacf_orig);
    g_object_unref(gui.hacf_fixed);

    return outcome;
}

static GtkWidget*
add_lattice_entry(GtkGrid *grid, gint col, gint row, const gchar *name,
                  GCallback callback, gpointer cbdata)
{
    GtkWidget *entry = gtk_entry_new();
    gtk_entry_set_width_chars(GTK_ENTRY(entry), 8);
    g_object_set_data(G_OBJECT(entry), "id", (gpointer)name);
    gwy_widget_set_activate_on_unfocus(entry, TRUE);
    gtk_grid_attach(grid, entry, col, row, 1, 1);
    g_signal_connect(entry, "activate", callback, cbdata);
    return entry;
}

static GtkWidget*
create_lattice_table(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    GtkWidget *label;
    GString *str = g_string_new(NULL);

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 2);
    gtk_grid_set_column_spacing(grid, 6);

    /* Header row. */
    g_string_assign(str, "x");
    if (strlen(gui->vf->units))
        g_string_append_printf(str, " [%s]", gui->vf->units);
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), str->str);
    gtk_grid_attach(grid, label, 1, 0, 1, 1);

    g_string_assign(str, "y");
    if (strlen(gui->vf->units))
        g_string_append_printf(str, " [%s]", gui->vf->units);
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), str->str);
    gtk_grid_attach(grid, label, 2, 0, 1, 1);

    g_string_assign(str, _("length"));
    if (strlen(gui->vf->units))
        g_string_append_printf(str, " [%s]", gui->vf->units);
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), str->str);
    gtk_grid_attach(grid, label, 3, 0, 1, 1);

    g_string_assign(str, _("angle"));
    if (strlen(gui->vfphi->units))
        g_string_append_printf(str, " [%s]", gui->vfphi->units);
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), str->str);
    gtk_grid_attach(grid, label, 4, 0, 1, 1);

    /* a1 */
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), "a<sub>1</sub>:");
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 1, 1, 1);

    gui->a1_x = add_lattice_entry(grid, 1, 1, "x", G_CALLBACK(a1_changed_manually), gui);
    gui->a1_y = add_lattice_entry(grid, 2, 1, "y", G_CALLBACK(a1_changed_manually), gui);
    gui->a1_len = add_lattice_entry(grid, 3, 1, "len", G_CALLBACK(a1_changed_manually), gui);
    gui->a1_phi = add_lattice_entry(grid, 4, 1, "phi", G_CALLBACK(a1_changed_manually), gui);

    /* a2 */
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), "a<sub>2</sub>:");
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 2, 1, 1);

    gui->a2_x = add_lattice_entry(grid, 1, 2, "x", G_CALLBACK(a2_changed_manually), gui);
    gui->a2_y = add_lattice_entry(grid, 2, 2, "y", G_CALLBACK(a2_changed_manually), gui);
    gui->a2_len = add_lattice_entry(grid, 3, 2, "len", G_CALLBACK(a2_changed_manually), gui);
    gui->a2_phi = add_lattice_entry(grid, 4, 2, "phi", G_CALLBACK(a2_changed_manually), gui);

    /* phi */
    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), "ϕ:");
    gtk_label_set_xalign(GTK_LABEL(label), 1.0);
    gtk_grid_attach(grid, label, 3, 3, 1, 1);

    gui->phi = label = gtk_label_new(NULL);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 4, 3, 1, 1);

    g_string_free(str, TRUE);

    return GTK_WIDGET(grid);
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    gint lattice = gwy_params_get_enum(params, PARAM_PRESET);
    gboolean different = gwy_params_get_boolean(params, PARAM_DIFFERENT_LENGTHS);

    if (id < 0 || id == PARAM_ACF_IMAGE) {
        calculate_acf_field(gui);
        gwy_field_data_changed(gui->acf);
    }
    if (id < 0 || id == PARAM_PREVIEW)
        switch_preview(gui);
    if (id < 0 || id == PARAM_ZOOM) {
        calculate_zoomed_fields(gui);
        gwy_set_data_preview_size(GWY_DATA_VIEW(gui->view), PREVIEW_SIZE);
    }
    if (id < 0 || id == PARAM_FIX_HACF) {
        set_acf_middle_row(gui);
        calculate_zoomed_fields(gui);
    }

    if (id < 0 || id == PARAM_PRESET) {
        gint preset = gwy_params_get_enum(params, PARAM_PRESET);
        gboolean is_user = (preset == LATTICE_USER_DEFINED);
        GwyParamTable *table = gui->table_lattice;
        gwy_param_table_set_sensitive(table, PARAM_A1, is_user);
        gwy_param_table_set_sensitive(table, PARAM_A2, is_user && different);
        gwy_param_table_set_sensitive(table, PARAM_PHI, is_user);
        if (!is_user) {
            gwy_param_table_set_double(table, PARAM_A1, lattice_presets[preset].a1);
            gwy_param_table_set_double(table, PARAM_A2, lattice_presets[preset].a2);
            gwy_param_table_set_double(table, PARAM_PHI, lattice_presets[preset].phi);
        }
    }
    if (id < 0 || id == PARAM_DIFFERENT_LENGTHS) {
        if (lattice == LATTICE_USER_DEFINED) {
            GwyParamTable *table = gui->table_lattice;
            gwy_param_table_set_sensitive(table, PARAM_A2, different);
            if (!different)
                gwy_param_table_set_double(table, PARAM_A2, gwy_params_get_double(params, PARAM_A1));
        }
    }

    if (id < 0 || id == PARAM_A1) {
        if (gwy_params_get_double(params, PARAM_A1) > 0.0)
            gui->invalid_corr &= ~INVALID_A1;
        else
            gui->invalid_corr |= INVALID_A1;

        if (!different) {
            GwyParamTable *table = gui->table_lattice;
            gwy_param_table_set_double(table, PARAM_A2, gwy_params_get_double(params, PARAM_A1));
        }
    }
    if (id < 0 || id == PARAM_A2) {
        if (gwy_params_get_double(params, PARAM_A2) > 0.0)
            gui->invalid_corr &= ~INVALID_A2;
        else
            gui->invalid_corr |= INVALID_A2;
    }
    if (id < 0 || id == PARAM_PHI) {
        gdouble phi = gwy_canonicalize_angle(gwy_params_get_double(params, PARAM_PHI), TRUE, FALSE);
        if (phi > 1e-3 && phi < G_PI - 1e-3)
            gui->invalid_corr &= ~INVALID_PHI;
        else
            gui->invalid_corr |= INVALID_PHI;
    }

    if (id != PARAM_DISTRIBUTE && id != PARAM_PREVIEW) {
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
        correction_may_be_invalid(gui);
    }
}

static void
correction_may_be_invalid(ModuleGUI *gui)
{
    gboolean sens = !gui->invalid_corr;

    gtk_dialog_set_response_sensitive(GTK_DIALOG(gui->dialog), GTK_RESPONSE_OK, sens);
    gwy_param_table_radio_set_sensitive(gui->table_options, PARAM_PREVIEW, PREVIEW_CORRECTED, sens);
    /* We should not be showing the corrected image when the correction is not valid. */
    if (!sens && gwy_params_get_enum(gui->args->params, PARAM_PREVIEW) == PREVIEW_CORRECTED)
        gwy_param_table_set_enum(gui->table_options, PARAM_PREVIEW, PREVIEW_DATA);
}

static void
dialog_response(G_GNUC_UNUSED GtkDialog *dialog, gint response, ModuleGUI *gui)
{
    if (response == GWY_RESPONSE_RESET)
        init_selection(gui);
    else if (response == GWY_RESPONSE_EXECUTE)
        estimate(gui);
    else if (response == GWY_RESPONSE_REFINE)
        refine(gui);
}

static gboolean
acffield_filter(GwyFile *data, gint id, gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    GwyField *acffield, *field = gui->args->field;

    if (!(acffield = gwy_file_get_image(data, id)))
        return FALSE;
    /* Do not check value, we may want to align channels of a different physical quantity.  But check
     * order-of-magnitude pixel size for elementary sanity. */
    if (gwy_field_is_incompatible(field, acffield, GWY_DATA_MISMATCH_LATERAL))
        return FALSE;

    gdouble rx = (gwy_field_get_dx(field)/gwy_field_get_dx(acffield));
    if (rx > 4.0 || rx < 0.25)
        return FALSE;

    gdouble ry = (gwy_field_get_dy(field)/gwy_field_get_dy(acffield));
    if (ry > 4.0 || ry < 0.25)
        return FALSE;

    return TRUE;
}

static void
vector_changed_manually(ModuleGUI *gui, GtkEntry *entry, gint which)
{
    const gchar *id = g_object_get_data(G_OBJECT(entry), "id");
    const gchar *text = gtk_entry_get_text(GTK_ENTRY(entry));
    gdouble value = g_strtod(text, NULL);
    gdouble *xy = gui->xy + which;

    gdouble x = xy[0];
    gdouble y = -xy[1];
    gdouble len = hypot(x, y);
    gdouble phi = atan2(y, x);
    GwyValueFormat *vf = gui->vf;

    if (gwy_strequal(id, "x"))
        xy[0] = vf->magnitude * value;
    else if (gwy_strequal(id, "y"))
        xy[1] = vf->magnitude * -value;
    else if (gwy_strequal(id, "len")) {
        xy[0] = vf->magnitude * value * cos(phi);
        xy[1] = vf->magnitude * value * -sin(phi);
    }
    else if (gwy_strequal(id, "phi")) {
        phi = gwy_deg2rad(value);
        xy[0] = len * cos(phi);
        xy[1] = len * -sin(phi);
    }

    /* This actually recalculates everything.  But it does not activate entries so we will not recurse. */
    gwy_selection_set_data(gui->args->selection, 1, gui->xy);
}

static void
a1_changed_manually(GtkEntry *entry, ModuleGUI *gui)
{
    vector_changed_manually(gui, entry, 0);
}

static void
a2_changed_manually(GtkEntry *entry,
                    ModuleGUI *gui)
{
    vector_changed_manually(gui, entry, 1);
}

static void
init_selection(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    gdouble xreal = gwy_field_get_xreal(args->field), yreal = gwy_field_get_yreal(args->field);
    gdouble xy[4] = { xreal/20, 0.0, 0.0, yreal/20 };
    gwy_selection_set_data(args->selection, 1, xy);
}

static void
switch_preview(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    ImageMode mode = gwy_params_get_enum(gui->args->params, PARAM_PREVIEW);
    GwyDataView *dataview = GWY_DATA_VIEW(gui->view);

    if (mode == PREVIEW_DATA) {
        gwy_data_view_set_field(dataview, args->field);
        gwy_data_view_set_gradient(dataview, NULL);
        gwy_setup_data_view(dataview, gui->data, GWY_FILE_IMAGE, gui->id,
                            GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_RANGE);
        if (!gwy_data_view_get_interactive_layer(dataview))
            gwy_data_view_set_interactive_layer(dataview, gui->vlayer);
    }
    else if (mode == PREVIEW_ACF) {
        /* We want full-colour-scale ACF. */
        gwy_data_view_set_field(dataview, gui->acf_zoomed);
        gwy_data_view_set_color_mapping(dataview, GWY_COLOR_MAPPING_FULL);
        if (!gwy_data_view_get_interactive_layer(dataview))
            gwy_data_view_set_interactive_layer(dataview, gui->vlayer);
    }
    else if (mode == PREVIEW_CORRECTED) {
        do_correction(gui);
        gwy_data_view_set_field(dataview, args->result);
        gwy_setup_data_view(dataview, gui->data, GWY_FILE_IMAGE, gui->id,
                            GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_RANGE);
        gwy_data_view_set_interactive_layer(dataview, NULL);
    }

    gwy_set_data_preview_size(dataview, PREVIEW_SIZE);
}

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

    if (!gui->acf)
        gui->acf = gwy_field_new_alike(args->field, FALSE);
    if (!gui->acf_zoomed)
        gui->acf_zoomed = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    if (!gui->hacf_orig)
        gui->hacf_orig = gwy_line_new(1, 1.0, FALSE);
    if (!gui->hacf_fixed)
        gui->hacf_fixed = gwy_line_new(1, 1.0, FALSE);

    GwyField *field = gwy_params_get_image(args->params, PARAM_ACF_IMAGE);
    if (field == gui->acf_calculated_for)
        return;

    gui->acf_calculated_for = field;
    field = gwy_field_copy(field);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gwy_field_add(field, -gwy_field_mean(field));
    gint acfwidth = MIN(MAX(xres/4, 64), xres/2);
    gint acfheight = MIN(MAX(yres/4, 64), yres/2);
    gwy_field_area_acf_2d(field, NULL, GWY_MASK_IGNORE, gui->acf, 0, 0, xres, yres, acfwidth, acfheight, NULL);
    g_object_unref(field);

    /* Remember the original and fixed middle rows so we may simply replace it (and do not have to keep two identical
     * fields around). */
    acfheight = gwy_field_get_yres(gui->acf);
    acfwidth = gwy_field_get_xres(gui->acf);
    gwy_field_get_row(gui->acf, gui->hacf_orig, acfheight/2);

    /* Remember interpolated middle row. */
    GwyField *mid = gwy_field_area_extract(gui->acf, 0, acfheight/2-1, acfwidth, 3);
    GwyNield *mask = gwy_nield_new(acfwidth, 3);
    gwy_nield_area_fill(mask, NULL, GWY_MASK_IGNORE, 0, 1, acfwidth, 1, 1);
    gwy_nield_set_val(mask, acfwidth/2, 1, 0);
    gwy_field_laplace_solve(mid, mask, GWY_LAPLACE_MASKED, 1.0);
    gwy_field_get_row(mid, gui->hacf_fixed, 1);
    g_object_unref(mask);
    g_object_unref(mid);

    set_acf_middle_row(gui);
}

static void
set_acf_middle_row(ModuleGUI *gui)
{
    gint acfheight = gwy_field_get_yres(gui->acf);

    if (gwy_params_get_boolean(gui->args->params, PARAM_FIX_HACF))
        gwy_field_set_row(gui->acf, gui->hacf_fixed, acfheight/2);
    else
        gwy_field_set_row(gui->acf, gui->hacf_orig, acfheight/2);

    gwy_field_data_changed(gui->acf);
}

static void
calculate_zoomed_fields(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    guint zoom = gwy_params_get_enum(args->params, PARAM_ZOOM);
    GwyField *zoomed = cut_field_to_zoom(gui->acf, zoom);
    gwy_field_assign(gui->acf_zoomed, zoomed);
    g_object_unref(zoomed);
    gwy_field_data_changed(gui->acf_zoomed);
}

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

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    width = (xres/zoom) | 1;
    height = (yres/zoom) | 1;
    if (width < 17)
        width = MAX(width, MIN(17, xres));
    if (height < 17)
        height = MAX(height, MIN(17, yres));

    if (width >= xres && height >= yres)
        return g_object_ref(field);

    zoomed = gwy_field_area_extract(field, (xres - width)/2, (yres - height)/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 void
refine(ModuleGUI *gui)
{
    gdouble xy[4];
    if (!gwy_selection_get_object(gui->args->selection, 0, xy))
        return;

    if (gwy_field_measure_lattice_acf(gui->acf, xy))
        gwy_selection_set_object(gui->args->selection, 0, xy);
}

static void
estimate(ModuleGUI *gui)
{
    gwy_clear(gui->xy, 4);
    if (gwy_field_measure_lattice_acf(gui->acf, gui->xy))
        gwy_selection_set_object(gui->args->selection, 0, gui->xy);
    else
        init_selection(gui);
}

static void
selection_changed(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    GwyValueFormat *vf;
    gdouble a1, a2, phi1, phi2, phi;
    GString *str = g_string_new(NULL);

    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
    if (!gwy_selection_get_data(args->selection, NULL)) {
        gui->invalid_corr |= INVALID_SEL;
        correction_may_be_invalid(gui);
        return;
    }

    gwy_selection_get_object(args->selection, 0, gui->xy);
    gdouble xy[4];
    gwy_assign(xy, gui->xy, 4);

    vf = gui->vf;
    g_string_printf(str, "%.*f", vf->precision, xy[0]/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a1_x), str->str);

    g_string_printf(str, "%.*f", vf->precision, -xy[1]/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a1_y), str->str);

    a1 = hypot(xy[0], xy[1]);
    g_string_printf(str, "%.*f", vf->precision, a1/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a1_len), str->str);

    vf = gui->vfphi;
    phi1 = atan2(-xy[1], xy[0]);
    g_string_printf(str, "%.*f", vf->precision, phi1/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a1_phi), str->str);

    vf = gui->vf;
    g_string_printf(str, "%.*f", vf->precision, xy[2]/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a2_x), str->str);

    g_string_printf(str, "%.*f", vf->precision, -xy[3]/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a2_y), str->str);

    a2 = hypot(xy[2], xy[3]);
    g_string_printf(str, "%.*f", vf->precision, a2/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a2_len), str->str);

    vf = gui->vfphi;
    phi2 = atan2(-xy[3], xy[2]);
    g_string_printf(str, "%.*f", vf->precision, phi2/vf->magnitude);
    gtk_entry_set_text(GTK_ENTRY(gui->a2_phi), str->str);

    phi = gwy_canonicalize_angle(phi2 - phi1, TRUE, TRUE);
    g_string_printf(str, "%.*f", vf->precision, phi/vf->magnitude);
    gtk_label_set_text(GTK_LABEL(gui->phi), str->str);

    g_string_free(str, TRUE);

    if (hypot(xy[0]/gwy_field_get_dx(args->field), xy[1]/gwy_field_get_dy(args->field)) >= 0.9
        && hypot(xy[2]/gwy_field_get_dx(args->field), xy[3]/gwy_field_get_dy(args->field)) >= 0.9
        && phi >= 1e-3
        && phi <= G_PI - 1e-3)
        gui->invalid_corr &= ~INVALID_SEL;
    else
        gui->invalid_corr |= INVALID_SEL;
    correction_may_be_invalid(gui);

    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    if (gwy_params_get_enum(gui->args->params, PARAM_PREVIEW) != PREVIEW_CORRECTED)
        return;

    do_correction(gui);
    switch_preview(gui);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
do_correction(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    GwyField *field = args->field;
    gdouble a1a2_corr[4], a1a2[4];

    if (gui->invalid_corr)
        return;

    gwy_selection_get_object(args->selection, 0, a1a2);
    fill_correct_vectors(args, a1a2_corr);
    g_clear_object(&args->result);
    args->result = create_corrected_field(field, a1a2, a1a2_corr,
                                          gwy_params_get_enum(args->params, PARAM_INTERPOLATION),
                                          gwy_params_get_enum(args->params, PARAM_SCALING));
}

static void
fill_correct_vectors(ModuleArgs *args, gdouble *a1a2)
{
    gdouble a1 = gwy_params_get_double(args->params, PARAM_A1);
    gdouble a2 = gwy_params_get_double(args->params, PARAM_A2);
    gdouble phi = gwy_params_get_double(args->params, PARAM_PHI);
    a1a2[0] = a1;
    a1a2[1] = 0.0;
    a1a2[2] = a2 * cos(phi);
    a1a2[3] = -a2 * sin(phi);
}

/* NB: a1a2_corr is modified according to the scaling to be correct for the returned data field. */
static GwyField*
create_corrected_field(GwyField *field,
                       const gdouble *a1a2,
                       gdouble *a1a2_corr,
                       GwyInterpolationType interp,
                       GwyAffineScalingType scaling)
{
    GwyField *corrected;
    gdouble invtrans[6];

    corrected = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    gwy_field_affine_prepare(field, corrected, a1a2, a1a2_corr, invtrans, scaling, TRUE, 1.0);
    gwy_field_affine(field, corrected, invtrans, interp, GWY_EXTERIOR_FIXED_VALUE, gwy_field_mean(field));

    return corrected;
}

static void
sanitise_params(ModuleArgs *args)
{
    GwyParams *params = args->params;
    if (gwy_params_get_enum(params, PARAM_PRESET) == LATTICE_USER_DEFINED) {
        if (!(gwy_params_get_double(params, PARAM_A1) > 0.0))
            gwy_params_reset(params, PARAM_A1);
        if (gwy_params_get_boolean(params, PARAM_DIFFERENT_LENGTHS)) {
            if (!(gwy_params_get_double(params, PARAM_A2) > 0.0))
                gwy_params_reset(params, PARAM_A2);
        }
        else
            gwy_params_set_double(params, PARAM_A2, gwy_params_get_double(params, PARAM_A1));

        gdouble phi = gwy_canonicalize_angle(gwy_params_get_double(params, PARAM_PHI), TRUE, FALSE);
        if (phi < 1e-3 || phi > G_PI - 1e-3)
            phi = 0.5*G_PI;
        gwy_params_set_double(params, PARAM_PHI, phi);
    }
}

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