/*
 *  $Id: graph-key.c 30146 2026-06-25 12:51:18Z 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 <pango/pango.h>
#include <pango/pangocairo.h>

#include "libgwyddion/macros.h"

#include "libgwyui/graph-utils.h"
#include "libgwyui/graph-key.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/graph-internal.h"

struct _GwyGraphKeyPrivate {
    GwyGraphModel *graph_model;

    PangoFontDescription *font_desc;
    GArray *samplepos;
    gint reqheight;
    gint reqwidth;

    gboolean enable_user_input;

    gulong model_notify_id;
    gulong curve_notify_id;
};

static void     finalize            (GObject *object);
static void     dispose             (GObject *object);
static void     get_preferred_width (GtkWidget *widget,
                                     gint *minimum,
                                     gint *natural);
static void     get_preferred_height(GtkWidget *widget,
                                     gint *minimum,
                                     gint *natural);
static gboolean draw                (GtkWidget *widget,
                                     cairo_t *cr);
static void     refresh_visible     (GwyGraphKey *label);
static void     refresh_all         (GwyGraphKey *label);
static void     calculate_size      (GwyGraphKey *label);
static void     model_notify        (GwyGraphKey *label,
                                     GParamSpec *param,
                                     GwyGraphModel *gmodel);
static void     curve_notify        (GwyGraphKey *label,
                                     gint i,
                                     GParamSpec *param);

static GtkWidgetClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyGraphKey, gwy_graph_key, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyGraphKey))


static void
gwy_graph_key_class_init(GwyGraphKeyClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_graph_key_parent_class;

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

    widget_class->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;
    /* FIXME: We should do something about fonts, but we do not create or destroy any windows.
    widget_class->realize = realize;
    widget_class->unrealize = unrealize;
    */
    widget_class->draw = draw;
}

static void
gwy_graph_key_init(GwyGraphKey *label)
{
    GwyGraphKeyPrivate *priv;

    priv = label->priv = gwy_graph_key_get_instance_private(label);
    priv->samplepos = g_array_new(FALSE, FALSE, sizeof(gint));

    PangoContext *context = gtk_widget_get_pango_context(GTK_WIDGET(label));
    PangoFontDescription *description = pango_context_get_font_description(context);

    /* Make major font a bit smaller */
    priv->font_desc = pango_font_description_copy(description);
    gint size = pango_font_description_get_size(priv->font_desc);
    size = MAX(1, size*10/11);
    pango_font_description_set_size(priv->font_desc, size);

    gtk_widget_set_has_window(GTK_WIDGET(label), FALSE);
}

