/*
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * See the COPYING file for license information.
 *
 * Guillaume Chazarain <guichaz@gmail.com>
 */

/************************************************************
 * The filenames tree, shown in images menus and serialized *
 ************************************************************/

#include <string.h>             /* strchr(), strrchr(), strlen() */

#include "gliv.h"
#include "tree.h"
#include "str_utils.h"
#include "files_list.h"
#include "timestamp.h"

/* Last made tree. */
static GNode *last_tree = NULL;

/* Its creation date. */
static DECLARE_TIMESTAMP(timestamp);

/* We make sure there is only one user. */
static volatile gboolean cancel = FALSE;
G_LOCK_DEFINE_STATIC(tree);

void end_using_tree(void)
{
    cancel = FALSE;
    G_UNLOCK(tree);
}

gboolean cancel_using_tree(void)
{
    cancel = TRUE;
    return FALSE;
}

gboolean canceled_using_tree(void)
{
    return cancel;
}

/* Constructor. */
static tree_item *new_item(gchar * name, gchar * path)
{
    tree_item *item;

    item = g_new(tree_item, 1);
    item->name = name;
    item->path = path;
    item->thumb_key = NULL;
    item->thumb = NULL;

    return item;
}

typedef enum {
    DIR_PARENT,
    DIR_EQUAL,
    DIR_DIFFERENT
} dir_type;

/* Whether dir1 is parent, equal or different from the dir where file2 is. */
static dir_type dir_wrt_file(const gchar * dir1, const gchar * file2)
{
    const gchar *ptr1;
    gchar *ptr2, *last_slash;
    dir_type res;

    if (dir1[0] == '\0')
        /* dir1 == "". */
        return DIR_PARENT;

    if (dir1[0] == '/' && dir1[1] == '\0' && file2[0] == '/')
        /* dir1 == "/" and file2 == "/...". */
        return strchr(file2 + 1, '/') == NULL ? DIR_EQUAL : DIR_PARENT;

    /* We temporarily cut the filename to have the dirname. */
    ptr2 = (gchar *) file2;
    last_slash = strrchr(file2, '/');
    if (last_slash)
        *last_slash = '\0';

    ptr1 = dir1;

    /* Skip identical characters in the paths. */
    while (*ptr1 != '\0' && *ptr1 == *ptr2) {
        ptr1++;
        ptr2++;
    }

    if (*ptr1 == *ptr2)
        res = DIR_EQUAL;

    else if (*ptr1 == '\0' && *ptr2 == '/')
        res = DIR_PARENT;

    else
        res = DIR_DIFFERENT;

    if (last_slash)
        *last_slash = '/';

    return res;
}

/* When file == parent"/a/b/c...", it returns "a". */
static gchar *first_dir(const gchar * file, const gchar * parent)
{
    const gchar *res;
    gint prefix_length;
    gchar *end;

    if (parent[0] == '\0' && file[0] == '/')
        return g_strdup("/");

    prefix_length = common_prefix_length(file, parent);
    file += prefix_length;
    parent += prefix_length;

    /* Suppress the leading separator. */
    if (file[0] == '/')
        file++;

    res = file;

    /* Keep only the first dir by looking for the second one, or the file */
    end = strchr(file, '/');

    /* and throwing it away. */
    return g_strndup(res, end - res);
}

static void make_tree_rec(GNode * parent, gchar *** array_ptr)
{
    gchar *this_dir;
    gchar *name, *path;
    GNode *node;
    tree_item *item;

    item = parent->data;
    this_dir = item->path;

    while (**array_ptr != NULL) {
        switch (dir_wrt_file(this_dir, **array_ptr)) {
        case DIR_PARENT:
            /* Subdirectory => add it, then recursive call. */
            name = first_dir(**array_ptr, this_dir);
            path = g_build_filename(this_dir, name, NULL);
            item = new_item(name, path);

            node = g_node_new(item);
            g_node_append(parent, node);

            make_tree_rec(node, array_ptr);
            break;

        case DIR_EQUAL:
            /* Same directory => add the filename, stay in the loop. */
            name = **array_ptr + strlen(this_dir);
            while (name[0] == '/')
                name++;

            item = new_item(g_strdup(name), **array_ptr);
            g_node_append_data(parent, item);

            /* I like stars :) */
            do {
                (*array_ptr)++;
            } while (**array_ptr != NULL &&
                     g_str_equal(**array_ptr, *(*array_ptr - 1)));
            break;

            /* case DIR_DIFFERENT: */
        default:
            /* Return from the recursive calls to find a common directory. */
            return;
        }
    }
}

