/*
 *  $Id: change-preview-cmap.c 30151 2026-06-25 13:17:45Z yeti-dn $
 *  Copyright (C) 2025-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 "libgwyddion/macros.h"
#include "libgwyui/utils.h"

#include "libgwyapp/gwyapp.h"
#include "libgwyapp/internal.h"

enum {
    PARAM_PREVIEW_CURVE,
    PARAM_PREVIEW_TYPE,
    PARAM_PREVIEW_IMAGE,
    PARAM_PREVIEW_OUTLIERS,
    PARAM_PREVIEW_UPDATE,
};

typedef enum {
    CMAP_PREVIEW_MEAN    = 1,
    CMAP_PREVIEW_MINIMUM = 2,
    CMAP_PREVIEW_MAXIMUM = 3,
    CMAP_PREVIEW_RMS     = 4,
    CMAP_PREVIEW_NPOINTS = 7,
    CMAP_PREVIEW_IMAGE   = 100,
} CMapPreviewType;

typedef struct {
    GtkWidget *dialog;
    GwyParams *params;
    GwyParamTable *table;
    GwyAppImageWindow *window;
    GwyLawn *lawn;

    GwyField *preview;
    GwyField *saved_preview;

    gboolean initial_preview;
} PreviewChooser;

static gboolean     preview_filter     (GwyFile *data,
                                        gint id,
                                        gpointer user_data);
static void         recalculate_preview(PreviewChooser *window);
static void         param_changed      (PreviewChooser *window,
                                        gint id,
                                        GwyParamTable *table);
static void         render_preview     (gpointer user_data);
static gdouble      lawn_reduce_length (gint ncurves,
                                        gint curvelength,
                                        const gdouble *curvedata,
                                        gpointer user_data);
static gdouble      lawn_reduce_avg    (gint ncurves,
                                        gint curvelength,
                                        const gdouble *curvedata,
                                        gpointer user_data);
static gdouble      lawn_reduce_min    (gint ncurves,
                                        gint curvelength,
                                        const gdouble *curvedata,
                                        gpointer user_data);
static gdouble      lawn_reduce_max    (gint ncurves,
                                        gint curvelength,
                                        const gdouble *curvedata,
                                        gpointer user_data);
static gdouble      lawn_reduce_rms    (gint ncurves,
                                        gint curvelength,
                                        const gdouble *curvedata,
                                        gpointer user_data);
static GwyParamDef* define_params      (void);

void
_gwy_app_change_cmap_preview(GwyAppImageWindow *window)
{
    static const guint simple_types[] = {
        CMAP_PREVIEW_MEAN, CMAP_PREVIEW_MINIMUM, CMAP_PREVIEW_MAXIMUM,
        CMAP_PREVIEW_RMS, CMAP_PREVIEW_NPOINTS,
    };

    GwyAppDataWindow *datawindow = GWY_APP_DATA_WINDOW(window);
    g_return_if_fail(gwy_app_data_window_get_data_kind(datawindow) == GWY_FILE_CMAP);
    gint id = gwy_app_data_window_get_id(datawindow);
    g_return_if_fail(id >= 0);

    PreviewChooser chooser;
    gwy_clear1(chooser);

    GwyFile *file = gwy_app_data_window_get_file(datawindow);
    chooser.window = window;
    chooser.preview = gwy_dict_get_object(GWY_DICT(file), gwy_file_key_cmap_picture(id));
    chooser.saved_preview = gwy_field_copy(chooser.preview);
    GwyLawn *lawn = chooser.lawn = gwy_file_get_cmap(file, id);

    GwyParams *params = chooser.params = _gwy_app_image_window_get_params(window);
    gboolean did_preview_setup = TRUE;
    if (!gwy_params_get_def(params)) {
        did_preview_setup = FALSE;
        gwy_params_set_def(params, define_params());
    }

    GwyDict *settings = gwy_app_settings_get();

    gboolean update = FALSE;
    if (gwy_dict_gis_boolean_by_name(settings, "/app/cmap-preview/update", &update))
        gwy_params_set_boolean(params, PARAM_PREVIEW_UPDATE, update);
    chooser.initial_preview = !update;

    gboolean outliers = FALSE;
    if (!did_preview_setup) {
        gwy_dict_gis_boolean_by_name(settings, "/app/cmap-preview/outliers", &outliers);
        gwy_params_set_boolean(params, PARAM_PREVIEW_OUTLIERS, outliers);
    }

    _gwy_load_preview_params_from_file(window, PARAM_PREVIEW_TYPE, PARAM_PREVIEW_CURVE, PARAM_PREVIEW_OUTLIERS);

    chooser.dialog = gwy_dialog_new(_("Change Curve Map Preview"));
    GwyDialog *dialog = GWY_DIALOG(chooser.dialog);
    gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window));
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    GwyParamTable *table = gwy_param_table_new(params);
    gwy_param_table_append_lawn_curve(table, PARAM_PREVIEW_CURVE, lawn);
    gwy_param_table_append_radio_header(table, PARAM_PREVIEW_TYPE);
    for (guint i = 0; i < G_N_ELEMENTS(simple_types); i++)
        gwy_param_table_append_radio_item(table, PARAM_PREVIEW_TYPE, simple_types[i]);
    gwy_param_table_append_radio_item(table, PARAM_PREVIEW_TYPE, CMAP_PREVIEW_IMAGE);
    gwy_param_table_append_image_id(table, PARAM_PREVIEW_IMAGE);
    gwy_param_table_data_id_set_filter(table, PARAM_PREVIEW_IMAGE, preview_filter, &chooser, NULL);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_PREVIEW_OUTLIERS);
    gwy_param_table_append_checkbox(table, PARAM_PREVIEW_UPDATE);

    gwy_dialog_add_content(dialog, gwy_param_table_widget(table), TRUE, TRUE, 0);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, render_preview, &chooser, NULL);
    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &chooser);

    GwyDialogOutcome outcome = gwy_dialog_run(dialog);
    if (outcome == GWY_DIALOG_CANCEL) {
        /* Make sure we get back the old preview, regardless what the user did in the dialog. */
        gwy_field_assign(chooser.preview, chooser.saved_preview);
        gwy_field_data_changed(chooser.preview);
        goto end;
    }
    if (outcome != GWY_DIALOG_HAVE_RESULT)
        recalculate_preview(&chooser);

    _gwy_save_preview_params_to_file(window, PARAM_PREVIEW_TYPE, PARAM_PREVIEW_CURVE, PARAM_PREVIEW_OUTLIERS);
    update = gwy_params_get_boolean(params, PARAM_PREVIEW_UPDATE);
    gwy_dict_set_boolean_by_name(settings, "/app/cmap-preview/update", update);
    outliers = gwy_params_get_boolean(params, PARAM_PREVIEW_OUTLIERS);
    gwy_dict_set_boolean_by_name(settings, "/app/cmap-preview/outliers", outliers);

