/*
 *  $Id: inventory.c 29477 2026-02-14 13:29:30Z yeti-dn $
 *  Copyright (C) 2003-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 <stdlib.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/inventory.h"

#define GWY_INVENTORY_TYPE_NAME "GwyInventory"

enum {
    SGNL_ITEM_INSERTED,
    SGNL_ITEM_DELETED,
    SGNL_ITEM_UPDATED,
    SGNL_ITEMS_REORDERED,
    SGNL_DEFAULT_CHANGED,
    NUM_SIGNALS
};

typedef struct {
    gpointer p;
    guint i;
} ArrayItem;

struct _GwyInventoryPrivate {
    GwyInventoryItemType item_type;
    gboolean needs_reindex;
    gboolean is_sorted;
    gboolean is_const;
    gboolean is_object;
    gboolean is_watchable;
    gboolean can_make_copies;
    gboolean has_default;

    GString *default_key;

    /* Item pointers (called storage). */
    GPtrArray *items;
    /* Index.  A map from storage position to sort position. May be %NULL. */
    GArray *idx;
    /* Reverse index.  A map from sort position to storage position. May be %NULL */
    GArray *ridx;
    /* Name hash.  A map from name to storage position. */
    GHashTable *hash;
};


static void          finalize            (GObject *object);
static guint         lookup              (GwyInventory *inventory,
                                          const gchar *name);
static void          make_hash           (GwyInventory *inventory);
static GwyInventory* new_real            (const GwyInventoryItemType *itype,
                                          guint nitems,
                                          gpointer *items,
                                          gboolean is_const);
static void          connect_to_item     (gpointer item,
                                          GwyInventory *inventory);
static void          disconnect_from_item(gpointer item,
                                          GwyInventory *inventory);
static void          reindex             (GwyInventory *inventory);
static void          item_updated_real   (GwyInventory *inventory,
                                          guint i);
static void          item_changed        (GwyInventory *inventory,
                                          gpointer item);
static gint          compare_indices     (gint *a,
                                          gint *b,
                                          GwyInventory *inventory);
static void          delete_nth_item_real(GwyInventory *inventory,
                                          const gchar *name,
                                          guint i);
static gchar*        invent_name         (GwyInventory *inventory,
                                          const gchar *prefix);

GObjectClass *parent_class = NULL;
static guint signals[NUM_SIGNALS] = { 0 };

G_DEFINE_TYPE_WITH_CODE(GwyInventory, gwy_inventory, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyInventory))

static void
gwy_inventory_class_init(GwyInventoryClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_inventory_parent_class;

    gobject_class->finalize = finalize;

    /**
     * GwyInventory::item-inserted:
     * @gwyinventory: The #GwyInventory which received the signal.
     * @arg1: Position an item was inserted at.
     *
     * The ::item-inserted signal is emitted when an item is inserted into an inventory.
     **/
    signals[SGNL_ITEM_INSERTED] = g_signal_new("item-inserted", type,
                                               G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                               G_STRUCT_OFFSET(GwyInventoryClass, item_inserted),
                                               NULL, NULL,
                                               g_cclosure_marshal_VOID__UINT,
                                               G_TYPE_NONE, 1, G_TYPE_UINT);
    g_signal_set_va_marshaller(signals[SGNL_ITEM_INSERTED], type, g_cclosure_marshal_VOID__UINTv);

    /**
     * GwyInventory::item-deleted:
     * @gwyinventory: The #GwyInventory which received the signal.
     * @arg1: Position an item was deleted from.
     *
     * The ::item-deleted signal is emitted when an item is deleted from an inventory.
     **/
    signals[SGNL_ITEM_DELETED] = g_signal_new("item-deleted", type,
                                              G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                              G_STRUCT_OFFSET(GwyInventoryClass, item_deleted),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__UINT,
                                              G_TYPE_NONE, 1, G_TYPE_UINT);
    g_signal_set_va_marshaller(signals[SGNL_ITEM_DELETED], type, g_cclosure_marshal_VOID__UINTv);

    /**
     * GwyInventory::item-updated:
     * @gwyinventory: The #GwyInventory which received the signal.
     * @arg1: Position of updated item.
     *
     * The ::item-updated signal is emitted when an item in an inventory is updated.
     **/
    signals[SGNL_ITEM_UPDATED] = g_signal_new("item-updated", type,
                                              G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                              G_STRUCT_OFFSET(GwyInventoryClass, item_updated),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__UINT,
                                              G_TYPE_NONE, 1, G_TYPE_UINT);
    g_signal_set_va_marshaller(signals[SGNL_ITEM_UPDATED], type, g_cclosure_marshal_VOID__UINTv);

    /**
     * GwyInventory::items-reordered:
     * @gwyinventory: The #GwyInventory which received the signal.
     * @arg1: New item order map as in #GtkTreeModel,
     *        @arg1[new_position] = old_position.
     *
     * The ::items-reordered signal is emitted when item in an inventory are reordered.
     **/
    signals[SGNL_ITEMS_REORDERED] = g_signal_new("items-reordered", type,
                                                 G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                                 G_STRUCT_OFFSET(GwyInventoryClass, items_reordered),
                                                 NULL, NULL,
                                                 g_cclosure_marshal_VOID__POINTER,
                                                 G_TYPE_NONE, 1, G_TYPE_POINTER);
    g_signal_set_va_marshaller(signals[SGNL_ITEMS_REORDERED], type, g_cclosure_marshal_VOID__POINTERv);

    /**
     * GwyInventory::default-changed:
     * @gwyinventory: The #GwyInventory which received the signal.
     *
     * The ::default-changed signal is emitted when either default inventory item name changes or the presence of such
     * an item in the inventory changes.
     **/
    signals[SGNL_DEFAULT_CHANGED] = g_signal_new("default-changed", type,
                                                 G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                                 G_STRUCT_OFFSET(GwyInventoryClass, default_changed),
                                                 NULL, NULL,
                                                 g_cclosure_marshal_VOID__VOID,
                                                 G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_DEFAULT_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);
}

