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

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

#define RUN_MODES GWY_RUN_INTERACTIVE

enum {
    PARAM_KEEP_OFFSETS,
    PARAM_CREATE_MASK,
    PARAM_UPDATE,
};

enum {
    NARGS = 8,
    USER_UNITS_ID = G_MAXINT,
};

enum {
    SENS_EXPR_OK   = 1 << 0,
    SENS_USERUINTS = 1 << 1
};

enum {
    STITCH_VALUE,
    STITCH_MASK,
    STITCH_DER_X,
    STITCH_DER_Y,
    STITCH_NVARS,
};

enum {
    STITCH_NARGS = NARGS * STITCH_NVARS + 2  /* 2 for x and y */
};

typedef enum {
    STITCH_OK   = 0,
    STITCH_DATA = 1,
} StitchError;

typedef GwyField* (*MakeFieldFunc)(GwyField *dfield);

typedef struct {
    GwyParams *params;
    GwyAppDataId objects[NARGS];
    gint nobjects_in_chooser;
    gint choosers[NARGS];
    gboolean enabled[NARGS];
    gdouble xoffset[NARGS];
    gdouble yoffset[NARGS];
    gdouble zoffset[NARGS];
    GwyField *result;
    GwyNield *mask;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GwyParamTable *table;
    StitchError err;
    GtkWidget *dialog;
    GtkWidget *view;
    GtkWidget *choosers[NARGS];
    GtkWidget *enabled[NARGS];
    GtkWidget *restore[NARGS];
    GtkAdjustment *xoffset[NARGS];
    GtkWidget *xoffset_spin[NARGS];
    GtkAdjustment *yoffset[NARGS];
    GtkWidget *yoffset_spin[NARGS];
    GtkAdjustment *zoffset[NARGS];
    GtkWidget *zoffset_spin[NARGS];
    GtkWidget *instant_update;
    GwyValueFormat *xyvf;
    GwyValueFormat *zvf;
} 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 void             image_chosen        (GwyDataChooser *chooser,
                                             ModuleGUI *gui);
static void             data_enabled        (ModuleGUI *gui,
                                             GtkToggleButton *check);
static void             offset_changed      (ModuleGUI *gui);
static void             restore_offset      (GtkWidget *button,
                                             ModuleGUI *gui);
static void             update_sensitivity  (ModuleGUI *gui);
static void             preview             (gpointer user_data);
static void             execute             (ModuleArgs *args);
static gboolean         other_image_filter  (GwyFile *data,
                                             gint id,
                                             gpointer user_data);
static void             get_object_ids      (GwyFile *data,
                                             gpointer user_data);
static void             sanitise_args       (ModuleArgs *args);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Stitch multiple images based on offsets of origins."),
    "Petr Grolich <petr.grolich@cmi.gov.cz>",
    "2.1",
    "Petr Klapetek & Petr Grolich",
    "2017",
};

GWY_MODULE_QUERY2(module_info, stitch)

static gboolean
module_register(void)
{
    gwy_image_func_register("stitch",
                            module_main,
                            N_("/M_ultidata/_Stitch..."),
                            GWY_ICON_STITCH,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Stitch images using offsets"));

    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, gwy_image_func_current());
    gwy_param_def_add_boolean(paramdef, PARAM_KEEP_OFFSETS, "keep_offsets", _("Keep global offsets"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_CREATE_MASK, "create_mask", _("Create _mask over exterior"), FALSE);
    gwy_param_def_add_instant_updates(paramdef, PARAM_UPDATE, "update", NULL, TRUE);
    return paramdef;
}

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

    ModuleArgs args;
    gwy_clear1(args);
    args.params = gwy_params_new_from_settings(define_module_params());
    /* FIXME: Why we make no attempt to take into account that the user has a particular image selected? We need to
     * populate the rows with images stitchable with the current one! */
    gwy_data_browser_foreach(get_object_ids, &args, NULL);

    gint datano, id;
    gwy_data_browser_get_current(GWY_APP_FIELD_ID, &id,
                                 GWY_APP_FILE_ID, &datano,
                                 0);
    sanitise_args(&args);
    args.result = gwy_field_new(PREVIEW_SIZE, PREVIEW_SIZE, 1.0, 1.0, TRUE);
    args.mask = gwy_nield_new(PREVIEW_SIZE, PREVIEW_SIZE);

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

    g_assert(!args.result ^ (outcome == GWY_DIALOG_HAVE_RESULT));
    if (outcome == GWY_DIALOG_PROCEED)
        execute(&args);

    gint newid = gwy_file_add_image(data, args.result);

    gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
    if (gwy_params_get_boolean(args.params, PARAM_CREATE_MASK))
        gwy_file_set_image_mask(data, newid, args.mask);
    gwy_file_set_title(data, GWY_FILE_IMAGE, newid, _("Stitched"), TRUE);
    gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                        data, GWY_FILE_IMAGE, newid,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE, FALSE);
    gwy_log_add(data, GWY_FILE_IMAGE, -1, newid);

