/*
 *  $Id: sci-text.c 30028 2026-06-09 13:45:30Z 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 <math.h>
#include <string.h>
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"

#include "libgwyui/icons.h"
#include "libgwyui/null-store.h"
#include "libgwyui/utils.h"
#include "libgwyui/sci-text.h"


enum {
    SGNL_EDITED,
    NUM_SIGNALS
};

enum {
    PROP_0,
    PROP_HAS_PREVIEW,
    NUM_PROPERTIES
};

struct _GwySciTextPrivate {
    gboolean has_preview;

    GtkWidget *entry;
    GtkWidget *frame;
    GtkWidget *preview;
    GtkWidget *symbols;

    gulong changed_id;
};

typedef struct {
    const gchar *icon_name;
    const gchar *start;
    const gchar *end;
    const gchar *label;
} GwySciTextTag;

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 text_edited   (GwySciText *scitext);
static void add_symbol    (GwySciText *scitext);
static void button_clicked(GwySciText *scitext,
                           GtkWidget *button);

static const GwySciTextTag tag_table[] = {
    { GWY_ICON_BOLD,        "<b>",   "</b>",   N_("_Bold"),        },
    { GWY_ICON_ITALIC,      "<i>",   "</i>",   N_("_Italic"),      },
    { GWY_ICON_SUBSCRIPT,   "<sub>", "</sub>", N_("_Subscript"),   },
    { GWY_ICON_SUPERSCRIPT, "<sup>", "</sup>", N_("Su_perscript"), },
};

static struct {
    const gchar *label;
    const gchar *symbol;
}
const symbol_list[] = {
    /* Uppercase Greek letters */
    { "Alpha", "Α" },
    { "Beta", "Β" },
    { "Gamma", "Γ" },
    { "Delta", "Δ" },
    { "Epsilon", "Ε" },
    { "Zeta", "Ζ" },
    { "Eta", "Η" },
    { "Theta", "Θ" },
    { "Iota", "Ι" },
    { "Kappa", "Κ" },
    { "Lambda", "Λ" },
    { "Mu", "Μ" },
    { "Nu", "Ν" },
    { "Xi", "Ξ" },
    { "Omicron", "Ο" },
    { "Pi", "Π" },
    { "Rho", "Ρ" },
    { "Sigma", "Σ" },
    { "Tau", "Τ" },
    { "Upsilon", "Υ" },
    { "Phi", "Φ" },
    { "Chi", "Χ" },
    { "Psi", "Ψ" },
    { "Omega", "Ω" },
    /* Lowercase Greek letters */
    { "alpha", "α" },
    { "beta", "β" },
    { "gamma", "γ" },
    { "delta", "δ" },
    { "epsilon", "ε" },
    { "variant epsilon", "ϵ" },
    { "zeta", "ζ" },
    { "eta", "η" },
    { "theta", "θ" },
    { "theta symbol", "ϑ" },
    { "iota", "ι" },
    { "kappa", "κ" },
    { "variant kappa", "ϰ" },
    { "lambda", "λ" },
    { "mu", "μ" },
    { "nu", "ν" },
    { "xi", "ξ" },
    { "omicron", "ο" },
    { "pi", "π" },
    { "pi symbol", "ϖ" },
    { "rho", "ρ" },
    { "varrho", "ϱ" },
    { "sigma", "σ" },
    { "variant sigma", "ς" },
    { "tau", "τ" },
    { "upsilon", "υ" },
    { "phi", "φ" },
    { "chi", "χ" },
    { "psi", "ψ" },
    { "omega", "ω" },
    /* Math symbols */
    { "Aleph", "א" },
    { "angle", "∠" },
    { "angle bracked right", "〉" },
    { "angle bracket left", "〈" },
    { "approximates", "≈" },
    { "arrow down", "↓" },
    { "arrow left", "←" },
    { "arrow right", "→" },
    { "arrow up", "↑" },
    { "circled plus", "⊕" },
    { "circled times", "⊗" },
    { "congruent", "≅" },
    { "degree", "°" },
    { "division", "÷" },
    { "dot", "⋅" },
    { "double arrow left", "⇐" },
    { "double arrow right", "⇒" },
    { "empty set", "∅" },
    { "equivalent", "≡" },
    { "exists", "∃" },
    { "floor-left", "⌊" },
    { "floor-right", "⌋" },
    { "for all", "∀" },
    { "greater or equal", "≥" },
    { "imaginary", "ℑ" },
    { "infinity", "∞" },
    { "integral", "∫" },
    { "less or equal", "≤" },
    { "logical and", "∧" },
    { "logical or", "∨" },
    { "micron", "µ" },
    { "middle dot", "·" },
    { "minus", "−" },
    { "minus or plus", "∓" },
    { "much larger", "≫" },
    { "much smaller", "≪" },
    { "nabla", "∇" },
    { "negation", "¬" },
    { "not equal", "≠" },
    { "parallel", "∥" },
    { "partial derivative", "∂" },
    { "per million", "‰" },
    { "perpendicular", "⊥" },
    { "plus or minus", "±" },
    { "product", "∏" },
    { "proportional", "∝" },
    { "real", "ℜ" },
    { "set contains", "∋" },
    { "set intersection", "∩" },
    { "set member", "∈" },
    { "set not member", "∉" },
    { "set not subset", "⊄" },
    { "set union", "∪" },
    { "square root", "√" },
    { "subset improper", "⊆" },
    { "subset proper", "⊂" },
    { "sum", "∑" },
    { "supperset", "⊃" },
    { "therefore", "∴" },
    { "tilde", "∼" },
    { "times", "×" },
    { "Weierstrass p", "℘" },
    /* Text stuff */
    { "bullet", "•" },
    { "cent", "¢" },
    { "copyright", "©" },
    { "currency", "¤" },
    { "dagger", "†" },
    { "double dagger", "‡" },
    { "elipsis", "…" },
    { "euro", "€" },
    { "paragraph", "§" },
    { "pound", "£" },
    { "registered", "®" },
    { "trademark", "™" },
    { "yen", "¥" },
};