static void
gwy_inventory_init(GwyInventory *inventory)
{
    inventory->priv = gwy_inventory_get_instance_private(inventory);
}

static void
finalize(GObject *object)
{
    GwyInventory *inventory = GWY_INVENTORY(object);
    GwyInventoryPrivate *priv = inventory->priv;

    if (priv->is_watchable)
        g_ptr_array_foreach(priv->items, (GFunc)&disconnect_from_item, inventory);
    if (priv->default_key)
        g_string_free(priv->default_key, TRUE);
    if (priv->hash)
        g_hash_table_destroy(priv->hash);
    if (priv->idx)
        g_array_free(priv->idx, TRUE);
    if (priv->ridx)
        g_array_free(priv->ridx, TRUE);
    if (priv->is_object)
        g_ptr_array_foreach(priv->items, (GFunc)&g_object_unref, NULL);
    g_ptr_array_free(priv->items, TRUE);

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

static inline guint
lookup(GwyInventory *inventory, const gchar *name)
{
    GwyInventoryPrivate *priv = inventory->priv;

    if (G_UNLIKELY(!priv->hash))
        make_hash(inventory);

    return GPOINTER_TO_UINT(g_hash_table_lookup(priv->hash, name));
}

static void
make_hash(GwyInventory *inventory)
{
    GwyInventoryPrivate *priv = inventory->priv;

    g_assert(!priv->hash);
    priv->hash = g_hash_table_new(g_str_hash, g_str_equal);

    const gchar* (*get_name)(gpointer) = priv->item_type.get_name;
    for (guint i = 0; i < priv->items->len; i++) {
        gpointer item;

        item = g_ptr_array_index(priv->items, i);
        g_hash_table_insert(priv->hash, (gpointer)get_name(item), GUINT_TO_POINTER(i+1));
    }
}

/**
 * gwy_inventory_new:
 * @itype: Type of items the inventory will contain.
 *
 * Creates a new inventory.
 *
 * Returns: The newly created inventory.
 **/
GwyInventory*
gwy_inventory_new(const GwyInventoryItemType *itype)
{
    return new_real(itype, 0, NULL, FALSE);
}

/**
 * gwy_inventory_new_filled:
 * @itype: Type of items the inventory will contain.
 * @nitems: The number of pointers in @items.
 * @items: (array length=nitems) (element-type GwyInventoryItemType):
 *         Item pointers to fill the newly created inventory with.
 *
 * Creates a new inventory and fills it with items.
 *
 * Returns: The newly created inventory.
 **/
GwyInventory*
gwy_inventory_new_filled(const GwyInventoryItemType *itype,
                         guint nitems,
                         gpointer *items)
{
    return new_real(itype, nitems, items, FALSE);
}

/**
 * gwy_inventory_new_from_array:
 * @itype: Type of items the inventory will contain.  Inventory keeps a copy of it, so it can be an automatic
 *         variable.
 * @item_size: Item size in bytes.
 * @nitems: The number of items in @items.
 * @items: (array length=nitems) (element-type GwyInventoryItemType):
 *         An array with items.  It will be directly used as thus must exist through the whole lifetime of inventory.
 *
 * Creates a new inventory from static item array.
 *
 * The inventory is neither modifiable nor sortable, it simply serves as an
 * adaptor for the array @items.
 *
 * Returns: The newly created inventory.
 **/
GwyInventory*
gwy_inventory_new_from_array(const GwyInventoryItemType *itype,
                             guint item_size,
                             guint nitems,
                             gconstpointer items)
{
    g_return_val_if_fail(items || !nitems, NULL);
    g_return_val_if_fail(item_size, NULL);

    gpointer *pitems = g_new(gpointer, nitems);
    for (guint i = 0; i < nitems; i++)
        pitems[i] = (gpointer)((const guchar*)items + i*item_size);

    GwyInventory *inventory = new_real(itype, nitems, pitems, TRUE);
    g_free(pitems);

    return inventory;
}

static GwyInventory*
new_real(const GwyInventoryItemType *itype,
         guint nitems,
         gpointer *items,
         gboolean is_const)
{
    GwyInventory *inventory = g_object_new(GWY_TYPE_INVENTORY, NULL);

    g_return_val_if_fail(itype, inventory);
    g_return_val_if_fail(itype->get_name, inventory);
    g_return_val_if_fail(items || !nitems, inventory);

    GwyInventoryPrivate *priv = inventory->priv;
    priv->item_type = *itype;
    if (itype->type) {
        priv->is_object = g_type_is_a(itype->type, G_TYPE_OBJECT);
        priv->is_watchable = (itype->watchable_signal != NULL);
    }

    priv->can_make_copies = itype->rename && itype->copy;
    priv->is_sorted = (itype->compare != NULL);
    priv->is_const = is_const;
    priv->items = g_ptr_array_sized_new(nitems);

    for (guint i = 0; i < nitems; i++) {
        g_ptr_array_add(priv->items, items[i]);
        if (priv->is_sorted && i)
            priv->is_sorted = (itype->compare(items[i-1], items[i]) < 0);
    }
    if (!is_const) {
        priv->idx = g_array_sized_new(FALSE, FALSE, sizeof(guint), nitems);
        priv->ridx = g_array_sized_new(FALSE, FALSE, sizeof(guint), nitems);
        for (guint i = 0; i < nitems; i++)
            g_array_append_val(priv->idx, i);
        g_array_append_vals(priv->ridx, priv->idx->data, nitems);
    }
    if (priv->is_object) {
        g_ptr_array_foreach(priv->items, (GFunc)&g_object_ref, NULL);
        if (priv->is_watchable)
            g_ptr_array_foreach(priv->items, (GFunc)&connect_to_item, inventory);
    }

    return inventory;
}

static void
disconnect_from_item(gpointer item, GwyInventory *inventory)
{
    g_signal_handlers_disconnect_by_func(item, item_changed, inventory);
}

static void
connect_to_item(gpointer item, GwyInventory *inventory)
{
    g_signal_connect_swapped(item, inventory->priv->item_type.watchable_signal, G_CALLBACK(item_changed), inventory);
}

/**
 * gwy_inventory_get_n_items:
 * @inventory: An inventory.
 *
 * Returns the number of items in an inventory.
 *
 * Returns: The number of items.
 **/
guint
gwy_inventory_get_n_items(GwyInventory *inventory)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), 0);
    return inventory->priv->items->len;
}

