/*
 *  $Id: graph-window-measure-dialog.c 30028 2026-06-09 13:45:30Z yeti-dn $
 *  Copyright (C) 2003-2025 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 "libgwyddion/macros.h"
#include "libgwyddion/math.h"

#include "libgwyui/option-menus.h"
#include "libgwyui/utils.h"
#include "libgwyui/null-store.h"
#include "libgwyui/combo-boxes.h"
#include "libgwyui/graph-window-measure-dialog.h"

/* FIXME: Reverse dependence, change sanity.h to normal API and get rid of parts of it, as needed. */

enum {
    COLUMN_X = 0,
    COLUMN_Y,
    COLUMN_DELTAX,
    COLUMN_DELTAY,
    COLUMN_ANGLE,
    NCOLUMNS
};

enum {
    ROW_CURVE = 0,
    ROW_METHOD,
};

typedef enum {
    METHOD_INTERSECTIONS = 0,
    METHOD_FREE_POINTS   = 1,
    METHOD_HORIZONTAL    = 2
} MethodType;

typedef struct {
    gdouble x;
    gdouble y;
    gdouble deltax;
    gdouble deltay;
    gdouble angle;
} MeasuredRecord;

struct GwyGraphWindowMeasureDialogPrivate {
    GwyGraphModel *gmodel;
    GwyGraphModel *dummy_gmodel;
    GwyGraph *graph;
    GtkWidget *grid;
    GtkWidget *method_chooser;
    GtkWidget *curve;
    GtkWidget *treeview;
    GtkAdjustment *index;
    GwyNullStore *store;
    GArray *values;

    gchar *xunitstr;
    gchar *yunitstr;
    gdouble xmag;
    gdouble ymag;

    MethodType method;

    GwySelection *selection;
    gulong selection_id;

    gulong graph_model_notify_id;
    gulong curve_data_changed_id;

    gboolean same_units;
};

static void               finalize               (GObject *object);
static void               dispose                (GObject *object);
static gboolean           deleted                (GtkWidget *widget,
                                                  GdkEventAny *event);
static void               show                   (GtkWidget *widget);
static void               hide                   (GtkWidget *widget);
static void               response               (GtkDialog *dialog,
                                                  gint response_id);
static void               render_value           (GtkTreeViewColumn *column,
                                                  GtkCellRenderer *renderer,
                                                  GtkTreeModel *model,
                                                  GtkTreeIter *iter,
                                                  gpointer user_data);
static void               connect_selection      (GwyGraphWindowMeasureDialog *dialog);
static void               disconnect_selection   (GwyGraphWindowMeasureDialog *dialog);
static void               selection_updated      (GwyGraphWindowMeasureDialog *dialog);
static void               graph_model_changed    (GwyGraphWindowMeasureDialog *dialog);
static void               curve_selected         (GtkComboBox *combo,
                                                  GwyGraphWindowMeasureDialog *dialog);
static void               method_switched        (GtkComboBox *combo,
                                                  GwyGraphWindowMeasureDialog *dialog);
static void               replace_curve_selector (GwyGraphWindowMeasureDialog *dialog);
static void               curve_data_changed     (GwyGraphWindowMeasureDialog *dialog,
                                                  gint curve);
static void               graph_model_notify     (GwyGraphWindowMeasureDialog *dialog,
                                                  const GParamSpec *pspec);
static void               update_units           (GwyGraphWindowMeasureDialog *dialog);
static void               recalculate_table      (GwyGraphWindowMeasureDialog *dialog);
static void               update_header_label    (GtkTreeView *treeview,
                                                  gint col,
                                                  const gchar *unitstr);
static GwyGraphStatusType graph_status_for_method(MethodType method);

static const gchar *column_headers[NCOLUMNS] = {
    N_("X"), N_("Y"), N_("ΔX"), N_("ΔY"), N_("Angle"),
};

static G_DEFINE_QUARK(gwy-graph-window-measure-dialog-column-id, column_id)

static GtkDialogClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyGraphWindowMeasureDialog, gwy_graph_window_measure_dialog, GTK_TYPE_DIALOG,
                        G_ADD_PRIVATE(GwyGraphWindowMeasureDialog))

