/*
 *  $Id: function-list.c 30072 2026-06-16 13:50:16Z yeti-dn $
 *  Copyright (C) 2026 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.
 */

// FIXME: We are currently hardcoding undo and redo. However, they should be moved from the toolbox elsewhere anyway?
// FIXME: Add an option for showing ‘remaning tool items’ (always as the last item, if enabled)?
// FIXME: Solve selection signals upon switching to another kind.

#include "config.h"
#include <string.h>
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyui/icons.h"
#include "libgwyui/utils.h"

#include "libgwyapp/function-list.h"
#include "libgwyapp/module-cmap.h"
#include "libgwyapp/module-file.h"
#include "libgwyapp/module-graph.h"
#include "libgwyapp/module-image.h"
#include "libgwyapp/module-synth.h"
#include "libgwyapp/module-tool.h"
#include "libgwyapp/module-volume.h"
#include "libgwyapp/module-xyz.h"
#include "libgwyapp/gwytool.h"
#include "libgwyapp/sanity.h"

enum {
    PROP_0,
    PROP_KIND,
    PROP_SHOW_REMAINING_TOOLS,
    NUM_PROPERTIES
};

typedef enum {
    FUNCTION_NONE = 0,
    FUNCTION_PLACEHOLDER,
    FUNCTION_BUILTIN,
    FUNCTION_FILE,
    FUNCTION_IMAGE,
    FUNCTION_GRAPH,
    FUNCTION_SPS,
    FUNCTION_TOOL,
    FUNCTION_SYNTH,
    FUNCTION_VOLUME,
    FUNCTION_XYZ,
    FUNCTION_CMAP,

    FUNCTION_NKINDS
} FunctionKind;

struct _GwyFunctionListPrivate {
    FunctionKind function_kind;
    gboolean show_remaning_tools;

    GArray *funcname_quarks[FUNCTION_NKINDS];
    GtkListStore *stores[FUNCTION_NKINDS];
    GHashTable *nice_names[FUNCTION_NKINDS];
};

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 GtkListStore* ensure_store      (GwyFunctionList *flist);
static void          add_columns       (GwyFunctionList *flist);

static const GwyEnum function_kinds[] = {
    { "",        FUNCTION_NONE,        },
    { "empty",   FUNCTION_PLACEHOLDER, },
    { "builtin", FUNCTION_BUILTIN,     },
    { "file",    FUNCTION_FILE,        },
    { "image",   FUNCTION_IMAGE,       },
    { "graph",   FUNCTION_GRAPH,       },
    { "sps",     FUNCTION_SPS,         },
    { "tool",    FUNCTION_TOOL,        },
    { "synth",   FUNCTION_SYNTH,       },
    { "volume",  FUNCTION_VOLUME,      },
    { "xyz",     FUNCTION_XYZ,         },
    { "cmap",    FUNCTION_CMAP,        },
};

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

G_DEFINE_TYPE_WITH_CODE(GwyFunctionList, gwy_function_list, GTK_TYPE_TREE_VIEW,
                        G_ADD_PRIVATE(GwyFunctionList))