static guint signals[NUM_SIGNALS];
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GtkBoxClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwySciText, gwy_sci_text, GTK_TYPE_BOX,
                        G_ADD_PRIVATE(GwySciText))

static void
gwy_sci_text_class_init(GwySciTextClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_sci_text_parent_class;

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

    /**
     * GwySciText::edited:
     * @gwyscitext: The #GwySciText which received the signal.
     *
     * The ::edited signal is emitted when the text in its entry changes to a valid markup.  If you need to react to
     * all changes in entry contents, you can use gwy_sci_text_get_entry() to get the entry and connect to its signal.
     **/
    signals[SGNL_EDITED] = g_signal_new("edited", type,
                                        G_SIGNAL_RUN_FIRST,
                                        G_STRUCT_OFFSET(GwySciTextClass, edited),
                                        NULL, NULL,
                                        g_cclosure_marshal_VOID__VOID,
                                        G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_EDITED], type, g_cclosure_marshal_VOID__VOIDv);

    properties[PROP_HAS_PREVIEW] = g_param_spec_boolean("has-preview", NULL,
                                                        "Whether scientific text has a preview",
                                                        TRUE,
                                                        GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
render_symbol(G_GNUC_UNUSED GtkCellLayout *layout, GtkCellRenderer *renderer,
              GtkTreeModel *model, GtkTreeIter *iter, G_GNUC_UNUSED gpointer data)
{
    gint row;
    gtk_tree_model_get(model, iter, 0, &row, -1);
    g_object_set(renderer, "text", symbol_list[row].symbol, NULL);
}

#if 0
static void
render_label(G_GNUC_UNUSED GtkCellLayout *layout, GtkCellRenderer *renderer,
             GtkTreeModel *model, GtkTreeIter *iter, G_GNUC_UNUSED gpointer data)
{
    gint row;
    gtk_tree_model_get(model, iter, 0, &row, -1);
    g_object_set(renderer, "text", symbol_list[row].label, NULL);
}
#endif

static void
gwy_sci_text_init(GwySciText *scitext)
{
    GwySciTextPrivate *priv;

    scitext->priv = priv = gwy_sci_text_get_instance_private(scitext);
    priv->has_preview = TRUE;

    GtkBox *box = GTK_BOX(scitext);
    gtk_orientable_set_orientation(GTK_ORIENTABLE(scitext), GTK_ORIENTATION_VERTICAL);

    /* Preview */
    priv->preview = gtk_label_new(NULL);
    gwy_set_widget_margins(priv->preview, 4, 4, 2, 2);
    priv->frame = gtk_frame_new(_("Preview"));
    gtk_widget_set_margin_bottom(priv->frame, 2);
    gtk_container_add(GTK_CONTAINER(priv->frame), priv->preview);
    gtk_box_pack_start(box, priv->frame, FALSE, FALSE, 0);
    gtk_widget_show(priv->frame);

    /* Entry */
    priv->entry = gtk_entry_new();
    gtk_box_pack_start(box, priv->entry, FALSE, FALSE, 0);
    priv->changed_id = g_signal_connect_swapped(priv->entry, "changed", G_CALLBACK(text_edited), scitext);
    gtk_widget_show(priv->entry);

    /* Actions */
    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(box, hbox, FALSE, FALSE, 2);

    GwyNullStore *store = gwy_null_store_new(G_N_ELEMENTS(symbol_list));
    priv->symbols = gtk_combo_box_new();
    gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(priv->symbols), 12);
    gtk_combo_box_set_model(GTK_COMBO_BOX(priv->symbols), GTK_TREE_MODEL(store));
    gtk_combo_box_set_active(GTK_COMBO_BOX(priv->symbols), 0);
    gtk_box_pack_start(GTK_BOX(hbox), priv->symbols, FALSE, FALSE, 0);

    GtkCellLayout *layout;
    GtkCellRenderer *renderer;

    /* FIXME GTK3 It no longer seems possible to use different rendering for the combo and the menu. We can have
     * various GtkCellLayouts, but only one of them is ever passed to the render_xxx() functions. */
    /* A compact cell layout for the popup (in table mode). */
    layout = GTK_CELL_LAYOUT(priv->symbols);
    renderer = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(layout, renderer, FALSE);
    gtk_cell_layout_set_cell_data_func(layout, renderer, render_symbol, NULL, NULL);

    GtkWidget *button = gtk_button_new_with_mnemonic(_("A_dd symbol"));
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(add_symbol), scitext);
    gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 4);

    for (guint i = 0; i < G_N_ELEMENTS(tag_table); i++) {
        GtkWidget *image = gtk_image_new_from_icon_name(tag_table[i].icon_name, GTK_ICON_SIZE_BUTTON);
        button = gtk_button_new();
        gtk_container_add(GTK_CONTAINER(button), image);
        gtk_box_pack_end(GTK_BOX(hbox), button, FALSE, FALSE, 0);
        g_signal_connect_swapped(button, "clicked", G_CALLBACK(button_clicked), scitext);
        g_object_set_data(G_OBJECT(button), "action", GUINT_TO_POINTER(i));
    }
    gtk_widget_show_all(hbox);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwySciText *scitext = GWY_SCI_TEXT(object);

    switch (prop_id) {
        case PROP_HAS_PREVIEW:
        gwy_sci_text_set_has_preview(scitext, g_value_get_boolean(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)
{
    GwySciTextPrivate *priv = GWY_SCI_TEXT(object)->priv;

    switch (prop_id) {
        case PROP_HAS_PREVIEW:
        g_value_set_boolean(value, priv->has_preview);
        break;

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

/**
 * gwy_sci_text_new:
 *
 * Creates a new scientific text entry.
 *
 * Returns: A newly created scientific text entry.
 **/
GtkWidget*
gwy_sci_text_new(void)
{
    return gtk_widget_new(GWY_TYPE_SCI_TEXT, NULL);
}

static void
text_edited(GwySciText *scitext)
{
    GError *err = NULL;
    PangoAttrList *attr_list = NULL;
    gboolean emit_edited = FALSE;

    GwySciTextPrivate *priv = scitext->priv;
    const gchar *input = gtk_entry_get_text(GTK_ENTRY(priv->entry));
    gchar *text = NULL;
    if (pango_parse_markup(input, -1, 0, &attr_list, &text, NULL, &err)) {
        if (priv->preview)
            gtk_label_set_markup(GTK_LABEL(priv->preview), input);
        emit_edited = TRUE;
    }
    g_free(text);
    if (attr_list)
        pango_attr_list_unref(attr_list);
    g_clear_error(&err);

    if (emit_edited)
        g_signal_emit(scitext, signals[SGNL_EDITED], 0);
}

static void
add_symbol(GwySciText *scitext)
{
    GwySciTextPrivate *priv = scitext->priv;
    GtkTreeIter iter;
    if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(priv->symbols), &iter))
        return;

    GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(priv->symbols));
    gint row;
    gtk_tree_model_get(model, &iter, 0, &row, -1);

    GtkEditable *editable = GTK_EDITABLE(priv->entry);
    gint pos = gtk_editable_get_position(editable);
    gtk_editable_insert_text(editable, symbol_list[row].symbol, -1, &pos);
    gtk_editable_set_position(editable, pos);
}

static gboolean
is_surrounded(const gchar *text, guint start, guint end,
              const guchar *before, const guchar *after)
{
    if (strncmp(text + end, after, strlen(after)))
        return FALSE;
    guint len = strlen(before);
    if (start < len || strncmp(text + start - len, before, len))
        return FALSE;
    return TRUE;
}

static gboolean
is_delimited(const gchar *text, guint start, guint end,
             const guchar *before, const guchar *after)
{
    guint blen = strlen(before), alen = strlen(after);
    if (end - start < blen + alen)
        return FALSE;
    if (strncmp(text, before, blen) || strncmp(text - alen, after, alen))
        return FALSE;
    return TRUE;
}

static void
button_clicked(GwySciText *scitext, GtkWidget *button)
{
    guint i = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(button), "action"));
    GwySciTextPrivate *priv = scitext->priv;
    GtkEditable *editable = GTK_EDITABLE(priv->entry);
    gint start, end;
    gboolean selected = gtk_editable_get_selection_bounds(editable, &start, &end);
    if (!selected) {
        start = gtk_editable_get_position(editable);
        end = start;
    }

    const GwySciTextTag *tag = tag_table + i;
    const gchar *text = gtk_entry_get_text(GTK_ENTRY(priv->entry));
    /* Try removing markup if the marked region matches the clicked markup. */
    if (is_surrounded(text, start, end, tag->start, tag->end)) {
        gtk_editable_delete_text(editable, end, end + strlen(tag->end));
        guint len = strlen(tag->start);
        gtk_editable_delete_text(editable, start - len, start);
        start -= len;
        end -= len;
    }
    else if (is_delimited(text, start, end, tag->start, tag->end)) {
        guint len = strlen(tag->end);
        gtk_editable_delete_text(editable, end - len, end);
        end -= len;
        gtk_editable_delete_text(editable, start, start + strlen(tag->start));
    }
    else {
        /* Otherwise add markup. */
        guint len = strlen(tag->start);
        gtk_editable_insert_text(editable, tag->start, -1, &start);
        end += len;
        gtk_editable_insert_text(editable, tag->end, -1, &end);
        end -= strlen(tag->end);
    }

    gtk_widget_grab_focus(GTK_WIDGET(priv->entry));
    if (selected)
        gtk_editable_select_region(editable, start, end);
}

