/*
 *  $Id: graph-data.c 29754 2026-04-09 09:35:42Z yeti-dn $
 *  Copyright (C) 2006-2024 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 <string.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"

#include "libgwyui/graph-data.h"
#include "libgwyui/graph-model.h"
#include "libgwyui/utils.h"

enum {
    /* The maximum value width */
    COL_WIDTH = sizeof("-0.12345e+308")
};

struct _GwyGraphDataPrivate {
    GwyGraphModel *graph_model;
    GwyNullStore *store;
    GArray *curves;
    GString *str;

    gulong curve_data_id;
    gulong notify_id;

    GwyValueFormat *vformatx;
    GwyValueFormat *vformaty;
};

static void dispose       (GObject *object);
static void finalize      (GObject *object);
static void update_headers(GwyGraphData *graph_data);
static void update_ncurves(GwyGraphData *graph_data);
static void update_nrows  (GwyGraphData *graph_data);
static void model_notify  (GwyGraphData *graph_data,
                           const GParamSpec *pspec);

static GtkTreeViewClass *parent_class = NULL;

static G_DEFINE_QUARK(gwy-graph-data-column-id, column_id)

G_DEFINE_TYPE_WITH_CODE(GwyGraphData, gwy_graph_data, GTK_TYPE_TREE_VIEW,
                        G_ADD_PRIVATE(GwyGraphData))

static void
gwy_graph_data_class_init(GwyGraphDataClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_graph_data_parent_class;

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

static void
gwy_graph_data_init(GwyGraphData *graph_data)
{
    GwyGraphDataPrivate *priv;

    graph_data->priv = priv = gwy_graph_data_get_instance_private(graph_data);

    priv->store = gwy_null_store_new(0);
    priv->curves = g_array_new(FALSE, FALSE, sizeof(GwyGraphCurveModel*));
    priv->str = g_string_new(NULL);

    GtkTreeView *treeview = GTK_TREE_VIEW(graph_data);
    gtk_tree_view_set_model(treeview, GTK_TREE_MODEL(priv->store));
    gtk_tree_view_set_fixed_height_mode(treeview, TRUE);
}

static void
finalize(GObject *object)
{
    GwyGraphDataPrivate *priv = GWY_GRAPH_DATA(object)->priv;

    g_array_free(priv->curves, TRUE);
    g_string_free(priv->str, TRUE);

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

static void
dispose(GObject *object)
{
    GwyGraphData *graph_data = GWY_GRAPH_DATA(object);
    GwyGraphDataPrivate *priv = graph_data->priv;

    gwy_graph_data_set_model(graph_data, NULL);
    g_clear_object(&priv->store);

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

/**
 * gwy_graph_data_new:
 *
 * Creates graph_data widget based on information in graph model.
 *
 * The #GtkTreeModel and the columns follow the graph model and must not be changed manually.
 *
 * Returns: A new graph_data widget.
 **/
GtkWidget*
gwy_graph_data_new(void)
{
    return gtk_widget_new(GWY_TYPE_GRAPH_DATA, NULL);
}

/**
 * gwy_graph_data_new_with_model:
 * @gmodel: A graph_data model.
 *
 * Creates graph_data widget based on information in graph model.
 *
 * The #GtkTreeModel and the columns follow the graph model and must not be changed manually.
 *
 * Returns: A new graph_data widget.
 **/
GtkWidget*
gwy_graph_data_new_with_model(GwyGraphModel *gmodel)
{
    GwyGraphData *graph_data = g_object_new(GWY_TYPE_GRAPH_DATA, NULL);

    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), GTK_WIDGET(graph_data));
    gwy_graph_data_set_model(graph_data, gmodel);

    return (GtkWidget*)graph_data;
}

/**
 * gwy_graph_data_set_model:
 * @graph_data: A graph data widget.
 * @gmodel: New graph_data model.
 *
 * Changes the graph model a graph data table displays.
 **/
void
gwy_graph_data_set_model(GwyGraphData *graph_data,
                         GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_GRAPH_DATA(graph_data));
    g_return_if_fail(!gmodel || GWY_IS_GRAPH_MODEL(gmodel));

    GwyGraphDataPrivate *priv = graph_data->priv;
    if (gwy_set_member_object(graph_data, gmodel, GWY_TYPE_GRAPH_MODEL, &priv->graph_model,
                              "notify", G_CALLBACK(model_notify), &priv->notify_id, G_CONNECT_SWAPPED,
                              "curve-data-changed", G_CALLBACK(update_nrows), &priv->curve_data_id, G_CONNECT_SWAPPED,
                              NULL))
        update_ncurves(graph_data);
}

