/*
 *  $Id: mcrop.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2010-2024 David Necas (Yeti), Petr Klapetek, Daniil Bratashov.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net, dn2010@gmail.com
 *
 *  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"

#define RUN_MODES GWY_RUN_INTERACTIVE

#define FIT_GRADIENT_NAME "__GwyFitDiffGradient"

enum {
    PARAM_OTHER_IMAGE,
    PARAM_NEW_IMAGE,
    PARAM_DISPLAY,
    PARAM_DIFF_COLOURMAP,
    PARAM_METHOD,
    PARAM_KERNEL_FRAC,

    INFO_OFFSET,
};

enum {
    MCROP_CORR_SEARCH_BRUTE_FORCE = 999,
};

typedef enum {
    MCROP_DISPLAY_CROPPED      = 0,
    MCROP_DISPLAY_CROPPEDOTHER = 1,
    MCROP_DISPLAY_DIFFERENCE   = 2,
} MCropDisplayType;

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *otherfield;
    GwyField *cropped;
    GwyField *croppedother;
    GwyField *diff;
    gint xoff;
    gint yoff;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GtkWidget *dataview;
    GwyParamTable *table;
    GwyGradient *orig_gradient;
    GwyGradient *diff_gradient;
} 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             param_changed       (ModuleGUI *gui,
                                             gint id);
static void             preview             (gpointer user_data);
static gboolean         other_image_filter  (GwyFile *data,
                                             gint id,
                                             gpointer user_data);
static gboolean         execute             (ModuleArgs *args,
                                             GtkWindow *wait_window);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Crops non-intersecting regions of two images."),
    "Daniil Bratashov <dn2010@gmail.com>",
    "2.0",
    "David Nečas (Yeti) & Petr Klapetek & Daniil Bratashov",
    "2010",
};

GWY_MODULE_QUERY2(module_info, mcrop)

static gboolean
module_register(void)
{
    gwy_image_func_register("mcrop",
                              module_main,
                              N_("/M_ultidata/Mutual C_rop..."),
                              GWY_ICON_MUTUAL_CROP,
                              RUN_MODES,
                              GWY_MENU_FLAG_IMAGE,
                              N_("Crop non-intersecting regions of two images"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum methods[] = {
        { N_("Correlation score"),       GWY_CORR_SEARCH_COVARIANCE_SCORE,  },
        { N_("Height difference score"), GWY_CORR_SEARCH_HEIGHT_DIFF_SCORE, },
        { N_("Phase-only score"),        GWY_CORR_SEARCH_PHASE_ONLY_SCORE,  },
        { N_("Brute force score"),       MCROP_CORR_SEARCH_BRUTE_FORCE,     },
    };
    static const GwyEnum displays[] = {
        { N_("Cropped first"),  MCROP_DISPLAY_CROPPED,      },
        { N_("Cropped second"), MCROP_DISPLAY_CROPPEDOTHER, },
        { N_("Difference"),     MCROP_DISPLAY_DIFFERENCE,   },
    };
    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_image_id(paramdef, PARAM_OTHER_IMAGE, "other_image", _("Second _image"));
    gwy_param_def_add_boolean(paramdef, PARAM_NEW_IMAGE, "new-image", _("Create new images"), TRUE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_DISPLAY, "display", C_("verb", "_Display"),
                              displays, G_N_ELEMENTS(displays), MCROP_DISPLAY_DIFFERENCE);
    gwy_param_def_add_boolean(paramdef, PARAM_DIFF_COLOURMAP, "diff_colourmap",
                              _("Show differences with _adapted color map"), TRUE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_METHOD, "method", _("_Method"),
                              methods, G_N_ELEMENTS(methods), GWY_CORR_SEARCH_PHASE_ONLY_SCORE);
    gwy_param_def_add_double(paramdef, PARAM_KERNEL_FRAC, "kernel_frac", _("_Kernel relative size"), 0.2, 0.8, 0.4);

    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    GwyDialogOutcome outcome;
    GwyAppDataId dataid;
    GwyFile *otherdata;
    ModuleArgs args;
    gint id, otherid, newid;
    GQuark quark, otherquark;

    g_return_if_fail(mode & RUN_MODES);

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

    args.params = gwy_params_new_from_settings(define_module_params());
    args.cropped = gwy_field_new(1, 1, 1, 1, TRUE);
    args.croppedother = gwy_field_new(1, 1, 1, 1, TRUE);
    args.diff = gwy_field_new(1, 1, 1, 1, TRUE);

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

    dataid = gwy_params_get_data_id(args.params, PARAM_OTHER_IMAGE);
    otherdata = gwy_data_browser_get_file(dataid.datano);
    otherid = dataid.id;

    /* We may act on images from two different files.  Undo and sorting things into files are a bit complicated. */
    if (outcome != GWY_DIALOG_HAVE_RESULT
        && !execute(&args, gwy_data_browser_get_window_for_data(data, GWY_FILE_IMAGE, id)))
        goto end;

    if (gwy_params_get_boolean(args.params, PARAM_NEW_IMAGE)) {
        newid = gwy_file_add_image(data, args.cropped);
        gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
        gwy_log_add(data, GWY_FILE_IMAGE, id, newid);

        newid = gwy_file_add_image(otherdata, args.croppedother);

        gwy_file_set_visible(otherdata, GWY_FILE_IMAGE, newid, TRUE);
        gwy_log_add(otherdata, GWY_FILE_IMAGE, otherid, newid);
    }
    else {
        quark = gwy_file_key_image(id);
        otherquark = gwy_file_key_image(otherid);
        if (otherdata == data)
            gwy_app_undo_qcheckpoint(GWY_DICT(data), quark, otherquark, 0);
        else {
            gwy_app_undo_qcheckpoint(GWY_DICT(data), quark, 0);
            gwy_app_undo_qcheckpoint(GWY_DICT(otherdata), otherquark, 0);
        }
        gwy_file_set_image(data, id, args.cropped);
        gwy_file_set_image(otherdata, otherid, args.croppedother);
        gwy_log_add(data, GWY_FILE_IMAGE, id, id);
        gwy_log_add(otherdata, GWY_FILE_IMAGE, otherid, otherid);
    }