/**
 * gwy_sci_text_get_markup:
 * @scitext: A scientific text widget.
 *
 * Gets the markup in a scientific text entry.
 *
 * The returned string may contain Pango markup. Use pango_parse_markup() to remove it if clean text is required.
 *
 * Returns: The markup as a newly allocated string.
 **/
gchar*
gwy_sci_text_get_markup(GwySciText *scitext)
{
    g_return_val_if_fail(GWY_IS_SCI_TEXT(scitext), NULL);
    return gtk_editable_get_chars(GTK_EDITABLE(scitext->priv->entry), 0, -1);
}

/**
 * gwy_sci_text_set_markup:
 * @scitext: A scientific text widget.
 * @markup: The markup to edit.
 *
 * Sets the text a scientific text widget displays.
 *
 * The text can contain Pango markup. In principle, the markup can even be more general than the one produced by
 * #GwySciText itself. The user will not be able to edit such markup using the buttons, but can still do so manually.
 **/
void
gwy_sci_text_set_markup(GwySciText *scitext, const gchar *markup)
{
    g_return_if_fail(GWY_IS_SCI_TEXT(scitext));

    /* The entry seems to emit two "changed" on set_markup(), first to "", the second to the real text.  But the first
     * makes the area thingy to immediately set the label to "" and thus to lose the real label. */
    GwySciTextPrivate *priv = scitext->priv;
    g_signal_handler_block(priv->entry, priv->changed_id);
    gtk_entry_set_text(GTK_ENTRY(priv->entry), markup);
    g_signal_handler_unblock(priv->entry, priv->changed_id);
    text_edited(scitext);
}

