/*
 *  $Id: gradient-editor.c 30028 2026-06-09 13:45:30Z yeti-dn $
 *  Copyright (C) 2025 David Necas (Yeti).
 *  E-mail: yeti@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 <glib/gi18n-lib.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/filters.h"
#include "libgwyddion/arithmetic.h"
#include "libgwyddion/gradient.h"
#include "libgwyui/gwyui.h"
#include "libgwyui/cairo-utils.h"

#include "libgwyapp/settings.h"
#include "libgwyapp/help.h"
#include "libgwyapp/resource-editor.h"
#include "libgwyapp/gradient-editor.h"

enum {
    PREVIEW_HEIGHT = 30,
};

enum {
    COLUMN_POSITION,
    COLUMN_SAMPLE,
    COLUMN_RED,
    COLUMN_GREEN,
    COLUMN_BLUE,
    NCOLUMNS
};

typedef enum {
    OPERATION_NONE = 0,
    OPERATION_ADD,
    OPERATION_REMOVE,
    OPERATION_MODIFY,
    OPERATION_SPREAD,
} OperationType;

typedef struct {
    OperationType optype;
    gint i;
} PendingOperation;

struct _GwyGradientEditorPrivate {
    GwyGradient *gradient;
    GtkWidget *color_editor;
    GtkWidget *preview;
    GtkWidget *treeview;
    GtkWidget *add;
    GtkWidget *remove;
    GtkWidget *spread;
    GtkTreeSelection *selection;
    GtkAdjustment *x;
    GwyNullStore *store;
    gulong gradient_id;
    gulong x_id;
    PendingOperation pendop;
    gint editing_point;
    gboolean switching_point;
};

static void       dispose              (GObject *object);
static void       construct_editor     (GwyResourceEditor *res_editor,
                                        GtkContainer *container);
static GtkWidget* create_point_treeview(GwyGradientEditor *editor);
static GtkWidget* create_buttons       (GwyGradientEditor *editor);
static void       add_point            (GwyGradientEditor *editor);
static void       remove_point         (GwyGradientEditor *editor);
static void       spread_points        (GwyGradientEditor *editor);
static void       render_gradient_point(GtkTreeViewColumn *column,
                                        GtkCellRenderer *renderer,
                                        GtkTreeModel *model,
                                        GtkTreeIter *iter,
                                        gpointer user_data);
static void       selection_changed    (GwyGradientEditor *editor,
                                        GtkTreeSelection *selection);
static void       switch_gradient_point(GwyGradientEditor *editor,
                                        gint i);
static void       apply_changes        (GwyResourceEditor *res_editor);
static void       switch_resource      (GwyResourceEditor *res_editor);
static void       gradient_changed     (GwyGradientEditor *editor);
static void       refresh_gradient     (GwyGradientEditor *editor,
                                        gint editing_point);
static void       color_changed        (GwyGradientEditor *editor);
static void       position_changed     (GwyGradientEditor *editor);

static GwyResourceEditorClass *parent_class = NULL;

static G_DEFINE_QUARK(column-id, column_id)

G_DEFINE_TYPE_WITH_CODE(GwyGradientEditor, gwy_gradient_editor, GWY_TYPE_RESOURCE_EDITOR,
                        G_ADD_PRIVATE(GwyGradientEditor))

static void
gwy_gradient_editor_class_init(GwyGradientEditorClass *klass)
{
    GwyResourceEditorClass *editor_class = GWY_RESOURCE_EDITOR_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_gradient_editor_parent_class;

    gobject_class->dispose = dispose;

    editor_class->resource_type = GWY_TYPE_GRADIENT;
    editor_class->base_resource = GWY_GRADIENT_DEFAULT;
    editor_class->window_title = _("Color Gradient Editor");
    editor_class->editor_title = _("Color Gradient ‘%s’");
    editor_class->construct_treeview = gwy_gradient_tree_view_new;
    editor_class->construct_editor = construct_editor;
    editor_class->apply_changes = apply_changes;
    editor_class->switch_resource = switch_resource;
    gwy_resource_editor_class_setup(editor_class);
}

static void
gwy_gradient_editor_init(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv;

    editor->priv = priv = gwy_gradient_editor_get_instance_private(editor);

    priv->editing_point = -1;
}

static void
dispose(GObject *object)
{
    GwyGradientEditorPrivate *priv = GWY_GRADIENT_EDITOR(object)->priv;

    g_clear_signal_handler(&priv->gradient_id, priv->gradient);
    g_clear_object(&priv->store);

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

/**
 * gwy_app_gradient_editor:
 *
 * Creates or presents color gradient editor.
 *
 * Gradient editor is singleton, therefore if it doesn't exist, this function creates and displays it.  If it already
 * exists, it simply calls gtk_window_present() on the existing instance.  It exists until it's closed by user.
 **/
