/*
 * SwamiguiTreeStore.c - Swami item tree store object
 *
 * Swami
 * Copyright (C) 1999-2014 Element Green <element@elementsofsound.org>
 *
 * 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; version 2
 * of the License only.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA or point your web browser to http://www.gnu.org.
 */
#include <stdio.h>
#include <string.h>
#include <gtk/gtk.h>

#include <libswami/libswami.h>

#include "SwamiguiTree.h"
#include "SwamiguiRoot.h"
#include "icons.h"
#include "i18n.h"


/* Local Prototypes */

static void swamigui_tree_store_class_init(SwamiguiTreeStoreClass *klass);
static void swamigui_tree_store_init(SwamiguiTreeStore *store);
static void swamigui_tree_store_finalize(GObject *object);
static inline void swamigui_tree_finish_insert(SwamiguiTreeStore *store,
        GObject *item,
        const char *label,
        char *icon,
        GtkTreeIter *iter);
static void tree_store_recursive_remove(SwamiguiTreeStore *store,
                                        GtkTreeIter *iter);

static GObjectClass *parent_class = NULL;

/* a cache of icon names (conserve memory, no need to store for every item) */
static GHashTable *icon_name_cache = NULL;

/*--- Initialization / deinitialization -------------------------------------*/

/***
 * Initialization
 */
void _swamigui_tree_store_init(void)
{
    /* a cache of icon names */
    icon_name_cache = g_hash_table_new_full(NULL, NULL,
                                            (GDestroyNotify)g_free, NULL);
}

/***
 * Deinitialization: free memory
 */
void _swamigui_tree_store_deinit(void)
{
    g_hash_table_destroy(icon_name_cache);
}

/*----- SwamiguiTreeStore object functions ----------------------------------*/

GType
swamigui_tree_store_get_type(void)
{
    static GType obj_type = 0;

    if(!obj_type)
    {
        static const GTypeInfo obj_info =
        {
            sizeof(SwamiguiTreeStoreClass), NULL, NULL,
            (GClassInitFunc) swamigui_tree_store_class_init, NULL, NULL,
            sizeof(SwamiguiTreeStore), 0,
            (GInstanceInitFunc) swamigui_tree_store_init,
        };

        obj_type = g_type_register_static(GTK_TYPE_TREE_STORE, "SwamiguiTreeStore",
                                          &obj_info, G_TYPE_FLAG_ABSTRACT);
    }

    return (obj_type);
}

static void
swamigui_tree_store_class_init(SwamiguiTreeStoreClass *klass)
{
    GObjectClass *obj_class = G_OBJECT_CLASS(klass);

    parent_class = g_type_class_peek_parent(klass);

    obj_class->finalize = swamigui_tree_store_finalize;

    icon_name_cache = g_hash_table_new_full(NULL, NULL,
                                            (GDestroyNotify)g_free, NULL);
}

static void
swamigui_tree_store_init(SwamiguiTreeStore *store)
{
    GType types[SWAMIGUI_TREE_STORE_NUM_COLUMNS] =
    { G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_OBJECT };

    /* we use pointer type for icon since its a static string and we don't want
       it getting duplicated */

    /* set up the tree store */
    gtk_tree_store_set_column_types(GTK_TREE_STORE(store),
                                    SWAMIGUI_TREE_STORE_NUM_COLUMNS, types);

    /* create a item->tree_node hash with a destroy notify on the item key to
       unref it, and a destroy on the tree iterator to free it */
    store->item_hash =
        g_hash_table_new_full(NULL, NULL,
                              (GDestroyNotify)g_object_unref,
                              (GDestroyNotify)gtk_tree_iter_free);
}

static void
swamigui_tree_store_finalize(GObject *object)
{
    SwamiguiTreeStore *store = SWAMIGUI_TREE_STORE(object);

    g_hash_table_destroy(store->item_hash);

    if(parent_class->finalize)
    {
        parent_class->finalize(object);
    }
}