static void
gwy_graph_window_measure_dialog_class_init(GwyGraphWindowMeasureDialogClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GtkDialogClass *dialog_class = GTK_DIALOG_CLASS(klass);

    parent_class = gwy_graph_window_measure_dialog_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->dispose = dispose;

    widget_class->delete_event = deleted;
    widget_class->show = show;
    widget_class->hide = hide;

    dialog_class->response = response;
}

static void
finalize(GObject *object)
{
    GwyGraphWindowMeasureDialog *dialog = GWY_GRAPH_WINDOW_MEASURE_DIALOG(object);
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    g_clear_object(&priv->dummy_gmodel);
    g_clear_object(&priv->store);
    g_free(priv->xunitstr);
    g_free(priv->yunitstr);
    g_array_free(priv->values, TRUE);

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

static void
dispose(GObject *object)
{
    GwyGraphWindowMeasureDialog *dialog = GWY_GRAPH_WINDOW_MEASURE_DIALOG(object);
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    disconnect_selection(dialog);
    priv->graph = NULL;
    graph_model_changed(dialog);

    G_OBJECT_CLASS(parent_class)->dispose(object);
}

static void
gwy_graph_window_measure_dialog_init(GwyGraphWindowMeasureDialog *dialog)
{
    static const GwyEnum method_types[] = {
        { N_("Intersections"),   METHOD_INTERSECTIONS, },
        { N_("Horizontal"),      METHOD_HORIZONTAL,    },
        { N_("Points anywhere"), METHOD_FREE_POINTS,   },
    };

    GwyGraphWindowMeasureDialogPrivate *priv;

    dialog->priv = priv = gwy_graph_window_measure_dialog_get_instance_private(dialog);

    priv->method = METHOD_INTERSECTIONS;
    priv->same_units = TRUE;
    priv->store = gwy_null_store_new(0);
    priv->dummy_gmodel = gwy_graph_model_new();
    priv->values = g_array_new(FALSE, FALSE, sizeof(MeasuredRecord));
    priv->xmag = priv->ymag = 1.0;
    priv->xunitstr = g_strdup("");
    priv->yunitstr = g_strdup("");

    gtk_window_set_title(GTK_WINDOW(dialog), _("Measure Distances"));
    gtk_window_set_default_size(GTK_WINDOW(dialog), -1, 480);

    GtkWidget *vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));

    priv->grid = gtk_grid_new();
    gtk_box_pack_start(GTK_BOX(vbox), priv->grid, FALSE, FALSE, 0);

    priv->method_chooser = gwy_enum_combo_box_new(method_types, G_N_ELEMENTS(method_types),
                                                  G_CALLBACK(method_switched), dialog, priv->method, TRUE);
    gwy_gtkgrid_attach_adjbar(GTK_GRID(priv->grid), ROW_METHOD, _("Method:"), NULL, G_OBJECT(priv->method_chooser),
                              GWY_HSCALE_WIDGET_NO_EXPAND);

    priv->treeview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(priv->store));
    GtkTreeView *treeview = GTK_TREE_VIEW(priv->treeview);
    gtk_tree_view_set_headers_visible(treeview, TRUE);

    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_NONE);

    GtkWidget *scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(scwin), GTK_WIDGET(treeview));
    gtk_box_pack_start(GTK_BOX(vbox), scwin, TRUE, TRUE, 0);

    for (guint i = 0; i < NCOLUMNS; i++) {
        GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
        GtkTreeViewColumn *column = gtk_tree_view_column_new();
        g_object_set_qdata(G_OBJECT(column), column_id_quark(), GUINT_TO_POINTER(i));
        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE);
        GtkWidget *header = gtk_label_new(_(column_headers[i]));
        gtk_widget_show(header);
        gtk_tree_view_column_set_widget(column, header);
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_value, dialog, NULL);
        gtk_tree_view_column_set_expand(column, TRUE);
        gtk_tree_view_append_column(treeview, column);
    }
    update_header_label(treeview, COLUMN_ANGLE, _("deg"));

    gwy_stock_add_button_to_dialog(GTK_DIALOG(dialog), GWY_STOCK_CLOSE);
    gwy_stock_add_button_to_dialog(GTK_DIALOG(dialog), GWY_STOCK_CLEAR);

    graph_model_changed(dialog);
}

static gboolean
deleted(GtkWidget *widget, G_GNUC_UNUSED GdkEventAny *event)
{
    gtk_widget_hide(widget);
    return TRUE;
}