void
gwy_app_gradient_editor(void)
{
    GwyGradientEditorClass *klass = g_type_class_ref(GWY_TYPE_GRADIENT_EDITOR);
    GwyResourceEditor *editor;

    if ((editor = GWY_RESOURCE_EDITOR_CLASS(klass)->instance)) {
        gtk_window_present(GTK_WINDOW(editor));
        g_type_class_unref(klass);
        return;
    }

    editor = g_object_new(GWY_TYPE_GRADIENT_EDITOR, NULL);
    gwy_resource_editor_setup(editor);
    g_type_class_unref(klass);
    gwy_help_add_to_window(GTK_WINDOW(editor), "color-map", "color-gradient-editor", GWY_HELP_DEFAULT);
    gtk_widget_show_all(GTK_WIDGET(editor));
}

static void
construct_editor(GwyResourceEditor *res_editor, GtkContainer *container)
{
    g_return_if_fail(GTK_IS_CONTAINER(container));

    GwyGradientEditor *editor = GWY_GRADIENT_EDITOR(res_editor);
    GwyGradientEditorPrivate *priv = editor->priv;

    priv->store = gwy_null_store_new(0);

    gtk_container_set_border_width(container, 4);

    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
    gtk_container_add(container, hbox);

    GtkWidget *leftvbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_pack_start(GTK_BOX(hbox), leftvbox, TRUE, TRUE, 0);

    gtk_box_pack_start(GTK_BOX(leftvbox), gwy_label_new_header("Stops"), FALSE, FALSE, 0);

    GtkWidget *treeview = priv->treeview = create_point_treeview(editor);
    GtkWidget *scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(scwin), treeview);
    gtk_box_pack_start(GTK_BOX(leftvbox), scwin, TRUE, TRUE, 0);

    GtkWidget *buttons = create_buttons(editor);
    gtk_box_pack_start(GTK_BOX(leftvbox), buttons, FALSE, FALSE, 0);

    GtkWidget *grid = gtk_grid_new();
    gtk_box_pack_start(GTK_BOX(leftvbox), grid, FALSE, TRUE, 0);
    gtk_widget_set_margin_bottom(grid, 8);

    priv->x = gtk_adjustment_new(0.5, 0.0, 1.0, 0.002, 0.05, 0.0);
    gwy_gtkgrid_attach_adjbar(GTK_GRID(grid), 0, _("Position:"), NULL, G_OBJECT(priv->x), GWY_HSCALE_LINEAR);
    priv->x_id = g_signal_connect_swapped(priv->x, "value-changed", G_CALLBACK(position_changed), editor);

    GtkWidget *rightvbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_pack_start(GTK_BOX(hbox), rightvbox, TRUE, TRUE, 0);

    gtk_box_pack_start(GTK_BOX(rightvbox), gwy_label_new_header("Gradient Preview"), FALSE, FALSE, 0);

    priv->preview = gwy_gradient_swatch_new();
    gtk_widget_set_hexpand(priv->preview, TRUE);
    gtk_widget_set_size_request(priv->preview, -1, PREVIEW_HEIGHT);
    gtk_box_pack_start(GTK_BOX(rightvbox), priv->preview, FALSE, TRUE, 0);

    GtkWidget *label = gwy_label_new_header("Stop Color");
    gtk_widget_set_margin_top(label, 12);
    gtk_box_pack_start(GTK_BOX(rightvbox), label, FALSE, FALSE, 0);

    priv->color_editor = gwy_color_editor_new();
    gwy_color_editor_set_use_alpha(GWY_COLOR_EDITOR(priv->color_editor), FALSE);
    g_signal_connect_swapped(priv->color_editor, "color-changed", G_CALLBACK(color_changed), editor);
    gtk_widget_set_sensitive(priv->color_editor, FALSE);
    gtk_box_pack_start(GTK_BOX(rightvbox), priv->color_editor, FALSE, FALSE, 0);

    switch_resource(res_editor);
}