/**
 * gwy_inventory_can_make_copies:
 * @inventory: An inventory.
 *
 * Returns whether an inventory can create new items itself.
 *
 * The prerequisite is that item type is a serializable object.  It enables functions like gwy_inventory_new_item().
 *
 * Returns: %TRUE if inventory can create new items itself.
 **/
gboolean
gwy_inventory_can_make_copies(GwyInventory *inventory)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), FALSE);
    return inventory->priv->can_make_copies;
}

/**
 * gwy_inventory_is_const:
 * @inventory: An inventory.
 *
 * Returns whether an inventory is an constant inventory.
 *
 * Not only you cannot modify a constant inventory, but functions like gwy_inventory_get_item() may return pointers to
 * constant memory.
 *
 * Returns: %TRUE if inventory is constant.
 **/
gboolean
gwy_inventory_is_const(GwyInventory *inventory)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), FALSE);
    return inventory->priv->is_const;
}

/**
 * gwy_inventory_get_item_type:
 * @inventory: An inventory.
 *
 * Returns the type of item an inventory holds.
 *
 * Returns: The item type.  It is owned by inventory and must not be modified or freed.
 **/
const GwyInventoryItemType*
gwy_inventory_get_item_type(GwyInventory *inventory)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);
    return &inventory->priv->item_type;
}

/**
 * gwy_inventory_get_item:
 * @inventory: An inventory.
 * @name: Item name.
 *
 * Looks up an item in an inventory.
 *
 * Returns: Item called @name, or %NULL if there is no such item.
 **/
gpointer
gwy_inventory_get_item(GwyInventory *inventory,
                       const gchar *name)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);

    guint i;
    if ((i = lookup(inventory, name)))
        return g_ptr_array_index(inventory->priv->items, i-1);
    else
        return NULL;
}

/**
 * gwy_inventory_get_item_or_default:
 * @inventory: An inventory.
 * @name: Item name.
 *
 * Looks up an item in an inventory, eventually falling back to default.
 *
 * The lookup order is: item of requested name, default item (if set), any inventory item, %NULL (can happen only when
 * inventory is empty).
 *
 * Returns: Item called @name, or default item.
 **/
gpointer
gwy_inventory_get_item_or_default(GwyInventory *inventory,
                                  const gchar *name)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    guint i;
    if (name && (i = lookup(inventory, name)))
        return g_ptr_array_index(priv->items, i-1);
    if (priv->has_default && (i = lookup(inventory, priv->default_key->str)))
        return g_ptr_array_index(priv->items, i-1);
    if (priv->items->len)
        return g_ptr_array_index(priv->items, 0);
    return NULL;
}

/**
 * gwy_inventory_get_nth_item:
 * @inventory: An inventory.
 * @n: Item position.  It must be between zero and the number of items in inventory, inclusive.  If it is equal to the
 *     number of items, %NULL is returned.  In other words, inventory behaves like a %NULL-terminated array, you can
 *     simply iterate over it until gwy_inventory_get_nth_item() returns %NULL.
 *
 * Returns item on given position in an inventory.
 *
 * Returns: Item at given position.
 **/
gpointer
gwy_inventory_get_nth_item(GwyInventory *inventory,
                           guint n)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_val_if_fail(n <= priv->items->len, NULL);
    if (G_UNLIKELY(n == priv->items->len))
        return NULL;
    if (priv->ridx)
        n = g_array_index(priv->ridx, guint, n);

    return g_ptr_array_index(priv->items, n);
}

/**
 * gwy_inventory_get_item_position:
 * @inventory: An inventory.
 * @name: Item name.
 *
 * Finds position of an item in an inventory.
 *
 * Returns: Item position, or (guint)-1 if there is no such item.
 **/
guint
gwy_inventory_get_item_position(GwyInventory *inventory,
                                const gchar *name)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), -1);

    GwyInventoryPrivate *priv = inventory->priv;
    guint i;
    if (!(i = lookup(inventory, name)))
        return (guint)-1;

    if (!priv->idx)
        return i-1;

    if (priv->needs_reindex)
        reindex(inventory);

    return g_array_index(priv->idx, guint, i-1);
}

