/*
 *  $Id: correct_perspective.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2021-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)

typedef enum {
    IMAGE_DATA,
    IMAGE_CORRECTED,
} ImageMode;

enum {
    PARAM_INTERPOLATION,
    PARAM_FIXRES,
    PARAM_XRES,
    PARAM_YRES,
    PARAM_IMAGE_MODE,
    PARAM_NEW_IMAGE,
    PARAM_DISTRIBUTE,
};

typedef struct {
    GwyParams *params;
    GwyField *field;
    gdouble xy[8];
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table;
    GtkWidget *view;
    GwyVectorLayer *vlayer;
    GwySelection *selection;
    GwyField *corrected;
} ModuleGUI;

static gboolean         module_register                (void);
static GwyParamDef*     define_module_params           (void);
static void             module_main                    (GwyFile *data,
                                                        GwyRunModeFlags mode);
static void             gather_quarks_for_one_image    (GwyDict *data,
                                                        gint id,
                                                        GArray *quarks);
static void             apply_correction_to_one_image  (ModuleArgs *args,
                                                        GwyFile *data,
                                                        gint id);
static GwyDialogOutcome run_gui                        (ModuleArgs *args,
                                                        GwyFile *data,
                                                        gint id);
static void             param_changed                  (ModuleGUI *gui,
                                                        gint id);
static void             dialog_response                (GwyDialog *dialog,
                                                        gint response,
                                                        ModuleGUI *gui);
static void             preview                        (gpointer user_data);
static void             init_coordinates               (GwyField *field,
                                                        gdouble *xy);
static void             selection_changed              (ModuleGUI *gui);
static void             guess_pixel_dimensions         (GwyParamTable *table,
                                                        GwySelection *selection,
                                                        GwyField *field);
static GwyField*        create_corrected_field         (GwyField *field,
                                                        const gdouble *xy,
                                                        gint xres,
                                                        gint yres,
                                                        GwyInterpolationType interp);
static gboolean         solve_projection_from_rectangle(const gdouble *xy,
                                                        gint xres,
                                                        gint yres,
                                                        gdouble *matrix);
static void             estimate_reasonable_dimensions (const gdouble *xy,
                                                        gdouble *lx,
                                                        gdouble *ly);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Corrects or applies perspective distortion of images."),
    "Yeti <yeti@gwyddion.net>",
    "2.3",
    "David Nečas (Yeti)",
    "2021",
};

GWY_MODULE_QUERY2(module_info, correct_perspective)

static gboolean
module_register(void)
{
    gwy_image_func_register("correct_perspective",
                            module_main,
                            N_("/_Distortion/_Perspective..."),
                            GWY_ICON_PERSPECTIVE_DISTORT,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Correct perspective distortion"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum image_modes[] = {
        { N_("_Data"),           IMAGE_DATA,      },
        { N_("Correc_ted data"), IMAGE_CORRECTED, },
    };
    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_enum(paramdef, PARAM_INTERPOLATION, "interpolation", NULL, GWY_TYPE_INTERPOLATION_TYPE,
                           GWY_INTERPOLATION_LINEAR);
    gwy_param_def_add_boolean(paramdef, PARAM_FIXRES, "fixres", _("Specify output _dimensions"), FALSE);
    gwy_param_def_add_int(paramdef, PARAM_XRES, "xres", _("_X resolution"), 2, 32768, 512);
    gwy_param_def_add_int(paramdef, PARAM_YRES, "yres", _("_Y resolution"), 2, 32768, 512);
    gwy_param_def_add_gwyenum(paramdef, PARAM_IMAGE_MODE, NULL, C_("verb", "Display"),
                              image_modes, G_N_ELEMENTS(image_modes), IMAGE_DATA);
    gwy_param_def_add_boolean(paramdef, PARAM_NEW_IMAGE, "new-image", _("Create new image"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_DISTRIBUTE, "distribute", _("_Apply to all compatible images"), FALSE);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    enum { compat_flags = GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_LATERAL };
    ModuleArgs args;
    GwyParams *params;
    GwyField *field;
    GwySelection *selection;
    GwyDialogOutcome outcome;
    gint i, id;
    gboolean distribute, new_image;
    GArray *undo_quarks;
    gint *image_ids;

    g_return_if_fail(mode & RUN_MODES);
    gwy_data_browser_get_current(GWY_APP_FIELD, &field,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(field);

    gwy_clear1(args);
    args.field = field;
    args.params = params = gwy_params_new_from_settings(define_module_params());
    init_coordinates(field, args.xy);
    GQuark selection_key = gwy_file_key_image_selection(id, "projective");
    if (gwy_dict_gis_object(GWY_DICT(data), selection_key, &selection)
        && gwy_selection_get_data(GWY_SELECTION(selection), NULL))
        gwy_selection_get_object(GWY_SELECTION(selection), 0, args.xy);

    outcome = run_gui(&args, data, id);
    gwy_params_save_to_settings(params);

    selection = gwy_selection_quad_new();
    gwy_selection_set_object(GWY_SELECTION(selection), 0, args.xy);
    gwy_dict_pass_object(GWY_DICT(data), selection_key, selection);

    if (outcome != GWY_DIALOG_PROCEED)
        goto end;

    new_image = gwy_params_get_boolean(params, PARAM_NEW_IMAGE);
    distribute = gwy_params_get_boolean(params, PARAM_DISTRIBUTE);
    if (!distribute) {
        if (!new_image) {
            undo_quarks = g_array_new(FALSE, FALSE, sizeof(GQuark));
            gather_quarks_for_one_image(GWY_DICT(data), id, undo_quarks);
            gwy_app_undo_qcheckpointv(GWY_DICT(data), undo_quarks->len, (GQuark*)undo_quarks->data);
            g_array_free(undo_quarks, TRUE);
        }
        apply_correction_to_one_image(&args, data, id);
        goto end;
    }

    image_ids = gwy_file_get_ids(data, GWY_FILE_IMAGE);
    g_object_ref(field);
    if (!new_image) {
        undo_quarks = g_array_new(FALSE, FALSE, sizeof(GQuark));
        for (i = 0; image_ids[i] != -1; i++) {
            GwyField *otherfield = gwy_file_get_image(data, image_ids[i]);
            if (!gwy_field_is_incompatible(field, otherfield, compat_flags))
                gather_quarks_for_one_image(GWY_DICT(data), image_ids[i], undo_quarks);
        }
        gwy_app_undo_qcheckpointv(GWY_DICT(data), undo_quarks->len, (GQuark*)undo_quarks->data);
        g_array_free(undo_quarks, TRUE);
    }
    for (i = 0; image_ids[i] != -1; i++) {
        GwyField *otherfield = gwy_file_get_image(data, image_ids[i]);
        if (gwy_field_is_incompatible(field, otherfield, compat_flags))
            continue;

        apply_correction_to_one_image(&args, data, image_ids[i]);
    }
    g_object_unref(field);
    g_free(image_ids);

end:
    g_object_unref(params);
}

static void
gather_quarks_for_one_image(GwyDict *data, gint id, GArray *quarks)
{
    GObject *object;
    GQuark quark;

    quark = gwy_file_key_image(id);
    object = gwy_dict_get_object(data, quark);
    g_assert(GWY_IS_FIELD(object));
    g_array_append_val(quarks, quark);

    quark = gwy_file_key_image_mask(id);
    if (gwy_dict_gis_object(data, quark, &object) && GWY_IS_NIELD(object))
        g_array_append_val(quarks, quark);

    quark = gwy_file_key_image_picture(id);
    if (gwy_dict_gis_object(data, quark, &object) && GWY_IS_FIELD(object))
        g_array_append_val(quarks, quark);
}

static void
apply_correction_to_one_image(ModuleArgs *args, GwyFile *data, gint id)
{
    GwyParams *params = args->params;
    GwyInterpolationType interpolation = gwy_params_get_enum(params, PARAM_INTERPOLATION);
    gboolean new_image = gwy_params_get_boolean(args->params, PARAM_NEW_IMAGE);
    gboolean distribute = gwy_params_get_boolean(params, PARAM_DISTRIBUTE);
    gboolean fixres = gwy_params_get_boolean(params, PARAM_FIXRES);
    gint xres = (fixres ? gwy_params_get_int(params, PARAM_XRES) : 0);
    gint yres = (fixres ? gwy_params_get_int(params, PARAM_YRES) : 0);
    GwyField *field = gwy_file_get_image(data, id), *show = NULL;
    GwyNield *mask = gwy_file_get_image_mask(data, id);
    gchar *title, *newtitle;
    gint newid;

    g_assert(GWY_IS_FIELD(field));

    gwy_dict_gis_object(GWY_DICT(data), gwy_file_key_image_picture(id), &show);

    field = create_corrected_field(field, args->xy, xres, yres, interpolation);
    if (!new_image) {
        gwy_file_set_image(data, id, field);
        newid = id;
    }
    else {
        newid = gwy_file_add_image(data, field);
        if (!distribute)
            gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
        title = gwy_file_get_display_title(data, GWY_FILE_IMAGE, id);
        newtitle = g_strconcat(title, " ", _("Corrected"), NULL);
        gwy_file_set_title(data, GWY_FILE_IMAGE, newid, newtitle, TRUE);
        g_free(newtitle);
        g_free(title);
    }
    g_object_unref(field);

    if (mask) {
        /* Mask transformation is hard. Just transform it as a Field and threshold to 0 and 1. */
        gint origxres = gwy_nield_get_xres(mask), origyres = gwy_nield_get_yres(mask);
        GwyField *mask_field = gwy_field_new(origxres, origyres, origxres, origyres, FALSE);
        gwy_field_fill_mask(mask_field, mask, 0.0, 1.0);
        field = create_corrected_field(mask_field, args->xy, xres, yres, GWY_INTERPOLATION_ROUND);
        g_object_unref(mask_field);
        mask = gwy_field_new_nield_alike(field);
        gwy_nield_mark_by_threshold(mask, mask_field, 0.5, TRUE);
        g_object_unref(mask_field);
        gwy_file_pass_image_mask(data, newid, mask);
    }
    if (show) {
        show = create_corrected_field(show, args->xy, xres, yres, interpolation);
        gwy_dict_pass_object(GWY_DICT(data), gwy_file_key_image_picture(newid), show);
    }

    if (!new_image) {
        gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_RANGE
                            | GWY_FILE_ITEM_MASK_COLOR | GWY_FILE_ITEM_REAL_SQUARE,
                            FALSE);
    }
    gwy_log_add(data, GWY_FILE_IMAGE, id, newid);
}

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

    gwy_clear1(gui);
    gui.args = args;

    gui.dialog = gwy_dialog_new(_("Projective Correction"));
    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_COLOR_MAPPING | GWY_FILE_ITEM_RANGE
                        | GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE);
    gui.selection = gwy_create_preview_vector_layer(GWY_DATA_VIEW(gui.view), GWY_TYPE_LAYER_QUAD, 1, TRUE);
    g_object_ref(gui.selection);
    gwy_selection_set_data(gui.selection, 1, args->xy);
    gui.vlayer = gwy_data_view_get_interactive_layer(GWY_DATA_VIEW(gui.view));
    gwy_layer_quad_set_n_lines(GWY_LAYER_QUAD(gui.vlayer), 3);
    g_object_ref(gui.vlayer);
    g_signal_connect_swapped(gui.selection, "changed", G_CALLBACK(selection_changed), &gui);

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

    table = gui.table = gwy_param_table_new(args->params);

    gwy_param_table_append_radio(table, PARAM_IMAGE_MODE);
    gwy_param_table_append_combo(table, PARAM_INTERPOLATION);

    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_FIXRES);
    gwy_param_table_append_slider(table, PARAM_XRES);
    gwy_param_table_set_unitstr(table, PARAM_XRES, _("px"));
    gwy_param_table_set_no_reset(table, PARAM_XRES, TRUE);
    gwy_param_table_append_slider(table, PARAM_YRES);
    gwy_param_table_set_unitstr(table, PARAM_YRES, _("px"));
    gwy_param_table_set_no_reset(table, PARAM_YRES, TRUE);

    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_NEW_IMAGE);
    gwy_param_table_append_checkbox(table, PARAM_DISTRIBUTE);

    if (!gwy_params_get_boolean(args->params, PARAM_FIXRES))
        guess_pixel_dimensions(table, gui.selection, args->field);

    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);
    g_signal_connect_after(dialog, "response", G_CALLBACK(dialog_response), &gui);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);

    outcome = gwy_dialog_run(dialog);

    g_object_unref(gui.selection);
    g_object_unref(gui.vlayer);

    return outcome;
}

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

    if (id < 0 || id == PARAM_FIXRES) {
        gboolean fixres = gwy_params_get_boolean(params, PARAM_FIXRES);
        gwy_param_table_set_sensitive(table, PARAM_XRES, fixres);
        gwy_param_table_set_sensitive(table, PARAM_YRES, fixres);
        if (!fixres)
            guess_pixel_dimensions(table, gui->selection, gui->args->field);
    }
    if (id < 0 || id == PARAM_INTERPOLATION) {
        g_clear_object(&gui->corrected);
        if (gwy_params_get_enum(params, PARAM_IMAGE_MODE) == IMAGE_CORRECTED)
            gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
    }
    if (id < 0 || id == PARAM_IMAGE_MODE)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

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

    if (response == GWY_RESPONSE_RESET) {
        init_coordinates(args->field, args->xy);
        gwy_selection_set_data(gui->selection, 1, args->xy);
        guess_pixel_dimensions(gui->table, gui->selection, args->field);
    }
}

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

    gwy_selection_get_object(gui->selection, 0, args->xy);
    if (!gwy_params_get_boolean(args->params, PARAM_FIXRES))
        guess_pixel_dimensions(gui->table, gui->selection, args->field);
    g_clear_object(&gui->corrected);
}