static GtkWidget*
create_point_treeview(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    GtkTreeView *treeview = GTK_TREE_VIEW(gtk_tree_view_new_with_model(GTK_TREE_MODEL(priv->store)));
    gtk_tree_view_set_headers_visible(treeview, TRUE);

    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;
    static const gchar *names[NCOLUMNS] = {
        N_("Position"), N_("Color"), N_("Red"), N_("Green"), N_("Blue"),
    };

    for (guint i = 0; i < NCOLUMNS; i++) {
        if (i == COLUMN_SAMPLE) {
            renderer = gwy_cell_renderer_color_new();
        }
        else {
            renderer = gtk_cell_renderer_text_new();
        }
        column = gtk_tree_view_column_new_with_attributes(_(names[i]), renderer, NULL);
        gtk_tree_view_column_set_expand(column, i == COLUMN_SAMPLE);
        g_object_set_qdata(G_OBJECT(column), column_id_quark(), GUINT_TO_POINTER(i));
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_gradient_point, editor, NULL);
        gtk_tree_view_append_column(treeview, column);
    }

    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE);
    g_signal_connect_swapped(selection, "changed", G_CALLBACK(selection_changed), editor);

    return GTK_WIDGET(treeview);
}

static void
render_gradient_point(GtkTreeViewColumn *column,
                      GtkCellRenderer *renderer,
                      GtkTreeModel *model,
                      GtkTreeIter *iter,
                      gpointer user_data)
{
    GwyGradientEditorPrivate *priv = GWY_GRADIENT_EDITOR(user_data)->priv;
    if (!priv->gradient)
        return;

    gint i;
    gtk_tree_model_get(model, iter, 0, &i, -1);
    GwyGradientPoint point = gwy_gradient_get_point(priv->gradient, i);

    guint column_id = GPOINTER_TO_UINT(g_object_get_qdata(G_OBJECT(column), column_id_quark()));
    if (column_id == COLUMN_SAMPLE) {
        g_object_set(renderer, "color", &point.color, NULL);
        return;
    }

    gchar *s;
    if (column_id == COLUMN_POSITION)
        s = g_strdup_printf("%.04f", point.x);
    else if (column_id == COLUMN_RED)
        s = g_strdup_printf("%.03f", point.color.r);
    else if (column_id == COLUMN_GREEN)
        s = g_strdup_printf("%.03f", point.color.g);
    else if (column_id == COLUMN_BLUE)
        s = g_strdup_printf("%.03f", point.color.b);
    else {
        g_assert_not_reached();
        s = g_strdup("???");
    }
    g_object_set(renderer, "text", s, NULL);
    g_free(s);
}

static GtkWidget*
create_buttons(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    GtkWidget *button;

    GtkWidget *buttonbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(buttonbox), TRUE);
    gtk_widget_set_hexpand(buttonbox, TRUE);

    button = priv->add = gwy_create_stock_button(GWY_STOCK_NEW);
    gtk_box_pack_start(GTK_BOX(buttonbox), button, TRUE, TRUE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(add_point), editor);

    button = priv->remove = gwy_create_stock_button(GWY_STOCK_DELETE);
    gtk_box_pack_start(GTK_BOX(buttonbox), button, TRUE, TRUE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(remove_point), editor);

    button = priv->spread = gtk_button_new_with_mnemonic(_("Spread _All"));
    gtk_box_pack_start(GTK_BOX(buttonbox), button, TRUE, TRUE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(spread_points), editor);

    return buttonbox;
}

static void
selection_changed(GwyGradientEditor *editor, GtkTreeSelection *selection)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    if (priv->switching_point)
        return;

    GtkTreeIter iter;
    GtkTreeModel *model;
    gint i = -1;
    if (gtk_tree_selection_get_selected(selection, &model, &iter))
        gtk_tree_model_get(model, &iter, 0, &i, -1);
    switch_gradient_point(editor, i);
}

static void
add_point(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    g_return_if_fail(priv->pendop.optype == OPERATION_NONE);
    g_return_if_fail(priv->editing_point+1 < gwy_gradient_get_n_points(priv->gradient));
    priv->pendop.optype = OPERATION_ADD;
    priv->pendop.i = priv->editing_point;
    gwy_resource_editor_queue_commit(GWY_RESOURCE_EDITOR(editor));
    gwy_resource_editor_commit(GWY_RESOURCE_EDITOR(editor));
}

