/*
 *  $Id: inventory-store.c 27952 2025-05-09 16:41:38Z yeti-dn $
 *  Copyright (C) 2005-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 "libgwyddion/macros.h"

#include "libgwyui/inventory-store.h"

typedef const gchar* (*GetTraitNameFunc)(gint i);

struct _GwyInventoryStorePrivate {
    gint stamp;
    GwyInventory *inventory;

    /* Cached inventory properties */
    const GwyInventoryItemType *item_type;

    gulong item_updated_id;
    gulong item_inserted_id;
    gulong item_deleted_id;
    gulong items_reordered_id;
    gulong handler_id;
};

static void              dispose        (GObject *object);
static void              tree_model_init(GtkTreeModelIface *iface);
static GtkTreeModelFlags get_flags      (GtkTreeModel *model);
static gint              get_n_columns  (GtkTreeModel *model);
static GType             get_column_type(GtkTreeModel *model,
                                         gint column);
static void              update_iter    (GwyInventoryStore *store,
                                         GtkTreeIter *iter);
static gboolean          get_tree_iter  (GtkTreeModel *model,
                                         GtkTreeIter *iter,
                                         GtkTreePath *path);
static GtkTreePath*      get_path       (GtkTreeModel *model,
                                         GtkTreeIter *iter);
static void              get_value      (GtkTreeModel *model,
                                         GtkTreeIter *iter,
                                         gint column,
                                         GValue *value);
static gboolean          iter_next      (GtkTreeModel *model,
                                         GtkTreeIter *iter);
static gboolean          iter_children  (GtkTreeModel *model,
                                         GtkTreeIter *iter,
                                         GtkTreeIter *parent);
static gboolean          iter_has_child (GtkTreeModel *model,
                                         GtkTreeIter *iter);
static gint              iter_n_children(GtkTreeModel *model,
                                         GtkTreeIter *iter);
static gboolean          iter_nth_child (GtkTreeModel *model,
                                         GtkTreeIter *iter,
                                         GtkTreeIter *parent,
                                         gint n);
static gboolean          iter_parent    (GtkTreeModel *model,
                                         GtkTreeIter *iter,
                                         GtkTreeIter *child);
static void              row_changed    (GwyInventory *inventory,
                                         guint i,
                                         GwyInventoryStore *store);
static void              row_inserted   (GwyInventory *inventory,
                                         guint i,
                                         GwyInventoryStore *store);
static void              row_deleted    (GwyInventory *inventory,
                                         guint i,
                                         GwyInventoryStore *store);
static void              rows_reordered (GwyInventory *inventory,
                                         gint *new_order,
                                         GwyInventoryStore *store);
static gboolean          check_item     (gpointer key,
                                         gpointer item,
                                         gpointer user_data);

static GObjectClass *parent_class = NULL;

G_DEFINE_TYPE_EXTENDED(GwyInventoryStore, gwy_inventory_store, G_TYPE_OBJECT, 0,
                       G_ADD_PRIVATE(GwyInventoryStore)
                       G_IMPLEMENT_INTERFACE(GTK_TYPE_TREE_MODEL, tree_model_init))

static void
gwy_inventory_store_class_init(GwyInventoryStoreClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    parent_class = gwy_inventory_store_parent_class;

    gobject_class->dispose = dispose;
}

static void
tree_model_init(GtkTreeModelIface *iface)
{
    iface->get_flags = get_flags;
    iface->get_n_columns = get_n_columns;
    iface->get_column_type = get_column_type;
    iface->get_iter = get_tree_iter;
    iface->get_path = get_path;
    iface->get_value = get_value;
    iface->iter_next = iter_next;
    iface->iter_children = iter_children;
    iface->iter_has_child = iter_has_child;
    iface->iter_n_children = iter_n_children;
    iface->iter_nth_child = iter_nth_child;
    iface->iter_parent = iter_parent;
}

static void
gwy_inventory_store_init(GwyInventoryStore *store)
{
    GwyInventoryStorePrivate *priv;

    store->priv = priv = gwy_inventory_store_get_instance_private(store);
    priv->stamp = g_random_int();
}

static void
dispose(GObject *object)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(object);
    GwyInventoryStorePrivate *priv = store->priv;

    if (priv->inventory) {
        g_signal_handler_disconnect(priv->inventory, priv->item_updated_id);
        g_signal_handler_disconnect(priv->inventory, priv->item_inserted_id);
        g_signal_handler_disconnect(priv->inventory, priv->item_deleted_id);
        g_signal_handler_disconnect(priv->inventory, priv->items_reordered_id);
    }
    g_clear_object(&priv->inventory);

    parent_class->dispose(object);
}