/**
 * swamigui_tree_store_insert:
 * @store: Swami tree store to insert tree node into
 * @item: Object to link to tree node or %NULL
 * @label: Label for node or %NULL to try and obtain it other ways.
 * @icon: Stock ID of icon (%NULL to use "icon" type property or category icon)
 * @parent: Pointer to parent tree node or %NULL if inserting a toplevel node
 * @pos: Position to insert new node at (0 = prepend, > sibling list to append)
 * @out_iter: Pointer to a user supplied GtkTreeIter structure to store the
 *   new node in or %NULL to ignore.
 *
 * Creates a new tree node and inserts it at the position given by
 * @parent and @pos, returning the created node in @out_iter (if
 * supplied). @item is an object to link with the created node.
 * It is also used if either @label or @icons is %NULL in which case
 * it obtains the information via other methods.
 */
void
swamigui_tree_store_insert(SwamiguiTreeStore *store, GObject *item,
                           const char *label, char *icon,
                           GtkTreeIter *parent, int pos, GtkTreeIter *out_iter)
{
    GtkTreeIter iter;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(!item || G_IS_OBJECT(item));

    gtk_tree_store_insert(GTK_TREE_STORE(store), &iter, parent, pos);
    swamigui_tree_finish_insert(store, item, label, icon, &iter);

    if(out_iter)
    {
        *out_iter = iter;
    }
}

/**
 * swamigui_tree_store_insert_before:
 * @store: Swami tree store to insert tree node into
 * @item: Object to link to tree node or %NULL
 * @label: Label for node or %NULL to try and obtain it other ways.
 * @icon: Stock ID of icon (%NULL to use "icon" type property or category icon)
 * @parent: Pointer to parent tree node or %NULL if inserting a toplevel node
 * @sibling: A sibling node to insert the new node before or %NULL to append
 * @out_iter: Pointer to a user supplied GtkTreeIter structure to store the
 *   new node in or %NULL to ignore.
 *
 * Like swamigui_tree_store_insert() but inserts the node before the
 * specified @sibling instead.
 */
void
swamigui_tree_store_insert_before(SwamiguiTreeStore *store, GObject *item,
                                  const char *label, char *icon,
                                  GtkTreeIter *parent, GtkTreeIter *sibling,
                                  GtkTreeIter *out_iter)
{
    GtkTreeIter iter;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(!item || G_IS_OBJECT(item));

    gtk_tree_store_insert_before(GTK_TREE_STORE(store), &iter,
                                 parent, sibling);
    swamigui_tree_finish_insert(store, item, label, icon, &iter);

    if(out_iter)
    {
        *out_iter = iter;
    }
}

/**
 * swamigui_tree_store_insert_after:
 * @store: Swami tree store to insert tree node into
 * @item: Object to link to tree node or %NULL
 * @label: Label for node or %NULL to try and obtain it other ways.
 * @icon: Stock ID of icon (%NULL to use "icon" type property or category icon)
 * @parent: Pointer to parent tree node or %NULL if inserting a toplevel node
 * @sibling: A sibling node to insert the new node after or %NULL to prepend
 * @out_iter: Pointer to a user supplied GtkTreeIter structure to store the
 *   new node in or %NULL to ignore.
 *
 * Like swamigui_tree_store_insert() but inserts the node after the
 * specified @sibling instead.
 */
void
swamigui_tree_store_insert_after(SwamiguiTreeStore *store, GObject *item,
                                 const char *label, char *icon,
                                 GtkTreeIter *parent, GtkTreeIter *sibling,
                                 GtkTreeIter *out_iter)
{
    GtkTreeIter iter;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(!item || G_IS_OBJECT(item));

    gtk_tree_store_insert_after(GTK_TREE_STORE(store), &iter, parent, sibling);
    swamigui_tree_finish_insert(store, item, label, icon, &iter);

    if(out_iter)
    {
        *out_iter = iter;
    }
}

