/*
 *  $Id: rotate.c 30111 2026-06-22 11:02:00Z yeti-dn $
 *  Copyright (C) 2003-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 <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

enum {
    PARAM_INTERPOLATION,
    PARAM_RESIZE,
    PARAM_SHOW_GRID,
    PARAM_CREATE_MASK,
    /* Rotate only. */
    PARAM_ANGLE,
    /* Unrotate only. */
    PARAM_SYMMETRY,
    INFO_DETECTED,
    INFO_CORRECTION,
};

typedef struct {
    GwyParams *params;
    GwyField *field;
    gboolean is_unrotate;
    /* Cached estimated values for input data field (unrotate). */
    GwyPlaneSymmetry symm;
    gdouble corrections[GWY_SYMMETRY_LAST];
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GwyField *smallfield;
    GtkWidget *dialog;
    GtkWidget *dataview;
    GwyParamTable *table;
    GwySelection *selection;
} ModuleGUI;

static gboolean         module_register        (void);
static GwyParamDef*     define_rotate_params   (void);
static GwyParamDef*     define_unrotate_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 void             update_grid            (ModuleGUI *gui);
static GwyPlaneSymmetry guess_unrotate_symmetry(GwyField *field,
                                                gdouble *corrections);
static gdouble          get_rotation_angle     (ModuleArgs *args);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Rotates data by arbitrary angle or to make characteristic directions parallel with x or y axis."),
    "Yeti <yeti@gwyddion.net>",
    "3.2",
    "David Nečas (Yeti) & Petr Klapetek",
    "2003",
};

GWY_MODULE_QUERY2(module_info, rotate)

