/*
 *  $Id: graph.c 29742 2026-04-06 09:18:09Z yeti-dn $
 *  Copyright (C) 2003-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 "libgwyddion/math.h"

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

enum {
    PROP_0,
    PROP_MODEL,
    NUM_PROPERTIES,
};

struct _GwyGraphPrivate {
    GwyGraphModel *graph_model;

    GwyGraphArea *area;
    GwySelection *zoom_selection;
    GwyGraphAxis *axis[4];
    GwyGraphCorner *corner[4];
    gboolean axis_visible[4];

    gboolean enable_user_input;

    gulong rescaled_id[4];
    gulong label_updated_id[4];
    gulong model_notify_id;
    gulong curve_data_changed_id;
    gulong zoom_finished_id;
};

static void finalize           (GObject *object);
static void set_property       (GObject *object,
                                guint prop_id,
                                const GValue *value,
                                GParamSpec *pspec);
static void get_property       (GObject *object,
                                guint prop_id,
                                GValue *value,
                                GParamSpec *pspec);
static void refresh_all        (GwyGraph *graph);
static void model_notify       (GwyGraph *graph,
                                GParamSpec *pspec,
                                GwyGraphModel *gmodel);
static void curve_data_changed (GwyGraph *graph,
                                gint i);
static void refresh_ranges     (GwyGraph *graph);
static void axis_rescaled      (GwyGraphAxis *axis,
                                GwyGraph *graph);
static void area_size_allocated(GwyGraph *graph);
static void zoomed             (GwyGraph *graph);
static void label_updated      (GwyGraphAxis *axis,
                                GParamSpec *pspec,
                                GwyGraph *graph);

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GtkWidgetClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyGraph, gwy_graph, GTK_TYPE_GRID,
                        G_ADD_PRIVATE(GwyGraph))

static void
gwy_graph_class_init(GwyGraphClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_graph_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->set_property = set_property;
    gobject_class->get_property = get_property;

    properties[PROP_MODEL] = g_param_spec_object("model", NULL,
                                                 "The graph model of the graph.",
                                                 GWY_TYPE_GRAPH_MODEL,
                                                 GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_graph_init(GwyGraph *graph)
{
    GwyGraphPrivate *priv;

    priv = graph->priv = gwy_graph_get_instance_private(graph);

    priv->area = GWY_GRAPH_AREA(gwy_graph_area_new());
    gwy_graph_area_set_status(priv->area, GWY_GRAPH_STATUS_PLAIN);
    priv->enable_user_input = TRUE;

    for (GtkPositionType edge = GTK_POS_LEFT; edge <= GTK_POS_BOTTOM; edge++) {
        GtkWidget *widget = gwy_graph_axis_new(edge);
        priv->axis[edge] = GWY_GRAPH_AXIS(widget);
        /* Init to the opposite to ensure gwy_graph_set_axis_visible() actually does all the setup. */
        priv->axis_visible[edge] = !(edge == GTK_POS_LEFT || edge == GTK_POS_BOTTOM);
        gwy_graph_set_axis_visible(graph, edge, (edge == GTK_POS_LEFT || edge == GTK_POS_BOTTOM));
        priv->rescaled_id[edge] = g_signal_connect(widget, "rescaled", G_CALLBACK(axis_rescaled), graph);
        priv->label_updated_id[edge] = g_signal_connect(widget, "notify::label", G_CALLBACK(label_updated), graph);
    }

    GtkGrid *grid = GTK_GRID(graph);
    gtk_grid_attach(grid, GTK_WIDGET(priv->axis[GTK_POS_TOP]), 1, 0, 1, 1);
    gtk_grid_attach(grid, GTK_WIDGET(priv->axis[GTK_POS_LEFT]), 0, 1, 1, 1);
    gtk_grid_attach(grid, GTK_WIDGET(priv->axis[GTK_POS_RIGHT]), 2, 1, 1, 1);
    gtk_grid_attach(grid, GTK_WIDGET(priv->axis[GTK_POS_BOTTOM]), 1, 2, 1, 1);

    for (gint i = 0; i < 4; i++) {
        priv->corner[i] = GWY_GRAPH_CORNER(gwy_graph_corner_new());
        gtk_grid_attach(grid, GTK_WIDGET(priv->corner[i]), 2*(i/2), 2*(i % 2), 1, 1);
        gtk_widget_show(GTK_WIDGET(priv->corner[i]));
    }

    gtk_grid_attach(grid, GTK_WIDGET(priv->area), 1, 1, 1, 1);
    gtk_widget_set_hexpand(GTK_WIDGET(priv->area), TRUE);
    gtk_widget_set_vexpand(GTK_WIDGET(priv->area), TRUE);
    gtk_widget_show_all(GTK_WIDGET(priv->area));

    g_signal_connect_swapped(priv->area, "size-allocate", G_CALLBACK(area_size_allocated), graph);
    gwy_set_member_object(graph, gwy_graph_area_get_selection(priv->area, GWY_GRAPH_STATUS_ZOOM),
                          GWY_TYPE_SELECTION_RECTANGLE, &priv->zoom_selection,
                          "finished", G_CALLBACK(zoomed), &priv->zoom_finished_id, G_CONNECT_SWAPPED,
                          NULL);
}