static void
gwy_function_list_class_init(GwyFunctionListClass *klass)
{
    //GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_function_list_parent_class;

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

    properties[PROP_SHOW_REMAINING_TOOLS] = g_param_spec_boolean("show-remaning-tools", NULL,
                                                                 "Whether to show the remaning tools item for tools",
                                                                 FALSE,
                                                                 GWY_GPARAM_RWE);
    properties[PROP_KIND] = g_param_spec_string("kind", NULL,
                                                "The kind of functions to list",
                                                "",
                                                GWY_GPARAM_RWE);
    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
finalize(GObject *object)
{
    GwyFunctionListPrivate *priv = GWY_FUNCTION_LIST(object)->priv;

    for (guint i = 0; i < FUNCTION_NKINDS; i++) {
        g_clear_object(priv->stores + i);
        GWY_FREE_ARRAY(priv->funcname_quarks[i]);
        if (priv->nice_names[i])
            g_hash_table_destroy(priv->nice_names[i]);
    }

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

static void
gwy_function_list_init(GwyFunctionList *flist)
{
    GwyFunctionListPrivate *priv = flist->priv = gwy_function_list_get_instance_private(flist);

    priv->function_kind = FUNCTION_NONE;
    ensure_store(flist);

    GtkTreeView *treeview = GTK_TREE_VIEW(flist);
    gtk_tree_view_set_model(treeview, GTK_TREE_MODEL(priv->stores[FUNCTION_NONE]));
    gtk_tree_view_set_headers_visible(treeview, FALSE);
    add_columns(flist);

    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyFunctionList *flist = GWY_FUNCTION_LIST(object);
    GwyFunctionListPrivate *priv = flist->priv;
    gboolean changed = FALSE;

    if (prop_id == PROP_KIND) {
        FunctionKind old_kind = priv->function_kind;
        gwy_function_list_set_kind(flist, g_value_get_string(value));
        changed = (old_kind != priv->function_kind);
    }
    else if (prop_id == PROP_SHOW_REMAINING_TOOLS) {
        gboolean b = g_value_get_boolean(value);
        if (!b != !priv->show_remaning_tools) {
            gwy_function_list_set_show_remaning_tools(flist, b);
            changed = TRUE;
        }
    }
    else {
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }

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

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyFunctionList *flist = GWY_FUNCTION_LIST(object);
    GwyFunctionListPrivate *priv = flist->priv;

    if (prop_id == PROP_KIND) {
        g_value_set_string(value, gwy_enum_to_string(flist->priv->function_kind,
                                                     function_kinds, G_N_ELEMENTS(function_kinds)));
    }
    else if (prop_id == PROP_SHOW_REMAINING_TOOLS)
        g_value_set_boolean(value, priv->show_remaning_tools);
    else {
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

/**
 * gwy_function_list_new: (constructor)
 * @kind: Type of functions to display.
 *
 * Creates a new list of program functions
 *
 * Returns: (transfer full): A newly created list of program functions.
 **/
GtkWidget*
gwy_function_list_new(const gchar *kind)
{
    GwyFunctionList *flist = g_object_new(GWY_TYPE_FUNCTION_LIST, NULL);
    gwy_function_list_set_kind(flist, kind);
    return GTK_WIDGET(flist);
}

/**
 * gwy_function_list_get_kind:
 * @flist: A list of program functions.
 *
 * Gets the kind of functions list of program functions is currently showing.
 *
 * Returns: The name of the function kind.
 **/
const gchar*
gwy_function_list_get_kind(GwyFunctionList *flist)
{
    g_return_val_if_fail(GWY_IS_FUNCTION_LIST(flist), NULL);
    return gwy_enum_to_string(flist->priv->function_kind, function_kinds, G_N_ELEMENTS(function_kinds));
}

/**
 * gwy_function_list_set_kind:
 * @flist: A list of program functions.
 * @kind: (nullable): Type of functions to display.
 *
 * Switches a list of program functions to a different kind of functions.
 *
 * The kind can be NULL or the empty string for an empty list, "empty" for the no-function placeholder or the name of
 * function kind like "image", "graph", "volume" or "synth".
 **/
void
gwy_function_list_set_kind(GwyFunctionList *flist,
                           const gchar *kind)
{
    g_return_if_fail(GWY_IS_FUNCTION_LIST(flist));
    if (!kind)
        kind = "";

    GwyFunctionListPrivate *priv = flist->priv;
    FunctionKind function_kind = gwy_string_to_enum(kind, function_kinds, G_N_ELEMENTS(function_kinds));
    g_return_if_fail((gint)function_kind >= 0);

    if (priv->function_kind == function_kind && priv->stores[function_kind])
        return;

    priv->function_kind = function_kind;
    GtkListStore *store = ensure_store(flist);

    GtkTreeView *treeview = GTK_TREE_VIEW(flist);
    GtkTreeModel *model = GTK_TREE_MODEL(store);
    // Switching the model emits a bunch of signals for no good reason. Blocking all handlers of a specific signal is
    // only possible with GLib ≥ 2.78. It was released in 2023, so it may be OK to require it.
    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
#if GLIB_CHECK_VERSION(2,78,0)
    guint signal_id = g_signal_lookup("changed", GTK_TYPE_TREE_SELECTION);
    guint nblocked = g_signal_handlers_block_matched(selection, G_SIGNAL_MATCH_ID, signal_id, 0, NULL, NULL, NULL);
#endif
    gtk_tree_view_set_model(treeview, model);

    GtkTreeIter iter;
    if (gtk_tree_model_get_iter_first(model, &iter))
        gtk_tree_selection_select_iter(selection, &iter);
    else
        gtk_tree_selection_unselect_all(selection);

#if GLIB_CHECK_VERSION(2,78,0)
    if (nblocked) {
        g_signal_handlers_unblock_matched(selection, G_SIGNAL_MATCH_ID, signal_id, 0, NULL, NULL, NULL);
        g_signal_emit(selection, signal_id, 0, NULL);
    }
#endif
}

/**
 * gwy_function_list_get_selected:
 * @flist: A list of program functions.
 *
 * Gets the name of selected function in a list of program functions.
 *
 * Combine the function with gwy_function_list_get_kind() to get the complete specification of the function.
 *
 * The return value may be %NULL if there are no functions. The name of the remaining tools placeholder is returned as
 * the empty string.
 *
 * Returns: (nullable): The name of selected function.
 **/
const gchar*
gwy_function_list_get_selected(GwyFunctionList *flist)
{
    g_return_val_if_fail(GWY_IS_FUNCTION_LIST(flist), NULL);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(flist));
    GtkTreeModel *model;
    GtkTreeIter iter;
    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return NULL;
    GQuark qname;
    gtk_tree_model_get(model, &iter, 0, &qname, -1);
    return qname ? g_quark_to_string(qname) : NULL;
}

static gboolean
find_by_name(GwyFunctionList *flist, const gchar *name, guint *idx)
{
    GQuark qname = g_quark_try_string(name);
    if (!qname) {
        g_warning("Trying to find an unknown string %s.", name);
        return FALSE;
    }

    GwyFunctionListPrivate *priv = flist->priv;
    GArray *funcname_quarks = priv->funcname_quarks[priv->function_kind];
    guint n = funcname_quarks->len;
    const GQuark *qdata = &g_array_index(funcname_quarks, GQuark, 0);
    for (guint i = 0; i < n; i++) {
        if (qname == qdata[i]) {
            *idx = i;
            return TRUE;
        }
    }
    return FALSE;
}

/**
 * gwy_function_list_select:
 * @flist: A list of program functions.
 * @name: Function name (of the current kind).
 * @scroll_to: %TRUE to scroll to the function to make it visible.
 *
 * Selects a function in a list of program functions.
 **/
gboolean
gwy_function_list_select(GwyFunctionList *flist,
                         const gchar *name,
                         gboolean scroll_to)
{
    g_return_val_if_fail(GWY_IS_FUNCTION_LIST(flist), FALSE);
    g_return_val_if_fail(name, FALSE);

    guint idx;
    if (!find_by_name(flist, name, &idx))
        return FALSE;

    GtkTreeView *treeview = GTK_TREE_VIEW(flist);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    GtkTreeModel *model = gtk_tree_view_get_model(treeview);
    GtkTreeIter iter;
    gboolean ok = gtk_tree_model_iter_nth_child(model, &iter, NULL, idx);
    g_return_val_if_fail(ok, FALSE);
    gtk_tree_selection_select_iter(selection, &iter);
    if (scroll_to) {
        GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
        gtk_tree_view_scroll_to_cell(treeview, path, NULL, TRUE, 0.5, 0.0);
        gtk_tree_path_free(path);
    }
    return TRUE;
}

/**
 * gwy_function_list_refresh_row:
 * @flist: A list of program functions.
 * @name: Function name (of the current kind).
 *
 * Refreshes information about a function in a list of program functions.
 **/
void
gwy_function_list_refresh_row(GwyFunctionList *flist,
                              const gchar *name)
{
    g_return_if_fail(GWY_IS_FUNCTION_LIST(flist));
    g_return_if_fail(name);

    guint idx;
    if (!find_by_name(flist, name, &idx))
        return;

    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(flist));
    GtkTreeIter iter;
    gboolean ok = gtk_tree_model_iter_nth_child(model, &iter, NULL, idx);
    if (ok) {
        GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
        gtk_tree_model_row_changed(model, path, &iter);
        gtk_tree_path_free(path);
    }
}

/**
 * gwy_function_list_set_show_remaning_tools:
 * @flist: A list of program functions.
 * @setting: %TRUE to show the remaning tools item; %FALSE to not show it.
 *
 * Sets whether a remaning tools item is shown at the end of a tool list.
 *
 * The setting only affects lists of tools. The remaning tool item is a useful placeholder when editing the toolbox.
 * It is not useful otherwise.
 **/
void
gwy_function_list_set_show_remaning_tools(GwyFunctionList *flist,
                                          gboolean setting)
{
    g_return_if_fail(GWY_IS_FUNCTION_LIST(flist));
    GwyFunctionListPrivate *priv = flist->priv;
    if (!setting == !priv->show_remaning_tools)
        return;
    priv->show_remaning_tools = setting;

    GtkListStore *store = priv->stores[FUNCTION_TOOL];
    GArray *funcname_quarks = priv->funcname_quarks[FUNCTION_TOOL];
    g_assert(!store == !funcname_quarks);
    if (!store)
        return;

    // Update the data structures now, even if we are showing a different list, to avoid doing a history-dependent
    // sync upon switching function kind.
    GtkTreeIter iter;
    guint n = funcname_quarks->len;
    if (setting) {
        GQuark quark = g_quark_from_static_string("");
        g_array_append_val(funcname_quarks, quark);
        gtk_list_store_insert_with_values(store, &iter, G_MAXINT, 0, quark, -1);
    }
    else {
        g_assert(n);
        g_array_remove_index(funcname_quarks, n-1);
        gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(store), &iter, NULL, n-1);
        gtk_list_store_remove(store, &iter);
    }
}

/**
 * gwy_function_list_get_show_remaning_tools:
 * @flist: A list of program functions.
 *
 * Reports whether a remaning tools item is shown at the end of a tool list.
 *
 * Returns: %TRUE if the remaning tools item is shown; %FALSE if it is disabled.
 **/
gboolean
gwy_function_list_get_show_remaning_tools(GwyFunctionList *flist)
{
    g_return_val_if_fail(GWY_IS_FUNCTION_LIST(flist), FALSE);
    return flist->priv->show_remaning_tools;
}

static void
populate_builtin_function_list(GArray *funcname_quarks)
{
    GQuark quark;

    quark = g_quark_from_string("undo");
    g_array_append_val(funcname_quarks, quark);
    quark = g_quark_from_string("redo");
    g_array_append_val(funcname_quarks, quark);
    // FIXME: There also used to be 3D view and maybe other things.
}

static void
populate_placeholder_function_list(GArray *funcname_quarks)
{
    GQuark quark;

    quark = g_quark_from_string("placeholder");
    g_array_append_val(funcname_quarks, quark);
}

static void
populate_function_list(const gchar *name, gpointer user_data)
{
    GArray *funcname_quarks = (GArray*)user_data;
    GQuark quark = g_quark_from_string(name);
    g_array_append_val(funcname_quarks, quark);
}

static gint
compare_func_names(gconstpointer pa, gconstpointer pb, gpointer user_data)
{
    GwyFunctionListPrivate *priv = ((GwyFunctionList*)user_data)->priv;
    FunctionKind kind = priv->function_kind;
    GQuark qa = *(const GQuark*)pa;
    GQuark qb = *(const GQuark*)pb;
    const gchar *namea = g_hash_table_lookup(priv->nice_names[kind], GSIZE_TO_POINTER(qa));
    const gchar *nameb = g_hash_table_lookup(priv->nice_names[kind], GSIZE_TO_POINTER(qb));
    return g_utf8_collate(namea, nameb);
}

static gchar*
function_make_nice_name(GQuark qname, FunctionKind kind)
{
    if (kind == FUNCTION_PLACEHOLDER)
        return g_strdup(_("placeholder"));

    if (!qname)
        return NULL;

    const gchar *name = g_quark_to_string(qname);
    if (kind == FUNCTION_TOOL) {
        if (name && *name) {
            GType gtype = g_type_from_name(name);
            GwyToolClass *tool_class = g_type_class_peek(gtype);
            return g_strdup(_(gwy_tool_class_get_title(tool_class)));
        }
        return g_strdup(_("remaining tools"));
    }
    if (kind == FUNCTION_BUILTIN) {
        if (gwy_strequal(name, "undo"))
            return g_strdup(_("Undo last action"));
        if (gwy_strequal(name, "redo"))
            return g_strdup(_("Redo again last undone action"));
        return NULL;
    }

    const gchar *menupath = NULL;
    if (kind == FUNCTION_IMAGE)
        menupath = gwy_image_func_get_menu_path(name);
    else if (kind == FUNCTION_GRAPH)
        menupath = gwy_graph_func_get_menu_path(name);
    // FIXME
    //else if (kind == FUNCTION_SPS)
    //    menupath = gwy_spectra_func_get_menu_path(name);
    else if (kind == FUNCTION_SYNTH)
        menupath = gwy_synth_func_get_menu_path(name);
    else if (kind == FUNCTION_VOLUME)
        menupath = gwy_volume_func_get_menu_path(name);
    else if (kind == FUNCTION_XYZ)
        menupath = gwy_xyz_func_get_menu_path(name);
    else if (kind == FUNCTION_CMAP)
        menupath = gwy_cmap_func_get_menu_path(name);

    if (menupath) {
        gchar *s = gwy_strip_menu_item_label(g_strdup(_(menupath)));
        const gchar *p = strrchr(s, '/');
        if (!p)
            return s;

        gchar *retval = g_strdup(p+1);
        g_free(s);
        return retval;
    }

    return NULL;
}

static GtkListStore*
ensure_store(GwyFunctionList *flist)
{
    GwyFunctionListPrivate *priv = flist->priv;
    FunctionKind kind = priv->function_kind;
    if (priv->stores[kind]) {
        g_assert(priv->funcname_quarks[kind] && priv->nice_names[kind]);
        return priv->stores[kind];
    }

    GtkListStore *store = priv->stores[kind] = gtk_list_store_new(1, G_TYPE_UINT);
    GArray *funcname_quarks = priv->funcname_quarks[kind] = g_array_new(FALSE, FALSE, sizeof(GQuark));
    GHashTable *nice_names = priv->nice_names[kind] = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                                                                            NULL, g_free);

    if (kind == FUNCTION_NONE)
        return store;

    if (kind == FUNCTION_BUILTIN)
        populate_builtin_function_list(funcname_quarks);
    else if (kind == FUNCTION_PLACEHOLDER)
        populate_placeholder_function_list(funcname_quarks);
    else if (kind == FUNCTION_FILE)
        gwy_file_func_foreach(populate_function_list, funcname_quarks);
    else if (kind == FUNCTION_IMAGE)
        gwy_image_func_foreach(populate_function_list, funcname_quarks);
    else if (kind == FUNCTION_GRAPH)
        gwy_graph_func_foreach(populate_function_list, funcname_quarks);
    else if (kind == FUNCTION_SPS) {
        //gwy_sps_func_foreach(populate_function_list, funcname_quarks);
    }
    else if (kind == FUNCTION_VOLUME)
        gwy_volume_func_foreach(populate_function_list, funcname_quarks);
    else if (kind == FUNCTION_XYZ)
        gwy_xyz_func_foreach(populate_function_list, funcname_quarks);
    else if (kind == FUNCTION_CMAP)
        gwy_cmap_func_foreach(populate_function_list, funcname_quarks);
    else if (kind == FUNCTION_SYNTH)
        gwy_synth_func_foreach(populate_function_list, funcname_quarks);
    else if (kind == FUNCTION_TOOL)
        gwy_tool_func_foreach(populate_function_list, funcname_quarks);
    else {
        g_assert_not_reached();
    }

    for (guint i = 0; i < funcname_quarks->len; i++) {
        GQuark quark = g_array_index(funcname_quarks, GQuark, i);
        gchar *nice_name = function_make_nice_name(quark, kind);
        g_hash_table_insert(nice_names, GSIZE_TO_POINTER(quark), nice_name);
    }
    g_array_sort_with_data(funcname_quarks, compare_func_names, flist);

    if (kind == FUNCTION_TOOL && priv->show_remaning_tools) {
        GQuark quark = g_quark_from_static_string("");
        g_array_append_val(funcname_quarks, quark);
    }

    for (guint i = 0; i < funcname_quarks->len; i++) {
        GQuark quark = g_array_index(funcname_quarks, GQuark, i);
        GtkTreeIter iter;
        gtk_list_store_insert_with_values(store, &iter, G_MAXINT, 0, quark, -1);
    }

    return store;
}