static gboolean
module_register(void)
{
    gwy_image_func_register("rotate",
                            module_main,
                            N_("/_Basic Operations/Rotate by _Angle..."),
                            GWY_ICON_ROTATE,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Rotate by arbitrary angle"));
    gwy_image_func_register("unrotate",
                            module_main,
                            N_("/_Correct Data/_Unrotate..."),
                            GWY_ICON_UNROTATE,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Automatically correct rotation in horizontal plane"));

    return TRUE;
}

static void
define_params_common(GwyParamDef *paramdef)
{
    static const GwyEnum resize_types[] = {
        { N_("_Same as original"),          GWY_ROTATE_RESIZE_SAME_SIZE, },
        { N_("_Expanded to complete data"), GWY_ROTATE_RESIZE_EXPAND,    },
        { N_("C_ut to valid data"),         GWY_ROTATE_RESIZE_CUT,       },
    };
    gwy_param_def_set_function_name(paramdef, gwy_image_func_current());
    gwy_param_def_add_enum(paramdef, PARAM_INTERPOLATION, "interp", NULL, GWY_TYPE_INTERPOLATION_TYPE,
                           GWY_INTERPOLATION_LINEAR);
    gwy_param_def_add_gwyenum(paramdef, PARAM_RESIZE, "resize", _("Result size"),
                              resize_types, G_N_ELEMENTS(resize_types), GWY_ROTATE_RESIZE_SAME_SIZE);
    gwy_param_def_add_boolean(paramdef, PARAM_CREATE_MASK, "create_mask", _("Create _mask over exterior"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_SHOW_GRID, "show_grid", _("Show _grid"), TRUE);
}

static GwyParamDef*
define_rotate_params(void)
{
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    define_params_common(paramdef);
    gwy_param_def_add_angle(paramdef, PARAM_ANGLE, "angle", _("Rotate by _angle"), FALSE, 1, 0.0);
    return paramdef;
}

static GwyParamDef*
define_unrotate_params(void)
{
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    define_params_common(paramdef);
    gwy_param_def_add_gwyenum(paramdef, PARAM_SYMMETRY, "symmetry", _("_Assume symmetry"),
                              gwy_plane_symmetry_get_enum(), -1, GWY_SYMMETRY_AUTO);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    const gchar *name = gwy_image_func_current();
    GwyField *field, *show;
    GwyNield *mask, *exterior_mask = NULL;
    gint oldid, newid;
    ModuleArgs args;
    gwy_clear1(args);
    GwyParams *params;

    g_return_if_fail(mode & RUN_MODES);
    gwy_data_browser_get_current(GWY_APP_FIELD, &field,
                                 GWY_APP_MASK_FIELD, &mask,
                                 GWY_APP_SHOW_FIELD, &show,
                                 GWY_APP_FIELD_ID, &oldid,
                                 0);
    g_return_if_fail(field);

    args.field = field;
    args.is_unrotate = gwy_strequal(name, "unrotate");
    if (args.is_unrotate) {
        args.symm = guess_unrotate_symmetry(args.field, args.corrections);
        params = args.params = gwy_params_new_from_settings(define_unrotate_params());
    }
    else
        params = args.params = gwy_params_new_from_settings(define_rotate_params());

    if (mode == GWY_RUN_INTERACTIVE) {
        GwyDialogOutcome outcome = run_gui(&args, data, oldid);
        gwy_params_save_to_settings(params);
        if (outcome != GWY_DIALOG_PROCEED)
            goto end;
    }

    gdouble angle = get_rotation_angle(&args);
    GwyInterpolationType interp = gwy_params_get_enum(params, PARAM_INTERPOLATION);
    GwyRotateResizeType resize = gwy_params_get_enum(params, PARAM_RESIZE);
    gboolean create_mask = gwy_params_get_boolean(params, PARAM_CREATE_MASK);

    if (mask || (create_mask && resize != GWY_ROTATE_RESIZE_CUT))
        exterior_mask = gwy_nield_new(1, 1);

    field = gwy_field_new_rotated(field, exterior_mask, angle, interp, resize);
    if (mask) {
        GwyField *mask_field = gwy_field_new_alike(args.field, FALSE);
        gwy_field_fill_mask(mask_field, mask, 0.0, 1.0);
        GwyField *rotated = gwy_field_new_rotated(mask_field, NULL, angle, GWY_INTERPOLATION_ROUND, resize);
        g_object_unref(mask_field);
        mask = gwy_field_new_nield_alike(rotated);
        gwy_nield_mark_by_threshold(mask, rotated, 0.5, TRUE);
        g_object_unref(rotated);
        /* The rotation fills exterior with average value of inside; which is kind of random and anyway unwanted for
         * masks.  Fill the exterior with either 0 or 1 (if we were asked to add an exterior mask). */
        gwy_nield_area_fill(mask, exterior_mask, GWY_MASK_INCLUDE,
                            0, 0, gwy_nield_get_xres(mask), gwy_nield_get_yres(mask),
                            create_mask ? 1 : 0);
    }
    else {
        /* If we were asked to add an exterior mask use exterior_mask as the mask.  Otherwise do nothing. */
        if (exterior_mask) {
            g_object_ref(exterior_mask);
            mask = exterior_mask;
        }
    }
    g_clear_object(&exterior_mask);

    if (show)
        show = gwy_field_new_rotated(show, NULL, angle, interp, resize);

    newid = gwy_file_add_image(data, field);

    gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
    gwy_file_sync_items(data, GWY_FILE_IMAGE, oldid,
                        data, GWY_FILE_IMAGE, newid,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_MASK_COLOR, FALSE);
    if (mask)
        gwy_file_pass_image_mask(data, newid, mask);
    if (show)
        gwy_dict_pass_object(GWY_DICT(data), gwy_file_key_image_picture(newid), show);

    gwy_file_set_title(data, GWY_FILE_IMAGE, newid, _("Rotated Data"), TRUE);
    gwy_log_add(data, GWY_FILE_IMAGE, oldid, newid);

end:
    g_object_unref(params);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GwyDialogOutcome outcome;
    GtkWidget *hbox, *dataview;
    GwyDialog *dialog;
    GwyParamTable *table;
    GwyField *rfield;
    const gchar *s;
    gint xres, yres;
    gdouble zoomval;
    ModuleGUI gui;

    gui.args = args;

    xres = gwy_field_get_xres(args->field);
    yres = gwy_field_get_yres(args->field);
    zoomval = (gdouble)PREVIEW_SIZE/MAX(xres, yres);
    gui.smallfield = gwy_field_new_resampled(args->field, xres*zoomval, yres*zoomval, GWY_INTERPOLATION_LINEAR);
    rfield = gwy_field_copy(gui.smallfield);

    gui.dialog = gwy_dialog_new(args->is_unrotate ? _("Correct Rotation") : _("Rotate"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    dataview = gui.dataview = gwy_create_preview(rfield, NULL, PREVIEW_SIZE);
    g_object_unref(rfield);
    gwy_setup_data_view(GWY_DATA_VIEW(dataview), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_MASK_COLOR);
    gui.selection = gwy_create_preview_vector_layer(GWY_DATA_VIEW(dataview), GWY_TYPE_LAYER_LATTICE, 1, FALSE);

    hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(dataview), TRUE);

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

    if (args->is_unrotate) {
        gwy_param_table_append_header(table, -1, _("Structure"));
        gwy_param_table_append_info(table, INFO_DETECTED, _("Detected"));
        s = gwy_C(gwy_enum_to_string(args->symm, gwy_plane_symmetry_get_enum(), -1));
        gwy_param_table_info_set_valuestr(table, INFO_DETECTED, s);
        gwy_param_table_append_combo(table, PARAM_SYMMETRY);
        gwy_param_table_append_info(table, INFO_CORRECTION, _("Correction"));
        gwy_param_table_set_unitstr(table, INFO_CORRECTION, _("deg"));
    }
    else {
        gwy_param_table_append_header(table, -1, _("Rotate"));
        gwy_param_table_append_slider(table, PARAM_ANGLE);
        gwy_param_table_slider_set_steps(table, PARAM_ANGLE, gwy_deg2rad(0.01), gwy_deg2rad(5.0));
    }

    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_combo(table, PARAM_INTERPOLATION);
    gwy_param_table_append_radio(table, PARAM_RESIZE);
    gwy_param_table_append_checkbox(table, PARAM_SHOW_GRID);
    gwy_param_table_append_checkbox(table, PARAM_CREATE_MASK);

    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_IMMEDIATE, preview, &gui, NULL);

    outcome = gwy_dialog_run(dialog);

    g_object_unref(gui.smallfield);

    return outcome;
}

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

    if (id < 0 || id == PARAM_RESIZE) {
        GwyRotateResizeType resize = gwy_params_get_enum(params, PARAM_RESIZE);
        gwy_param_table_set_sensitive(table, PARAM_CREATE_MASK, resize != GWY_ROTATE_RESIZE_CUT);
    }

    if (id < 0 || id == PARAM_SHOW_GRID)
        update_grid(gui);

    if (args->is_unrotate && (id < 0 || id == PARAM_SYMMETRY)) {
        gchar *s = g_strdup_printf("%.2f", gwy_rad2deg(get_rotation_angle(args)));
        gwy_param_table_info_set_valuestr(table, INFO_CORRECTION, s);
        g_free(s);
    }

    if (id != PARAM_INTERPOLATION && id != PARAM_CREATE_MASK && id != PARAM_SHOW_GRID)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    gdouble angle = get_rotation_angle(args);
    GwyInterpolationType interp = gwy_params_get_enum(params, PARAM_INTERPOLATION);
    GwyRotateResizeType resize = gwy_params_get_enum(params, PARAM_RESIZE);
    GwyDataView *dataview = GWY_DATA_VIEW(gui->dataview);

    GwyField *rfield = gwy_field_new_rotated(gui->smallfield, NULL, angle, interp, resize);
    gwy_data_view_set_field(dataview, rfield);
    g_object_unref(rfield);
    gwy_set_data_preview_size(dataview, PREVIEW_SIZE);
    gtk_widget_set_size_request(gui->dataview, PREVIEW_SIZE, -1);
    update_grid(gui);
}

static void
update_grid(ModuleGUI *gui)
{
    GwySelection *selection = gui->selection;
    GwyParams *params = gui->args->params;
    GwyField *field = gui->smallfield;
    gdouble xy[4];

    if (!gwy_params_get_boolean(params, PARAM_SHOW_GRID)) {
        gwy_selection_clear(selection);
        return;
    }

    xy[0] = gwy_field_get_xreal(field)/12.0;
    xy[1] = xy[2] = 0.0;
    xy[3] = gwy_field_get_yreal(field)/12.0;
    gwy_selection_set_data(selection, 1, xy);
}

static GwyPlaneSymmetry
guess_unrotate_symmetry(GwyField *field, gdouble *corrections)
{
    enum { nder = 4800 };
    GwyLine *derdist;
    GwyPlaneSymmetry symm;

    derdist = gwy_line_new(nder, 2.0*G_PI, FALSE);
    gwy_field_slope_distribution(field, derdist, 5);
    symm = gwy_field_unrotate_find_corrections(derdist, corrections);
    g_object_unref(derdist);

    return symm;
}

static gdouble
get_rotation_angle(ModuleArgs *args)
{
    GwyPlaneSymmetry symm;

    if (!args->is_unrotate)
        return gwy_params_get_double(args->params, PARAM_ANGLE);

    symm = gwy_params_get_enum(args->params, PARAM_SYMMETRY);
    if (symm == GWY_SYMMETRY_AUTO)
        symm = args->symm;
    return args->corrections[symm];
}

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