end:
    g_clear_object(&chooser.saved_preview);
}

static gboolean
preview_filter(GwyFile *data, gint id, gpointer user_data)
{
    const GwyDataMismatchFlags flags = GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_LATERAL;
    PreviewChooser *chooser = (PreviewChooser*)user_data;
    if (!chooser->lawn)
        return FALSE;

    GwyField *field = gwy_file_get_image(data, id);
    return !gwy_field_is_incompatible_with_lawn(field, chooser->lawn, flags);
}

static void
recalculate_preview(PreviewChooser *chooser)
{
    GwyAppDataWindow *window = GWY_APP_DATA_WINDOW(chooser->window);
    GwyFile *file = gwy_app_data_window_get_file(window);
    gint id = gwy_app_data_window_get_id(window);
    GwyLawn *lawn = gwy_file_get_cmap(file, id);
    GwyParams *params = chooser->params;
    CMapPreviewType type = gwy_params_get_enum(params, PARAM_PREVIEW_TYPE);
    gboolean outliers = gwy_params_get_boolean(params, PARAM_PREVIEW_OUTLIERS);
    gint curveno = gwy_params_get_int(params, PARAM_PREVIEW_CURVE);
    GwyField *preview = chooser->preview;
    GwyCurveReduceFunction reduce_func = NULL;

    if (type == CMAP_PREVIEW_MEAN)
        reduce_func = lawn_reduce_avg;
    else if (type == CMAP_PREVIEW_MINIMUM)
        reduce_func = lawn_reduce_min;
    else if (type == CMAP_PREVIEW_MAXIMUM)
        reduce_func = lawn_reduce_max;
    else if (type == CMAP_PREVIEW_RMS)
        reduce_func = lawn_reduce_rms;
    else if (type == CMAP_PREVIEW_NPOINTS)
        reduce_func = lawn_reduce_length;
    else if (type == CMAP_PREVIEW_IMAGE)
        gwy_field_assign(preview, gwy_params_get_image(params, PARAM_PREVIEW_IMAGE));
    else {
        g_return_if_reached();
    }

    if (reduce_func) {
        GwyNield *mask = NULL;

        gwy_lawn_reduce_to_plane(lawn, preview, reduce_func, GUINT_TO_POINTER(curveno));
        if (type == CMAP_PREVIEW_NPOINTS)
            gwy_unit_clear(gwy_field_get_unit_z(preview));
        else {
            gwy_unit_assign(gwy_field_get_unit_z(preview), gwy_lawn_get_unit_curve(lawn, curveno));
            GwyField *length = gwy_field_new_alike(preview, FALSE);
            gwy_lawn_reduce_to_plane(lawn, length, lawn_reduce_length, GUINT_TO_POINTER(curveno));
            mask = gwy_field_new_nield_alike(preview);
            gwy_nield_mark_by_threshold(mask, length, 0.5, TRUE);
            g_object_unref(length);
            gwy_field_laplace_solve(preview, mask, GWY_LAPLACE_UNMASKED, 0.5);
        }

        if (outliers) {
            if (!mask)
                mask = gwy_field_new_nield_alike(preview);
            gwy_field_mark_outliers_iqr(preview, mask, 3.0, 3.0);
            gwy_field_laplace_solve(preview, mask, GWY_LAPLACE_MASKED, 1.0);
        }

        g_clear_object(&mask);
    }

    gwy_dict_set_object(GWY_DICT(file), gwy_file_key_cmap_picture(id), preview);
    gwy_field_data_changed(preview);
}