end:
    g_object_unref(args.params);
    g_object_unref(args.result);
    g_object_unref(args.mask);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    ModuleGUI gui;
    gdouble xoffset, yoffset, zoffset;
    gchar *title;
    GwyField *dfield;
    GtkWidget *hbox, *vbox, *chooser, *label;
    GwyAppDataId* active_object;

    gwy_clear1(gui);
    gui.args = args;
    gui.err = STITCH_OK;

    dfield = gwy_file_get_image(data, id);
    gui.xyvf = gwy_field_get_value_format_xy(dfield, GWY_UNIT_FORMAT_VFMARKUP, NULL);
    gui.zvf = gwy_field_get_value_format_z(dfield, GWY_UNIT_FORMAT_VFMARKUP, NULL);

    gui.dialog = gwy_dialog_new(_("Stitch"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), hbox, FALSE, FALSE, 4);

    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
    /* Ensure no wild changes of the dialog size due to non-square data. */
    gtk_widget_set_size_request(vbox, PREVIEW_SIZE, PREVIEW_SIZE);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 4);

    gui.view = gwy_create_preview(args->result, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.view), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE);
    gtk_box_pack_start(GTK_BOX(vbox), gui.view, FALSE, FALSE, 0);
    gwy_set_data_preview_size(GWY_DATA_VIEW(gui.view), PREVIEW_SIZE);

    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 4);

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 2);
    gtk_grid_set_column_spacing(grid, 6);
    gtk_container_set_border_width(GTK_CONTAINER(grid), 4);
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(grid), FALSE, TRUE, 0);

    gint row = 0;
    label = gtk_label_new(_("Channels"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, row, 1, 1);

    title = g_strdup_printf("X offset [%s]", gui.xyvf->units);
    label = gtk_label_new(title);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 2, row, 1, 1);
    g_free(title);

    title = g_strdup_printf("Y offset [%s]", gui.xyvf->units);
    label = gtk_label_new(title);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 3, row, 1, 1);
    g_free(title);

    title = g_strdup_printf("Z offset [%s]", gui.zvf->units);
    label = gtk_label_new(title);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 4, row, 1, 1);
    g_free(title);

    row++;

    for (guint i = 0; i < NARGS; i++) {
        chooser = gwy_data_chooser_new(GWY_FILE_IMAGE);
        gwy_data_chooser_set_filter(GWY_DATA_CHOOSER(chooser), other_image_filter, &args->objects, NULL);
        /* XXX: This is weird. If we know we have fewer than NARGS images in total, we can just not show any more
         * rows, right? */
        if (i < args->nobjects_in_chooser)
            active_object = args->objects + i;
        else
            active_object = args->objects + args->nobjects_in_chooser - 1;

        gwy_data_chooser_set_active_id(GWY_DATA_CHOOSER(chooser), active_object);
        g_object_set_data(G_OBJECT(chooser), "id", GUINT_TO_POINTER(i));
        g_signal_connect(chooser, "changed", G_CALLBACK(image_chosen), &gui);
        gtk_grid_attach(grid, chooser, 0, row, 1, 1);
        gtk_label_set_mnemonic_widget(GTK_LABEL(label), chooser);
        gui.choosers[i] = chooser;

        gui.enabled[i] = gtk_check_button_new();
        g_object_set_data(G_OBJECT(gui.enabled[i]), "id", GUINT_TO_POINTER(i));
        args->enabled[i] = (i < args->nobjects_in_chooser);
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gui.enabled[i]), args->enabled[i]);
        gtk_widget_set_tooltip_text(gui.enabled[i], _("Stitch data"));
        gtk_grid_attach(grid, gui.enabled[i], 1, row, 1, 1);
        g_signal_connect_swapped(gui.enabled[i], "clicked", G_CALLBACK(data_enabled), &gui);

        xoffset = yoffset = zoffset = 0.0;
        data = gwy_data_browser_get_file(active_object->datano);
        if (data) {
            dfield = gwy_file_get_image(data, active_object->id);
            xoffset = gwy_field_get_xoffset(dfield);
            yoffset = gwy_field_get_yoffset(dfield);
            zoffset = gwy_field_mean(dfield);
        }

        gui.xoffset[i] = gtk_adjustment_new(0, -10000, 10000, 0.1, 1, 0);
        gui.xoffset_spin[i] = gtk_spin_button_new(gui.xoffset[i], 0, gui.xyvf->precision + 2);
        gtk_adjustment_set_value(gui.xoffset[i], xoffset/gui.xyvf->magnitude);
        g_signal_connect_swapped(gui.xoffset[i], "value-changed", G_CALLBACK(offset_changed), &gui);
        gtk_grid_attach(grid, gui.xoffset_spin[i], 2, row, 1, 1);

        gui.yoffset[i] = gtk_adjustment_new(0, -10000, 10000, 0.1, 1, 0);
        gui.yoffset_spin[i] = gtk_spin_button_new(gui.yoffset[i], 0, gui.xyvf->precision + 2);
        gtk_adjustment_set_value(gui.yoffset[i], yoffset/gui.xyvf->magnitude);
        g_signal_connect_swapped(gui.yoffset[i], "value-changed", G_CALLBACK(offset_changed), &gui);
        gtk_grid_attach(grid, gui.yoffset_spin[i], 3, row, 1, 1);

        gui.zoffset[i] = gtk_adjustment_new(0, -10000, 10000, 0.01, 1, 0);
        gui.zoffset_spin[i] = gtk_spin_button_new(gui.yoffset[i], 0, gui.zvf->precision + 2);
        gtk_adjustment_set_value(gui.zoffset[i], zoffset/gui.zvf->magnitude);
        g_signal_connect_swapped(gui.zoffset[i], "value-changed", G_CALLBACK(offset_changed), &gui);
        gtk_grid_attach(grid, gui.zoffset_spin[i], 4, row, 1, 1);

        gui.restore[i] = gtk_button_new_with_label(_("Restore"));
        g_object_set_data(G_OBJECT(gui.restore[i]), "id", GUINT_TO_POINTER(i));
        gtk_grid_attach(grid, gui.restore[i], 5, row, 1, 1);
        g_signal_connect(gui.restore[i], "clicked", G_CALLBACK(restore_offset), &gui);

        row++;
    }

    GwyParamTable *table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_checkbox(table, PARAM_KEEP_OFFSETS);
    gwy_param_table_append_checkbox(table, PARAM_CREATE_MASK);
    gwy_param_table_append_checkbox(table, PARAM_UPDATE);
    gtk_box_pack_start(GTK_BOX(vbox), gwy_param_table_widget(table), FALSE, TRUE, 0);

    update_sensitivity(&gui);
    //g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &gui);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);

    GwyDialogOutcome outcome = gwy_dialog_run(dialog);

    gwy_value_format_free(gui.xyvf);
    gwy_value_format_free(gui.zvf);

    return outcome;
}