/**
 * gwy_graph_data_get_model:
 * @graph_data: A graph_data widget.
 *
 * Gets the graph model a graph data table displays.
 *
 * Returns: The graph model associated with this #GwyGraphData widget.
 **/
GwyGraphModel*
gwy_graph_data_get_model(GwyGraphData *graph_data)
{
    g_return_val_if_fail(GWY_IS_GRAPH_DATA(graph_data), NULL);
    return graph_data->priv->graph_model;
}

static void
model_notify(GwyGraphData *graph_data, const GParamSpec *pspec)
{
    const gchar *name = pspec->name;
    gwy_debug("notify::%s from model", name);

    if (gwy_strequal(name, "n-curves")) {
        update_ncurves(graph_data);
        return;
    }

    if (gwy_strequal(name, "axis-label-bottom") || gwy_strequal(name, "axis-label-left")) {
        update_headers(graph_data);
        return;
    }
}

static void
render_data(GtkTreeViewColumn *column,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            gpointer data)
{
    GwyGraphDataPrivate *priv = GWY_GRAPH_DATA(data)->priv;
    gint id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(column), column_id_quark()));

    /* Be fault-tolerant */
    if (!priv->graph_model || id >= priv->curves->len)
        return;

    GwyGraphCurveModel *gcmodel = g_array_index(priv->curves, GwyGraphCurveModel*, id);
    gint row;
    gtk_tree_model_get(model, iter, 0, &row, -1);

    if (row >= gwy_graph_curve_model_get_n_data(gcmodel)) {
        g_object_set(renderer, "text", "", NULL);
        return;
    }

    const gdouble *d;
    if (g_object_get_qdata(G_OBJECT(renderer), column_id_quark()))
        d = gwy_graph_curve_model_get_ydata(gcmodel);
    else
        d = gwy_graph_curve_model_get_xdata(gcmodel);

    /* TODO: improve formatting using some value format (may be hard to do for all value range) */
    g_string_printf(priv->str, "%g", d[row]);
    g_object_set(renderer, "text", priv->str->str, NULL);
}

static inline void
pack_renderer(GwyGraphData *graph_data, GtkTreeViewColumn *column, gint id)
{
    GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
    gtk_cell_renderer_text_set_fixed_height_from_font(GTK_CELL_RENDERER_TEXT(renderer), 1);
    g_object_set(renderer, "xalign", 1.0, NULL);
    if (id)
        g_object_set_qdata(G_OBJECT(renderer), column_id_quark(), GINT_TO_POINTER(id));
    gtk_tree_view_column_pack_start(column, renderer, TRUE);
    gtk_tree_view_column_set_cell_data_func(column, renderer, render_data, graph_data, NULL);
}

static void
update_ncurves(GwyGraphData *graph_data)
{
    GwyGraphDataPrivate *priv = graph_data->priv;
    guint ncolumns = priv->curves->len, ncurves = 0;

    gwy_debug("old ncurves: %d", ncolumns);

    /* Rebuild the array */
    for (guint i = 0; i < priv->curves->len; i++)
        g_clear_object(&g_array_index(priv->curves, GwyGraphCurveModel*, i));
    g_array_set_size(priv->curves, 0);

    if (priv->graph_model) {
        ncurves = gwy_graph_model_get_n_curves(priv->graph_model);
        for (guint i = 0; i < ncurves; i++) {
            GwyGraphCurveModel *gcmodel = gwy_graph_model_get_curve(priv->graph_model, i);
            g_object_ref(gcmodel);
            g_array_append_val(priv->curves, gcmodel);
        }
    }
    gwy_debug("ncurves: %u", ncurves);

    /* Update the number of columns. */
    GtkTreeView *treeview = GTK_TREE_VIEW(graph_data);

    if (ncolumns) {
        /* Remove the sacrificial colum which is always there, unless the widget is empty. */
        GtkTreeViewColumn *column = gtk_tree_view_get_column(treeview, ncolumns);
        gtk_tree_view_remove_column(treeview, column);
    }

    while (ncolumns > ncurves) {
        ncolumns--;
        gwy_debug("removing column %d", ncolumns);
        GtkTreeViewColumn *column = gtk_tree_view_get_column(treeview, ncolumns);
        gtk_tree_view_remove_column(treeview, column);
    }

    while (ncolumns < ncurves) {
        gwy_debug("adding column %d", ncolumns);
        GtkTreeViewColumn *column = gtk_tree_view_column_new();
        g_object_set_qdata(G_OBJECT(column), column_id_quark(), GINT_TO_POINTER(ncolumns));

        pack_renderer(graph_data, column, 0);
        pack_renderer(graph_data, column, 1);

        GtkWidget *label, *grid = gtk_grid_new();
        gtk_widget_set_hexpand(grid, TRUE);
        label = gtk_label_new(NULL);
        gtk_widget_set_hexpand(label, TRUE);
        gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 2, 1);
        label = gtk_label_new(NULL);
        gtk_widget_set_hexpand(label, TRUE);
        gtk_label_set_width_chars(GTK_LABEL(label), COL_WIDTH);
        gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
        label = gtk_label_new(NULL);
        gtk_widget_set_hexpand(label, TRUE);
        gtk_label_set_width_chars(GTK_LABEL(label), COL_WIDTH);
        gtk_grid_attach(GTK_GRID(grid), label, 1, 1, 1, 1);
        gtk_widget_show_all(grid);
        gtk_tree_view_column_set_widget(column, grid);

        gint width;
        gtk_widget_get_preferred_width(grid, NULL, &width);
        gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
        gtk_tree_view_column_set_fixed_width(column, width);
        gtk_tree_view_append_column(treeview, column);

        ncolumns++;
    }

    /* XXX: This seems silly, but without it GTK3 chooses the last column as the victim and expands it to fill the
     * entire width. Even, worse, the header does not expand and is squashed on the left. So we add a sacrificial
     * column and let GTK+ expand that.  */
    GtkTreeViewColumn *column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
    gtk_tree_view_column_set_expand(column, TRUE);
    gtk_tree_view_append_column(treeview, column);

    if (priv->graph_model)
        update_headers(graph_data);

    if (priv->store)
        update_nrows(graph_data);
}