static void
dispose(GObject *object)
{
    GwyGraphKeyPrivate *priv = GWY_GRAPH_KEY(object)->priv;

    g_clear_signal_handler(&priv->model_notify_id, priv->graph_model);
    g_clear_signal_handler(&priv->curve_notify_id, priv->graph_model);
    g_clear_object(&priv->graph_model);

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

static void
finalize(GObject *object)
{
    GwyGraphKeyPrivate *priv = GWY_GRAPH_KEY(object)->priv;

    pango_font_description_free(priv->font_desc);
    g_array_free(priv->samplepos, TRUE);

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

/**
 * gwy_graph_key_new:
 *
 * Creates a new graph label.
 *
 * Returns: A new graph label widget as a #GtkWidget.
 **/
GtkWidget*
gwy_graph_key_new(void)
{
    return gtk_widget_new(GWY_TYPE_GRAPH_KEY, NULL);
}

static void
get_preferred_width(GtkWidget *widget,
                    gint *minimum, gint *natural)
{
    GwyGraphKeyPrivate *priv = GWY_GRAPH_KEY(widget)->priv;
    *minimum = *natural = priv->reqwidth;
}

static void
get_preferred_height(GtkWidget *widget,
                     gint *minimum, gint *natural)
{
    GwyGraphKeyPrivate *priv = GWY_GRAPH_KEY(widget)->priv;
    *minimum = *natural = priv->reqheight;
}

/**
 * gwy_graph_key_set_model:
 * @label: A graph label.
 * @gmodel: New graph model.
 *
 * Sets new model of a graph label.
 **/
void
gwy_graph_key_set_model(GwyGraphKey *label,
                        GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_GRAPH_KEY(label));
    g_return_if_fail(!gmodel || GWY_IS_GRAPH_MODEL(gmodel));

    GwyGraphKeyPrivate *priv = label->priv;
    if (!gwy_set_member_object(label, gmodel, GWY_TYPE_GRAPH_MODEL, &priv->graph_model,
                               "notify", G_CALLBACK(model_notify), &priv->model_notify_id, G_CONNECT_SWAPPED,
                               "curve-notify", G_CALLBACK(curve_notify), &priv->curve_notify_id, G_CONNECT_SWAPPED,
                               NULL))
        return;

    refresh_all(label);
    refresh_visible(label);
}

/**
 * gwy_graph_key_get_model:
 * @label: A graph label.
 *
 * Gets the model of a graph key.
 *
 * Returns: The graph model this graph label takes curve properties and descriptions from.
 **/
GwyGraphModel*
gwy_graph_key_get_model(GwyGraphKey *label)
{
    g_return_val_if_fail(GWY_IS_GRAPH_KEY(label), NULL);
    return label->priv->graph_model;
}

static void
model_notify(GwyGraphKey *label,
             GParamSpec *pspec,
             G_GNUC_UNUSED GwyGraphModel *gmodel)
{
    /* FIXME: A bit simplistic */
    if (g_str_has_prefix(pspec->name, "label-")) {
        const gchar *name = pspec->name + strlen("label-");

        if (gwy_strequal(name, "visible"))
            refresh_visible(label);
        else
            refresh_all(label);
    }

    if (gwy_strequal(pspec->name, "n-curves")) {
        refresh_all(label);
        return;
    }
}

static void
curve_notify(GwyGraphKey *label,
             G_GNUC_UNUSED gint i,
             GParamSpec *pspec)
{
    /* These three should not change label size */
    if (gwy_stramong(pspec->name, "color", "line-style", "point-type", NULL))
        gtk_widget_queue_draw(GTK_WIDGET(label));
    else
        refresh_all(label);
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    GwyGraphKey *label = GWY_GRAPH_KEY(widget);
    GwyGraphKeyPrivate *priv = label->priv;
    GwyGraphModel *gmodel = priv->graph_model;

    if (!gmodel)
        return FALSE;

    PangoLayout *layout = gtk_widget_create_pango_layout(widget, NULL);
    pango_layout_set_font_description(layout, priv->font_desc);

    gboolean reversed = gmodel->priv->label_reverse;
    gint frame_off = gmodel->priv->label_frame_thickness/2;
    gint ypos = 5 + frame_off;

    cairo_save(cr);
    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_paint(cr);
    cairo_restore(cr);

    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);
    gint winwidth = allocation.width, winheight = allocation.height;

    gint nc = gwy_graph_model_get_n_curves(gmodel);
    for (gint i = 0; i < nc; i++) {
        GwyGraphCurveModel *gcmodel = gwy_graph_model_get_curve(gmodel, i);
        GwyGraphCurveMode mode = gcmodel->priv->mode;
        if (mode == GWY_GRAPH_CURVE_HIDDEN)
            continue;

        pango_layout_set_markup(layout, gcmodel->priv->description->str, gcmodel->priv->description->len);
        PangoRectangle rect;
        pango_layout_get_pixel_extents(layout, NULL, &rect);

        cairo_save(cr);
        cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
        if (reversed)
            cairo_move_to(cr, winwidth - rect.width - 25 - frame_off, ypos);
        else
            cairo_move_to(cr, 25 + frame_off, ypos);
        pango_cairo_show_layout(cr, layout);
        cairo_restore(cr);

        g_array_index(priv->samplepos, gint, i) = ypos;
        gint yc = ypos + rect.height/2;
        GwyRGBA *color = &gcmodel->priv->color;
        if (mode == GWY_GRAPH_CURVE_LINE || mode == GWY_GRAPH_CURVE_LINE_POINTS) {
            gint x1, x2;
            if (reversed) {
                x1 = winwidth - 20 - frame_off;
                x2 = winwidth - 5;
            }
            else {
                x1 = 5 + frame_off;
                x2 = 20 + frame_off;
            }
            gwy_cairo_draw_line(cr, x1, yc, x2, yc, gcmodel->priv->line_style, gcmodel->priv->line_width, color);
        }
        if (mode == GWY_GRAPH_CURVE_POINTS || mode == GWY_GRAPH_CURVE_LINE_POINTS) {
            gint x = (reversed ? winwidth - 13 - frame_off : 12 + frame_off);
            gwy_cairo_draw_symbol(cr, x, yc, gcmodel->priv->point_type, gcmodel->priv->point_size, color);
        }
        ypos += rect.height + 5;
    }

    gdouble fwidth = gmodel->priv->label_frame_thickness;
    if (fwidth > 0) {
        cairo_save(cr);
        cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
        cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER);
        cairo_set_line_width(cr, fwidth);
        cairo_rectangle(cr,
                        0.5*fwidth, 0.5*fwidth,
                        winwidth - 0.5*fwidth, winheight - 0.5*fwidth);
        cairo_stroke(cr);
        cairo_restore(cr);
    }

    return FALSE;
}