static gboolean
other_image_filter(GwyFile *data,
                   gint id,
                   gpointer user_data)
{
    GwyAppDataId *dataid = (GwyAppDataId*)user_data;
    GwyField *op1 = gwy_file_get_image(data, id);
    GwyField *op2 = gwy_file_get_image(gwy_data_browser_get_file(dataid->datano), dataid->id);

    return !gwy_field_is_incompatible(op1, op2,
                                      GWY_DATA_MISMATCH_MEASURE
                                      | GWY_DATA_MISMATCH_LATERAL
                                      | GWY_DATA_MISMATCH_VALUE);
}

static void
get_object_ids(GwyFile *data, gpointer user_data)
{
    ModuleArgs *args = (ModuleArgs*)user_data;

    if (args->nobjects_in_chooser > NARGS)
        return;

    gint *ids = gwy_file_get_ids(data, GWY_FILE_IMAGE);
    for (gint i = 0; ids[i] >= 0; i++) {
        args->objects[args->nobjects_in_chooser].id = ids[i];
        args->objects[args->nobjects_in_chooser].datano = gwy_file_get_id(data);
        args->nobjects_in_chooser++;
    }
    g_free(ids);
}

static void
format_values(ModuleGUI *gui, guint i)
{
    ModuleArgs *args = gui->args;
    GwyFile *data = gwy_data_browser_get_file(args->objects[i].datano);
    g_return_if_fail(data);
    GwyField *dfield = gwy_file_get_image(data, args->objects[i].id);
    gtk_adjustment_set_value(gui->xoffset[i], gwy_field_get_xoffset(dfield)/gui->xyvf->magnitude);
    gtk_adjustment_set_value(gui->yoffset[i], gwy_field_get_yoffset(dfield)/gui->xyvf->magnitude);
    gtk_adjustment_set_value(gui->zoffset[i], gwy_field_mean(dfield)/gui->zvf->magnitude);
}