static inline void
swamigui_tree_finish_insert(SwamiguiTreeStore *store, GObject *item,
                            const char *label, char *icon,
                            GtkTreeIter *iter)
{
    char *item_label = NULL;
    char *item_icon = NULL;
    GtkTreeIter *copy;
    gint category;

    if(!label)
    {
        if(IPATCH_IS_ITEM(item))
        {
            g_object_get(item, "title", &item_label, NULL);
        }
        else
        {
            swami_object_get(item, "name", &item_label, NULL);
        }

        if(!item_label)
        {
            ipatch_type_object_get(item, "name", &item_label, NULL);
        }

        if(!item_label)
        {
            item_label = g_strdup(_("Untitled"));
        }

        label = item_label;
    }

    if(!icon)
    {
        /* get item icon from type property */
        ipatch_type_object_get(G_OBJECT(item), "icon", &item_icon,
                               "category", &category,
                               NULL);

        if(item_icon)
        {
            /* lookup icon name in hash (no need to store name for every icon) */
            icon = g_hash_table_lookup(icon_name_cache, item_icon);

            if(!icon)
            {
                icon = g_strdup(item_icon);
                g_hash_table_insert(icon_name_cache, icon, icon);
            }

            g_free(item_icon);
        }

        /* if no type icon get category icon, gets default if category not set */
        if(!icon)
        {
            icon = swamigui_icon_get_category_icon(category);
        }
    }

    gtk_tree_store_set(GTK_TREE_STORE(store), iter,
                       SWAMIGUI_TREE_STORE_LABEL_COLUMN, label,
                       SWAMIGUI_TREE_STORE_ICON_COLUMN, icon,
                       SWAMIGUI_TREE_STORE_OBJECT_COLUMN, item,
                       -1);

    if(item)
    {
        copy = gtk_tree_iter_copy(iter);
        g_object_ref(item);		/* ++ ref item for item_hash */
        g_hash_table_insert(store->item_hash, item, copy);
    }

    if(item_label)
    {
        g_free(item_label);
    }
}

/**
 * swamigui_tree_store_change:
 * @store: Swami tree store
 * @item: Object in @store to update
 * @label: New label or %NULL to keep old
 * @icon: GdkPixbuf icon or %NULL to keep old
 *
 * Changes a row in a Swami tree store.
 */
void
swamigui_tree_store_change(SwamiguiTreeStore *store, GObject *item,
                           const char *label, char *icon)
{
    GtkTreeIter iter;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(G_IS_OBJECT(item));

    if(!swamigui_tree_store_item_get_node(store, item, &iter))
    {
        return;
    }

    if(label)
        gtk_tree_store_set(GTK_TREE_STORE(store), &iter,
                           SWAMIGUI_TREE_STORE_LABEL_COLUMN, label, -1);

    if(icon)
        gtk_tree_store_set(GTK_TREE_STORE(store), &iter,
                           SWAMIGUI_TREE_STORE_ICON_COLUMN, icon, -1);
}

/**
 * swamigui_tree_store_remove:
 * @store: Swami tree store
 * @item: Object in @store to remove
 *
 * Removes a node (and all its children) from a Swami tree store.
 */
void
swamigui_tree_store_remove(SwamiguiTreeStore *store, GObject *item)
{
    GtkTreeIter iter;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(G_IS_OBJECT(item));

    if(swamigui_tree_store_item_get_node(store, item, &iter))
    {
        if(gtk_tree_model_iter_has_child(GTK_TREE_MODEL(store), &iter))
        {
            tree_store_recursive_remove(store, &iter);
        }
        else
        {
            // !! Remove item from hash before GtkTree to prevent callbacks thinking item still exists
            g_hash_table_remove(store->item_hash, item);
            gtk_tree_store_remove(GTK_TREE_STORE(store), &iter);
        }
    }
}

/* recursively remove tree nodes from a Swami tree store and remove the
   item->node links in item_hash table */
static void
tree_store_recursive_remove(SwamiguiTreeStore *store, GtkTreeIter *iter)
{
    GtkTreeIter children, remove;
    gboolean retval;
    register GObject *item; /* register to conserve recursive stack */

    if(gtk_tree_model_iter_children((GtkTreeModel *)store, &children, iter))
    {
        do
        {
            remove = children;
            retval = gtk_tree_model_iter_next((GtkTreeModel *)store, &children);
            tree_store_recursive_remove(store, &remove);
        }
        while(retval);
    }

    // !! Remove item from hash before GtkTree to prevent callbacks thinking item still exists
    item = swamigui_tree_store_node_get_item(store, iter);
    g_hash_table_remove(store->item_hash, item);

    gtk_tree_store_remove((GtkTreeStore *)store, iter);
}

/**
 * swamigui_tree_store_move_before:
 * @store: Swami tree store
 * @item: Item to move
 * @position: Location to move item before or %NULL for last position
 *
 * Move an item from its current location to before @position.  @position
 * must be at the same level as @item in the tree.
 */
void
swamigui_tree_store_move_before(SwamiguiTreeStore *store, GObject *item,
                                GtkTreeIter *position)
{
    GtkTreeIter iter;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(G_IS_OBJECT(item));

    if(swamigui_tree_store_item_get_node(store, item, &iter))
    {
        gtk_tree_store_move_before(GTK_TREE_STORE(store), &iter, position);
    }
}