static void
guess_pixel_dimensions(GwyParamTable *table, GwySelection *selection, GwyField *field)
{
    gdouble xypix[8], xy[8];
    gdouble newxpix, newypix;
    gint i;

    gwy_selection_get_object(selection, 0, xy);
    for (i = 0; i < 4; i++) {
        xypix[2*i + 0] = gwy_field_rtoj(field, xy[2*i + 0]);
        xypix[2*i + 1] = gwy_field_rtoi(field, xy[2*i + 1]);
    }
    estimate_reasonable_dimensions(xypix, &newxpix, &newypix);
    gwy_param_table_set_int(table, PARAM_XRES, MAX(GWY_ROUND(newxpix + 1.0), 2));
    gwy_param_table_set_int(table, PARAM_YRES, MAX(GWY_ROUND(newypix + 1.0), 2));
}

static void
init_coordinates(GwyField *field, gdouble *xy)
{
    gdouble xreal, yreal;
    gint i, j;

    xreal = gwy_field_get_xreal(field);
    yreal = gwy_field_get_yreal(field);
    for (i = 0; i < 2; i++) {
        for (j = 0; j < 2; j++) {
            xy[2*(2*i + j) + 0] = 0.25*xreal + 0.5*(i ? 1-j : j)*xreal;
            xy[2*(2*i + j) + 1] = 0.25*yreal + 0.5*i*yreal;
        }
    }
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;
    ImageMode mode = gwy_params_get_enum(args->params, PARAM_IMAGE_MODE);
    GwyDataView *dataview = GWY_DATA_VIEW(gui->view);

    if (mode == IMAGE_DATA) {
        gwy_data_view_set_field(dataview, args->field);
        gwy_data_view_set_interactive_layer(dataview, gui->vlayer);
        gwy_set_data_preview_size(dataview, PREVIEW_SIZE);
        return;
    }

    if (!gui->corrected) {
        GwyInterpolationType interpolation = gwy_params_get_enum(args->params, PARAM_INTERPOLATION);
        gui->corrected = create_corrected_field(args->field, args->xy, 0, 0, interpolation);
    }
    gwy_data_view_set_field(dataview, gui->corrected);
    gwy_data_view_set_interactive_layer(dataview, NULL);
    gwy_set_data_preview_size(dataview, PREVIEW_SIZE);
}