static void
image_chosen(GwyDataChooser *chooser, ModuleGUI *gui)
{
    guint i = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(chooser), "id"));
    ModuleArgs *args = gui->args;

    gwy_data_chooser_get_active_id(GWY_DATA_CHOOSER(gui->choosers[i]), &args->objects[i]);
    format_values(gui, i);
}

static void
data_enabled(ModuleGUI *gui, GtkToggleButton *check)
{
    guint i = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(check), "id"));
    ModuleArgs *args = gui->args;
    args->enabled[i] = gtk_toggle_button_get_active(check);

    gui->err |= STITCH_DATA;
    for (guint j = 0; j < NARGS; j++) {
        if (args->enabled[j]) {
            gui->err &= ~STITCH_DATA;
            break;
        }
    }

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

static void
offset_changed(ModuleGUI *gui)
{
    for (guint i = 0; i < NARGS; i++) {
        gui->args->xoffset[i] = gtk_adjustment_get_value(gui->xoffset[i]) * gui->xyvf->magnitude;
        gui->args->yoffset[i] = gtk_adjustment_get_value(gui->yoffset[i]) * gui->xyvf->magnitude;
        gui->args->zoffset[i] = gtk_adjustment_get_value(gui->zoffset[i]) * gui->zvf->magnitude;
    }

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

static void
restore_offset(GtkWidget *button, ModuleGUI *gui)
{
    guint i = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(button), "id"));
    ModuleArgs *args = gui->args;

    gwy_data_chooser_get_active_id(GWY_DATA_CHOOSER(gui->choosers[i]), &args->objects[i]);
    format_values(gui, i);
}

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

    for (guint i = 0; i < NARGS; i++) {
        gboolean sensitive = args->enabled[i];
        gtk_widget_set_sensitive(gui->choosers[i], sensitive);
        gtk_widget_set_sensitive(gui->xoffset_spin[i], sensitive);
        gtk_widget_set_sensitive(gui->yoffset_spin[i], sensitive);
        gtk_widget_set_sensitive(gui->zoffset_spin[i], sensitive);
        gtk_widget_set_sensitive(gui->restore[i], sensitive);
    }

    gboolean ok = (gui->err == STITCH_OK);
    GtkDialog *dialog = GTK_DIALOG(gui->dialog);
    gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_OK, ok);
    // XXX: GwyDialog should take care of this
    //gtk_dialog_set_response_sensitive(dialog, RESPONSE_PREVIEW, (ok && !args->instant_update));
}

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

    /* We can also get here by activation of the entry so check again. */
    if (gui->err != STITCH_OK) {
        gwy_data_view_set_field(GWY_DATA_VIEW(gui->view), NULL);
        gwy_set_data_preview_size(GWY_DATA_VIEW(gui->view), PREVIEW_SIZE);
        return;
    }

    ModuleArgs *args = gui->args;
    execute(args);
    g_return_if_fail(args->result);

    gwy_field_data_changed(args->result);
    gwy_data_view_set_field(GWY_DATA_VIEW(gui->view), args->result);
    gwy_set_data_preview_size(GWY_DATA_VIEW(gui->view), PREVIEW_SIZE);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
