/*
 *  $Id: wrapvalue.c 30077 2026-06-16 16:47:03Z yeti-dn $
 *  Copyright (C) 2019-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 <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

typedef enum {
    DATA_UNIT_DEG,
    DATA_UNIT_UNITLESS,
    DATA_UNIT_OTHER,
} DataUnitType;

typedef enum {
    WRAP_VALUE_RANGE_USER,
    WRAP_VALUE_RANGE_360_DEG,
    WRAP_VALUE_RANGE_180_DEG,
    WRAP_VALUE_RANGE_2PI,
    WRAP_VALUE_RANGE_PI,
    WRAP_VALUE_RANGE_1,
    WRAP_VALUE_RANGE_KEEP,
} WrapValueRangeType;

enum {
    PARAM_OFFSET,
    PARAM_RANGE,
    PARAM_TYPE_DEG,
    PARAM_TYPE_UNITLESS,
    PARAM_TYPE,
};

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *result;
    /* Cached input data properties. */
    gdouble min;
    gdouble max;
    DataUnitType unit_type;
} ModuleArgs;

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

static gboolean           module_register     (void);
static GwyParamDef*       define_module_params(void);
static void               module_main         (GwyFile *data,
                                               GwyRunModeFlags mode);
static void               execute             (ModuleArgs *args);
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 gdouble            get_range           (ModuleArgs *args);
static WrapValueRangeType get_range_type      (ModuleArgs *args);
static void               sanitise_params     (ModuleArgs *args);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Wraps periodic values to a different range."),
    "Yeti <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti)",
    "2019",
};

GWY_MODULE_QUERY2(module_info, wrapvalue)

