/*
 *  $Id: gl-label.c 29656 2026-03-26 10:21:10Z yeti-dn $
 *  Copyright (C) 2005-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 <string.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/serialize.h"
#include "libgwyddion/serializable-utils.h"

#include "libgwyui/gl-label.h"

#define TYPE_NAME "GwyGLLabel"

enum {
    PROP_0,
    PROP_DELTA_X,
    PROP_DELTA_Y,
    PROP_ROTATION,
    PROP_SIZE,
    PROP_FIXED_SIZE,
    PROP_TEXT,
    PROP_DEFAULT_TEXT,
    NUM_PROPERTIES
};

enum {
    ITEM_DELTA_X, ITEM_DELTA_Y,
    ITEM_ROTATION,
    ITEM_SIZE, ITEM_FIXED_SIZE,
    ITEM_DEFAULT_TEXT, ITEM_TEXT,
    NUM_ITEMS
};

struct _GwyGLLabelPrivate {
    gdouble delta_x;
    gdouble delta_y;
    gdouble rotation;
    gdouble size;
    gboolean fixed_size;
    gchar *default_text;
    GString *text;
};

static void             finalize              (GObject *object);
static void             serializable_init     (GwySerializableInterface *iface);
static void             serializable_itemize  (GwySerializable *serializable,
                                               GwySerializableGroup *group);
static gboolean         serializable_construct(GwySerializable *serializable,
                                               GwySerializableGroup *group,
                                               GwyErrorList **error_list);
static GwySerializable* serializable_copy     (GwySerializable *serializable);
static void             serializable_assign   (GwySerializable *destination,
                                               GwySerializable *source);
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 GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GObjectClass *parent_class = NULL;

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "delta_x",         .ctype = GWY_SERIALIZABLE_DOUBLE,   },
    { .name = "delta_y",         .ctype = GWY_SERIALIZABLE_DOUBLE,   },
    { .name = "rotation",        .ctype = GWY_SERIALIZABLE_DOUBLE,   },
    { .name = "size",            .ctype = GWY_SERIALIZABLE_DOUBLE,   },
    { .name = "fixed_size",      .ctype = GWY_SERIALIZABLE_BOOLEAN,  },
    { .name = "default_text",    .ctype = GWY_SERIALIZABLE_STRING,   },
    { .name = "text",            .ctype = GWY_SERIALIZABLE_STRING,   },
};

G_DEFINE_TYPE_WITH_CODE(GwyGLLabel, gwy_gl_label, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyGLLabel)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
define_properties(void)
{
    if (properties[PROP_DELTA_X])
        return;

    /**
     * GwyGLLabel:delta-x:
     *
     * The :delta-x property represents horizontal label offset in pixels (in screen coordinates after mapping from 3D
     * to 2D).
     **/
    properties[PROP_DELTA_X] = g_param_spec_double("delta-x", NULL,
                                                   "Horizontal label offset, in pixels",
                                                   -1000, 1000, 0.0,
                                                   GWY_GPARAM_RWE);

    /**
     * GwyGLLabel:delta-y:
     *
     * The :delta-y property represents vertical label offset in pixels (in screen coordinates after mapping from 3D
     * to 2D).
     **/
    properties[PROP_DELTA_Y] = g_param_spec_double("delta-y", NULL,
                                                   "Vertical label offset, in pixels",
                                                   -1000, 1000, 0.0,
                                                   GWY_GPARAM_RWE);

    /**
     * GwyGLLabel:rotation:
     *
     * The :rotation property represents label rotation in radians, counterclokwise (on screen, after mapping from 3D
     * to 2D).
     **/
    properties[PROP_ROTATION] = g_param_spec_double("rotation", NULL,
                                                    "Label rotation in radians, counterclokwise",
                                                    -G_PI, G_PI, 0.0,
                                                    GWY_GPARAM_RWE);

    /**
     * GwyGLLabel:size:
     *
     * The :size property represents label size in pixels.  When :fixed_size is %FALSE, its value is overwritten with
     * automatic size.
     **/
    properties[PROP_SIZE] = g_param_spec_double("size", NULL,
                                                "Label font size",
                                                1.0, 100.0, 14.0,
                                                GWY_GPARAM_RWE);

    /**
     * GwyGLLabel:fixed-size:
     *
     * The :fixed-size property controls whether the :size property is kept and honoured, or conversely ignored and
     * overwritten with automatic size.
     **/
    properties[PROP_FIXED_SIZE] = g_param_spec_boolean("fixed-size", NULL,
                                                       "Whether label size is fixed and doesn't scale",
                                                       FALSE,
                                                       GWY_GPARAM_RWE);

    properties[PROP_TEXT] = g_param_spec_string("text", NULL,
                                                "The label text template",
                                                "",
                                                GWY_GPARAM_RWE);
    properties[PROP_DEFAULT_TEXT] = g_param_spec_string("default-text", NULL,
                                                        "Default label text",
                                                        "",
                                                        G_PARAM_READWRITE
                                                        | G_PARAM_CONSTRUCT_ONLY
                                                        | G_PARAM_STATIC_STRINGS);
}

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->construct = serializable_construct;
    iface->copy      = serializable_copy;
    iface->assign    = serializable_assign;

    define_properties();
    serializable_items[ITEM_DELTA_X].aux.pspec = properties[PROP_DELTA_X];
    serializable_items[ITEM_DELTA_Y].aux.pspec = properties[PROP_DELTA_Y];
    serializable_items[ITEM_ROTATION].aux.pspec = properties[PROP_ROTATION];
    serializable_items[ITEM_SIZE].aux.pspec = properties[PROP_SIZE];
    serializable_items[ITEM_FIXED_SIZE].aux.pspec = properties[PROP_FIXED_SIZE];
    gwy_fill_serializable_defaults_pspec(serializable_items, NUM_ITEMS, TRUE);
}