static void
param_changed(PreviewChooser *chooser, gint id, GwyParamTable *table)
{
    gboolean has_image = !!gwy_params_get_image(chooser->params, PARAM_PREVIEW_IMAGE);
    CMapPreviewType type = gwy_params_get_enum(chooser->params, PARAM_PREVIEW_TYPE);

    if (id < 0 && !has_image) {
        if (type == CMAP_PREVIEW_IMAGE)
            gwy_param_table_set_enum(table, PARAM_PREVIEW_TYPE, (type = CMAP_PREVIEW_MEAN));
        gwy_param_table_radio_set_sensitive(table, PARAM_PREVIEW_TYPE, CMAP_PREVIEW_IMAGE, FALSE);
    }
    if (id < 0 || id == PARAM_PREVIEW_TYPE) {
        gwy_param_table_set_sensitive(table, PARAM_PREVIEW_CURVE,
                                      type != CMAP_PREVIEW_NPOINTS && type != CMAP_PREVIEW_IMAGE);
        gwy_param_table_set_sensitive(table, PARAM_PREVIEW_OUTLIERS, type != CMAP_PREVIEW_IMAGE);
        gwy_param_table_set_sensitive(table, PARAM_PREVIEW_IMAGE, type == CMAP_PREVIEW_IMAGE);
    }
    gwy_dialog_invalidate(GWY_DIALOG(chooser->dialog));
}

static void
render_preview(gpointer user_data)
{
    PreviewChooser *chooser = (PreviewChooser*)user_data;

    /* GwyDialog runs an initial preview on startup, but we do not really want it here without instant updates enabled
     * because it just feels disorienting. */
    if (chooser->initial_preview) {
        chooser->initial_preview = FALSE;
        return;
    }
    recalculate_preview(chooser);
    gwy_dialog_have_result(GWY_DIALOG(chooser->dialog));
}