static gboolean
module_register(void)
{
    gwy_image_func_register("wrapvalue",
                            module_main,
                            N_("/_Basic Operations/_Wrap Value..."),
                            GWY_ICON_WRAP_VALUE,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Rewrap periodic values"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum deg_types[] = {
        { N_("360 deg"),         WRAP_VALUE_RANGE_360_DEG, },
        { N_("180 deg"),         WRAP_VALUE_RANGE_180_DEG, },
        { N_("_Keep unchanged"), WRAP_VALUE_RANGE_KEEP,    },
        { N_("Specify _range"),  WRAP_VALUE_RANGE_USER,    },
    };
    static const GwyEnum unitless_types[] = {
        { N_("2π"),              WRAP_VALUE_RANGE_2PI,  },
        { N_("π"),               WRAP_VALUE_RANGE_PI,   },
        { N_("1"),               WRAP_VALUE_RANGE_1,    },
        { N_("_Keep unchanged"), WRAP_VALUE_RANGE_KEEP, },
        { N_("Specify _range"),  WRAP_VALUE_RANGE_USER, },
    };
    static const GwyEnum types[] = {
        { N_("_Keep unchanged"), WRAP_VALUE_RANGE_KEEP, },
        { N_("Specify _range"),  WRAP_VALUE_RANGE_USER, },
    };
    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_double(paramdef, PARAM_OFFSET, "offset", _("O_ffset"), -G_MAXDOUBLE, G_MAXDOUBLE, 0.0);
    gwy_param_def_add_double(paramdef, PARAM_RANGE, "range", _("Specify _range"), G_MINDOUBLE, G_MAXDOUBLE, 1.0);
    gwy_param_def_add_gwyenum(paramdef, PARAM_TYPE, "type", _("Value range"),
                              types, G_N_ELEMENTS(types), WRAP_VALUE_RANGE_KEEP);
    gwy_param_def_add_gwyenum(paramdef, PARAM_TYPE_DEG, "type-deg", _("Value range"),
                              deg_types, G_N_ELEMENTS(deg_types), WRAP_VALUE_RANGE_KEEP);
    gwy_param_def_add_gwyenum(paramdef, PARAM_TYPE_UNITLESS, "type-unitless", _("Value range"),
                              unitless_types, G_N_ELEMENTS(unitless_types), WRAP_VALUE_RANGE_KEEP);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;
    ModuleArgs args;
    GQuark quark;
    GwyUnit *unit;
    gint id;

    g_return_if_fail(mode & RUN_MODES);
    gwy_data_browser_get_current(GWY_APP_FIELD, &args.field,
                                 GWY_APP_FIELD_ID, &id,
                                 GWY_APP_FIELD_KEY, &quark,
                                 0);
    g_return_if_fail(args.field);
    gwy_field_min_max(args.field, &args.min, &args.max);
    unit = gwy_field_get_unit_z(args.field);
    if (gwy_unit_equal_string(unit, "deg"))
        args.unit_type = DATA_UNIT_DEG;
    else if (gwy_unit_equal_string(unit, NULL))
        args.unit_type = DATA_UNIT_UNITLESS;
    else
        args.unit_type = DATA_UNIT_OTHER;

    args.result = gwy_field_new_alike(args.field, TRUE);

    args.params = gwy_params_new_from_settings(define_module_params());
    sanitise_params(&args);
    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;
    }
    if (outcome != GWY_DIALOG_HAVE_RESULT)
        execute(&args);

    gwy_app_undo_qcheckpointv(GWY_DICT(data), 1, &quark);
    gwy_file_set_image(data, id, args.result);
    gwy_log_add(data, GWY_FILE_IMAGE, id, id);

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

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GtkWidget *hbox, *dataview;
    GwyDialog *dialog;
    GwyParamTable *table;
    GwyDialogOutcome outcome;
    ModuleGUI gui;
    GwyValueFormat *vf;
    gdouble range = get_range(args);

    gwy_clear1(gui);
    gui.args = args;
    vf = gwy_field_get_value_format_z(args->field, GWY_UNIT_FORMAT_VFMARKUP, NULL);

    gui.dialog = gwy_dialog_new(_("Wrap Value"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    dataview = gwy_create_preview(args->result, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(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(dataview), FALSE);

    table = gui.table = gwy_param_table_new(args->params);
    if (args->unit_type == DATA_UNIT_DEG)
        gwy_param_table_append_radio(table, PARAM_TYPE_DEG);
    else if (args->unit_type == DATA_UNIT_UNITLESS)
        gwy_param_table_append_radio(table, PARAM_TYPE_UNITLESS);
    else
        gwy_param_table_append_radio(table, PARAM_TYPE);

    gwy_param_table_append_entry(table, PARAM_RANGE);
    gwy_param_table_entry_set_value_format(table, PARAM_RANGE, vf);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_slider(table, PARAM_OFFSET);
    gwy_param_table_slider_set_factor(table, PARAM_OFFSET, 1.0/vf->magnitude);
    gwy_param_table_set_unitstr(table, PARAM_OFFSET, vf->units);
    /* Must do this here, otherwise the entry tries to accommodate G_MAXDOUBLE values. */
    gwy_param_table_slider_restrict_range(gui.table, PARAM_OFFSET, -range, range);

    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);

    gwy_value_format_free(vf);

    return outcome;
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    WrapValueRangeType type = WRAP_VALUE_RANGE_USER;
    gboolean changing_type = FALSE, changing_range = FALSE;

    if ((id < 0 && args->unit_type == DATA_UNIT_DEG) || id == PARAM_TYPE_DEG) {
        type = gwy_params_get_enum(params, PARAM_TYPE_DEG);
        changing_type = TRUE;
    }
    if ((id < 0 && args->unit_type == DATA_UNIT_UNITLESS) || id == PARAM_TYPE_UNITLESS) {
        type = gwy_params_get_enum(params, PARAM_TYPE_UNITLESS);
        changing_type = TRUE;
    }
    if ((id < 0 && args->unit_type == DATA_UNIT_OTHER) || id == PARAM_TYPE) {
        type = gwy_params_get_enum(params, PARAM_TYPE);
        changing_type = TRUE;
    }

    if (changing_type) {
        gwy_param_table_set_sensitive(gui->table, PARAM_RANGE, type == WRAP_VALUE_RANGE_USER);
        changing_range = TRUE;
    }

    if (id < 0 || id == PARAM_RANGE || changing_range) {
        gdouble range = get_range(args);
        gwy_param_table_slider_restrict_range(gui->table, PARAM_OFFSET, -range, range);
    }

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

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

    execute(gui->args);
    gwy_field_data_changed(gui->args->result);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
execute(ModuleArgs *args)
{
    GwyField *field = args->field, *result = args->result;
    gdouble offset = gwy_params_get_double(args->params, PARAM_OFFSET);
    gdouble addback = offset;
    WrapValueRangeType type = get_range_type(args);
    gdouble range = get_range(args);

    if (!(range > 0.0)) {
        gwy_field_clear(result);
        return;
    }

    gwy_field_copy_data(field, result);

    // When the user says keep, wrap within the current [min,max] range, treating data minimum as zero.
    if (type == WRAP_VALUE_RANGE_KEEP) {
        offset += args->min;
        addback = args->min;
    }

    gint n = gwy_field_get_xres(result)*gwy_field_get_yres(result);
    gdouble *d = gwy_field_get_data(result);
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(d,n,offset,addback,range)
#endif
    for (gint i = 0; i < n; i++) {
        d[i] = fmod(d[i] - offset, range);
        if (d[i] < 0.0)
            d[i] += range;
        d[i] += addback;
    }
}

static gdouble
get_range(ModuleArgs *args)
{
    /* NB: USER is unused and KEEP must be handled specially */
    static const gdouble ranges[] = {
        0.0, 360.0, 180.0, 2.0*G_PI, G_PI, 1.0, 0.0,
    };

    GwyParams *params = args->params;
    WrapValueRangeType type = get_range_type(args);

    if (type == WRAP_VALUE_RANGE_USER)
        return gwy_params_get_double(params, PARAM_RANGE);
    if (type == WRAP_VALUE_RANGE_KEEP)
        return args->max - args->min;

    return ranges[type];
}

static WrapValueRangeType
get_range_type(ModuleArgs *args)
{
    GwyParams *params = args->params;

    if (args->unit_type == DATA_UNIT_DEG)
        return gwy_params_get_enum(params, PARAM_TYPE_DEG);
    if (args->unit_type == DATA_UNIT_UNITLESS)
        return gwy_params_get_enum(params, PARAM_TYPE_UNITLESS);
    return gwy_params_get_enum(params, PARAM_TYPE);
}

static void
sanitise_params(ModuleArgs *args)
{
    GwyParams *params = args->params;
    gdouble r = args->max - args->min;
    gdouble range = get_range(args);
    gdouble offset = gwy_params_get_double(params, PARAM_OFFSET);

    /* If remembered range differs wildly from data range, try to guess something of reasonable order of magnitude... */
    if (!r)
        gwy_params_set_double(params, PARAM_RANGE, (range = 1.0));
    else if (!range || !(fabs(log(r/range)) < 2.0))
        gwy_params_set_double(params, PARAM_RANGE, (range = r));

    gwy_params_set_double(params, PARAM_OFFSET, CLAMP(offset, -range, range));
}

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