/**
 * gwy_sci_text_get_entry:
 * @scitext: A scientific text widget.
 *
 * Gets the entry widget of a scientific text entry.
 *
 * It can be used as a target of a mnemonic label or possibly to watch all changes (not just changes to a valid
 * markup).
 *
 * Returns: The entry widget, no reference is added.
 **/
GtkWidget*
gwy_sci_text_get_entry(GwySciText *scitext)
{
    g_return_val_if_fail(GWY_IS_SCI_TEXT(scitext), NULL);
    return scitext->priv->entry;
}

/**
 * gwy_sci_text_get_has_preview:
 * @scitext: A scientific text widget.
 *
 * Tests the display of a preview in a scientific text entry.
 *
 * Returns: %TRUE if there is a preview, %FALSE if preview is not shown.
 **/
gboolean
gwy_sci_text_get_has_preview(GwySciText *scitext)
{
    g_return_val_if_fail(GWY_IS_SCI_TEXT(scitext), FALSE);
    return scitext->priv->has_preview;
}

/**
 * gwy_sci_text_set_has_preview:
 * @scitext: A scientific text widget.
 * @has_preview: %TRUE to display a preview, %FALSE to disable it.
 *
 * Sets the display of a preview in a scientific text entry.
 **/
void
gwy_sci_text_set_has_preview(GwySciText *scitext, gboolean has_preview)
{
    GwySciTextPrivate *priv = scitext->priv;
    if (!priv->has_preview == !has_preview)
        return;

    priv->has_preview = !!has_preview;
    if (has_preview) {
        gtk_widget_set_no_show_all(priv->frame, FALSE);
        gtk_widget_show(priv->frame);
    }
    else {
        gtk_widget_hide(priv->frame);
        gtk_widget_set_no_show_all(priv->frame, TRUE);
    }

    g_object_notify_by_pspec(G_OBJECT(scitext), properties[PROP_HAS_PREVIEW]);
}

/**
 * SECTION: sci-text
 * @title: GwySciText
 * @short_description: Text entry with markup and special symbol helper widgets
 **/

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