static void
finalize(GObject *object)
{
    GwyGraphPrivate *priv = GWY_GRAPH(object)->priv;

    g_clear_signal_handler(&priv->zoom_finished_id, priv->zoom_selection);
    g_clear_signal_handler(&priv->model_notify_id, priv->graph_model);
    g_clear_signal_handler(&priv->curve_data_changed_id, priv->graph_model);
    /* Do not disconnect from axes; they are already gone. */
    g_clear_object(&priv->graph_model);
    g_clear_object(&priv->zoom_selection);

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

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyGraph *graph = GWY_GRAPH(object);

    switch (prop_id) {
        case PROP_MODEL:
        gwy_graph_set_model(graph, g_value_get_object(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyGraphPrivate *priv = GWY_GRAPH(object)->priv;

    switch (prop_id) {
        case PROP_MODEL:
        g_value_set_object(value, priv->graph_model);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

/**
 * gwy_graph_new:
 * @gmodel: A graph model (may be %NULL).
 *
 * Creates graph widget based on information in model.
 *
 * Returns: new graph widget.
 **/
GtkWidget*
gwy_graph_new(GwyGraphModel *gmodel)
{
    GtkWidget *widget = gtk_widget_new(GWY_TYPE_GRAPH, NULL);
    GwyGraph *graph = GWY_GRAPH(widget);

    if (gmodel)
        gwy_graph_set_model(graph, gmodel);

    return widget;
}

static void
refresh_all(GwyGraph *graph)
{
    GwyGraphPrivate *priv = graph->priv;

    if (!priv->graph_model)
        return;

    GwyGraphModel *gmodel = priv->graph_model;
    GwyUnit *siunit;

    siunit = gwy_graph_model_get_unit_x(gmodel);
    gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_BOTTOM]), siunit);
    gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_TOP]), siunit);

    siunit = gwy_graph_model_get_unit_y(gmodel);
    gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_LEFT]), siunit);
    gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_RIGHT]), siunit);

    for (GtkPositionType edge = GTK_POS_LEFT; edge <= GTK_POS_BOTTOM; edge++) {
        const gchar *label = gmodel ? gwy_graph_model_get_axis_label(gmodel, edge) : NULL;
        gwy_graph_axis_set_label(priv->axis[edge], label);
    }

    refresh_ranges(graph);
}

/**
 * gwy_graph_set_model:
 * @graph: A graph widget.
 * @gmodel: New graph model
 *
 * Changes the model a graph displays.
 *
 * Everything in graph widgets will be reset to reflect the new data.
 **/
void
gwy_graph_set_model(GwyGraph *graph, GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_GRAPH(graph));
    g_return_if_fail(!gmodel || GWY_IS_GRAPH_MODEL(gmodel));

    GwyGraphPrivate *priv = graph->priv;
    if (!gwy_set_member_object(graph, gmodel, GWY_TYPE_GRAPH_MODEL, &priv->graph_model,
                               "notify", G_CALLBACK(model_notify), &priv->model_notify_id, G_CONNECT_SWAPPED,
                               "curve-data-changed", G_CALLBACK(curve_data_changed),
                               &priv->curve_data_changed_id, G_CONNECT_SWAPPED,
                               NULL))
        return;

    gwy_graph_area_set_model(priv->area, gmodel);
    refresh_all(graph);
    g_object_notify_by_pspec(G_OBJECT(graph), properties[PROP_MODEL]);
}