/**
 * gwy_inventory_reindex:
 * @inventory: An inventory.
 *
 * Updates @idx of @inventory to match @ridx.
 *
 * Note positions in hash are 1-based (to allow %NULL work as no-such-item),
 * but position in @items and @idx are 0-based.
 **/
static void
reindex(GwyInventory *inventory)
{
    GwyInventoryPrivate *priv = inventory->priv;
    g_return_if_fail(priv->ridx);

    for (guint i = 0; i < priv->items->len; i++) {
        guint n = g_array_index(priv->ridx, guint, i);
        g_array_index(priv->idx, guint, n) = i;
    }

    priv->needs_reindex = FALSE;
}

/**
 * gwy_inventory_foreach:
 * @inventory: An inventory.
 * @function: (scope call): A function to call on each item.  It must not modify @inventory.
 * @user_data: Data passed to @function.
 *
 * Calls a function on each item of an inventory, in order.
 *
 * @function's first argument is item position (transformed with
 * GUINT_TO_POINTER()), second is item pointer, and the last is @user_data.
 **/
void
gwy_inventory_foreach(GwyInventory *inventory,
                      GHFunc function,
                      gpointer user_data)
{
    g_return_if_fail(GWY_IS_INVENTORY(inventory));
    g_return_if_fail(function);

    GwyInventoryPrivate *priv = inventory->priv;
    guint n = priv->items->len;
    if (priv->ridx) {
        for (guint i = 0; i < n; i++) {
            guint j = g_array_index(priv->ridx, guint, i);
            gpointer item = g_ptr_array_index(priv->items, j);
            function(GUINT_TO_POINTER(i), item, user_data);
        }
    }
    else {
        for (guint i = 0; i < n; i++) {
            gpointer item = g_ptr_array_index(priv->items, i);
            function(GUINT_TO_POINTER(i), item, user_data);
        }
    }
}

/**
 * gwy_inventory_find:
 * @inventory: An inventory.
 * @predicate: (scope call): A function testing some item property.  It must not modify @inventory.
 * @user_data: Data passed to @predicate.
 *
 * Finds an inventory item using user-specified predicate function.
 *
 * @predicate is called for each item in @inventory (in order) until it returns %TRUE.  Its arguments are the same as
 * in gwy_inventory_foreach().
 *
 * Returns: The item for which @predicate returned %TRUE.  If there is no such item in the inventory, %NULL is
 *          returned.
 **/
gpointer
gwy_inventory_find(GwyInventory *inventory,
                   GHRFunc predicate,
                   gpointer user_data)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);
    g_return_val_if_fail(predicate, NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    guint n = priv->items->len;
    if (priv->ridx) {
        for (guint i = 0; i < n; i++) {
            guint j = g_array_index(priv->ridx, guint, i);
            gpointer item = g_ptr_array_index(priv->items, j);
            if (predicate(GUINT_TO_POINTER(i), item, user_data))
                return item;
        }
    }
    else {
        for (guint i = 0; i < n; i++) {
            gpointer item = g_ptr_array_index(priv->items, i);
            if (predicate(GUINT_TO_POINTER(i), item, user_data))
                return item;
        }
    }

    return NULL;
}

/**
 * gwy_inventory_get_default_item:
 * @inventory: An inventory.
 *
 * Returns the default item of an inventory.
 *
 * Returns: The default item.  If there is no default item, %NULL is returned.
 **/
gpointer
gwy_inventory_get_default_item(GwyInventory *inventory)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    if (!priv->has_default)
        return NULL;

    guint i;
    if ((i = lookup(inventory, priv->default_key->str)))
        return g_ptr_array_index(priv->items, i-1);
    else
        return NULL;
}

/**
 * gwy_inventory_get_default_item_name:
 * @inventory: An inventory.
 *
 * Returns the name of the default item of an inventory.
 *
 * Returns: The default item name, %NULL if no default name is set. Item of this name may or may not exist in the
 *          inventory.
 **/
const gchar*
gwy_inventory_get_default_item_name(GwyInventory *inventory)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    if (!priv->has_default)
        return NULL;

    return priv->default_key->str;
}

/**
 * gwy_inventory_set_default_item_name:
 * @inventory: An inventory.
 * @name: (nullable): Item name, pass %NULL to unset default item.
 *
 * Sets the default of an inventory.
 *
 * Item @name must already exist in the inventory.
 **/
void
gwy_inventory_set_default_item_name(GwyInventory *inventory,
                                    const gchar *name)
{
    g_return_if_fail(GWY_IS_INVENTORY(inventory));

    GwyInventoryPrivate *priv = inventory->priv;
    gboolean emit_change = FALSE;
    if (!name) {
        emit_change = priv->has_default;
        priv->has_default = FALSE;
    }
    else {
        if (!priv->has_default) {
            emit_change = TRUE;
            priv->has_default = TRUE;
        }

        if (!priv->default_key) {
            priv->default_key = g_string_new(name);
            emit_change = TRUE;
        }
        else {
            if (!gwy_strequal(priv->default_key->str, name)) {
                g_string_assign(priv->default_key, name);
                emit_change = TRUE;
            }
        }
    }

    if (emit_change)
        g_signal_emit(inventory, signals[SGNL_DEFAULT_CHANGED], 0);
}

/**
 * gwy_inventory_item_updated_real:
 * @inventory: An inventory.
 * @i: Storage position of updated item.
 *
 * Emits "item-updated" signal.
 **/