static gdouble
lawn_reduce_length(G_GNUC_UNUSED gint ncurves,
                   gint curvelength,
                   G_GNUC_UNUSED const gdouble *curvedata,
                   G_GNUC_UNUSED gpointer user_data)
{
    return curvelength;
}

static gdouble
lawn_reduce_avg(gint ncurves, gint curvelength, const gdouble *curvedata, gpointer user_data)
{
    guint i, idx = GPOINTER_TO_UINT(user_data);
    gdouble s = 0.0;

    g_return_val_if_fail(idx < ncurves, 0.0);
    if (!curvelength)
        return 0.0;

    curvedata += idx*curvelength;
    for (i = 0; i < curvelength; i++)
        s += curvedata[i];
    return s/curvelength;
}

static gdouble
lawn_reduce_rms(gint ncurves, gint curvelength, const gdouble *curvedata, gpointer user_data)
{
    guint i, idx = GPOINTER_TO_UINT(user_data);
    gdouble m, s = 0.0;

    g_return_val_if_fail(idx < ncurves, 0.0);
    if (!curvelength)
        return 0.0;

    m = lawn_reduce_avg(ncurves, curvelength, curvedata, user_data);
    curvedata += idx*curvelength;
    for (i = 0; i < curvelength; i++)
        s += (curvedata[i] - m)*(curvedata[i] - m);
    return sqrt(s/curvelength);
}

static gdouble
lawn_reduce_min(gint ncurves, gint curvelength, const gdouble *curvedata, gpointer user_data)
{
    guint i, idx = GPOINTER_TO_UINT(user_data);
    gdouble m = G_MAXDOUBLE;

    g_return_val_if_fail(idx < ncurves, 0.0);
    if (!curvelength)
        return 0.0;

    curvedata += idx*curvelength;
    for (i = 0; i < curvelength; i++)
        m = fmin(m, curvedata[i]);
    return m;
}

static gdouble
lawn_reduce_max(gint ncurves, gint curvelength, const gdouble *curvedata, gpointer user_data)
{
    guint i, idx = GPOINTER_TO_UINT(user_data);
    gdouble m = -G_MAXDOUBLE;

    g_return_val_if_fail(idx < ncurves, 0.0);
    if (!curvelength)
        return 0.0;

    curvedata += idx*curvelength;
    for (i = 0; i < curvelength; i++)
        m = fmax(m, curvedata[i]);
    return m;
}

static GwyParamDef*
define_params(void)
{
    static const GwyEnum types[] = {
        { N_("Mean"),             CMAP_PREVIEW_MEAN,    },
        { N_("Minimum"),          CMAP_PREVIEW_MINIMUM, },
        { N_("Maximum"),          CMAP_PREVIEW_MAXIMUM, },
        { N_("RMS"),              CMAP_PREVIEW_RMS,     },
        { N_("Number of points"), CMAP_PREVIEW_NPOINTS, },
        { N_("Image"),            CMAP_PREVIEW_IMAGE,   },
    };
    static GwyParamDef *pardef = NULL;

    if (pardef)
        return pardef;

    pardef = gwy_param_def_new();
    gwy_param_def_add_lawn_curve(pardef, PARAM_PREVIEW_CURVE, NULL, NULL);
    gwy_param_def_add_gwyenum(pardef, PARAM_PREVIEW_TYPE, NULL, _("Preview quantity"),
                              types, G_N_ELEMENTS(types), CMAP_PREVIEW_MEAN);
    gwy_param_def_add_image_id(pardef, PARAM_PREVIEW_IMAGE, NULL, NULL);
    gwy_param_def_add_boolean(pardef, PARAM_PREVIEW_OUTLIERS, NULL, _("Ignore outliers"), FALSE);
    gwy_param_def_add_instant_updates(pardef, PARAM_PREVIEW_UPDATE, NULL, NULL, FALSE);

    return pardef;
}

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