static void
show(GtkWidget *widget)
{
    GwyGraphWindowMeasureDialog *dialog = GWY_GRAPH_WINDOW_MEASURE_DIALOG(widget);
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    GTK_WIDGET_CLASS(parent_class)->show(widget);
    gwy_graph_set_status(priv->graph, graph_status_for_method(priv->method));
    connect_selection(dialog);
}

static void
hide(GtkWidget *widget)
{
    GwyGraphWindowMeasureDialog *dialog = GWY_GRAPH_WINDOW_MEASURE_DIALOG(widget);
    disconnect_selection(dialog);
    GTK_WIDGET_CLASS(parent_class)->hide(widget);
}

static void
response(GtkDialog *dialog, gint response_id)
{
    GwyGraphWindowMeasureDialogPrivate *priv = GWY_GRAPH_WINDOW_MEASURE_DIALOG(dialog)->priv;

    if (response_id == GWY_RESPONSE_CLEAR) {
        gwy_selection_clear(priv->selection);
        return;
    }
    gtk_widget_hide(GTK_WIDGET(dialog));
}

GtkWidget*
gwy_graph_window_measure_dialog_new(GwyGraph *graph)
{
    GwyGraphWindowMeasureDialog *dialog;

    dialog = GWY_GRAPH_WINDOW_MEASURE_DIALOG(g_object_new(GWY_TYPE_GRAPH_WINDOW_MEASURE_DIALOG, NULL));

    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;
    priv->graph = graph;
    g_signal_connect_swapped(graph, "notify::model", G_CALLBACK(graph_model_changed), dialog);
    graph_model_changed(dialog);

    return GTK_WIDGET(dialog);
}

static void
graph_model_notify(GwyGraphWindowMeasureDialog *dialog,
                   const GParamSpec *pspec)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    if (gwy_stramong(pspec->name, "unit-x", "unit-y", NULL)) {
        update_units(dialog);
        if (priv->selection_id)
            gtk_widget_queue_draw(priv->treeview);
    }
}

static void
update_units(GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    GtkTreeView *treeview = GTK_TREE_VIEW(priv->treeview);
    GwyGraphAxis *xaxis = GWY_GRAPH_AXIS(gwy_graph_get_axis(priv->graph, GTK_POS_BOTTOM));
    GwyGraphAxis *yaxis = GWY_GRAPH_AXIS(gwy_graph_get_axis(priv->graph, GTK_POS_LEFT));
    gdouble mag;

    /* Use the same formats as graph axes.
     * FIXME: Is this OK for logscale? */
    mag = gwy_graph_axis_get_magnification(xaxis);
    if (gwy_assign_string(&priv->xunitstr, gwy_graph_axis_get_magnification_string(xaxis)) || mag != priv->xmag) {
        priv->xmag = mag;
        update_header_label(treeview, COLUMN_X, priv->xunitstr);
        update_header_label(treeview, COLUMN_DELTAX, priv->xunitstr);
    }

    mag = gwy_graph_axis_get_magnification(yaxis);
    if (gwy_assign_string(&priv->yunitstr, gwy_graph_axis_get_magnification_string(yaxis)) || mag != priv->ymag) {
        priv->ymag = mag;
        update_header_label(treeview, COLUMN_Y, priv->yunitstr);
        update_header_label(treeview, COLUMN_DELTAY, priv->yunitstr);
    }

    GwyUnit *xunit, *yunit;
    g_object_get(priv->gmodel, "unit-x", &xunit, "unit-y", &yunit, NULL);
    priv->same_units = gwy_unit_equal(xunit, yunit);
    g_object_unref(xunit);
    g_object_unref(yunit);
}

static void
curve_data_changed(GwyGraphWindowMeasureDialog *dialog,
                   gint curve)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    if (priv->selection_id && gwy_enum_combo_box_get_active(GTK_COMBO_BOX(priv->curve)) == curve)
        recalculate_table(dialog);
}

static void
curve_selected(G_GNUC_UNUSED GtkComboBox *combo, GwyGraphWindowMeasureDialog *dialog)
{
    recalculate_table(dialog);
}