end:
    g_object_unref(args.cropped);
    g_object_unref(args.croppedother);
    g_object_unref(args.diff);
    g_object_unref(args.params);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GwyDialogOutcome outcome;
    ModuleGUI gui;
    GwyDialog *dialog;
    GtkWidget *hbox;
    GwyParamTable *table;

    gui.args = args;
    gui.orig_gradient = gwy_gradients_get_gradient(gwy_file_get_palette(data, GWY_FILE_IMAGE, id));
    gui.diff_gradient = g_object_new(GWY_TYPE_GRADIENT, "name", FIT_GRADIENT_NAME, NULL);

    gui.dialog = gwy_dialog_new(_("Mutual Crop"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    gui.dataview = gwy_create_preview(args->diff, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.dataview), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE);
    hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(gui.dataview), FALSE);

    table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_image_id(table, PARAM_OTHER_IMAGE);
    gwy_param_table_data_id_set_filter(table, PARAM_OTHER_IMAGE, other_image_filter, args->field, NULL);
    gwy_param_table_append_combo(table, PARAM_METHOD);
    gwy_param_table_append_slider(table, PARAM_KERNEL_FRAC);
    gwy_param_table_append_checkbox(table, PARAM_NEW_IMAGE);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_radio(table, PARAM_DISPLAY);
    gwy_param_table_append_checkbox(table, PARAM_DIFF_COLOURMAP);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_info(table, INFO_OFFSET, _("Second image offset"));
    gwy_param_table_set_unitstr(table, INFO_OFFSET, _("px"));

    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

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

    outcome = gwy_dialog_run(dialog);

    g_object_unref(gui.diff_gradient);

    return outcome;
}