/**
 * gwy_graph_get_model:
 * @graph: A graph widget.
 *
 * Gets the model of a graph.
 *
 * Returns: The graph model this graph widget displays.
 **/
GwyGraphModel*
gwy_graph_get_model(GwyGraph *graph)
{
    g_return_val_if_fail(GWY_IS_GRAPH(graph), NULL);
    return graph->priv->graph_model;
}

static void
model_notify(GwyGraph *graph,
             GParamSpec *pspec,
             GwyGraphModel *gmodel)
{
    GwyGraphPrivate *priv = graph->priv;

    if (g_str_has_prefix(pspec->name, "axis-label-")) {
        /* Axis labels */
        const gchar *which = pspec->name + strlen("axis-label-");
        gchar *label = NULL;

        g_object_get(gmodel, pspec->name, &label, NULL);
        if (gwy_strequal(which, "left"))
            gwy_graph_axis_set_label(priv->axis[GTK_POS_LEFT], label);
        else if (gwy_strequal(which, "bottom"))
            gwy_graph_axis_set_label(priv->axis[GTK_POS_BOTTOM], label);
        else if (gwy_strequal(which, "right"))
            gwy_graph_axis_set_label(priv->axis[GTK_POS_RIGHT], label);
        else if (gwy_strequal(which, "top"))
            gwy_graph_axis_set_label(priv->axis[GTK_POS_TOP], label);
        g_free(label);
    }
    else if (g_str_has_prefix(pspec->name, "unit-")) {
        /* Units */
        const gchar *name = pspec->name + strlen("unit-");
        GwyUnit *unit = NULL;

        /* Both model and axis assign units by value so this is correct */
        g_object_get(gmodel, pspec->name, &unit, NULL);
        if (gwy_strequal(name, "x")) {
            gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_BOTTOM]), unit);
            gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_TOP]), unit);
        }
        else if (gwy_strequal(name, "y")) {
            gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_LEFT]), unit);
            gwy_unit_assign(gwy_graph_axis_get_unit(priv->axis[GTK_POS_RIGHT]), unit);
        }
        g_object_unref(unit);
    }
    else if (g_str_has_prefix(pspec->name, "x-") || g_str_has_prefix(pspec->name, "y-")) {
        /* Ranges */
        refresh_ranges(graph);
    }
    else if (gwy_strequal(pspec->name, "n-curves")) {
        /* Number of curves */
        curve_data_changed(graph, -1);
    }

    gwy_debug("ignoring changed model property <%s>", pspec->name);
}

static void
curve_data_changed(GwyGraph *graph,
                   G_GNUC_UNUSED gint i)
{
    refresh_ranges(graph);
}

static void
refresh_ranges(GwyGraph *graph)
{
    GwyGraphPrivate *priv = graph->priv;
    GwyGraphModel *gmodel = priv->graph_model;
    gdouble xmin, xmax, ymin, ymax;
    gboolean xlg, ylg;

    g_object_get(gmodel, "x-logarithmic", &xlg, "y-logarithmic", &ylg, NULL);
    gwy_graph_axis_set_logarithmic(priv->axis[GTK_POS_BOTTOM], xlg);
    gwy_graph_axis_set_logarithmic(priv->axis[GTK_POS_TOP], xlg);
    gwy_graph_axis_set_logarithmic(priv->axis[GTK_POS_LEFT], ylg);
    gwy_graph_axis_set_logarithmic(priv->axis[GTK_POS_RIGHT], ylg);

    /* Request range */
    if (!gwy_graph_model_get_ranges(gmodel, xlg, ylg, &xmin, &xmax, &ymin, &ymax)) {
        xmin = xlg ? 0.1 : 0.0;
        ymin = ylg ? 0.1 : 0.0;
        xmax = ymax = 1.0;
    }

    gwy_debug("%p: req x:(%g,%g) y:(%g,%g)", graph, xmin, xmax, ymin, ymax);

    gwy_graph_axis_request_range(priv->axis[GTK_POS_BOTTOM], xmin, xmax);
    gwy_graph_axis_request_range(priv->axis[GTK_POS_TOP], xmin, xmax);
    gwy_graph_axis_request_range(priv->axis[GTK_POS_LEFT], ymin, ymax);
    gwy_graph_axis_request_range(priv->axis[GTK_POS_RIGHT], ymin, ymax);
    /* The range propagation happens in "rescaled" handler. */
    if (!priv->axis_visible[GTK_POS_LEFT])
        _gwy_graph_axis_adjust(priv->axis[GTK_POS_LEFT]);
    if (!priv->axis_visible[GTK_POS_BOTTOM])
        _gwy_graph_axis_adjust(priv->axis[GTK_POS_BOTTOM]);
}

