/*
 *  $Id: polydistort.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2007-2026 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@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 <string.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES (GWY_RUN_INTERACTIVE | GWY_RUN_IMMEDIATE)

enum {
    MAX_DEGREE = 3,
    NCOEFF     = (MAX_DEGREE + 1)*(MAX_DEGREE + 2)/2,
};

enum {
    PARAM_INTERP,
    PARAM_EXTERIOR,
    PARAM_DISPLAY,
    PARAM_UPDATE,
    PARAM_XCOEFF_0,
    PARAM_YCOEFF_0 = PARAM_XCOEFF_0 + NCOEFF,

    WIDGET_XCOEFF = PARAM_YCOEFF_0 + NCOEFF,
    WIDGET_YCOEFF,
};

typedef enum {
    PREVIEW_TRANSFORMED = 0,
    PREVIEW_ORIGINAL  = 1,
    PREVIEW_LAST
} DistortPreviewType;

/* Data for this function. */
typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyNield *mask;
    GwyField *show;
    GwyField *result_field;
    GwyNield *result_mask;
    GwyField *result_show;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GtkWidget *view;
    GwyParamTable *table;
    GtkWidget **xcoeff;
    GtkWidget **ycoeff;
} ModuleGUI;

static gboolean         module_register      (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 GtkWidget*       coeff_table_new_x    (gpointer user_data);
static GtkWidget*       coeff_table_new_y    (gpointer user_data);
static GtkWidget*       coeff_table_new      (GtkWidget **entry,
                                              const gchar *id,
                                              ModuleGUI *gui);
static void             dialog_response      (GtkDialog *dialog,
                                              gint response,
                                              ModuleGUI *gui);
static void             format_value         (ModuleGUI *gui,
                                              gint id);
static void             coeff_changed        (GtkEntry *entry,
                                              ModuleGUI *gui);
static void             preview              (gpointer user_data);
static void             execute              (ModuleArgs *args,
                                              gboolean main_field,
                                              gboolean aux_fields);
static void             distort_one_field    (GwyParams *params,
                                              GwyField *field,
                                              GwyField *result,
                                              gboolean is_mask);
static void             coeff_index_to_powers(guint k,
                                              guint *x,
                                              guint *y);
static gint             param_block_start    (const gchar *what);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Applies polynomial distortion in the horizontal plane."),
    "Yeti <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti) & Petr Klapetek",
    "2007",
};

GWY_MODULE_QUERY2(module_info, polydistort)