static void
project(gdouble x, gdouble y, gdouble *px, gdouble *py, gpointer user_data)
{
    const gdouble *matrix = (const gdouble*)user_data;
    const gdouble *mx = matrix, *my = matrix + 3, *m1 = matrix + 6;
    gdouble d;

    d = m1[0]*x + m1[1]*y + m1[2];
    *px = (mx[0]*x + mx[1]*y + mx[2])/d;
    *py = (my[0]*x + my[1]*y + my[2])/d;
}

static void
estimate_reasonable_dimensions(const gdouble *xy, gdouble *lx, gdouble *ly)
{
    gdouble lx1, lx2, ly1, ly2;

    lx1 = hypot(xy[2] - xy[0], xy[3] - xy[1]);
    lx2 = hypot(xy[6] - xy[4], xy[7] - xy[5]);
    ly1 = hypot(xy[4] - xy[2], xy[5] - xy[3]);
    ly2 = hypot(xy[0] - xy[6], xy[1] - xy[5]);

    *lx = hypot(lx1, lx2)/G_SQRT2;
    *ly = hypot(ly1, ly2)/G_SQRT2;
}

static GwyField*
create_corrected_field(GwyField *field,
                       const gdouble *xy,
                       gint xres, gint yres,
                       GwyInterpolationType interp)
{
    GwyField *corrected;
    gdouble matrix[9], xypix[8];
    gdouble newxreal, newyreal, newxpix, newypix;
    gint i, newxres, newyres;

    estimate_reasonable_dimensions(xy, &newxreal, &newyreal);
    for (i = 0; i < 4; i++) {
        xypix[2*i + 0] = gwy_field_rtoj(field, xy[2*i + 0]);
        xypix[2*i + 1] = gwy_field_rtoi(field, xy[2*i + 1]);
    }
    if (xres && yres) {
        newxpix = newxres = xres;
        newypix = newyres = yres;
    }
    else {
        estimate_reasonable_dimensions(xypix, &newxpix, &newypix);
        newxres = MAX(GWY_ROUND(newxpix + 1.0), 2);
        newyres = MAX(GWY_ROUND(newypix + 1.0), 2);
    }
    corrected = gwy_field_new(newxres, newyres, newxreal*newxres/newxpix, newyreal*newyres/newypix, FALSE);
    gwy_field_copy_units(field, corrected);

    solve_projection_from_rectangle(xypix, newxres, newyres, matrix);
    gwy_field_distort(field, corrected, project, matrix, interp, GWY_EXTERIOR_MIRROR, 0.0);

    return corrected;
}