/**
 * swamigui_tree_store_move_after:
 * @store: Swami tree store
 * @item: Item to move
 * @position: Location to move item after or %NULL for first position
 *
 * Move an item from its current location to after @position.  @position
 * must be at the same level as @item in the tree.
 */
void
swamigui_tree_store_move_after(SwamiguiTreeStore *store, GObject *item,
                               GtkTreeIter *position)
{
    GtkTreeIter iter;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(G_IS_OBJECT(item));

    if(swamigui_tree_store_item_get_node(store, item, &iter))
    {
        gtk_tree_store_move_after(GTK_TREE_STORE(store), &iter, position);
    }
}

/**
 * swamigui_tree_store_item_get_node:
 * @store: Swami tree store
 * @item: Item that is in @store
 * @iter: Pointer to a GtkTreeIter structure to store the linked tree node
 *
 * Gets a tree node for @item in @store. The tree node is stored in @iter which
 * should be a pointer to a user supplied GtkTreeIter structure.
 *
 * Returns: %TRUE if @iter was set (@item has a linked node in @store), %FALSE
 *   otherwise
 */
gboolean
swamigui_tree_store_item_get_node(SwamiguiTreeStore *store, GObject *item,
                                  GtkTreeIter *iter)
{
    GtkTreeIter *lookup_iter;

    g_return_val_if_fail(SWAMIGUI_IS_TREE_STORE(store), FALSE);
    g_return_val_if_fail(G_IS_OBJECT(item), FALSE);

    lookup_iter = g_hash_table_lookup(store->item_hash, item);

    if(!lookup_iter)
    {
        return (FALSE);
    }

    if(iter)
    {
        *iter = *lookup_iter;
    }

    return (TRUE);
}

/**
 * swamigui_tree_store_node_get_item:
 * @store: Swami tree store
 * @iter: Node in @store to find linked item of
 *
 * Gets the linked item object for @iter node in @store. The returned item
 * is not referenced but can be safely used as long as the tree model isn't
 * changed (possibly causing item to be destroyed). The item should be ref'd
 * if used for extended periods.
 *
 * Returns: The item object linked with @iter node or %NULL if not found.
 *   Item has NOT been referenced, see note above.
 */
GObject *
swamigui_tree_store_node_get_item(SwamiguiTreeStore *store, GtkTreeIter *iter)
{
    GObject *item;

    g_return_val_if_fail(SWAMIGUI_IS_TREE_STORE(store), NULL);
    g_return_val_if_fail(iter != NULL, NULL);

    gtk_tree_model_get(GTK_TREE_MODEL(store), iter,
                       SWAMIGUI_TREE_STORE_OBJECT_COLUMN, &item,         // ++ ref item
                       -1);

    if(item)
    {
        g_object_unref(item);    // -- unref item
    }

    return (item);
}

/**
 * swamigui_tree_store_add:
 * @store: Swami tree store
 * @item: Item to add
 *
 * Add an item to a tree store using the item_add class method (specific to
 * tree store types).
 */
void
swamigui_tree_store_add(SwamiguiTreeStore *store, GObject *item)
{
    SwamiguiTreeStoreClass *klass;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(G_IS_OBJECT(item));

    klass = SWAMIGUI_TREE_STORE_GET_CLASS(store);
    g_return_if_fail(klass->item_add != NULL);

    klass->item_add(store, item);
}

/**
 * swamigui_tree_store_changed:
 * @store: Swami tree store
 * @item: Item that changed
 *
 * This function updates the title and sorting of an item that changed using
 * the item_changed class method (specific to tree store types).
 * Note that re-ordering of the changed item may occur in
 * a delayed fashion, to prevent delays while user is typing a name for example.
 */
void
swamigui_tree_store_changed(SwamiguiTreeStore *store, GObject *item)
{
    SwamiguiTreeStoreClass *klass;

    g_return_if_fail(SWAMIGUI_IS_TREE_STORE(store));
    g_return_if_fail(G_IS_OBJECT(item));

    klass = SWAMIGUI_TREE_STORE_GET_CLASS(store);
    g_return_if_fail(klass->item_changed != NULL);

    klass->item_changed(store, item);
}