static gboolean
module_register(void)
{
    gwy_image_func_register("polydistort",
                            module_main,
                            N_("/_Distortion/Pol_ynomial..."),
                            GWY_ICON_POLY_DISTORT,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Applies polynomial distortion in the horizontal plane"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum displays[] = {
        { N_("Ori_ginal"),    PREVIEW_ORIGINAL,    },
        { N_("_Transformed"), PREVIEW_TRANSFORMED, },
    };
    static const GwyEnum exteriors[] = {
        { N_("Minimum"),                  GWY_EXTERIOR_FIXED_VALUE,   },
        { gwy_NC("exterior", "Border"),   GWY_EXTERIOR_BORDER, },
        { gwy_NC("exterior", "Mirror"),   GWY_EXTERIOR_MIRROR, },
        { gwy_NC("exterior", "Periodic"), GWY_EXTERIOR_PERIODIC,      },
    };
    static GwyParamDef *paramdef = NULL;
    static gchar **xcoeff = NULL, **ycoeff = NULL;
    guint i, j;

    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_INTERP, "interp", NULL,
                           GWY_TYPE_INTERPOLATION_TYPE, GWY_INTERPOLATION_BSPLINE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_EXTERIOR, "exterior", _("_Exterior type"),
                              exteriors, G_N_ELEMENTS(exteriors), GWY_EXTERIOR_FIXED_VALUE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_DISPLAY, NULL, C_("verb", "Display"),
                              displays, G_N_ELEMENTS(displays), PREVIEW_TRANSFORMED);
    gwy_param_def_add_instant_updates(paramdef, PARAM_UPDATE, "update", NULL, FALSE);

    /* Do not free the parameter names. They must exist to the end of the program. */
    xcoeff = g_new(gchar*, NCOEFF);
    for (guint k = 0; k < NCOEFF; k++) {
        coeff_index_to_powers(k, &j, &i);
        xcoeff[k] = g_strdup_printf("xcoeff-%u-%u", i, j);
        gwy_param_def_add_double(paramdef, PARAM_XCOEFF_0 + k, xcoeff[k], NULL, -100.0, 100.0, j == 1 && i == 0);
    }

    ycoeff = g_new(gchar*, NCOEFF);
    for (guint k = 0; k < NCOEFF; k++) {
        coeff_index_to_powers(k, &j, &i);
        ycoeff[k] = g_strdup_printf("ycoeff-%u-%u", i, j);
        gwy_param_def_add_double(paramdef, PARAM_YCOEFF_0 + k, ycoeff[k], NULL, -100.0, 100.0, j == 0 && i == 1);
    }

    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    ModuleArgs args;
    gint id, newid;

    g_return_if_fail(mode & RUN_MODES);
    gwy_clear1(args);
    args.params = gwy_params_new_from_settings(define_module_params());
    gwy_data_browser_get_current(GWY_APP_FIELD, &args.field,
                                 GWY_APP_MASK_FIELD, &args.mask,
                                 GWY_APP_SHOW_FIELD, &args.show,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(args.field);

    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;
    args.result_field = gwy_field_copy(args.field);
    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, data, id);
        gwy_params_save_to_settings(args.params);
        if (outcome == GWY_DIALOG_CANCEL)
            goto end;
    }
    execute(&args, outcome != GWY_DIALOG_HAVE_RESULT, TRUE);

    newid = gwy_file_add_image(data, args.result_field);

    gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
    gwy_file_set_title(data, GWY_FILE_IMAGE, newid, _("Distorted"), TRUE);
    if (args.result_mask)
        gwy_file_set_image_mask(data, newid, args.result_mask);
    if (args.result_show)
        gwy_dict_pass_object(GWY_DICT(data), gwy_file_key_image_picture(newid), args.result_show);
    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);

end:
    g_clear_object(&args.result_field);
    g_clear_object(&args.result_mask);
    g_clear_object(&args.result_show);
    g_object_unref(args.params);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    ModuleGUI gui;

    gwy_clear1(gui);
    gui.args = args;

    gui.dialog = gwy_dialog_new(_("Distort by Polynomial"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_RESET, GWY_STOCK_UPDATE, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    gui.view = gwy_create_preview(args->result_field, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.view), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_REAL_SQUARE);

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

    GwyParamTable *table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_combo(table, PARAM_INTERP);
    gwy_param_table_append_combo(table, PARAM_EXTERIOR);
    gwy_param_table_append_radio_row(table, PARAM_DISPLAY);
    gwy_param_table_append_header(table, -1, _("X Coefficients"));
    gwy_param_table_append_foreign(table, WIDGET_XCOEFF, coeff_table_new_x, &gui, NULL);
    gwy_param_table_append_header(table, -1, _("Y Coefficients"));
    gwy_param_table_append_foreign(table, WIDGET_YCOEFF, coeff_table_new_y, &gui, NULL);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_UPDATE);

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

    return gwy_dialog_run(dialog);
}

static GtkWidget*
coeff_table_new_x(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;

    g_assert(!gui->xcoeff);
    gui->xcoeff = g_new(GtkWidget*, NCOEFF);
    return coeff_table_new(gui->xcoeff, "x", gui);
}

static GtkWidget*
coeff_table_new_y(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;

    g_assert(!gui->ycoeff);
    gui->ycoeff = g_new(GtkWidget*, NCOEFF);
    return coeff_table_new(gui->ycoeff, "y", gui);
}