static void
update_display(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    MCropDisplayType display = gwy_params_get_enum(args->params, PARAM_DISPLAY);
    gboolean diff_colourmap = gwy_params_get_boolean(gui->args->params, PARAM_DIFF_COLOURMAP);
    GwyColorMappingType mapping = GWY_COLOR_MAPPING_FULL;
    GwyDataView *dataview = GWY_DATA_VIEW(gui->dataview);
    gdouble min, max, dispmin, dispmax;

    if (display == MCROP_DISPLAY_CROPPED) {
        gwy_data_view_set_field(dataview, args->cropped);
    }
    else if (display == MCROP_DISPLAY_CROPPEDOTHER) {
        gwy_data_view_set_field(dataview, args->croppedother);
    }
    else if (display == MCROP_DISPLAY_DIFFERENCE) {
        gwy_data_view_set_field(dataview, args->diff);
        if (diff_colourmap) {
            mapping = GWY_COLOR_MAPPING_FIXED;
            gwy_field_min_max(args->diff, &min, &max);
            gwy_field_autorange(args->diff, &dispmin, &dispmax);
            gwy_set_gradient_for_residuals(gui->diff_gradient, min, max, &dispmin, &dispmax);
            gwy_data_view_set_fixed_color_range(dataview, dispmin, dispmax);
            gwy_data_view_set_gradient(dataview, gui->diff_gradient);
        }
        else
            mapping = GWY_COLOR_MAPPING_AUTO;
    }

    gwy_data_view_set_color_mapping(dataview, mapping);
    if (mapping != GWY_COLOR_MAPPING_FIXED)
        gwy_data_view_set_gradient(dataview, gui->orig_gradient);
    gwy_set_data_preview_size(GWY_DATA_VIEW(gui->dataview), PREVIEW_SIZE);
    /* Prevent the size changing wildly the moment someone touches the size adjbars. */
    gtk_widget_set_size_request(gui->dataview, PREVIEW_SIZE, PREVIEW_SIZE);
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;

    if (id < 0 || id == PARAM_OTHER_IMAGE) {
        gboolean is_none = gwy_params_data_id_is_none(params, PARAM_OTHER_IMAGE);

        args->otherfield = gwy_params_get_image(params, PARAM_OTHER_IMAGE);
        gtk_dialog_set_response_sensitive(GTK_DIALOG(gui->dialog), GTK_RESPONSE_OK, !is_none);
    }

    if (id < 0 || id == PARAM_DISPLAY || id == PARAM_DIFF_COLOURMAP)
        update_display(gui);

    if (id != PARAM_DISPLAY && id != PARAM_DIFF_COLOURMAP && id != PARAM_NEW_IMAGE)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;
    gchar *s;

    if (!execute(args, GTK_WINDOW(gui->dialog))) {
        gwy_param_table_info_set_valuestr(gui->table, INFO_OFFSET, "");
        return;
    }

    gwy_field_data_changed(args->diff);
    update_display(gui);
    s = g_strdup_printf("(%d, %d)", args->xoff, args->yoff);
    gwy_param_table_info_set_valuestr(gui->table, INFO_OFFSET, s);
    g_free(s);

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

static gboolean
other_image_filter(GwyFile *data, gint id, gpointer user_data)
{
    GwyField *otherfield, *field = (GwyField*)user_data;

    if (!(otherfield = gwy_file_get_image(data, id)))
        return FALSE;
    if (otherfield == field)
        return FALSE;
    return !gwy_field_is_incompatible(otherfield, field,
                                      GWY_DATA_MISMATCH_MEASURE
                                      | GWY_DATA_MISMATCH_LATERAL
                                      | GWY_DATA_MISMATCH_VALUE);
}

static gboolean
get_score_iteratively(GwyField *field, GwyField *kernel_field, GwyField *score,
                      GtkWindow *wait_window)
{
    GwyComputationState *state;
    gboolean ok = FALSE;

    state = gwy_field_correlate_init(field, kernel_field, score);
    gwy_app_wait_start(wait_window, _("Initializing..."));
    gwy_field_correlate_iteration(state);
    if (!gwy_app_wait_set_message(_("Correlating...")))
        goto cancelled;

    do {
        gwy_field_correlate_iteration(state);
        if (!gwy_app_wait_set_fraction(state->fraction))
            goto cancelled;
    } while (state->state != GWY_COMPUTATION_STATE_FINISHED);

    ok = TRUE;

cancelled:
    gwy_field_correlate_finalize(state);
    gwy_app_wait_finish();

    return ok;
}

static gboolean
execute(ModuleArgs *args, GtkWindow *wait_window)
{
    GwyParams *params = args->params;
    GwyField *field1 = args->field, *field2 = args->otherfield;
    GwyField *correlation_data, *correlation_kernel, *correlation_score;
    guint method = gwy_params_get_enum(params, PARAM_METHOD);
    gdouble kfrac = gwy_params_get_double(params, PARAM_KERNEL_FRAC);
    GdkRectangle cdata;
    gint max_col, max_row;
    gint x1l, x1r, y1t, y1b, x2l, x2r, y2t, y2b;
    gint xres1, xres2, yres1, yres2;
    gint kxres, kyres, kx, ky;
    gdouble x, y, z;
    gboolean ok = FALSE, swapped = FALSE;

    xres1 = gwy_field_get_xres(field1);
    xres2 = gwy_field_get_xres(field2);
    yres1 = gwy_field_get_yres(field1);
    yres2 = gwy_field_get_yres(field2);

    if (xres1*yres1 < xres2*yres2) {
        GWY_SWAP(GwyField*, field1, field2);
        GWY_SWAP(gint, xres1, xres2);
        GWY_SWAP(gint, yres1, yres2);
        swapped = TRUE;
    }

    cdata.x = 0;
    cdata.y = 0;
    cdata.width = xres1;
    cdata.height = yres1;
    kxres = GWY_ROUND(kfrac*cdata.width) | 1;
    kxres = MIN(xres2, kxres);
    kyres = GWY_ROUND(kfrac*cdata.height) | 1;
    kyres = MIN(yres2, kyres);
    kx = MAX(0, xres2/2 - kxres/2);
    ky = MAX(0, yres2/2 - kyres/2);

    correlation_data = gwy_field_area_extract(field1, cdata.x, cdata.y, cdata.width, cdata.height);
    correlation_kernel = gwy_field_area_extract(field2, kx, ky, kxres, kyres);
    correlation_score = gwy_field_new_alike(correlation_data, FALSE);

    if (method == MCROP_CORR_SEARCH_BRUTE_FORCE) {
        if (!get_score_iteratively(correlation_data, correlation_kernel, correlation_score, wait_window))
            goto end;
    }
    else {
        gwy_field_correlation_search(correlation_data, correlation_kernel, NULL, correlation_score,
                                     method, 0.1, GWY_EXTERIOR_BORDER, 0.0);
    }

    gwy_field_get_local_maxima_list(correlation_score, &x, &y, &z, 1, 0.0, -G_MAXDOUBLE, FALSE);
    max_col = GWY_ROUND(x);
    max_row = GWY_ROUND(y);
    gwy_debug("c: %d %d %dx%d  k: %d %d %dx%d",
              cdata.x, cdata.y,
              cdata.width, cdata.height,
              kx, ky, kxres, kyres);
    gwy_debug("max: %d %d", max_col, max_row);
    args->xoff = max_col - xres2/2;
    args->yoff = max_col - yres2/2;

    x1l = MAX(0, MAX(max_col - xres1/2, max_col - xres2/2));
    y1b = MAX(0, MAX(max_row - yres1/2, max_row - yres2/2));
    x1r = MIN(xres1, MIN(max_col + xres1/2, max_col + xres2/2));
    y1t = MIN(yres1, MIN(max_row + yres1/2, max_row + yres2/2));

    x2l = MAX(0, xres2/2 - max_col);
    x2r = x2l + x1r - x1l;
    y2b = MAX(0, yres2/2 - max_row);
    y2t = y2b + y1t - y1b;

    gwy_debug("%d %d %d %d", x1l, y1b, x1r, y1t);
    gwy_debug("%d %d %d %d", x2l, y2b, x2r, y2t);

    gwy_field_assign(args->cropped, field1);
    gwy_field_crop(args->cropped, x1l, y1b, x1r - x1l, y1t - y1b);

    gwy_field_assign(args->croppedother, field2);
    gwy_field_crop(args->croppedother, x2l, y2b, x2r - x2l, y2t - y2b);

    if (swapped) {
        GWY_SWAP(GwyField*, args->cropped, args->croppedother);
        args->xoff = -args->xoff;
        args->yoff = -args->yoff;
    }

    gwy_field_assign(args->diff, args->cropped);
    gwy_field_subtract_fields(args->diff, args->diff, args->croppedother);

    ok = TRUE;

end:

    g_object_unref(correlation_score);
    g_object_unref(correlation_data);
    g_object_unref(correlation_kernel);

    return ok;
}

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