static void
remove_point(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    g_return_if_fail(priv->pendop.optype == OPERATION_NONE);
    g_return_if_fail(priv->editing_point > 0);
    priv->pendop.optype = OPERATION_REMOVE;
    priv->pendop.i = priv->editing_point;
    gwy_resource_editor_queue_commit(GWY_RESOURCE_EDITOR(editor));
    gwy_resource_editor_commit(GWY_RESOURCE_EDITOR(editor));
}

static void
spread_points(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    g_return_if_fail(priv->pendop.optype == OPERATION_NONE);
    priv->pendop.optype = OPERATION_SPREAD;
    priv->pendop.i = priv->editing_point;
    gwy_resource_editor_queue_commit(GWY_RESOURCE_EDITOR(editor));
    gwy_resource_editor_commit(GWY_RESOURCE_EDITOR(editor));
}

static void
switch_gradient_point(GwyGradientEditor *editor, gint i)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    g_return_if_fail(!priv->switching_point);

    priv->switching_point = TRUE;
    /* FIXME: Can this cause troubles by becoming an out-of-order position? */
    GwyGradientPoint point = { 0.5, { 0.5, 0.5, 0.5, 1.0 } };
    gint npoints = 1;
    if (priv->gradient && i >= 0) {
        npoints = gwy_gradient_get_n_points(priv->gradient);
        point = gwy_gradient_get_point(priv->gradient, i);
    }
    gtk_widget_set_sensitive(priv->color_editor, priv->gradient && i >= 0);
    gtk_widget_set_sensitive(priv->add, i+1 < npoints);
    gtk_widget_set_sensitive(priv->remove, i > 0 && i+1 < npoints);
    gtk_widget_set_sensitive(priv->spread, npoints > 2);

    gtk_adjustment_set_value(priv->x, point.x);
    gwy_color_editor_set_previous_color(GWY_COLOR_EDITOR(priv->color_editor), &point.color);
    gwy_color_editor_set_color(GWY_COLOR_EDITOR(priv->color_editor), &point.color);
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(priv->x), i > 0 && i+1 < npoints);
    priv->switching_point = FALSE;
    priv->editing_point = i;
}

static void
apply_changes(GwyResourceEditor *res_editor)
{
    GwyGradient *gradient = GWY_GRADIENT(gwy_resource_editor_get_edited(res_editor));
    g_return_if_fail(gradient && gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));

    GwyGradientEditor *editor = GWY_GRADIENT_EDITOR(res_editor);
    GwyGradientEditorPrivate *priv = editor->priv;
    GwyGradientPoint point;
    gint i = priv->pendop.i;
    OperationType optype = priv->pendop.optype;

    if (optype == OPERATION_MODIFY) {
        point.x = gtk_adjustment_get_value(priv->x);
        gwy_color_editor_get_color(GWY_COLOR_EDITOR(priv->color_editor), &point.color);
        point.color.a = 1.0;
        gwy_gradient_set_point(gradient, i, &point);
    }
    else if (optype == OPERATION_ADD) {
        GwyGradientPoint prev = gwy_gradient_get_point(gradient, i);
        GwyGradientPoint next = gwy_gradient_get_point(gradient, i+1);
        gdouble q = (prev.x == next.x ? 0.5 : (point.x - prev.x)/(next.x - prev.x));
        gwy_rgba_interpolate(&prev.color, &next.color, q, &point.color);
        point.x = 0.5*(prev.x + next.x);
        gwy_gradient_insert_point(gradient, i+1, &point);
        refresh_gradient(editor, i);
    }
    else if (optype == OPERATION_REMOVE) {
        gwy_gradient_delete_point(gradient, i);
        refresh_gradient(editor, i);
    }
    else if (optype == OPERATION_SPREAD) {
        gint npoints;
        const GwyGradientPoint *oldpoints = gwy_gradient_get_points(gradient, &npoints);
        GwyGradientPoint newpoints[npoints];
        gwy_assign(newpoints, oldpoints, npoints);
        for (gint j = 0; j < npoints; j++)
            newpoints[j].x = j/(npoints - 1.0);
        gwy_gradient_set_points(gradient, npoints, newpoints);
        refresh_gradient(editor, i);
    }
    else {
        g_assert_not_reached();
    }
    priv->pendop.optype = OPERATION_NONE;
}