static gboolean
solve_projection(const gdouble *xyfrom, const gdouble *xyto, gdouble *matrix)
{
    gdouble a[64], rhs[8];
    guint i;

    gwy_clear(a, 64);
    for (i = 0; i < 4; i++) {
        gdouble xf = xyfrom[2*i + 0], yf = xyfrom[2*i + 1];
        gdouble xt = xyto[2*i + 0], yt = xyto[2*i + 1];
        gdouble *axrow = a + 16*i, *ayrow = axrow + 8, *r = rhs + 2*i;

        axrow[0] = ayrow[3] = xf;
        axrow[1] = ayrow[4] = yf;
        axrow[2] = ayrow[5] = 1.0;
        axrow[6] = -xf*xt;
        axrow[7] = -yf*xt;
        ayrow[6] = -xf*yt;
        ayrow[7] = -yf*yt;
        r[0] = xt;
        r[1] = yt;
    }

    if (!gwy_math_lin_solve_rewrite(8, a, rhs, matrix))
        return FALSE;

    matrix[8] = 1.0;
    return TRUE;
}

static gboolean
solve_projection_from_rectangle(const gdouble *xy, gint xres, gint yres, gdouble *matrix)
{
    gdouble rectangle[8];

    rectangle[0] = rectangle[1] = rectangle[3] = rectangle[6] = 0.5;
    rectangle[2] = rectangle[4] = xres - 0.5;
    rectangle[5] = rectangle[7] = yres - 0.5;

    return solve_projection(rectangle, xy, matrix);
}

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