static void
area_size_allocated(GwyGraph *graph)
{
    GwyGraphPrivate *priv = graph->priv;

    /* If axes are not visible nothing ever gets any size allocation signals and we never recompute the ticks. */
    if (!priv->axis_visible[GTK_POS_LEFT])
        _gwy_graph_axis_adjust(priv->axis[GTK_POS_LEFT]);
    if (!priv->axis_visible[GTK_POS_BOTTOM])
        _gwy_graph_axis_adjust(priv->axis[GTK_POS_BOTTOM]);
}

static void
axis_rescaled(GwyGraphAxis *axis, GwyGraph *graph)
{
    GwyGraphPrivate *priv = graph->priv;

    if (!priv->graph_model)
        return;

    gwy_debug("%p: axis %p", graph, axis);

    gdouble min, max;
    gwy_graph_axis_get_range(axis, &min, &max);
    if (axis == priv->axis[GTK_POS_BOTTOM])
        gwy_graph_area_set_x_range(priv->area, min, max);
    if (axis == priv->axis[GTK_POS_LEFT])
        gwy_graph_area_set_y_range(priv->area, min, max);

    GwyGraphGridType grid_type;
    g_object_get(priv->graph_model, "grid-type", &grid_type, NULL);
    if (grid_type == GWY_GRAPH_GRID_AUTO) {
        guint nticks;
        gdouble *ticks = gwy_graph_axis_get_major_ticks(axis, &nticks);

        if (axis == priv->axis[GTK_POS_BOTTOM])
            gwy_graph_area_set_x_grid_data(priv->area, nticks, ticks);
        if (axis == priv->axis[GTK_POS_LEFT])
            gwy_graph_area_set_y_grid_data(priv->area, nticks, ticks);

        g_free(ticks);
    }
}

/**
 * gwy_graph_get_axis:
 * @graph: A graph widget.
 * @edge: Graph edge
 *
 * Gets a graph axis.
 *
 * Returns: The axis (of given orientation) within the graph widget.
 **/
GtkWidget*
gwy_graph_get_axis(GwyGraph *graph, GtkPositionType edge)
{
    g_return_val_if_fail(GWY_IS_GRAPH(graph), NULL);
    g_return_val_if_fail(edge <= GTK_POS_BOTTOM, NULL);

    return GTK_WIDGET(graph->priv->axis[edge]);
}

/**
 * gwy_graph_set_axis_visible:
 * @graph: A graph widget.
 * @edge: Graph edge
 * @is_visible: set/unset axis visibility within graph widget
 *
 * Sets the visibility of graph axis of given orientation.
 **/
void
gwy_graph_set_axis_visible(GwyGraph *graph,
                           GtkPositionType edge,
                           gboolean is_visible)
{
    g_return_if_fail(GWY_IS_GRAPH(graph));
    g_return_if_fail(edge <= GTK_POS_BOTTOM);

    GwyGraphPrivate *priv = graph->priv;
    if (!is_visible == !priv->axis_visible[edge])
        return;

    GtkWidget *widget = GTK_WIDGET(priv->axis[edge]);
    if (is_visible) {
        gtk_widget_set_no_show_all(widget, FALSE);
        gtk_widget_show(widget);
    }
    else {
        gtk_widget_hide(widget);
        gtk_widget_set_no_show_all(widget, TRUE);
    }
    priv->axis_visible[edge] = !!is_visible;
}

/**
 * gwy_graph_get_area:
 * @graph: A graph widget.
 *
 * Gets the area widget of a graph.
 *
 * Returns: The graph area widget within the graph.
 **/