static void
graph_model_changed(GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    GwyGraphModel *gmodel = priv->graph ? gwy_graph_get_model(priv->graph) : NULL;
    if (!gmodel)
        gmodel = priv->dummy_gmodel;

    if (!gwy_set_member_object(dialog, gmodel, GWY_TYPE_GRAPH_MODEL, &priv->gmodel,
                               "notify", graph_model_notify,
                               &priv->graph_model_notify_id, G_CONNECT_SWAPPED,
                               "curve-data-changed", curve_data_changed,
                               &priv->curve_data_changed_id, G_CONNECT_SWAPPED,
                               NULL))
        return;

    replace_curve_selector(dialog);
}

static void
method_switched(GtkComboBox *combo, GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    priv->method = gwy_enum_combo_box_get_active(combo);
    disconnect_selection(dialog);
    gwy_graph_set_status(GWY_GRAPH(priv->graph), graph_status_for_method(priv->method));
    connect_selection(dialog);
    recalculate_table(dialog);
}

static void
replace_curve_selector(GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    if (priv->curve)
        gtk_widget_destroy(priv->curve);

    /* FIXME FIXME FIXME the combo cannot handle model changes yet! */
    priv->curve = gwy_combo_box_graph_curve_new(G_CALLBACK(curve_selected), dialog, priv->gmodel, 0);
    gwy_gtkgrid_attach_adjbar(GTK_GRID(priv->grid), ROW_CURVE, _("Curve:"), NULL,
                              G_OBJECT(priv->curve), GWY_HSCALE_WIDGET_NO_EXPAND);
    curve_selected(GTK_COMBO_BOX(priv->curve), dialog);
}

static void
render_value(GtkTreeViewColumn *column, GtkCellRenderer *renderer,
             GtkTreeModel *model, GtkTreeIter *iter,
             gpointer user_data)
{
    GwyGraphWindowMeasureDialog *dialog = (GwyGraphWindowMeasureDialog*)user_data;
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    guint id = GPOINTER_TO_UINT(g_object_get_qdata(G_OBJECT(column), column_id_quark()));
    guint i;
    gtk_tree_model_get(model, iter, 0, &i, -1);
    const MeasuredRecord *row = &g_array_index(priv->values, MeasuredRecord, i);
    gchar *s = NULL;
    if (id == COLUMN_X)
        s = g_strdup_printf("%.3f", row->x/priv->xmag);
    else if (id == COLUMN_Y)
        s = g_strdup_printf("%.3f", row->y/priv->ymag);
    else if (i) {
        if (id == COLUMN_DELTAX)
            s = g_strdup_printf("%.3f", row->deltax/priv->xmag);
        else if (id == COLUMN_DELTAY)
            s = g_strdup_printf("%.3f", row->deltay/priv->ymag);
        else if (id == COLUMN_ANGLE && priv->same_units)
            s = g_strdup_printf("%.3f", gwy_rad2deg(row->angle));
    }

    if (s) {
        g_object_set(renderer, "text", s, NULL);
        g_free(s);
    }
    else {
        g_object_set(renderer, "text", "", NULL);
    }
}

static void
connect_selection(GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;
    GwyGraphArea *area = GWY_GRAPH_AREA(gwy_graph_get_area(priv->graph));
    GwyGraphStatusType status = gwy_graph_area_get_status(area);

    GwySelection *selection = priv->selection = gwy_graph_area_get_selection(area, status);
    g_object_ref(selection);
    gwy_selection_set_max_objects(selection, 1024);
    priv->selection_id = g_signal_connect_swapped(selection, "changed", G_CALLBACK(selection_updated), dialog);
}

static void
disconnect_selection(GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;
    g_clear_signal_handler(&priv->selection_id, priv->selection);
    g_clear_object(&priv->selection);
}

static void
update_header_label(GtkTreeView *treeview, gint col, const gchar *unitstr)
{
    const gchar *header = column_headers[col];
    gchar *s;

    if (unitstr && *unitstr)
        s = g_strconcat("<b>", header, "</b> [", unitstr, "]", NULL);
    else
        s = g_strconcat("<b>", header, "</b>", NULL);

    GtkWidget *label = gtk_tree_view_column_get_widget(gtk_tree_view_get_column(treeview, col));
    gtk_label_set_markup(GTK_LABEL(label), s);
    g_free(s);
}