static void
item_updated_real(GwyInventory *inventory, guint i)
{
    GwyInventoryPrivate *priv = inventory->priv;

    if (!priv->idx) {
        g_signal_emit(inventory, signals[SGNL_ITEM_UPDATED], 0, i);
        return;
    }

    if (priv->needs_reindex)
        reindex(inventory);

    i = g_array_index(priv->idx, guint, i);
    g_signal_emit(inventory, signals[SGNL_ITEM_UPDATED], 0, i);
}

/**
 * gwy_inventory_item_updated:
 * @inventory: An inventory.
 * @name: Item name.
 *
 * Notifies inventory an item was updated.
 *
 * This function makes sense primarily for non-object items, as object items
 * can notify inventory via signals.
 **/
void
gwy_inventory_item_updated(GwyInventory *inventory,
                           const gchar *name)
{
    g_return_if_fail(GWY_IS_INVENTORY(inventory));

    guint i;
    if (!(i = lookup(inventory, name)))
        g_warning("Item `%s' does not exist", name);
    else
        item_updated_real(inventory, i-1);
}

/**
 * gwy_inventory_nth_item_updated:
 * @inventory: An inventory.
 * @n: Item position.
 *
 * Notifies inventory item on given position was updated.
 *
 * This function makes sense primarily for non-object items, as object items can provide @watchable_signal.
 **/
void
gwy_inventory_nth_item_updated(GwyInventory *inventory,
                               guint n)
{
    g_return_if_fail(GWY_IS_INVENTORY(inventory));
    g_return_if_fail(n < inventory->priv->items->len);

    g_signal_emit(inventory, signals[SGNL_ITEM_UPDATED], 0, n);
}

/**
 * gwy_inventory_item_changed:
 * @inventory: An inventory.
 * @item: An item that has changed.
 *
 * Handles inventory item `changed' signal.
 **/
static void
item_changed(GwyInventory *inventory, gpointer item)
{
    const gchar *name = inventory->priv->item_type.get_name(item);
    guint i = lookup(inventory, name);
    g_assert(i);
    item_updated_real(inventory, i-1);
}

/**
 * gwy_inventory_insert_item:
 * @inventory: An inventory.
 * @item: An item to insert.
 *
 * Inserts an item into an inventory.
 *
 * Item of the same name must not exist yet.
 *
 * If the inventory is sorted, item is inserted to keep order.  If the
 * inventory is unsorted, item is simply added to the end.
 *
 * Returns: @item, for convenience.
 **/
gpointer
gwy_inventory_insert_item(GwyInventory *inventory,
                          gpointer item)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);
    g_return_val_if_fail(item, NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_val_if_fail(!priv->is_const, NULL);

    const gchar *name = priv->item_type.get_name(item);
    if (lookup(inventory, name)) {
        g_warning("Item `%s' already exists", name);
        return NULL;
    }

    if (priv->is_object)
        g_object_ref(item);

    /* Insert into index array */
    guint m;
    if (priv->is_sorted && priv->items->len) {
        guint j0 = 0;
        guint j1 = priv->items->len - 1;
        gpointer mp;
        while (j1 - j0 > 1) {
            m = (j0 + j1 + 1)/2;
            mp = g_ptr_array_index(priv->items, g_array_index(priv->ridx, guint, m));
            if (priv->item_type.compare(item, mp) >= 0)
                j0 = m;
            else
                j1 = m;
        }

        mp = g_ptr_array_index(priv->items, g_array_index(priv->ridx, guint, j0));
        if (priv->item_type.compare(item, mp) < 0)
            m = j0;
        else {
            mp = g_ptr_array_index(priv->items, g_array_index(priv->ridx, guint, j1));
            if (priv->item_type.compare(item, mp) < 0)
                m = j1;
            else
                m = j1+1;
        }

        g_array_insert_val(priv->ridx, m, priv->items->len);
        priv->needs_reindex = TRUE;
    }
    else {
        m = priv->items->len;
        g_array_append_val(priv->ridx, priv->items->len);
    }

    g_array_append_val(priv->idx, m);
    g_ptr_array_add(priv->items, item);
    g_hash_table_insert(priv->hash, (gpointer)name, GUINT_TO_POINTER(priv->items->len));

    if (priv->is_watchable)
        connect_to_item(item, inventory);

    g_signal_emit(inventory, signals[SGNL_ITEM_INSERTED], 0, m);
    if (priv->has_default && gwy_strequal(name, priv->default_key->str))
        g_signal_emit(inventory, signals[SGNL_DEFAULT_CHANGED], 0);

    return item;
}

/**
 * gwy_inventory_insert_nth_item:
 * @inventory: An inventory.
 * @item: An item to insert.
 * @n: Position to insert @item to.
 *
 * Inserts an item to an explicit position in an inventory.
 *
 * Item of the same name must not exist yet.
 *
 * If an item is inserted into a position where it does not belong according to the comparison function, the inventory
 * becomes unsorted as if gwy_inventory_forget_order() was called.
 *
 * Returns: @item, for convenience.
 **/