static void
switch_resource(GwyResourceEditor *res_editor)
{
    GwyGradient *gradient = GWY_GRADIENT(gwy_resource_editor_get_edited(res_editor));
    g_return_if_fail(gradient && gwy_resource_is_modifiable(GWY_RESOURCE(gradient)));

    GwyGradientEditor *editor = GWY_GRADIENT_EDITOR(res_editor);
    GwyGradientEditorPrivate *priv = editor->priv;

    gint editing_point = priv->editing_point;
    priv->editing_point = -1;

    if (!gwy_set_member_object(res_editor, gradient, GWY_TYPE_GRADIENT, &priv->gradient,
                               "data-changed", G_CALLBACK(gradient_changed), &priv->gradient_id, G_CONNECT_SWAPPED,
                               NULL))
        return;

    gwy_gradient_swatch_set_gradient(GWY_GRADIENT_SWATCH(priv->preview), gradient);
    refresh_gradient(editor, editing_point);
}

static void
gradient_changed(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;

    refresh_gradient(editor, priv->editing_point);
}

static void
refresh_gradient(GwyGradientEditor *editor, gint editing_point)
{
    GwyGradientEditorPrivate *priv = editor->priv;

    /* GtkTreeSelection does something strange duirng a resize, even when we select the first row before doing any
     * changes. Just remove the model and plug it anew since we are changing all rows anyway. */
    gtk_tree_view_set_model(GTK_TREE_VIEW(priv->treeview), NULL);
    gint npoints = gwy_gradient_get_n_points(priv->gradient);
    gwy_null_store_set_n_rows(priv->store, npoints);
    gtk_tree_view_set_model(GTK_TREE_VIEW(priv->treeview), GTK_TREE_MODEL(priv->store));
    editing_point = CLAMP(editing_point, 0, npoints-1);

    GtkTreeIter iter;
    if (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(priv->store), &iter, NULL, editing_point)) {
        GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview));
        gtk_tree_selection_select_iter(selection, &iter);
    }
}

static void
color_changed(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    gint i = priv->editing_point;
    if (priv->switching_point || i < 0)
        return;

    g_return_if_fail(priv->pendop.optype == OPERATION_NONE || priv->pendop.optype == OPERATION_MODIFY);
    priv->pendop.optype = OPERATION_MODIFY;
    priv->pendop.i = i;
    gwy_resource_editor_queue_commit(GWY_RESOURCE_EDITOR(editor));
    gwy_null_store_row_changed(priv->store, i);
}

static void
position_changed(GwyGradientEditor *editor)
{
    GwyGradientEditorPrivate *priv = editor->priv;
    gint i = priv->editing_point;
    if (priv->switching_point || i < 0)
        return;

    GwyGradient *gradient = priv->gradient;
    gint npoints = gwy_gradient_get_n_points(gradient);
    g_return_if_fail(i > 0 && i < npoints-1);
    gdouble x = gtk_adjustment_get_value(priv->x);
    GwyGradientPoint point = gwy_gradient_get_point(gradient, i);
    GwyGradientPoint prevpoint = gwy_gradient_get_point(gradient, i-1);
    GwyGradientPoint nextpoint = gwy_gradient_get_point(gradient, i+1);
    if (x < prevpoint.x || x > nextpoint.x) {
        g_signal_handler_block(priv->x, priv->x_id);
        x = CLAMP(x, prevpoint.x, nextpoint.x);
        gtk_adjustment_set_value(priv->x, x);
        g_signal_handler_unblock(priv->x, priv->x_id);
    }

    if (x != point.x) {
        g_return_if_fail(priv->pendop.optype == OPERATION_NONE || priv->pendop.optype == OPERATION_MODIFY);
        priv->pendop.optype = OPERATION_MODIFY;
        priv->pendop.i = i;
        gwy_resource_editor_queue_commit(GWY_RESOURCE_EDITOR(editor));
        gwy_null_store_row_changed(priv->store, i);
    }
}

/**
 * SECTION:gradient-editor
 * @title: GwyGradientEditor
 * @short_description: Color gradient editor
 *
 * #GwyGradientEditor is the application color gradient editor.  The interface is currently extremely simple:
 * gwy_app_gradient_editor() invokes the editor (or brings it forward) and then it's user-controlled.
 **/

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