/*
 *  $Id: graph-curves.c 29576 2026-03-05 18:45:19Z yeti-dn $
 *  Copyright (C) 2007-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 <glib/gi18n-lib.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"

#include "libgwyui/graph-curves.h"
#include "libgwyui/graph-model.h"
#include "libgwyui/utils.h"
#include "libgwyui/graph-internal.h"

enum {
    COLUMN_DESCRIPTION,
    COLUMN_MODE,
    COLUMN_COLOR,
    COLUMN_POINT_TYPE,
    COLUMN_LINE_STYLE,
    NVIEW_COLUMNS
};

enum {
    COLUMN_ID,
    COLUMN_MODEL,
    NMODEL_COLUMNS
};

struct _GwyGraphCurvesPrivate {
    GwyGraphModel *gmodel;
    GtkListStore *curves;
    GdkPixbuf *pixbuf;

    gulong n_curves_id;
    gulong curve_notify_id;
};

static void     dispose         (GObject *object);
static void     finalize        (GObject *object);
static gboolean key_pressed     (GtkWidget *widget,
                                 GdkEventKey *event);
static void     n_curves_changed(GwyGraphCurves *curves);
static void     curve_notify    (GwyGraphCurves *curves,
                                 gint id,
                                 const GParamSpec *pspec);
static void     setup           (GwyGraphCurves *curves);

static GtkTreeViewClass *parent_class = NULL;

static G_DEFINE_QUARK(gwy-graph-curves-column-id, column_id)
static G_DEFINE_QUARK(gwy-graph-curves-combomodel, model)

G_DEFINE_TYPE_WITH_CODE(GwyGraphCurves, gwy_graph_curves, GTK_TYPE_TREE_VIEW,
                        G_ADD_PRIVATE(GwyGraphCurves))

static void
gwy_graph_curves_class_init(GwyGraphCurvesClass *klass)
{
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_graph_curves_parent_class;

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

    widget_class->key_press_event = key_pressed;
}

static void
gwy_graph_curves_init(GwyGraphCurves *curves)
{
    GwyGraphCurvesPrivate *priv;

    curves->priv = priv = gwy_graph_curves_get_instance_private(curves);
    priv->curves = gtk_list_store_new(NMODEL_COLUMNS, G_TYPE_INT, GWY_TYPE_GRAPH_CURVE_MODEL);

    gtk_tree_view_set_model(GTK_TREE_VIEW(curves), GTK_TREE_MODEL(priv->curves));
    setup(curves);
}

static void
finalize(GObject *object)
{
    GwyGraphCurvesPrivate *priv = GWY_GRAPH_CURVES(object)->priv;

    g_object_unref(priv->curves);

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

static void
dispose(GObject *object)
{
    GwyGraphCurves *curves = GWY_GRAPH_CURVES(object);

    gwy_graph_curves_set_model(curves, NULL);
    g_clear_object(&curves->priv->pixbuf);

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

static gboolean
key_pressed(GtkWidget *widget, GdkEventKey *event)
{
    GwyGraphCurvesPrivate *priv = GWY_GRAPH_CURVES(widget)->priv;

    if (event->keyval == GDK_KEY_Delete) {
        GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget));
        GtkTreeIter iter;
        if (priv->gmodel && gtk_tree_selection_get_selected(selection, NULL, &iter)) {
            GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(priv->curves), &iter);
            const gint *indices = gtk_tree_path_get_indices(path);
            gwy_graph_model_remove_curve(priv->gmodel, indices[0]);
            gtk_tree_path_free(path);
        }

        return TRUE;
    }

    return GTK_WIDGET_CLASS(parent_class)->key_press_event(widget, event);
}

/**
 * gwy_graph_curves_new:
 *
 * Creates graph curve list 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 curve list.
 **/
GtkWidget*
gwy_graph_curves_new(void)
{
    return gtk_widget_new(GWY_TYPE_GRAPH_CURVES, NULL);
}

/**
 * gwy_graph_curves_new_with_model:
 * @gmodel: A graph model.
 *
 * Creates graph curve list 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 curve list.
 **/
GtkWidget*
gwy_graph_curves_new_with_model(GwyGraphModel *gmodel)
{
    GwyGraphCurves *curves = g_object_new(GWY_TYPE_GRAPH_CURVES, NULL);
    g_return_val_if_fail(GWY_IS_GRAPH_MODEL(gmodel), (GtkWidget*)curves);
    gwy_graph_curves_set_model(curves, gmodel);
    return (GtkWidget*)curves;
}

/**
 * gwy_graph_curves_set_model:
 * @curves: A graph curve list.
 * @gmodel: New graph model.
 *
 * Changes the graph model a graph curve list.
 **/
