/*
 *  $Id: level3.c 28788 2025-11-04 17:13:02Z yeti-dn $
 *  Copyright (C) 2003-2022 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>

enum {
    COLUMN_I, COLUMN_X, COLUMN_Y, COLUMN_Z, NCOLUMNS
};

enum {
    PARAM_RADIUS,
    PARAM_INSTANT_APPLY,
    PARAM_SET_ZERO,
};

#define GWY_TYPE_TOOL_LEVEL3            (gwy_tool_level3_get_type())
#define GWY_TOOL_LEVEL3(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_LEVEL3, GwyToolLevel3))
#define GWY_IS_TOOL_LEVEL3(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_LEVEL3))
#define GWY_TOOL_LEVEL3_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_LEVEL3, GwyToolLevel3Class))

typedef struct _GwyToolLevel3      GwyToolLevel3;
typedef struct _GwyToolLevel3Class GwyToolLevel3Class;

struct _GwyToolLevel3 {
    GwyPlainTool parent_instance;

    GwyParams *params;
    gboolean allow_undo;

    GtkTreeView *treeview;
    GtkTreeModel *model;
    GwyParamTable *table;
};

struct _GwyToolLevel3Class {
    GwyPlainToolClass parent_class;
};

static gboolean     module_register                   (void);
static GwyParamDef* define_module_params              (void);
static GType        gwy_tool_level3_get_type          (void)                      G_GNUC_CONST;
static void         gwy_tool_level3_finalize          (GObject *object);
static void         gwy_tool_level3_init_dialog       (GwyToolLevel3 *tool);
static void         gwy_tool_level3_data_switched     (GwyTool *gwytool,
                                                       GwyDataView *data_view);
static void         gwy_tool_level3_data_changed      (GwyPlainTool *plain_tool);
static void         gwy_tool_level3_response          (GwyTool *tool,
                                                       gint response_id);
static void         gwy_tool_level3_selection_changed (GwyPlainTool *plain_tool,
                                                       gint hint);
static void         gyw_tool_level3_selection_finished(GwyPlainTool *plain_tool);
static void         gwy_tool_level3_apply             (GwyToolLevel3 *tool);
static void         param_changed                     (GwyToolLevel3 *tool,
                                                       gint id);
static void         update_headers                    (GwyToolLevel3 *tool);
static void         render_cell                       (GtkCellLayout *layout,
                                                       GtkCellRenderer *renderer,
                                                       GtkTreeModel *model,
                                                       GtkTreeIter *iter,
                                                       gpointer user_data);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Three-point level tool, levels data by subtracting a plane fitted through three selected points."),
    "Yeti <yeti@gwyddion.net>",
    "3.0",
    "David Nečas (Yeti) & Petr Klapetek",
    "2003",
};

GWY_MODULE_QUERY2(module_info, level3)

G_DEFINE_TYPE(GwyToolLevel3, gwy_tool_level3, GWY_TYPE_PLAIN_TOOL)

static gboolean
module_register(void)
{
    gwy_tool_func_register(GWY_TYPE_TOOL_LEVEL3);

    return TRUE;
}

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

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "level3");
    gwy_param_def_add_int(paramdef, PARAM_RADIUS, "radius", _("_Averaging radius"), 1, 32, 1);
    gwy_param_def_add_boolean(paramdef, PARAM_INSTANT_APPLY, "instant_apply", _("_Instant apply"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_SET_ZERO, "set_zero", _("Set plane to _zero"), FALSE);

    return paramdef;
}

static void
gwy_tool_level3_class_init(GwyToolLevel3Class *klass)
{
    GwyPlainToolClass *ptool_class = GWY_PLAIN_TOOL_CLASS(klass);
    GwyToolClass *tool_class = GWY_TOOL_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = gwy_tool_level3_finalize;

    tool_class->icon_name = GWY_ICON_LEVEL_TRIANGLE;
    tool_class->title = _("Three Point Level");
    tool_class->tooltip = _("Level data by fitting a plane through three points");
    tool_class->prefix = "/module/level3";
    tool_class->data_switched = gwy_tool_level3_data_switched;
    tool_class->response = gwy_tool_level3_response;

    ptool_class->data_changed = gwy_tool_level3_data_changed;
    ptool_class->selection_changed = gwy_tool_level3_selection_changed;
    ptool_class->selection_finished = gyw_tool_level3_selection_finished;
}

static void
gwy_tool_level3_finalize(GObject *object)
{
    GwyToolLevel3 *tool = GWY_TOOL_LEVEL3(object);

    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);
    if (tool->model) {
        gtk_tree_view_set_model(tool->treeview, NULL);
        g_clear_object(&tool->model);
    }

    G_OBJECT_CLASS(gwy_tool_level3_parent_class)->finalize(object);
}

static void
gwy_tool_level3_init(GwyToolLevel3 *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    plain_tool->unit_style = GWY_UNIT_FORMAT_MARKUP;
    tool->params = gwy_params_new_from_settings(define_module_params());
    gwy_plain_tool_connect_selection(plain_tool, GWY_TYPE_LAYER_POINT, "point");

    gwy_tool_level3_init_dialog(tool);
}

static void
gwy_tool_level3_init_dialog(GwyToolLevel3 *tool)
{
    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GtkTreeViewColumn *column;
    GtkCellRenderer *renderer;
    GtkWidget *label;
    GwyParamTable *table;
    GwyNullStore *store;
    guint i;

    store = gwy_null_store_new(3);
    tool->model = GTK_TREE_MODEL(store);
    tool->treeview = GTK_TREE_VIEW(gtk_tree_view_new_with_model(tool->model));

    for (i = 0; i < NCOLUMNS; i++) {
        column = gtk_tree_view_column_new();
        g_object_set_data(G_OBJECT(column), "id", GUINT_TO_POINTER(i));
        renderer = gtk_cell_renderer_text_new();
        g_object_set(renderer, "xalign", 1.0, NULL);
        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE);
        gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_cell, tool, NULL);
        label = gtk_label_new(NULL);
        gtk_tree_view_column_set_widget(column, label);
        gtk_widget_show(label);
        gtk_tree_view_append_column(tool->treeview, column);
    }

    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), GTK_WIDGET(tool->treeview), TRUE, TRUE, 0);

    table = tool->table = gwy_param_table_new(tool->params);
    gwy_param_table_append_slider(table, PARAM_RADIUS);
    gwy_param_table_set_unitstr(table, PARAM_RADIUS, _("px"));
    gwy_param_table_append_checkbox(table, PARAM_INSTANT_APPLY);
    gwy_param_table_append_checkbox(table, PARAM_SET_ZERO);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), gwy_param_table_widget(table), FALSE, FALSE, 0);
    gwy_plain_tool_add_param_table(GWY_PLAIN_TOOL(tool), table);

    gwy_plain_tool_add_clear_button(GWY_PLAIN_TOOL(tool));
    gwy_tool_add_hide_button(GWY_TOOL(tool), FALSE);
    gwy_plain_tool_add_apply_button(GWY_PLAIN_TOOL(tool));
    gtk_dialog_set_default_response(dialog, GTK_RESPONSE_APPLY);
    gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_APPLY, FALSE);
    gwy_help_add_to_tool_dialog(dialog, GWY_TOOL(tool), GWY_HELP_NO_BUTTON);

    update_headers(tool);
    g_signal_connect_swapped(tool->table, "param-changed", G_CALLBACK(param_changed), tool);

    gtk_widget_show_all(gtk_dialog_get_content_area(dialog));
}

static void
gwy_tool_level3_data_switched(GwyTool *gwytool,
                              GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolLevel3 *tool = GWY_TOOL_LEVEL3(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);

    GWY_TOOL_CLASS(gwy_tool_level3_parent_class)->data_switched(gwytool, data_view);

    if (ignore || plain_tool->init_failed)
        return;

    if (data_view) {
        gwy_object_set_or_reset(plain_tool->layer, GWY_TYPE_LAYER_POINT,
                                "marker-radius", gwy_params_get_int(tool->params, PARAM_RADIUS) - 1.0,
                                "editable", TRUE,
                                "focus", -1,
                                NULL);
        gwy_selection_set_max_objects(plain_tool->selection, 3);
        tool->allow_undo = TRUE;
    }

    update_headers(tool);
}

static void
gwy_tool_level3_data_changed(GwyPlainTool *plain_tool)
{
    GwyToolLevel3 *tool = GWY_TOOL_LEVEL3(plain_tool);

    update_headers(tool);
    gwy_null_store_rows_changed(GWY_NULL_STORE(tool->model), 0, 2);
}

static void
gwy_tool_level3_response(GwyTool *tool,
                         gint response_id)
{
    GWY_TOOL_CLASS(gwy_tool_level3_parent_class)->response(tool, response_id);

    if (response_id == GTK_RESPONSE_APPLY)
        gwy_tool_level3_apply(GWY_TOOL_LEVEL3(tool));
}

static void
gwy_tool_level3_selection_changed(GwyPlainTool *plain_tool,
                                  gint hint)
{
    GwyToolLevel3 *tool = GWY_TOOL_LEVEL3(plain_tool);
    GwyNullStore *store = GWY_NULL_STORE(tool->model);
    gboolean instant_apply = gwy_params_get_boolean(tool->params, PARAM_INSTANT_APPLY);
    gint n = (plain_tool->selection ? gwy_selection_get_data(plain_tool->selection, NULL) : 0);

    g_return_if_fail(hint <= 3);

    if (hint < 0)
        gwy_null_store_rows_changed(GWY_NULL_STORE(tool->model), 0, 2);
    else
        gwy_null_store_row_changed(store, hint);

    gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY,
                                      n == 3 && !instant_apply);

    if (n == 3 && instant_apply) {
        gwy_tool_level3_apply(tool);
        tool->allow_undo = FALSE;
    }
    else
        tool->allow_undo = TRUE;
}

static void
gyw_tool_level3_selection_finished(GwyPlainTool *plain_tool)
{
    GwyToolLevel3 *tool = GWY_TOOL_LEVEL3(plain_tool);
    gboolean instant_apply = gwy_params_get_boolean(tool->params, PARAM_INSTANT_APPLY);
    gint n = (plain_tool->selection ? gwy_selection_get_data(plain_tool->selection, NULL) : 0);

    if (n == 3 && instant_apply)
        tool->allow_undo = TRUE;
}

static void
param_changed(GwyToolLevel3 *tool, gint id)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyParams *params = tool->params;
    gboolean instant_apply = gwy_params_get_boolean(tool->params, PARAM_INSTANT_APPLY);
    gint n = (plain_tool->selection ? gwy_selection_get_data(plain_tool->selection, NULL) : 0);

    if (id < 0 || id == PARAM_RADIUS) {
        if (plain_tool->layer)
            g_object_set(plain_tool->layer, "marker-radius", gwy_params_get_int(params, PARAM_RADIUS) - 1.0, NULL);
    }
    if (id < 0 || id == PARAM_INSTANT_APPLY) {
        gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY,
                                          n == 3 && !instant_apply);
    }

    if (n == 3 && instant_apply)
        gwy_tool_level3_apply(tool);
}

static void
update_header(GwyToolLevel3 *tool,
              guint col,
              GString *str,
              const gchar *title,
              GwyValueFormat *vf)
{
    GtkTreeViewColumn *column = gtk_tree_view_get_column(tool->treeview, col);
    GtkLabel *label = GTK_LABEL(gtk_tree_view_column_get_widget(column));

    g_string_assign(str, "<b>");
    g_string_append(str, title);
    g_string_append(str, "</b>");
    if (vf)
        g_string_append_printf(str, " [%s]", vf->units);
    gtk_label_set_markup(label, str->str);
}

static void
update_headers(GwyToolLevel3 *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GString *str;

    str = g_string_new("");
    update_header(tool, COLUMN_I, str, "n", NULL);
    update_header(tool, COLUMN_X, str, "x", plain_tool->coord_format);
    update_header(tool, COLUMN_Y, str, "y", plain_tool->coord_format);
    update_header(tool, COLUMN_Z, str, _("Value"), plain_tool->value_format);
    g_string_free(str, TRUE);
}

static void
render_cell(GtkCellLayout *layout,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            gpointer user_data)
{
    GwyToolLevel3 *tool = (GwyToolLevel3*)user_data;
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    gint id = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(layout), "id"));
    const GwyValueFormat *vf = NULL;
    gchar buf[32];
    gdouble point[2];
    gdouble val;
    guint idx;

    gtk_tree_model_get(model, iter, 0, &idx, -1);
    if (id == COLUMN_I) {
        g_snprintf(buf, sizeof(buf), "%d", idx + 1);
        g_object_set(renderer, "text", buf, NULL);
        return;
    }

    if (!plain_tool->selection || !gwy_selection_get_object(plain_tool->selection, idx, point)) {
        g_object_set(renderer, "text", "", NULL);
        return;
    }

    if (id == COLUMN_X) {
        vf = plain_tool->coord_format;
        val = point[0];
    }
    else if (id == COLUMN_Y) {
        vf = plain_tool->coord_format;
        val = point[1];
    }
    else if (id == COLUMN_Z) {
        vf = plain_tool->value_format;
        val = gwy_plain_tool_get_z_average(plain_tool->field, point,
                                           gwy_params_get_int(tool->params, PARAM_RADIUS));
    }
    else {
        g_return_if_reached();
    }

    if (vf)
        g_snprintf(buf, sizeof(buf), "%.*f", vf->precision, val/vf->magnitude);
    else
        g_snprintf(buf, sizeof(buf), "%.3g", val);

    g_object_set(renderer, "text", buf, NULL);
}

static void
gwy_tool_level3_apply(GwyToolLevel3 *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyField *field = plain_tool->field;
    gint radius = gwy_params_get_int(tool->params, PARAM_RADIUS);
    gboolean set_zero = gwy_params_get_boolean(tool->params, PARAM_SET_ZERO);
    gdouble points[9], z[3], coeffs[3];
    gint xres, yres, i;

    g_return_if_fail(plain_tool->id >= 0 && field != NULL);

    if (gwy_selection_get_data(plain_tool->selection, points) < 3) {
        g_warning("Apply invoked with less than 3 points");
        return;
    }

    /* Find the plane levelling coeffs so that values in the three points will be all zeroes
     *
     *  /       \   /  \     /  \
     * | x1 y1 1 | | bx |   | z1 |
     * | x2 y2 1 | | by | = | z2 |
     * | x3 y3 1 | | c  |   | z3 |
     *  \       /   \  /     \  /
     *
     */
    for (i = 0; i < 3; i++)
        z[i] = gwy_plain_tool_get_z_average(field, points + 2*i, radius);
    points[7] = points[5];
    points[6] = points[4];
    points[4] = points[3];
    points[3] = points[2];
    points[2] = points[5] = points[8] = 1.0;
    gwy_math_lin_solve_rewrite(3, points, z, coeffs);
    /* To keep mean value intact, the mean value of the plane we add to the data has to be zero, i.e., in the center
     * of the data the value must be zero. */
    coeffs[0] = gwy_field_jtor(field, coeffs[0]);
    coeffs[1] = gwy_field_itor(field, coeffs[1]);
    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    if (!set_zero)
        coeffs[2] = -0.5*(coeffs[0]*xres + coeffs[1]*yres);
    if (tool->allow_undo) {
        gwy_app_undo_qcheckpoint(plain_tool->container, gwy_file_key_image(plain_tool->id), 0);
        gwy_params_save_to_settings(tool->params);   /* Ensure correct parameters in the log. */
        gwy_plain_tool_log_add(plain_tool);
    }
    gwy_field_plane_level(field, coeffs[2], coeffs[0], coeffs[1]);

    gwy_field_data_changed(field);
}

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