static gboolean destroy_node(GNode * node)
{
    tree_item *item = node->data;

    if (G_NODE_IS_LEAF(node))
        g_free(item->thumb_key);
    else
        g_free(item->path);

    g_free(item->name);
    g_free(item);
    return FALSE;
}

static void set_parent(GNode * node, gpointer parent)
{
    node->parent = parent;
}

static void compress_node(GNode * node)
{
    GNode *child;
    tree_item *item, *child_item;
    gchar *new_name;

    if (g_node_nth_child(node, 1) != NULL)
        /* More than one child */
        return;

    item = node->data;

    child = g_node_first_child(node);
    child_item = child->data;

    /* Skip one level. */
    g_node_children_foreach(child, G_TRAVERSE_ALL, set_parent, node);
    node->children = child->children;

    /* Concatenate the names. */
    new_name = g_build_filename(item->name, child_item->name, NULL);
    g_free(item->name);
    item->name = new_name;

    /* Use the child path. */
    g_free(item->path);
    item->path = child_item->path;
    child_item->path = NULL;

    /* Get rid of the compressed child. */
    destroy_node(child);
    child->parent = NULL;
    child->children = NULL;
    g_node_destroy(child);
}

static gboolean to_utf8(GNode * node)
{
    tree_item *item;
    gchar *converted;

    item = node->data;
    converted = (gchar *) filename_to_utf8(item->name);
    if (item->name != converted) {
        g_free(item->name);
        item->name = g_strdup(converted);
    }

    return FALSE;
}

/*
 * We change the tree structure so it is
 * safer to use our own traverse function.
 */
static void compress_tree(GNode * node)
{
    if (G_NODE_IS_LEAF(node) == FALSE) {
        g_node_children_foreach(node, G_TRAVERSE_ALL,
                                (GNodeForeachFunc) compress_tree, NULL);
        compress_node(node);
    }
}

static void print_tree(GNode * tree, gint level);

/* Can we avoid rebuilding a tree? */
static gboolean last_tree_ok(void)
{
    timestamp_t list_timestamp = get_list_timestamp();

    if (last_tree == NULL)
        /* No tree to reuse. */
        return FALSE;

    /* Check if the list has changed. */
    return up_to_date(timestamp, list_timestamp);
}

gboolean more_recent_than_tree(timestamp_t ts)
{
    if (last_tree_ok() == FALSE)
        return FALSE;

    return up_to_date(ts, timestamp);
}

GNode *make_tree(void)
{
    GNode *tree;
    tree_item *item;
    gchar **array;
    gchar **array_ptr;

    if (G_TRYLOCK(tree) == FALSE)
        return NULL;

    cancel = FALSE;
    if (last_tree_ok())
        return last_tree;

    invalidate_last_tree();

    array_ptr = array = get_sorted_files_array();
    if (array == NULL) {
        G_UNLOCK(tree);
        return NULL;
    }

    item = new_item(g_strdup(""), g_strdup(""));
    tree = g_node_new(item);

    /* Build the tree. */
    while (*array_ptr != NULL)
        make_tree_rec(tree, &array_ptr);

    g_free(array);

    compress_tree(tree);

    g_node_traverse(tree, G_POST_ORDER, G_TRAVERSE_ALL, -1,
                    (GNodeTraverseFunc) to_utf8, NULL);

    print_tree(tree, 0);

    last_tree = tree;
    touch(&timestamp);
    return tree;
}

void destroy_tree(GNode * tree)
{
    g_node_traverse(tree, G_POST_ORDER, G_TRAVERSE_ALL, -1,
                    (GNodeTraverseFunc) destroy_node, NULL);

    g_node_destroy(tree);
}

void invalidate_last_tree(void)
{
    if (last_tree) {
        destroy_tree(last_tree);
        last_tree = NULL;
        reset_timestamp(&timestamp);
    }
}

static gboolean increment(GNode * unused, gint * count)
{
    (*count)++;
    return FALSE;
}

gint tree_count_files(void)
{
    gint count = 0;

    g_node_traverse(last_tree, G_IN_ORDER, G_TRAVERSE_LEAFS, -1,
                    (GNodeTraverseFunc) increment, &count);

    return count;
}

static void print_tree(GNode * tree, gint level)
{
#if 0
    gint i;
    tree_item *item = tree->data;

    for (i = 0; i < level; i++)
        printf("    ");

    printf("name: \"%s\", path: \"%s\"\n", item->name, item->path);

    level++;
    g_node_children_foreach(tree, G_TRAVERSE_ALL,
                            print_tree, GINT_TO_POINTER(level));
#endif
}