void
gwy_graph_curves_set_model(GwyGraphCurves *curves,
                           GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_GRAPH_CURVES(curves));

    GwyGraphCurvesPrivate *priv = curves->priv;
    if (gwy_set_member_object(curves, gmodel, GWY_TYPE_GRAPH_MODEL, &priv->gmodel,
                              "notify::n-curves", G_CALLBACK(n_curves_changed), &priv->n_curves_id, G_CONNECT_SWAPPED,
                              "curve-notify", G_CALLBACK(curve_notify), &priv->curve_notify_id, G_CONNECT_SWAPPED,
                              NULL)) {
        n_curves_changed(curves);
    }
}

/**
 * gwy_graph_curves_get_model:
 * @curves: A graph curve list.
 *
 * Gets the graph model a graph curve list displays.
 *
 * Returns: The graph model associated with this #GwyGraphCurves widget.
 **/
GwyGraphModel*
gwy_graph_curves_get_model(GwyGraphCurves *curves)
{
    g_return_val_if_fail(GWY_IS_GRAPH_CURVES(curves), NULL);

    return curves->priv->gmodel;
}

static void
n_curves_changed(GwyGraphCurves *curves)
{
    GwyGraphCurvesPrivate *priv = curves->priv;
    GtkListStore *store = priv->curves;
    GtkTreeModel *model = GTK_TREE_MODEL(store);
    gint i, ncurves = 0, nrows = gtk_tree_model_iter_n_children(model, NULL);

    if (priv->gmodel)
        ncurves = gwy_graph_model_get_n_curves(priv->gmodel);
    gwy_debug("old ncurves %u, new ncurves: %u", nrows, ncurves);

    /* Rebuild the model from scratch */
    GtkTreeIter iter;
    gtk_tree_model_get_iter_first(model, &iter);
    for (i = 0; i < MAX(ncurves, nrows); i++) {
        if (i < MIN(ncurves, nrows)) {
            GwyGraphCurveModel *gcmodel = gwy_graph_model_get_curve(priv->gmodel, i);

            gtk_list_store_set(store, &iter, COLUMN_MODEL, gcmodel, -1);
            gtk_tree_model_iter_next(model, &iter);
        }
        else if (i < ncurves) {
            GwyGraphCurveModel *gcmodel = gwy_graph_model_get_curve(priv->gmodel, i);

            gtk_list_store_insert_with_values(store, &iter, G_MAXINT,
                                              COLUMN_ID, i,
                                              COLUMN_MODEL, gcmodel,
                                              -1);
        }
        else if (i < nrows)
            gtk_list_store_remove(store, &iter);
        else
            g_assert_not_reached();
    }
}

static void
curve_notify(GwyGraphCurves *curves, gint id, G_GNUC_UNUSED const GParamSpec *pspec)
{
    gwy_list_store_row_changed(curves->priv->curves, NULL, NULL, id);
}

/* XXX: O(n) */
static gboolean
find_enum_row(GtkTreeModel *model,
              GtkTreeIter *iter,
              gint value)
{
    gint v;

    if (!model || !gtk_tree_model_get_iter_first(model, iter))
        return FALSE;

    do {
        gtk_tree_model_get(model, iter, GWY_GRAPH_COMBO_COLUMN_VALUE, &v, -1);
        if (v == value)
            return TRUE;
    } while (gtk_tree_model_iter_next(model, iter));

    return FALSE;
}