gpointer
gwy_inventory_insert_nth_item(GwyInventory *inventory,
                              gpointer item,
                              guint n)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);
    g_return_val_if_fail(item, NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_val_if_fail(!priv->is_const, NULL);
    g_return_val_if_fail(n <= priv->items->len, NULL);

    const gchar *name = priv->item_type.get_name(item);
    if (lookup(inventory, name)) {
        g_warning("Item `%s' already exists", name);
        return NULL;
    }

    if (priv->is_object)
        g_object_ref(item);

    g_array_insert_val(priv->ridx, n, priv->items->len);
    priv->needs_reindex = TRUE;

    g_array_append_val(priv->idx, n);    /* value does not matter */
    g_ptr_array_add(priv->items, item);
    g_hash_table_insert(priv->hash, (gpointer)name, GUINT_TO_POINTER(priv->items->len));

    if (priv->is_sorted) {
        gpointer mp;

        if (n > 0) {
            mp = g_ptr_array_index(priv->items, g_array_index(priv->ridx, guint, n-1));
            if (priv->item_type.compare(item, mp) < 0)
                priv->is_sorted = FALSE;
        }
        if (priv->is_sorted && n+1 < priv->items->len) {
            mp = g_ptr_array_index(priv->items, g_array_index(priv->ridx, guint, n+1));
            if (priv->item_type.compare(item, mp) > 0)
                priv->is_sorted = FALSE;
        }
    }

    if (priv->is_watchable)
        connect_to_item(item, inventory);

    g_signal_emit(inventory, signals[SGNL_ITEM_INSERTED], 0, n);
    if (priv->has_default && gwy_strequal(name, priv->default_key->str))
        g_signal_emit(inventory, signals[SGNL_DEFAULT_CHANGED], 0);

    return item;
}

/**
 * gwy_inventory_restore_order:
 * @inventory: An inventory.
 *
 * Assures an inventory is sorted.
 **/
void
gwy_inventory_restore_order(GwyInventory *inventory)
{
    g_return_if_fail(GWY_IS_INVENTORY(inventory));

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_if_fail(!priv->is_const);
    if (priv->is_sorted || !priv->item_type.compare)
        return;

    /* Make sure old order is remembered in @idx */
    if (priv->needs_reindex)
        reindex(inventory);
    g_array_sort_with_data(priv->ridx, (GCompareDataFunc)compare_indices, inventory);

    gint *new_order = g_new(gint, priv->items->len);

    /* Fill new_order with indices: new_order[new_position] = old_position */
    for (guint i = 0; i < priv->ridx->len; i++)
        new_order[i] = g_array_index(priv->idx, guint, g_array_index(priv->ridx, guint, i));
    priv->needs_reindex = TRUE;
    priv->is_sorted = TRUE;

    g_signal_emit(inventory, signals[SGNL_ITEMS_REORDERED], 0, new_order);
    g_free(new_order);
}

/**
 * gwy_inventory_forget_order:
 * @inventory: An inventory.
 *
 * Forces an inventory to be unsorted.
 *
 * The item positions do not change, but future gwy_inventory_insert_item() will not try to insert items in order.
 **/
void
gwy_inventory_forget_order(GwyInventory *inventory)
{
    g_return_if_fail(GWY_IS_INVENTORY(inventory));
    GwyInventoryPrivate *priv = inventory->priv;
    g_return_if_fail(!priv->is_const);
    priv->is_sorted = FALSE;
}

static gint
compare_indices(gint *a, gint *b, GwyInventory *inventory)
{
    GwyInventoryPrivate *priv = inventory->priv;
    gpointer pa = g_ptr_array_index(priv->items, *a), pb = g_ptr_array_index(priv->items, *b);
    return priv->item_type.compare(pa, pb);
}

/**
 * gwy_inventory_delete_nth_item_real:
 * @inventory: An inventory.
 * @name: Item name (to avoid double lookups from gwy_inventory_delete_item()).
 * @i: Storage position of item to remove.
 *
 * Removes an item from an inventory given its physical position.
 *
 * A kind of g_array_remove_index_fast(), but updating references.
 **/
static void
delete_nth_item_real(GwyInventory *inventory,
                     const gchar *name,
                     guint i)
{
    gboolean emit_change = FALSE;

    GwyInventoryPrivate *priv = inventory->priv;
    gpointer mp = g_ptr_array_index(priv->items, i);
    if (priv->item_type.is_fixed && priv->item_type.is_fixed(mp)) {
        g_warning("Cannot delete fixed item `%s'", name);
        return;
    }
    if (priv->has_default && gwy_strequal(name, priv->default_key->str))
        emit_change = TRUE;

    if (priv->is_watchable)
        disconnect_from_item(mp, inventory);

    if (priv->needs_reindex)
        reindex(inventory);

    guint n = g_array_index(priv->idx, guint, i);
    guint last = priv->items->len - 1;

    /* Move last item of @items to position of removed item */
    if (priv->hash)
        g_hash_table_remove(priv->hash, name);

    if (priv->item_type.dismantle)
        priv->item_type.dismantle(mp);

    if (i < last) {
        gpointer lp = g_ptr_array_index(priv->items, last);
        g_ptr_array_index(priv->items, i) = lp;
        name = priv->item_type.get_name(lp);
        if (priv->hash)
            g_hash_table_insert(priv->hash, (gpointer)name, GUINT_TO_POINTER(i+1));
        g_array_index(priv->ridx, guint, g_array_index(priv->idx, guint, last)) = i;
    }
    g_array_remove_index(priv->ridx, n);
    g_ptr_array_set_size(priv->items, last);
    g_array_set_size(priv->idx, last);
    priv->needs_reindex = TRUE;

    if (priv->is_object)
        g_object_unref(mp);

    g_signal_emit(inventory, signals[SGNL_ITEM_DELETED], 0, n);
    if (emit_change)
        g_signal_emit(inventory, signals[SGNL_DEFAULT_CHANGED], 0);
}