static void
update_headers(GwyGraphData *graph_data)
{
    GwyGraphDataPrivate *priv = graph_data->priv;
    GtkTreeView *treeview = GTK_TREE_VIEW(graph_data);
    const gchar *xlabel = gwy_graph_model_get_axis_label(priv->graph_model, GTK_POS_BOTTOM);
    const gchar *ylabel = gwy_graph_model_get_axis_label(priv->graph_model, GTK_POS_LEFT);
    GwyUnit *siunit;

    g_object_get(priv->graph_model, "unit-x", &siunit, NULL);
    gchar *sx = gwy_unit_get_string(siunit, GWY_UNIT_FORMAT_MARKUP);
    g_object_unref(siunit);

    g_object_get(priv->graph_model, "unit-y", &siunit, NULL);
    gchar *sy = gwy_unit_get_string(siunit, GWY_UNIT_FORMAT_MARKUP);
    g_object_unref(siunit);

    GString *str = priv->str;
    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *gcmodel = g_array_index(priv->curves, GwyGraphCurveModel*, i);
        GtkTreeViewColumn *column = gtk_tree_view_get_column(treeview, i);
        GtkWidget *grid = gtk_tree_view_column_get_widget(column);
        GtkWidget *label = gtk_grid_get_child_at(GTK_GRID(grid), 0, 0);
        gchar *desc;
        g_object_get(gcmodel, "description", &desc, NULL);
        gtk_label_set_markup(GTK_LABEL(label), desc);
        g_free(desc);

        label = gtk_grid_get_child_at(GTK_GRID(grid), 0, 1);
        g_string_assign(str, xlabel);
        if (sx && *sx) {
            g_string_append(str, " [");
            g_string_append(str, sx);
            g_string_append(str, "]");
        }
        gtk_label_set_markup(GTK_LABEL(label), str->str);

        label = gtk_grid_get_child_at(GTK_GRID(grid), 1, 1);
        g_string_assign(str, ylabel);
        if (sy && *sy) {
            g_string_append(str, " [");
            g_string_append(str, sy);
            g_string_append(str, "]");
        }
        gtk_label_set_markup(GTK_LABEL(label), str->str);
    }

    g_free(sx);
    g_free(sy);
}

static void
update_nrows(GwyGraphData *graph_data)
{
    GwyGraphDataPrivate *priv = graph_data->priv;
    guint max = 0;

    for (guint i = 0; i < priv->curves->len; i++) {
        GwyGraphCurveModel *gcmodel = g_array_index(priv->curves, GwyGraphCurveModel*, i);
        guint n = gwy_graph_curve_model_get_n_data(gcmodel);
        if (n > max)
            max = n;
    }

    gwy_debug("nrows: %d", max);
    gwy_null_store_set_n_rows(priv->store, max);
}

/**
 * SECTION: graph-data
 * @title: GwyGraphData
 * @short_description: Graph data table
 *
 * #GwyGraphData displays data values from #GwyGraphModel curves in a table. While it is a #GtkTreeView, it uses
 * a dummy tree model (#GwyNullStore) and its content is determined by the graph model.
 **/

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