execute(ModuleArgs *args)
{
    gboolean keep_offsets = gwy_params_get_boolean(args->params, PARAM_KEEP_OFFSETS);
    GwyField *dfield = NULL, *df_shift, *result = args->result;
    GwyNield *mask = args->mask;
    gint i, xres = 0, yres = 0, width, height, nfields = 0, destcol, destrow;
    gdouble xreal = 0.0, yreal = 0.0, left = 0.0, top = 0.0, right = 0.0, bottom = 0.0, x, y;
    GwyUnit *siunitxy = NULL, *siunitz = NULL;
    GwyFile *data;

    for (i = 0; i < NARGS; i++) {
        if (!args->enabled[i])
            continue;

        data = gwy_data_browser_get_file(args->objects[i].datano);
        g_return_if_fail(data);
        dfield = gwy_file_get_image(data, args->objects[i].id);

        if (!nfields) {
            left = args->xoffset[i];
            top = args->yoffset[i];
            right = args->xoffset[i] + gwy_field_get_xreal(dfield);
            bottom = args->yoffset[i] + gwy_field_get_yreal(dfield);
            siunitxy = gwy_field_get_unit_xy(dfield);
            siunitz = gwy_field_get_unit_z(dfield);
        }
        else {
            left = fmin(left, args->xoffset[i]);
            top = fmin(top, args->yoffset[i]);
            right = fmax(right, args->xoffset[i] + gwy_field_get_xreal(dfield));
            bottom = fmax(bottom, args->yoffset[i] + gwy_field_get_yreal(dfield));
        }

        nfields++;
    }

    xreal = right - left;
    yreal = bottom - top;

    g_return_if_fail((xreal > 0.0) && (yreal > 0.0) && nfields && dfield);

    xres = GWY_ROUND(gwy_field_rtoj(dfield, xreal));
    yres = GWY_ROUND(gwy_field_rtoi(dfield, yreal));
    xres = MAX(xres, 1);
    yres = MAX(yres, 1);

    gwy_field_resize(result, xres, yres);
    gwy_field_clear(result);
    gwy_field_set_xreal(result, xreal);
    gwy_field_set_yreal(result, yreal);
    gwy_field_set_xoffset(result, keep_offsets ? left : 0.0);
    gwy_field_set_yoffset(result, keep_offsets ? top : 0.0);
    gwy_unit_assign(gwy_field_get_unit_xy(result), siunitxy);
    gwy_unit_assign(gwy_field_get_unit_z(result), siunitz);

    gwy_nield_resize(mask, xres, yres);
    gwy_nield_fill(mask, 1);

    for (i = 0; i < NARGS; i++) {
        if (!args->enabled[i])
            continue;

        data = gwy_data_browser_get_file(args->objects[i].datano);
        dfield = gwy_file_get_image(data, args->objects[i].id);
        width = gwy_field_get_xres(dfield);
        height = gwy_field_get_yres(dfield);
        x = args->xoffset[i] - left;
        y = args->yoffset[i] - top;
        destcol = GWY_ROUND(gwy_field_rtoj(dfield, x));
        destrow = GWY_ROUND(gwy_field_rtoi(dfield, y));
        if (destcol < xres && destrow < yres && destcol + width > 0 && destrow + height > 0) {
            /* shift data to provide consistent data */
            df_shift = gwy_field_copy(dfield);
            gwy_field_add(df_shift, -args->zoffset[i]);

            /* Deal with odd rounding errors. */
            if (destcol < 0) {
                width += destcol;
                destcol = 0;
            }
            if (destrow < 0) {
                width += destrow;
                destrow = 0;
            }
            width = MIN(width, xres - destcol);
            height = MIN(height, yres - destrow);

            gwy_field_area_copy(df_shift, result, 0, 0, width, height, destcol, destrow);
            gwy_nield_area_clear(mask, destcol, destrow, width, height);
            g_object_unref(df_shift);
        }
    }
}

static void
sanitise_args(ModuleArgs *args)
{
    for (gint i = 1; i < args->nobjects_in_chooser; i++) {
        if (!gwy_app_data_id_verify_channel(args->objects + i))
            args->objects[i] = args->objects[0];
    }
}

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