static GtkTreeModelFlags
get_flags(G_GNUC_UNUSED GtkTreeModel *model)
{
    return GTK_TREE_MODEL_ITERS_PERSIST | GTK_TREE_MODEL_LIST_ONLY;
}

static gint
get_n_columns(GtkTreeModel *model)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);
    gint n;

    store->priv->item_type->get_traits(&n);
    /* +1 for "item" column */
    return n+1;
}

static GType
get_column_type(GtkTreeModel *model,
                gint column)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);

    /* Zeroth is item itself */
    if (!column)
        return G_TYPE_POINTER;

    return store->priv->item_type->get_traits(NULL)[column-1];
}

static inline void
update_iter(GwyInventoryStore *store,
            GtkTreeIter *iter)
{
    GwyInventoryStorePrivate *priv = store->priv;

    if (iter->stamp == priv->stamp)
        return;

    const gchar *name = priv->item_type->get_name(iter->user_data);
    gint i = gwy_inventory_get_item_position(priv->inventory, name);

    iter->stamp = priv->stamp;
    iter->user_data2 = GUINT_TO_POINTER(i);
}

static gboolean
get_tree_iter(GtkTreeModel *model,
              GtkTreeIter *iter,
              GtkTreePath *path)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);
    GwyInventoryStorePrivate *priv = store->priv;

    g_return_val_if_fail(gtk_tree_path_get_depth(path) > 0, FALSE);

    gint i = gtk_tree_path_get_indices(path)[0];

    if (i >= gwy_inventory_get_n_items(priv->inventory))
        return FALSE;

    /* GwyInventoryStore has presistent iters, because it uses item pointers themselves as @user_data and an item
     * pointer is valid as long as the item exists.
     *
     * This always works but needs a round trip item -> name -> position -> position+1 -> item to get next iter.  So
     * we also store item position in @user_data2 and use that directly if inventory has not changed. If it has
     * changed, we can update it using the slower method.
     *
     * To sum it up:
     * @stamp: Corresponds to store's @stamp, but does not have to match.
     * @user_data: Pointer to item.
     * @user_data2: Position in inventory.
     */
    iter->stamp = priv->stamp;
    iter->user_data = gwy_inventory_get_nth_item(priv->inventory, i);
    iter->user_data2 = GUINT_TO_POINTER(i);

    return TRUE;
}

static GtkTreePath*
get_path(GtkTreeModel *model,
         GtkTreeIter *iter)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);
    GtkTreePath *path = gtk_tree_path_new();

    update_iter(store, iter);
    gtk_tree_path_append_index(path, GPOINTER_TO_UINT(iter->user_data2));

    return path;
}

static void
get_value(GtkTreeModel *model,
          GtkTreeIter *iter,
          gint column,
          GValue *value)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);

    if (!column) {
        g_value_init(value, G_TYPE_POINTER);
        g_value_set_pointer(value, iter->user_data);
        return;
    }

    store->priv->item_type->get_trait_value(iter->user_data, column-1, value);
}

static gboolean
iter_next(GtkTreeModel *model,
          GtkTreeIter *iter)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);

    update_iter(store, iter);

    gint i = GPOINTER_TO_UINT(iter->user_data2) + 1;
    iter->user_data = gwy_inventory_get_nth_item(store->priv->inventory, i);
    iter->user_data2 = GUINT_TO_POINTER(i);
    return iter->user_data != NULL;
}

static gboolean
iter_children(GtkTreeModel *model,
              GtkTreeIter *iter,
              GtkTreeIter *parent)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);
    GwyInventoryStorePrivate *priv = store->priv;

    if (parent)
        return FALSE;

    if (!gwy_inventory_get_n_items(priv->inventory))
        return FALSE;

    iter->stamp = priv->stamp;
    iter->user_data = gwy_inventory_get_nth_item(priv->inventory, 0);
    iter->user_data2 = GUINT_TO_POINTER(0);
    return TRUE;
}

static gboolean
iter_has_child(G_GNUC_UNUSED GtkTreeModel *model,
               G_GNUC_UNUSED GtkTreeIter *iter)
{
    return FALSE;
}

static gint
iter_n_children(GtkTreeModel *model,
                GtkTreeIter *iter)
{
    return iter ? 0 : gwy_inventory_get_n_items(GWY_INVENTORY_STORE(model)->priv->inventory);
}