static void
render_column(GtkCellLayout *column,
              GtkCellRenderer *renderer,
              GtkTreeModel *model,
              GtkTreeIter *iter,
              gpointer data)
{
    GwyGraphCurvesPrivate *priv = GWY_GRAPH_CURVES(data)->priv;

    GwyGraphCurveModel *gcmodel;
    gtk_tree_model_get(model, iter, COLUMN_MODEL, &gcmodel, -1);
    /* Be fault-tolerant */
    if (!gcmodel)
        return;

    gint id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(column), column_id_quark()));
    if (id == COLUMN_DESCRIPTION) {
        gchar *s;
        g_object_get(gcmodel, "description", &s, NULL);
        g_object_set(renderer, "text", s, NULL);
        g_free(s);
    }
    else if (id == COLUMN_COLOR) {
        GwyRGBA *color;
        g_object_get(gcmodel, "color", &color, NULL);
        guint32 pixel = 0xff | gwy_rgba_to_pixbuf_pixel(color);
        gwy_rgba_free(color);
        gdk_pixbuf_fill(priv->pixbuf, pixel);
    }
    else if (id == COLUMN_MODE) {
        gint v;
        g_object_get(gcmodel, "mode", &v, NULL);
        const GwyEnum *modes = gwy_graph_curve_mode_get_enum();
        const gchar *s = gwy_enum_to_string(v, modes, -1);
        if (s && *s)
            g_object_set(renderer, "text", gwy_C(s), NULL);
    }
    else if (id == COLUMN_POINT_TYPE || id == COLUMN_LINE_STYLE) {
        GtkTreeIter comboiter;
        gint v;

        if (id == COLUMN_POINT_TYPE)
            g_object_get(gcmodel, "point-type", &v, NULL);
        else if (id == COLUMN_LINE_STYLE)
            g_object_get(gcmodel, "line-style", &v, NULL);
        else {
            g_return_if_reached();   /* This is here to shut up clang. */
        }

        GtkTreeModel *combomodel = g_object_get_qdata(G_OBJECT(column), model_quark());
        if (find_enum_row(combomodel, &comboiter, v)) {
            if (GTK_IS_CELL_RENDERER_TEXT(renderer)) {
                gchar *s;
                gtk_tree_model_get(combomodel, &comboiter, GWY_GRAPH_COMBO_COLUMN_NAME, &s, -1);
                g_object_set(renderer, "text", gwy_C(s), NULL);
                g_free(s);
            }
            else if (GTK_IS_CELL_RENDERER_PIXBUF(renderer)) {
                GdkPixbuf *pixbuf;
                gtk_tree_model_get(combomodel, &comboiter, GWY_GRAPH_COMBO_COLUMN_PIXBUF, &pixbuf, -1);
                g_object_set(renderer, "pixbuf", pixbuf, NULL);
                g_object_unref(pixbuf);
            }
        }
    }
    else {
        g_return_if_reached();
    }
}

static void
setup(GwyGraphCurves *curves)
{
    GwyGraphCurvesPrivate *priv = curves->priv;
    GtkTreeView *treeview = GTK_TREE_VIEW(curves);
    GtkTreeViewColumn *column;
    GtkCellRenderer *renderer;
    gint width, height;

    /* Description */
    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, _("Description"));
    g_object_set_qdata(G_OBJECT(column), column_id_quark(), GINT_TO_POINTER(COLUMN_DESCRIPTION));
    gtk_tree_view_append_column(treeview, column);

    renderer = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_column, curves, NULL);

    /* Mode */
    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, _("Mode"));
    g_object_set_qdata(G_OBJECT(column), column_id_quark(), GINT_TO_POINTER(COLUMN_MODE));
    gtk_tree_view_append_column(treeview, column);

    renderer = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_column, curves, NULL);

    /* Color */
    gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
    width |= 1;
    height |= 1;
    priv->pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, GWY_ROUND(1.618*height), height);

    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, _("Color"));
    g_object_set_qdata(G_OBJECT(column), column_id_quark(), GINT_TO_POINTER(COLUMN_COLOR));
    gtk_tree_view_append_column(treeview, column);

    renderer = gtk_cell_renderer_pixbuf_new();
    g_object_set(renderer, "pixbuf", priv->pixbuf, NULL);
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_column, curves, NULL);

    /* Point type */
    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, _("Point Type"));
    g_object_set_qdata(G_OBJECT(column), column_id_quark(), GINT_TO_POINTER(COLUMN_POINT_TYPE));
    /* The models are static so we simply keep them around and do not care. */
    g_object_set_qdata(G_OBJECT(column), model_quark(), _gwy_graph_get_point_type_store());
    gtk_tree_view_append_column(treeview, column);

    renderer = gtk_cell_renderer_pixbuf_new();
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_column, curves, NULL);

    renderer = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_column, curves, NULL);

    /* Line style */
    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, _("Line Style"));
    g_object_set_qdata(G_OBJECT(column), column_id_quark(), GINT_TO_POINTER(COLUMN_LINE_STYLE));
    /* The models are static so we simply keep them around and do not care. */
    g_object_set_qdata(G_OBJECT(column), model_quark(), _gwy_graph_get_line_style_store());
    gtk_tree_view_append_column(treeview, column);

    renderer = gtk_cell_renderer_pixbuf_new();
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_column, curves, NULL);

    renderer = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_column, curves, NULL);
}

/**
 * SECTION: graph-curves
 * @title: GwyGraphCurves
 * @short_description: Graph curve list
 *
 * #GwyGraphCurves displays the list of #GwyGraphModel curve properties in a table. While it is a #GtkTreeView, it
 * uses a fixed simplistic tree model and its content is determined by the graph model. It is an error to modify the
 * tree view structure.
 **/

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