/* TODO GTK3 this should be done by the parent, not the graph label widget itself! */
static void
refresh_visible(GwyGraphKey *label)
{
    GwyGraphKeyPrivate *priv = label->priv;
    gboolean visible = FALSE;
    if (priv->graph_model)
        g_object_get(priv->graph_model, "label-visible", &visible, NULL);

    GtkWidget *widget = GTK_WIDGET(label);
    gtk_widget_set_no_show_all(widget, !visible);
    if (visible)
        gtk_widget_show(widget);
    else
        gtk_widget_hide(widget);
}

static void
refresh_all(GwyGraphKey *label)
{
    GwyGraphKeyPrivate *priv = label->priv;
    gint nc = priv->graph_model ? gwy_graph_model_get_n_curves(priv->graph_model) : 0;
    g_array_set_size(priv->samplepos, nc);
    calculate_size(label);
    gtk_widget_queue_resize(GTK_WIDGET(label));
    gtk_widget_queue_draw(GTK_WIDGET(label));
}

/* Determine requested size of label
 * (It will be needed by grapharea to put the label into layout) */
/* FIXME: It seems to overestimate the height a bit, it's visible on labels
 * with many curves.  It also seems to strangely underestimate the width
 * for curve descriptions under certain length. */
static void
calculate_size(GwyGraphKey *label)
{
    GwyGraphKeyPrivate *priv = label->priv;
    GwyGraphModel *gmodel = priv->graph_model;

    priv->reqheight = 0;
    priv->reqwidth = 0;

    PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(label), NULL);

    gint nc = gmodel ? gwy_graph_model_get_n_curves(gmodel) : 0;
    for (gint i = 0; i < nc; i++) {
        GwyGraphCurveModel *gcmodel = gwy_graph_model_get_curve(gmodel, i);
        if (gcmodel->priv->mode == GWY_GRAPH_CURVE_HIDDEN)
            continue;
        pango_layout_set_font_description(layout, priv->font_desc);
        pango_layout_set_markup(layout, gcmodel->priv->description->str, gcmodel->priv->description->len);
        PangoRectangle rect;
        pango_layout_get_pixel_extents(layout, NULL, &rect);

        priv->reqwidth = MAX(priv->reqwidth, rect.width);
        priv->reqheight += rect.height + 5;
    }
    g_object_unref(layout);
    priv->reqheight = MAX(priv->reqheight, 20);

    if (gmodel) {
        priv->reqwidth += 30 + 2*gmodel->priv->label_frame_thickness;
        priv->reqheight += 2*gmodel->priv->label_frame_thickness;
    }
}

/**
 * gwy_graph_key_enable_user_input:
 * @label: A graph label.
 * @enable: Whether to enable the user input.
 *
 * Enables or disables user input to a graph label.
 **/
void
gwy_graph_key_enable_user_input(GwyGraphKey *label, gboolean enable)
{
    g_return_if_fail(GWY_IS_GRAPH_KEY(label));
    label->priv->enable_user_input = !!enable;
}

/**
 * SECTION: graph-key
 * @title: GwyGraphKey
 * @short_description: Graph curve key
 *
 * #GwyGraphKey is a part of #GwyGraph, it renders frame with graph curve key.
 **/

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