static GtkWidget*
coeff_table_new(GtkWidget **entry, const gchar *what, ModuleGUI *gui)
{
    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 2);
    gtk_grid_set_column_spacing(grid, 6);

    guint i, j, param_start = param_block_start(what);
    for (guint k = 0; k < NCOEFF; k++) {
        coeff_index_to_powers(k, &j, &i);
        entry[k] = gtk_entry_new();
        gtk_entry_set_width_chars(GTK_ENTRY(entry[k]), 6);
        gtk_grid_attach(grid, entry[k], j+1, i+1, 1, 1);
        g_object_set_data(G_OBJECT(entry[k]), "y", GUINT_TO_POINTER(i));
        g_object_set_data(G_OBJECT(entry[k]), "x", GUINT_TO_POINTER(j));
        g_object_set_data(G_OBJECT(entry[k]), "what", (gpointer)what);
        format_value(gui, param_start + k);
        g_signal_connect(entry[k], "activate", G_CALLBACK(coeff_changed), gui);
        gwy_widget_set_activate_on_unfocus(entry[k], TRUE);
    }

    GString *str = g_string_new(NULL);
    for (i = 0; i <= MAX_DEGREE; i++) {
        GtkWidget *label = gtk_label_new(NULL);
        if (i == 0)
            gtk_label_set_text(GTK_LABEL(label), "1");
        else if (i == 1)
            gtk_label_set_text(GTK_LABEL(label), "y");
        else {
            g_string_printf(str, "y<sup>%u</sup>", i);
            gtk_label_set_markup(GTK_LABEL(label), str->str);
        }
        gtk_grid_attach(grid, label, 0, i+1, 1, 1);
    }

    for (j = 0; j < MAX_DEGREE + 1; j++) {
        GtkWidget *label = gtk_label_new(NULL);
        if (j == 0)
            gtk_label_set_text(GTK_LABEL(label), "1");
        else if (j == 1)
            gtk_label_set_text(GTK_LABEL(label), "x");
        else {
            g_string_printf(str, "x<sup>%u</sup>", j);
            gtk_label_set_markup(GTK_LABEL(label), str->str);
        }
        gtk_grid_attach(grid, label, j+1, 0, 1, 1);
    }

    return GTK_WIDGET(grid);
}

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

    if (id < 0 || id == PARAM_DISPLAY) {
        GwyDataView *dataview = GWY_DATA_VIEW(gui->view);

        if (display == PREVIEW_TRANSFORMED)
            gwy_data_view_set_field(dataview, args->result_field);
        else if (display == PREVIEW_ORIGINAL)
            gwy_data_view_set_field(dataview, args->field);
        else {
            g_assert_not_reached();
        }
    }

    if (display == PREVIEW_TRANSFORMED && id != PARAM_DISPLAY && id != PARAM_UPDATE)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
dialog_response(GtkDialog *dialog, gint response, ModuleGUI *gui)
{
    if (response == GWY_RESPONSE_RESET) {
        GwyParams *params = gui->args->params;
        for (guint i = 0; i < NCOEFF; i++) {
            gwy_params_reset(params, PARAM_XCOEFF_0 + i);
            format_value(gui, PARAM_XCOEFF_0 + i);
            gwy_params_reset(params, PARAM_YCOEFF_0 + i);
            format_value(gui, PARAM_YCOEFF_0 + i);
        }
        gwy_dialog_invalidate(GWY_DIALOG(dialog));
    }
}

static void
format_value(ModuleGUI *gui, gint id)
{
    GwyParams *params = gui->args->params;
    gdouble value = gwy_params_get_double(params, id);
    GtkWidget *entry = NULL;
    if (id >= PARAM_XCOEFF_0 && id < PARAM_XCOEFF_0 + NCOEFF)
        entry = gui->xcoeff[id - PARAM_XCOEFF_0];
    else if (id >= PARAM_YCOEFF_0 && id < PARAM_YCOEFF_0 + NCOEFF)
        entry = gui->ycoeff[id - PARAM_YCOEFF_0];
    else {
        g_return_if_reached();
    }

    gchar *s = g_strdup_printf("%.8g", value);
    gtk_entry_set_text(GTK_ENTRY(entry), s);
    g_free(s);
}