static void
gwy_gl_label_class_init(GwyGLLabelClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_gl_label_parent_class;

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

    define_properties();
    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_gl_label_init(GwyGLLabel *label)
{
    GwyGLLabelPrivate *priv;

    priv = label->priv = gwy_gl_label_get_instance_private(label);
    priv->delta_x = G_PARAM_SPEC_DOUBLE(properties[PROP_DELTA_X])->default_value;
    priv->delta_y = G_PARAM_SPEC_DOUBLE(properties[PROP_DELTA_Y])->default_value;
    priv->rotation = G_PARAM_SPEC_DOUBLE(properties[PROP_ROTATION])->default_value;
    priv->size = G_PARAM_SPEC_DOUBLE(properties[PROP_SIZE])->default_value;
    /* default text is a construction property */
    priv->text = g_string_new(NULL);
}

static void
finalize(GObject *object)
{
    GwyGLLabel *label = (GwyGLLabel*)object;

    g_free(label->priv->default_text);
    GWY_FREE_STRING(label->priv->text);

    parent_class->finalize(object);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyGLLabelPrivate *priv = GWY_GL_LABEL(object)->priv;
    gdouble d;
    gboolean b;
    gboolean changed = FALSE;

    switch (prop_id) {
        case PROP_DELTA_X:
        if ((changed = (priv->delta_x != (d = g_value_get_double(value)))))
            priv->delta_x = d;
        break;

        case PROP_DELTA_Y:
        if ((changed = (priv->delta_y != (d = g_value_get_double(value)))))
            priv->delta_y = d;
        break;

        case PROP_ROTATION:
        if ((changed = (priv->rotation != (d = g_value_get_double(value)))))
            priv->rotation = d;
        break;

        case PROP_SIZE:
        if ((changed = (priv->size != (d = g_value_get_double(value)))))
            priv->size = d;
        break;

        case PROP_FIXED_SIZE:
        if ((changed = (priv->fixed_size != (b = g_value_get_boolean(value)))))
            priv->fixed_size = b;
        break;

        case PROP_TEXT:
        changed = gwy_assign_gstring(priv->text, g_value_get_string(value));
        break;

        case PROP_DEFAULT_TEXT:
        /* This can happen only on construction */
        g_assert(!priv->default_text);
        priv->default_text = g_value_dup_string(value);
        changed = TRUE;
        gwy_assign_gstring(priv->text, priv->default_text);
        break;

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

    if (changed)
        g_object_notify_by_pspec(object, properties[prop_id]);
}

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

    switch (prop_id) {
        case PROP_DELTA_X:
        g_value_set_double(value, priv->delta_x);
        break;

        case PROP_DELTA_Y:
        g_value_set_double(value, priv->delta_y);
        break;

        case PROP_ROTATION:
        g_value_set_double(value, priv->rotation);
        break;

        case PROP_SIZE:
        g_value_set_double(value, priv->size);
        break;

        case PROP_FIXED_SIZE:
        g_value_set_boolean(value, priv->fixed_size);
        break;

        case PROP_TEXT:
        g_value_set_string(value, priv->text->str);
        break;

        case PROP_DEFAULT_TEXT:
        g_value_set_string(value, priv->default_text);
        break;

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

/**
 * gwy_gl_label_new:
 * @default_text: Label default text.
 *
 * Creates a new 3D view label.
 *
 * Returns: A newly created 3D label.
 **/
GwyGLLabel*
gwy_gl_label_new(const gchar *default_text)
{
    return g_object_new(GWY_TYPE_GL_LABEL, "default-text", default_text, NULL);
}

/**
 * gwy_gl_label_set_text:
 * @label: An OpenGL view label.
 * @text: New label text.
 *
 * Sets the text of an OpenGL view label.
 **/
void
gwy_gl_label_set_text(GwyGLLabel *label,
                      const gchar *text)
{
    g_return_if_fail(GWY_IS_GL_LABEL(label));
    if (gwy_strequal(text, label->priv->text->str))
        return;

    g_string_assign(label->priv->text, text);
    g_object_notify_by_pspec(G_OBJECT(label), properties[PROP_TEXT]);
}

/**
 * gwy_gl_label_get_text:
 * @label: An OpenGL view label.
 *
 * Gets the text of an OpenGL view label.
 *
 * Returns: The label text.  The returned string is owned by label and must no be modified or freed.
 **/
const gchar*
gwy_gl_label_get_text(GwyGLLabel *label)
{
    g_return_val_if_fail(GWY_IS_GL_LABEL(label), NULL);
    return label->priv->text->str;
}

/**
 * gwy_gl_label_expand_text:
 * @label: An OpenGL view label.
 * @variables: Hash table with variable values.
 *
 * Substitutes variables in label text.
 *
 * Returns: A newly allocated string with variables from @variables substituted with values.
 **/
gchar*
gwy_gl_label_expand_text(GwyGLLabel *label,
                         GHashTable *variables)
{
    GwyGLLabelPrivate *priv = label->priv;
    GString *buffer, *key;
    gchar *s, *lb;

    g_return_val_if_fail(GWY_IS_GL_LABEL(label), NULL);
    lb = priv->text->str;

    buffer = g_string_new(NULL);
    key = g_string_new(NULL);
    while (lb && *lb) {
        if (!(s = strchr(lb, '$'))) {
            g_string_append(buffer, lb);
            break;
        }
        g_string_append_len(buffer, lb, s - lb);
        lb = s + 1;
        if (!g_ascii_isalpha(*lb)) {
            g_string_append_c(buffer, '$');
            continue;
        }

        for (s = lb; g_ascii_isalpha(*s); s++)
            ;
        g_string_append_len(g_string_truncate(key, 0), lb, s-lb);
        g_string_ascii_down(key);
        s = g_hash_table_lookup(variables, key->str);
        if (s) {
            g_string_append(buffer, s);
            lb += strlen(key->str);
        }
        else
            g_string_append_c(buffer, '$');
    }

    g_string_free(key, TRUE);
    return g_string_free(buffer, FALSE);
}

/**
 * gwy_gl_label_reset:
 * @label: An OpenGL view label.
 *
 * Resets all 3D label properties and text to default values.
 **/
void
gwy_gl_label_reset(GwyGLLabel *label)
{
    g_return_if_fail(GWY_IS_GL_LABEL(label));

    GwyGLLabelPrivate *priv = label->priv;
    GObject *object = G_OBJECT(label);
    gdouble defval;

    g_object_freeze_notify(object);
    if (priv->fixed_size) {
        priv->fixed_size = FALSE;
        g_object_notify_by_pspec(object, properties[PROP_FIXED_SIZE]);
    }
    if (priv->delta_x != (defval = G_PARAM_SPEC_DOUBLE(properties[PROP_DELTA_X])->default_value)) {
        priv->delta_x = defval;
        g_object_notify_by_pspec(object, properties[PROP_DELTA_X]);
    }
    if (priv->delta_y != (defval = G_PARAM_SPEC_DOUBLE(properties[PROP_DELTA_Y])->default_value)) {
        priv->delta_y = defval;
        g_object_notify_by_pspec(object, properties[PROP_DELTA_Y]);
    }
    if (priv->rotation != (defval = G_PARAM_SPEC_DOUBLE(properties[PROP_ROTATION])->default_value)) {
        priv->rotation = defval;
        g_object_notify_by_pspec(object, properties[PROP_ROTATION]);
    }
    if (priv->size != (defval = G_PARAM_SPEC_DOUBLE(properties[PROP_SIZE])->default_value)) {
        priv->size = defval;
        g_object_notify_by_pspec(object, properties[PROP_SIZE]);
    }
    gwy_gl_label_set_text(label, priv->default_text);
    g_object_thaw_notify(object);
}

/**
 * gwy_gl_label_reset_text:
 * @label: An OpenGL view label.
 *
 * Resets 3D label text to default values.
 **/
void
gwy_gl_label_reset_text(GwyGLLabel *label)
{
    g_return_if_fail(GWY_IS_GL_LABEL(label));
    gwy_gl_label_set_text(label, label->priv->default_text);
}

/**
 * gwy_gl_label_user_size:
 * @label: An OpenGL view label.
 * @user_size: Size of the text to be set.
 *
 * Possibly sets size of an OpenGL view label.
 *
 * If label size is fixed, the function does not change and it is simply returned. Otherwise label size is changed and
 * @user_size itself is returned.
 *
 * Returns: Size of label.
 **/
gdouble
gwy_gl_label_user_size(GwyGLLabel *label,
                       gdouble user_size)
{
    g_return_val_if_fail(GWY_IS_GL_LABEL(label), 0.0);

    GwyGLLabelPrivate *priv = label->priv;
    if (priv->fixed_size)
        return priv->size;

    if (priv->size != user_size) {
        priv->size = user_size;
        g_object_notify_by_pspec(G_OBJECT(label), properties[PROP_SIZE]);
    }
    return user_size;
}

/**
 * gwy_gl_label_get_delta_x:
 * @label: An OpenGL view label.
 *
 * Gets the horizontal offset of an OpenGL view label.
 *
 * Returns: The horizontal offset in pixels.
 **/
gdouble
gwy_gl_label_get_delta_x(GwyGLLabel *label)
{
    g_return_val_if_fail(GWY_IS_GL_LABEL(label), 0.0);
    return label->priv->delta_x;
}

/**
 * gwy_gl_label_get_delta_y:
 * @label: An OpenGL view label.
 *
 * Gets the vertical offset of an OpenGL view label.
 *
 * Returns: The vertical offset in pixels.
 **/
gdouble
gwy_gl_label_get_delta_y(GwyGLLabel *label)
{
    g_return_val_if_fail(GWY_IS_GL_LABEL(label), 0.0);
    return label->priv->delta_y;
}

/**
 * gwy_gl_label_get_rotation:
 * @label: An OpenGL view label.
 *
 * Gets the rotation of an OpenGL view label.
 *
 * Returns: The rotation angle in radians.
 **/
gdouble
gwy_gl_label_get_rotation(GwyGLLabel *label)
{
    g_return_val_if_fail(GWY_IS_GL_LABEL(label), 0.0);
    return label->priv->rotation;
}

/**
 * gwy_gl_label_get_size:
 * @label: An OpenGL view label.
 *
 * Gets the font size of an OpenGL view label.
 *
 * Returns: The font size.
 **/
gdouble
gwy_gl_label_get_size(GwyGLLabel *label)
{
    g_return_val_if_fail(GWY_IS_GL_LABEL(label), 14.0);
    return label->priv->size;
}

/**
 * gwy_gl_label_get_fixed_size:
 * @label: An OpenGL view label.
 *
 * Gets whether an OpenGL view label used a fixed user-sed size.
 *
 * Returns: %TRUE if font size is user-set, %FALSE if it is automatic.
 **/
gboolean
gwy_gl_label_get_fixed_size(GwyGLLabel *label)
{
    g_return_val_if_fail(GWY_IS_GL_LABEL(label), FALSE);
    return label->priv->fixed_size;
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyGLLabel *label = GWY_GL_LABEL(serializable);
    GwyGLLabelPrivate *priv = label->priv;

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_DELTA_X, priv->delta_x);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_DELTA_Y, priv->delta_y);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_ROTATION, priv->rotation);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_SIZE, priv->size);
    gwy_serializable_group_append_boolean(group, serializable_items + ITEM_FIXED_SIZE, priv->fixed_size);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_DEFAULT_TEXT, priv->default_text);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_TEXT, priv->text->str);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwySerializableItem its[NUM_ITEMS], *it;
    gwy_assign(its, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, TYPE_NAME, error_list);

    GwyGLLabel *label = GWY_GL_LABEL(serializable);
    GwyGLLabelPrivate *priv = label->priv;

    priv->delta_x = its[ITEM_DELTA_X].value.v_double;
    priv->delta_y = its[ITEM_DELTA_Y].value.v_double;
    priv->rotation = gwy_canonicalize_angle(its[ITEM_ROTATION].value.v_double, FALSE, FALSE);
    priv->size = its[ITEM_SIZE].value.v_double;

    it = its + ITEM_DEFAULT_TEXT;
    if (it->value.v_string) {
        priv->default_text = it->value.v_string;
        it->value.v_string = NULL;
    }

    it = its + ITEM_TEXT;
    if (it->value.v_string) {
        g_string_assign(priv->text, it->value.v_string);
        GWY_FREE(it->value.v_string);
    }
    else if (priv->default_text)
        g_string_assign(priv->text, priv->default_text);

    g_free(its[ITEM_TEXT].value.v_string);

    return TRUE;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyGLLabel *label = GWY_GL_LABEL(serializable);
    GwyGLLabelPrivate *priv = label->priv;

    GwyGLLabel *copy = gwy_gl_label_new(priv->default_text);
    GwyGLLabelPrivate *cpriv = copy->priv;

    cpriv->delta_x = priv->delta_x;
    cpriv->delta_y = priv->delta_y;
    cpriv->rotation = priv->rotation;
    cpriv->size = priv->size;
    cpriv->fixed_size = priv->fixed_size;
    g_string_assign(cpriv->text, priv->text->str);

    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyGLLabel *destlabel = GWY_GL_LABEL(destination), *srclabel = GWY_GL_LABEL(source);
    GwyGLLabelPrivate *dpriv = destlabel->priv, *spriv = srclabel->priv;

    if (!gwy_strequal(spriv->default_text, dpriv->default_text)) {
        g_warning("Trying to change construction-only property by cloning");
        gwy_assign_string(&dpriv->default_text, spriv->default_text);
    }

    GObject *object = G_OBJECT(destination);
    g_object_freeze_notify(object);
    if (dpriv->fixed_size != spriv->fixed_size) {
        dpriv->fixed_size = !!spriv->fixed_size;
        g_object_notify_by_pspec(object, properties[PROP_FIXED_SIZE]);
    }
    if (dpriv->delta_x != spriv->delta_x) {
        dpriv->delta_x = spriv->delta_x;
        g_object_notify_by_pspec(object, properties[PROP_DELTA_X]);
    }
    if (dpriv->delta_y != spriv->delta_y) {
        dpriv->delta_y = spriv->delta_y;
        g_object_notify_by_pspec(object, properties[PROP_DELTA_Y]);
    }
    if (dpriv->rotation != spriv->rotation) {
        dpriv->rotation = spriv->rotation;
        g_object_notify_by_pspec(object, properties[PROP_ROTATION]);
    }
    if (dpriv->size != spriv->size) {
        dpriv->size = spriv->size;
        g_object_notify_by_pspec(object, properties[PROP_SIZE]);
    }
    gwy_gl_label_set_text(destlabel, spriv->text->str);
    g_object_thaw_notify(object);
}

/**
 * SECTION: gl-label
 * @title: GwyGLLabel
 * @short_description: Information on label on GwyGLView
 **/

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