static gboolean
iter_nth_child(GtkTreeModel *model,
               GtkTreeIter *iter,
               GtkTreeIter *parent,
               gint n)
{
    GwyInventoryStore *store = GWY_INVENTORY_STORE(model);
    GwyInventoryStorePrivate *priv = store->priv;

    if (parent)
        return FALSE;

    if (n >= gwy_inventory_get_n_items(priv->inventory))
        return FALSE;

    iter->stamp = priv->stamp;
    iter->user_data = gwy_inventory_get_nth_item(priv->inventory, n);
    iter->user_data2 = GUINT_TO_POINTER(n);
    return TRUE;
}

static gboolean
iter_parent(G_GNUC_UNUSED GtkTreeModel *model,
            G_GNUC_UNUSED GtkTreeIter *iter,
            G_GNUC_UNUSED GtkTreeIter *child)
{
    return FALSE;
}

static void
row_changed(GwyInventory *inventory,
            guint i,
            GwyInventoryStore *store)
{
    GtkTreeIter iter = {
        .stamp = store->priv->stamp,
        .user_data = gwy_inventory_get_nth_item(inventory, i),
    };

    GtkTreePath *path = gtk_tree_path_new();
    gtk_tree_path_append_index(path, i);
    gtk_tree_model_row_changed(GTK_TREE_MODEL(store), path, &iter);
    gtk_tree_path_free(path);
}

static void
row_inserted(GwyInventory *inventory,
             guint i,
             GwyInventoryStore *store)
{
    GwyInventoryStorePrivate *priv = store->priv;

    priv->stamp++;

    GtkTreeIter iter = {
        .stamp = priv->stamp,
        .user_data = gwy_inventory_get_nth_item(inventory, i),
        .user_data2 = GUINT_TO_POINTER(i),
    };
    GtkTreePath *path = gtk_tree_path_new();
    gtk_tree_path_append_index(path, i);
    gtk_tree_model_row_inserted(GTK_TREE_MODEL(store), path, &iter);
    gtk_tree_path_free(path);
}

static void
row_deleted(G_GNUC_UNUSED GwyInventory *inventory,
            guint i,
            GwyInventoryStore *store)
{
    store->priv->stamp++;

    GtkTreePath *path = gtk_tree_path_new();
    gtk_tree_path_append_index(path, i);
    gtk_tree_model_row_deleted(GTK_TREE_MODEL(store), path);
    gtk_tree_path_free(path);
}

static void
rows_reordered(G_GNUC_UNUSED GwyInventory *inventory,
               gint *new_order,
               GwyInventoryStore *store)
{
    store->priv->stamp++;

    GtkTreePath *path = gtk_tree_path_new();
    gtk_tree_model_rows_reordered(GTK_TREE_MODEL(store), path, NULL, new_order);
    gtk_tree_path_free(path);
}

/**
 * gwy_inventory_store_new:
 * @inventory: An inventory.
 *
 * Creates a new #GtkTreeModel wrapper around a #GwyInventory.
 *
 * Returns: The newly created inventory store.
 **/
GwyInventoryStore*
gwy_inventory_store_new(GwyInventory *inventory)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);

    const GwyInventoryItemType *item_type = gwy_inventory_get_item_type(inventory);
    g_return_val_if_fail(item_type->get_name, NULL);
    g_return_val_if_fail(item_type->get_traits, NULL);
    g_return_val_if_fail(item_type->get_trait_value, NULL);

    g_object_ref(inventory);
    GwyInventoryStore *store = g_object_new(GWY_TYPE_INVENTORY_STORE, NULL);
    GwyInventoryStorePrivate *priv = store->priv;

    priv->inventory = inventory;
    priv->item_type = item_type;

    priv->item_updated_id  = g_signal_connect(inventory, "item-updated", G_CALLBACK(row_changed), store);
    priv->item_inserted_id = g_signal_connect(inventory, "item-inserted", G_CALLBACK(row_inserted), store);
    priv->item_deleted_id = g_signal_connect(inventory, "item-deleted", G_CALLBACK(row_deleted), store);
    priv->items_reordered_id = g_signal_connect(inventory, "items-reordered", G_CALLBACK(rows_reordered), store);

    return store;
}

/**
 * gwy_inventory_store_get_inventory:
 * @store: An inventory store.
 *
 * Gets the inventory a inventory store wraps.
 *
 * Returns: The underlying inventory (its reference count is not increased).
 **/
GwyInventory*
gwy_inventory_store_get_inventory(GwyInventoryStore *store)
{
    g_return_val_if_fail(GWY_IS_INVENTORY_STORE(store), NULL);
    return store->priv->inventory;
}