static void
coeff_changed(GtkEntry *entry, ModuleGUI *gui)
{
    guint i = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(entry), "y"));
    guint j = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(entry), "x"));
    const gchar *what = g_object_get_data(G_OBJECT(entry), "what");
    guint param_start = param_block_start(what);
    gdouble value = g_strtod(gtk_entry_get_text(entry), NULL);
    guint id = param_start + i*(MAX_DEGREE + 1) + j;
    if (gwy_params_set_double(gui->args->params, id, value)) {
        format_value(gui, id);
        gwy_param_table_param_changed(gui->table, id);
    }
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    execute(gui->args, TRUE, FALSE);
    gwy_field_data_changed(gui->args->result_field);
    /* This means we only have the main field. The other two still need to be distorted as needed. */
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
execute(ModuleArgs *args, gboolean main_field, gboolean aux_fields)
{
    GwyParams *params = args->params;

    if (main_field)
        distort_one_field(params, args->field, args->result_field, FALSE);

    if (!aux_fields)
        return;

    if (args->mask) {
        GwyField *mask_field = gwy_field_new_alike(args->field, FALSE);
        gwy_field_fill_mask(mask_field, args->mask, 0.0, 1.0);
        GwyField *distorted = gwy_field_new_alike(args->field, FALSE);
        distort_one_field(params, mask_field, distorted, TRUE);
        g_object_unref(mask_field);
        if (!args->result_mask)
            args->result_mask = gwy_field_new_nield_alike(distorted);
        gwy_nield_mark_by_threshold(args->result_mask, distorted, 0.5, TRUE);
        g_object_unref(distorted);
    }
    if (args->show) {
        if (!args->result_show)
            args->result_show = gwy_field_new_alike(args->show, FALSE);
        distort_one_field(params, args->show, args->result_show, FALSE);
    }
}

static inline gdouble
distort_coord(gdouble x, gdouble y, const gdouble *a)
{
    return (a[0] + x*(a[1] + x*(a[2] + x*a[3])) + y*(a[4] + x*(a[5] + x*a[6]) + y*(a[7] + x*a[8] + y*a[9])));
}

static void
distort_transform(gdouble x, gdouble y,
                  gdouble *px, gdouble *py,
                  gpointer user_data)
{
    const gdouble *param = (const gdouble*)user_data;

    x /= param[2*NCOEFF + 0];
    y /= param[2*NCOEFF + 1];
    *px = param[2*NCOEFF + 0]*distort_coord(x, y, param);
    *py = param[2*NCOEFF + 1]*distort_coord(x, y, param + NCOEFF);
}

static void
distort_one_field(GwyParams *params,
                  GwyField *field,
                  GwyField *result,
                  gboolean is_mask)
{
    /* XXX: Degree hardcoded in distort_coord() to avoid cycles and if-elses. */
    g_assert(MAX_DEGREE == 3);

    gdouble *transform_param = g_new(gdouble, 2*(NCOEFF + 1));
    for (guint i = 0; i < NCOEFF; i++) {
        transform_param[i] = gwy_params_get_double(params, PARAM_XCOEFF_0 + i);
        transform_param[NCOEFF + i] = gwy_params_get_double(params, PARAM_YCOEFF_0 + i);
    }
    transform_param[2*NCOEFF + 0] = gwy_field_get_xres(result);
    transform_param[2*NCOEFF + 1] = gwy_field_get_yres(result);

    GwyInterpolationType interp = gwy_params_get_enum(params, PARAM_INTERP);
    if (is_mask)
        interp = GWY_INTERPOLATION_ROUND;

    GwyExteriorType exterior = gwy_params_get_enum(params, PARAM_EXTERIOR);
    gdouble val = 0.0;
    if (exterior == GWY_EXTERIOR_FIXED_VALUE)
        val = gwy_field_min(field);

    gwy_field_distort(field, result, distort_transform, transform_param, interp, exterior, val);
    g_free(transform_param);
}

static void
coeff_index_to_powers(guint k, guint *x, guint *y)
{
    guint row_origin = 0, next_row_origin;
    for (guint i = 0; i <= MAX_DEGREE; i++) {
        next_row_origin = row_origin + MAX_DEGREE+1 - i;
        if (k >= row_origin && k < next_row_origin) {
            *y = i;
            *x = k - row_origin;
            return;
        }
        row_origin = next_row_origin;
    }
    *x = *y = 0;
    g_assert_not_reached();
}

static gint
param_block_start(const gchar *what)
{
    if (gwy_strequal(what, "x"))
        return PARAM_XCOEFF_0;
    if (gwy_strequal(what, "y"))
        return PARAM_YCOEFF_0;
    g_return_val_if_reached(PARAM_XCOEFF_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 : */