static gdouble
get_y_for_x(const gdouble *xdata, const gdouble *ydata, gint ndata,
            gdouble x, gboolean *found)
{
    /* Find the tightest enclosing interval, even for unordered data. */
    gint ibefore = ndata+1, iafter = -1;
    gdouble xbefore = -G_MAXDOUBLE, xafter = G_MAXDOUBLE;
    for (gint i = 0; i < ndata; i++) {
        if (xdata[i] <= x && xdata[i] > xbefore)
            ibefore = i;
        if (xdata[i] >= x && xdata[i] < xafter)
            iafter = i;
    }
    if (ibefore == ndata+1 || iafter == -1) {
        *found = FALSE;
        return 0.0;
    }
    *found = TRUE;

    gdouble ybefore = ydata[ibefore], yafter = ydata[iafter];
    if (xbefore >= xafter)
        return 0.5*(ybefore + yafter);

    return (ybefore*(xafter - x) + yafter*(x - xbefore))/(xafter - xbefore);
}

static void
selection_updated(GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;
    GwyGraphStatusType status = gwy_graph_get_status(priv->graph);

    if (status == graph_status_for_method(priv->method))
        recalculate_table(dialog);
}

static void
recalculate_table(GwyGraphWindowMeasureDialog *dialog)
{
    GwyGraphWindowMeasureDialogPrivate *priv = dialog->priv;

    GtkTreeView *treeview = GTK_TREE_VIEW(priv->treeview);
    gtk_tree_view_set_model(treeview, NULL);

    GArray *values = priv->values;
    g_array_set_size(values, 0);
    if (!priv->selection)
        goto finish;

    GwyGraphModel *gmodel = priv->gmodel;
    gint ncurves = gwy_graph_model_get_n_curves(gmodel);
    if (!ncurves)
        goto finish;
    gint curve = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(priv->curve));
    if (curve < 0 || curve >= ncurves)
        goto finish;

    gint nsel = gwy_selection_get_n_objects(priv->selection);
    const gdouble *seldata = gwy_selection_get_data_array(priv->selection);
    if (!nsel)
        goto finish;

    MethodType method = priv->method;
    g_return_if_fail(gwy_graph_get_status(priv->graph) == graph_status_for_method(method));

    update_units(dialog);

    GwyGraphCurveModel *cmodel = gwy_graph_model_get_curve(gmodel, curve);
    const gdouble *xdata = gwy_graph_curve_model_get_xdata(cmodel);
    const gdouble *ydata = gwy_graph_curve_model_get_ydata(cmodel);
    gint ndata = gwy_graph_curve_model_get_n_data(cmodel);

    for (gint i = 0; i < nsel; i++) {
        gboolean found = TRUE;

        MeasuredRecord row;
        gwy_clear1(row);
        if (method == METHOD_INTERSECTIONS) {
            row.x = seldata[i];
            row.y = get_y_for_x(xdata, ydata, ndata, row.x, &found);
        }
        else if (method == METHOD_FREE_POINTS) {
            row.x = seldata[2*i];
            row.y = seldata[2*i + 1];
        }
        else if (method == METHOD_HORIZONTAL) {
            row.y = seldata[i];
        }
        else {
            g_assert_not_reached();
        }

        if (!found)
            continue;

        if (i) {
            const MeasuredRecord *prev = &g_array_index(values, MeasuredRecord, i-1);
            row.deltax = row.x - prev->x;
            row.deltay = row.y - prev->y;
            row.angle = atan2(row.deltay, row.deltax);
        }
        g_array_append_val(values, row);
    }

finish:
    gwy_null_store_set_n_rows(priv->store, values->len);
    gtk_tree_view_set_model(treeview, GTK_TREE_MODEL(priv->store));
}

static GwyGraphStatusType
graph_status_for_method(MethodType method)
{
    static const struct {
        MethodType method;
        GwyGraphStatusType status;
    }
    method_status_map[] = {
        { METHOD_INTERSECTIONS, GWY_GRAPH_STATUS_XLINES, },
        { METHOD_HORIZONTAL,    GWY_GRAPH_STATUS_YLINES, },
        { METHOD_FREE_POINTS,   GWY_GRAPH_STATUS_POINTS, },
    };

    for (guint i = 0; i < G_N_ELEMENTS(method_status_map); i++) {
        if (method == method_status_map[i].method)
            return method_status_map[i].status;
    }

    g_assert_not_reached();
    return GWY_GRAPH_STATUS_PLAIN;
}

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