/**
 * gwy_inventory_store_get_column_by_name:
 * @store: An inventory store.
 * @name: Trait (column) name.
 *
 * Gets tree model column corresponding to a trait name.
 *
 * The underlying inventory must support trait names, except for @name <literal>"item"</literal> which always works
 * (and always maps to 0).
 *
 * Returns: The underlying inventory (its reference count is not increased).
 **/
gint
gwy_inventory_store_get_column_by_name(GwyInventoryStore *store,
                                       const gchar *name)
{
    g_return_val_if_fail(GWY_IS_INVENTORY_STORE(store), -1);
    if (gwy_strequal(name, "item"))
        return 0;

    GwyInventoryStorePrivate *priv = store->priv;
    GetTraitNameFunc get_name = priv->item_type->get_trait_name;
    g_return_val_if_fail(get_name, -1);

    gint n;
    priv->item_type->get_traits(&n);

    for (gint i = 0; i < n; i++) {
        if (gwy_strequal(name, get_name(i)))
            return i+1;
    }
    return -1;
}

/**
 * gwy_inventory_store_get_iter:
 * @store: An inventory store.
 * @name: Item name.
 * @iter: Tree iterator to set to point to item named @name.
 *
 * Initializes a tree iterator to row corresponding to a inventory item.
 *
 * Returns: %TRUE if @iter is valid, that is the item exists, %FALSE if @iter was not set.
 **/
gboolean
gwy_inventory_store_get_iter(GwyInventoryStore *store,
                             const gchar *name,
                             GtkTreeIter *iter)
{
    g_return_val_if_fail(GWY_IS_INVENTORY_STORE(store), FALSE);
    g_return_val_if_fail(iter, FALSE);

    GwyInventoryStorePrivate *priv = store->priv;
    guint i = gwy_inventory_get_item_position(priv->inventory, name);
    if (i == (guint)-1)
        return FALSE;

    iter->stamp = priv->stamp;
    iter->user_data = gwy_inventory_get_nth_item(priv->inventory, i);
    g_assert(iter->user_data);
    iter->user_data2 = GUINT_TO_POINTER(i);

    return TRUE;
}

/**
 * gwy_inventory_store_iter_is_valid:
 * @store: An inventory store.
 * @iter: A #GtkTreeIter.
 *
 * Checks if the given iter is a valid iter for this inventory store.
 *
 * <warning>This function is slow. Only use it for debugging and/or testing purposes.</warning>
 *
 * Returns: %TRUE if the iter is valid, %FALSE if the iter is invalid.
 **/
gboolean
gwy_inventory_store_iter_is_valid(GwyInventoryStore *store,
                                  GtkTreeIter *iter)
{
    g_return_val_if_fail(GWY_IS_INVENTORY_STORE(store), FALSE);

    if (!iter || !iter->user_data)
        return FALSE;

    /* Make a copy because we use the iter as a scratch pad */
    GwyInventoryStorePrivate *priv = store->priv;
    GtkTreeIter copy = *iter;
    if (!gwy_inventory_find(priv->inventory, check_item, &copy))
        return FALSE;

    /* Iters with different stamps are valid if just @user_data matches item pointer.  But if stamps match,
     * @user_data2 must match item position */
    return iter->stamp != priv->stamp || iter->user_data2 == copy.user_data2;
}

static gboolean
check_item(gpointer key,
           gpointer item,
           gpointer user_data)
{
    GtkTreeIter *iter = (GtkTreeIter*)user_data;

    if (iter->user_data != item)
        return FALSE;

    iter->user_data2 = key;
    return TRUE;
}

/**
 * SECTION:inventory-store
 * @title: GwyInventoryStore
 * @short_description: GtkTreeModel wrapper around GwyInventory
 * @see_also: #GwyInventory -- the actual data container,
 *            #GtkListStore, #GtkTreeStore -- Gtk+ tree model implementations
 *
 * #GwyInventoryStore is a simple adaptor class that wraps #GwyInventory in #GtkTreeModel interface.  It is list-only
 * and has persistent iterators.  It offers no methods to manipulate items, this should be done on the underlying
 * inventory.
 *
 * #GwyInventoryStore maps inventory item traits to virtual #GtkTreeModel columns.  Zeroth column is always of type
 * %G_TYPE_POINTER and contains item itself.  It exists even if item don't export any traits.  Columns from 1 onward
 * are formed by item traits.  You can obtain column id of a named item trait with
 * gwy_inventory_store_get_column_by_name().
 **/

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