static GtkCellRenderer*
append_column(GtkTreeView *treeview, gboolean new_column,
              GType renderer_gtype, GtkTreeCellDataFunc cell_func, gpointer cell_data,
              gint fixed_width, gint fixed_height, gboolean expand)
{
    GtkTreeViewColumn *column;
    if (new_column) {
        column = gtk_tree_view_column_new();
        gtk_tree_view_column_set_expand(column, expand);
        gtk_tree_view_append_column(treeview, column);
    }
    else
        column = gtk_tree_view_get_column(treeview, gtk_tree_view_get_n_columns(treeview)-1);

    GtkCellRenderer *renderer = g_object_new(renderer_gtype, NULL);
    gtk_tree_view_column_pack_start(column, renderer, FALSE);
    if (fixed_width != -1 || fixed_height != -1)
        gtk_cell_renderer_set_fixed_size(renderer, fixed_width, fixed_height);
    gtk_tree_view_column_set_cell_data_func(column, renderer, cell_func, cell_data, NULL);

    return renderer;
}

static GdkPixbuf*
load_icon(const gchar *icon_name, GtkIconTheme *theme)
{
    if (!theme)
        theme = gtk_icon_theme_get_default();

    return gtk_icon_theme_load_icon(theme, icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR,
                                    GTK_ICON_LOOKUP_USE_BUILTIN | GTK_ICON_LOOKUP_DIR_LTR,
                                    NULL);
}