/**
 * gwy_inventory_delete_item:
 * @inventory: An inventory.
 * @name: Name of item to delete.
 *
 * Deletes an item from an inventory.
 *
 * Returns: %TRUE if item was deleted.
 **/
gboolean
gwy_inventory_delete_item(GwyInventory *inventory,
                          const gchar *name)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), FALSE);

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_val_if_fail(!priv->is_const, FALSE);

    guint i;
    if (!(i = lookup(inventory, name))) {
        g_warning("Item `%s' does not exist", name);
        return FALSE;
    }

    delete_nth_item_real(inventory, name, i-1);

    return TRUE;
}

/**
 * gwy_inventory_delete_nth_item:
 * @inventory: An inventory.
 * @n: Position of @item to delete.
 *
 * Deletes an item on given position from an inventory.
 *
 * Returns: %TRUE if item was deleted.
 **/
gboolean
gwy_inventory_delete_nth_item(GwyInventory *inventory,
                              guint n)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), FALSE);

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_val_if_fail(!priv->is_const, FALSE);
    g_return_val_if_fail(n < priv->items->len, FALSE);

    guint i = g_array_index(priv->ridx, guint, n);
    const gchar *name = priv->item_type.get_name(g_ptr_array_index(priv->items, i));
    delete_nth_item_real(inventory, name, i);

    return TRUE;
}

/**
 * gwy_inventory_rename_item:
 * @inventory: An inventory.
 * @name: Name of item to rename.
 * @newname: New name of item.
 *
 * Renames an inventory item.
 *
 * If an item of name @newname is already present in @inventory, the rename will fail.
 *
 * Returns: The item, for convenience.
 **/
gpointer
gwy_inventory_rename_item(GwyInventory *inventory,
                          const gchar *name,
                          const gchar *newname)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);
    g_return_val_if_fail(newname, NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_val_if_fail(!priv->is_const, NULL);
    g_return_val_if_fail(priv->item_type.rename, NULL);

    guint i;
    if (!(i = lookup(inventory, name))) {
        g_warning("Item `%s' does not exist", name);
        return NULL;
    }
    gpointer mp = g_ptr_array_index(priv->items, i-1);
    if (priv->item_type.is_fixed && priv->item_type.is_fixed(mp)) {
        g_warning("Cannot rename fixed item `%s'", name);
        return NULL;
    }
    if (gwy_strequal(name, newname))
        return mp;

    if (lookup(inventory, newname)) {
        g_warning("Item `%s' already exists", newname);
        return NULL;
    }

    g_hash_table_remove(priv->hash, name);
    priv->item_type.rename(mp, newname);
    /* The item may refuse the new name (because of weird characters, for instance). It may respond by renaming itself
     * to a modified newname. So fetch the actual new name. */
    newname = priv->item_type.get_name(mp);
    g_hash_table_insert(priv->hash, (gpointer)newname, GUINT_TO_POINTER(i));

    if (priv->needs_reindex)
        reindex(inventory);
    if (priv->is_sorted) {
        priv->is_sorted = FALSE;
        gwy_inventory_restore_order(inventory);
        if (priv->needs_reindex)
            reindex(inventory);
        i = lookup(inventory, newname);
    }

    g_signal_emit(inventory, signals[SGNL_ITEM_UPDATED], 0, g_array_index(priv->idx, guint, i-1));
    if (priv->has_default
        && (gwy_strequal(name, priv->default_key->str) || gwy_strequal(newname, priv->default_key->str)))
        g_signal_emit(inventory, signals[SGNL_DEFAULT_CHANGED], 0);

    return mp;
}

/**
 * gwy_inventory_new_item:
 * @inventory: An inventory.
 * @name: (nullable):
 *        Name of item to duplicate, may be %NULL to use default item (the same happens when @name does not exist).
 * @newname: (nullable):
 *           Name of new item, it must not exist yet.  It may be %NULL, the new name is based on @name then.
 *
 * Creates a new item as a copy of existing one and inserts it to inventory.
 *
 * The newly created item can be called differently than @newname if that already exists.
 *
 * Returns: The newly added item.
 **/
gpointer
gwy_inventory_new_item(GwyInventory *inventory,
                       const gchar *name,
                       const gchar *newname)
{
    g_return_val_if_fail(GWY_IS_INVENTORY(inventory), NULL);

    GwyInventoryPrivate *priv = inventory->priv;
    g_return_val_if_fail(!priv->is_const, NULL);
    g_return_val_if_fail(priv->can_make_copies, NULL);

    /* Find which item we should base copy on */
    if (!name && priv->has_default)
        name = priv->default_key->str;

    guint i = 0;
    if ((!name || !(i = lookup(inventory, name))) && priv->items->len)
        i = 1;

    gpointer item = NULL;
    gchar *freeme = NULL;
    if (i) {
        item = g_ptr_array_index(priv->items, i-1);
        name = priv->item_type.get_name(item);
    }

    if (!name || !item) {
        g_warning("No default item to base new item on");
        return NULL;
    }

    /* Find new name */
    if (!newname)
        newname = freeme = invent_name(inventory, name);
    else if (lookup(inventory, newname))
        newname = freeme = invent_name(inventory, newname);

    /* Create new item */
    item = priv->item_type.copy(item);
    priv->item_type.rename(item, newname);
    gwy_inventory_insert_item(inventory, item);
    g_free(freeme);

    return item;
}