GtkWidget*
gwy_graph_get_area(GwyGraph *graph)
{
    g_return_val_if_fail(GWY_IS_GRAPH(graph), NULL);
    return GTK_WIDGET(graph->priv->area);
}

/**
 * gwy_graph_set_status:
 * @graph: A graph widget.
 * @status: graph status
 *
 * Sets the status of a graph widget.
 *
 * The status determines how the graph reacts on mouse events. This includes point or area selection and zooming.
 **/
void
gwy_graph_set_status(GwyGraph *graph, GwyGraphStatusType status)
{
    g_return_if_fail(GWY_IS_GRAPH(graph));
    gwy_graph_area_set_status(graph->priv->area, status);
}

/**
 * gwy_graph_get_status:
 * @graph: A graph widget.
 *
 * Get the status of a graph widget.
 *
 * See gwy_graph_set_status() for more.
 *
 * Returns: The current graph status.
 **/
GwyGraphStatusType
gwy_graph_get_status(GwyGraph *graph)
{
    g_return_val_if_fail(GWY_IS_GRAPH(graph), GWY_GRAPH_STATUS_PLAIN);
    return gwy_graph_area_get_status(graph->priv->area);
}

/**
 * gwy_graph_enable_user_input:
 * @graph: A graph widget.
 * @enable: whether to enable user input
 *
 * Enables/disables all the graph/curve settings dialogs to be invoked by
 * mouse clicks.
 **/
void
gwy_graph_enable_user_input(GwyGraph *graph, gboolean enable)
{
    g_return_if_fail(GWY_IS_GRAPH(graph));

    enable = !!enable;
    GwyGraphPrivate *priv = graph->priv;
    priv->enable_user_input = enable;
    gwy_graph_area_enable_user_input(priv->area, enable);

    for (GtkPositionType edge = GTK_POS_LEFT; edge <= GTK_POS_BOTTOM; edge++)
        gwy_graph_axis_enable_label_edit(priv->axis[edge], enable);
}

static void
zoomed(GwyGraph *graph)
{
    GwyGraphPrivate *priv = graph->priv;

    GwySelection *selection = priv->zoom_selection;
    if (gwy_graph_area_get_status(priv->area) != GWY_GRAPH_STATUS_ZOOM || gwy_selection_get_n_objects(selection) != 1)
        return;

    gdouble rect[4];
    gwy_selection_get_object(selection, 0, rect);

    gdouble x_reqmin = fmin(rect[0], rect[2]);
    gdouble x_reqmax = fmax(rect[0], rect[2]);
    gdouble y_reqmin = fmin(rect[1], rect[3]);
    gdouble y_reqmax = fmax(rect[1], rect[3]);
    /* This in turn causes graph refresh including axes rescale */
    g_object_set(priv->graph_model,
                 "x-min", x_reqmin, "x-min-set", TRUE,
                 "x-max", x_reqmax, "x-max-set", TRUE,
                 "y-min", y_reqmin, "y-min-set", TRUE,
                 "y-max", y_reqmax, "y-max-set", TRUE,
                 NULL);

    gwy_graph_set_status(graph, GWY_GRAPH_STATUS_PLAIN);
}

static void
label_updated(GwyGraphAxis *axis,
              G_GNUC_UNUSED GParamSpec *pspec,
              GwyGraph *graph)
{
    GwyGraphPrivate *priv = graph->priv;

    if (priv->graph_model) {
        gwy_graph_model_set_axis_label(priv->graph_model,
                                       gwy_graph_axis_get_position(axis),
                                       gwy_graph_axis_get_label(axis));
    }
}

/**
 * SECTION: graph
 * @title: GwyGraph
 * @short_description: Widget for displaying graphs
 *
 * #GwyGraph is a basic widget for displaying graphs. It consists of several widgets that can also be used separately
 * (at least in principle): #GwyGraphArea forms the main part of the graph, #GwyGraphAxis is used for the axes,
 * #GwyGraphLabel represents the key and #GwyGraphCorner is a dummy widget (at this moment) used for graph corners.
 *
 * Persisent graph properties and data are represented with #GwyGraphModel. Changes to the model are automatically
 * reflected in the graph.
 **/

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