static const gchar*
function_icon_name(GQuark qname, FunctionKind kind)
{
    if (!qname)
        return NULL;

    const gchar *name = g_quark_to_string(qname);
    // Remaning tools has an empty function name.
    if (!*name)
        return NULL;

    if (kind == FUNCTION_IMAGE)
        return gwy_image_func_get_icon_name(name);
    if (kind == FUNCTION_GRAPH)
        return gwy_graph_func_get_icon_name(name);
    //if (kind == FUNCTION_SPS)
    //    return gwy_spectra_func_get_icon_name(name);
    if (kind == FUNCTION_SYNTH)
        return gwy_synth_func_get_icon_name(name);
    if (kind == FUNCTION_VOLUME)
        return gwy_volume_func_get_icon_name(name);
    if (kind == FUNCTION_XYZ)
        return gwy_xyz_func_get_icon_name(name);
    if (kind == FUNCTION_CMAP)
        return gwy_cmap_func_get_icon_name(name);
    if (kind == FUNCTION_TOOL) {
        GType gtype = g_type_from_name(name);
        GwyToolClass *tool_class = g_type_class_peek(gtype);
        return gwy_tool_class_get_icon_name(tool_class);
    }
    if (kind == FUNCTION_BUILTIN) {
        if (gwy_strequal(name, "undo"))
            return GWY_ICON_GTK_UNDO;
        if (gwy_strequal(name, "redo"))
            return GWY_ICON_GTK_REDO;
        return NULL;
    }

    return NULL;
}