/**
 * gwy_inventory_invent_name:
 * @inventory: An inventory.
 * @prefix: Name prefix.
 *
 * Finds a name of form "prefix number" that does not identify any item in an inventory yet.
 *
 * Returns: The invented name as a string that is owned by this function and valid only until next call to it.
 **/
static gchar*
invent_name(GwyInventory *inventory, const gchar *prefix)
{
    GString *str = g_string_new(prefix ? prefix : _("Untitled"));
    if (!lookup(inventory, str->str))
        return g_string_free(str, FALSE);

    const gchar *p, *last = str->str + MAX(str->len-1, 0);
    for (p = last; p >= str->str; p--) {
        if (!g_ascii_isdigit(*p))
            break;
    }
    if (p == last || (p >= str->str && !g_ascii_isspace(*p)))
        p = last;
    while (p >= str->str && g_ascii_isspace(*p))
        p--;
    g_string_truncate(str, p+1 - str->str);

    g_string_append_c(str, ' ');
    guint n = str->len;
    for (guint i = 1; i < 10000; i++) {
        g_string_append_printf(str, "%d", i);
        if (!lookup(inventory, str->str))
            return g_string_free(str, FALSE);

        g_string_truncate(str, n);
    }
    g_string_free(str, TRUE);
    g_assert_not_reached();
    return NULL;
}

/**
 * SECTION: inventory
 * @title: GwyInventory
 * @short_description: Ordered item inventory, indexed by both name and position.
 * @see_also: #GwyDict
 *
 * #GwyInventory is a uniform container that offers both hash table and array (sorted or unsorted) interfaces.  Both
 * types of read access are fast, operations that modify it may be slower.  Inventory can also maintain a notion of
 * default item.
 *
 * #GwyInventory can be used both as an actual container for some data, or just wrap a static array with a the same
 * interface so the actual storage is opaque to inventory user.  The former kind of inventories can be created with
 * gwy_inventory_new() or gwy_inventory_new_filled(); constant inventory is created with
 * gwy_inventory_new_from_array().  Contantess of an inventory can be tested with gwy_inventory_is_const().
 *
 * Possible operations with data items stored in an inventory are specified upon inventory creation with
 * #GwyInventoryItemType structure.  Not all fields are mandatory, with items allowing more operations the inventory
 * is more capable too.  For example, if items offer a method to make copies, gwy_inventory_new_item() can be used to
 * directly create new items in the inventory (this capability can be tested with gwy_inventory_can_make_copies()).
 *
 * Item can have `traits', that is data that can be obtained generically. They are similar to #GObject properties.
 * Actually, if items are objects, they should simply map object properties to traits.  But it is possible to define
 * traits for simple structures too.
 **/

/**
 * GwyInventoryItemType:
 * @type: Object type, if item is object or other type with registered GType.
 *        May be zero to indicate an unregistered item type.
 *        If items are objects, inventory takes a reference on them.
 * @watchable_signal: Item signal name to watch, used only for objects.
 *                    When item emits this signal, inventory emits "item-updated" signal for it.
 *                    May be %NULL to indicate no signal should be watched, you can still emit "item-updated" with
 *                    gwy_inventory_item_updated() or gwy_inventory_nth_item_updated().
 * @is_fixed: If not %NULL and returns %TRUE for some item, such an item cannot be removed from inventory, fixed items
 *            can be only added.  This is checked each time an attempt is made to remove an item.
 * @get_name: Returns item name (the string is owned by item and it is assumed to exist until item ceases to exist or
 *            is renamed). This function is obligatory.
 * @compare: Item comparation function for sorting.
 *           If %NULL, inventory never attempts to keep any item order and gwy_inventory_restore_order() does nothing.
 *           Otherwise inventory is sorted unless sorting is (temporarily) disabled with gwy_inventory_forget_order()
 *           or it was created with gwy_inventory_new_filled() and the initial array was not sorted according to
 *           @compare.
 * @rename: Function to rename an item.  If not %NULL, calls to gwy_inventory_rename_item() are possible.  Note items
 *          must not be renamed by any other means than this method, because when an item is renamed and inventory
 *          does not know it, very bad things will happen and you will lose all your good karma.
 * @dismantle: Called on item before it's removed from inventory.  May be %NULL.
 * @copy: Method to create a copy of an item.  If this function and @rename are defined, calls to
 *        gwy_inventory_new_item() are possible.  Inventory sets the copy's name immediately after creation, so it
 *        normally does not matter which name @copy gives it.
 * @get_traits: Function to get item traits.  It returns array of item trait #GTypes (keeping its ownership) and if
 *              @nitems is not %NULL, it stores the length of returned array there.
 * @get_trait_name: Returns name of @i-th trait (keeping ownership of the returned string).  It is not obligatory, but
 *                  advisable to give traits names.
 * @get_trait_value: Sets @value to value of @i-th trait of item.
 *
 * Infromation about a #GwyInventory item type.
 *
 * Note only one of the fields must be always defined: @get_name.  All the others give inventory (and thus inventory
 * users) some additional powers over items.  They may be set to %NULL or 0 if particular item type does not (want to)
 * support this operation.
 *
 * The three trait methods are not used by #GwyInventory itself, but allows #GwyInventoryStore to generically map item
 * properties to virtual columns of a #GtkTreeModel.  If items are objects, you will usually want to directly map some
 * or all #GObject properties to item traits.  If they are plain C structs or something else, you can easily export
 * their data members as virtual #GtkTreeModel columns by defining traits for them.
 **/

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