static void
render_icon(G_GNUC_UNUSED GtkTreeViewColumn *column,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            gpointer user_data)
{
    GwyFunctionList *flist = (GwyFunctionList*)user_data;
    GQuark qname;
    const gchar *id;

    gtk_tree_model_get(model, iter, 0, &qname, -1);
    if ((id = function_icon_name(qname, flist->priv->function_kind))) {
        GdkPixbuf *pixbuf = load_icon(id, NULL);
        g_object_set(renderer, "pixbuf", pixbuf, NULL);
        g_clear_object(&pixbuf);
    }
    else {
        g_object_set(renderer, "pixbuf", NULL, NULL);
    }
}

static void
render_name(G_GNUC_UNUSED GtkTreeViewColumn *column,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            gpointer user_data)
{
    GwyFunctionList *flist = (GwyFunctionList*)user_data;
    GwyFunctionListPrivate *priv = flist->priv;
    GQuark qname;

    gtk_tree_model_get(model, iter, 0, &qname, -1);
    const gchar *nice_name = g_hash_table_lookup(priv->nice_names[priv->function_kind], GSIZE_TO_POINTER(qname));
    g_object_set(renderer, "text", nice_name, NULL);
}

static void
add_columns(GwyFunctionList *flist)
{
    gint width, height;
    gtk_icon_size_lookup(GTK_ICON_SIZE_LARGE_TOOLBAR, &width, &height);

    GtkTreeView *treeview = GTK_TREE_VIEW(flist);
    append_column(treeview, TRUE, GTK_TYPE_CELL_RENDERER_PIXBUF, render_icon, flist, width, height, FALSE);
    append_column(treeview, FALSE, GTK_TYPE_CELL_RENDERER_TEXT, render_name, flist, -1, height, FALSE);
}

/**
 * SECTION: function-list
 * @title: GwyFunctionList
 * @short_description: List of program functions
 **/

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

