/*
 * 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 files list *
 ******************/

#include <sys/stat.h>           /* stat() */
#include <stdlib.h>             /* qsort() */
#include <stdio.h>              /* remove(), perror(), stdin, getdelim() */
#include <sys/types.h>          /* size_t */

#include "gliv.h"
#include "files_list.h"
#include "options.h"
#include "loading.h"
#include "str_utils.h"
#include "foreach_file.h"
#include "next_image.h"
#include "messages.h"
#include "windows.h"
#include "collection.h"
#include "formats.h"
#include "timestamp.h"
#include "pathset.h"
#include "strnatcmp.h"

#ifndef HAVE_GETDELIM
#include "../lib/getdelim.h"
#endif

extern options_struct *options;
extern GlivImage *current_image;

static GList *files_list = NULL;
static gint list_length = 0;
static GList *list_end = NULL;
static DECLARE_TIMESTAMP(timestamp);    /* Last modification. */
static GList *obsolete_nodes = NULL;    /* Nodes to delete when possible */

gint get_list_length(void)
{
    return list_length;
}

GList *get_list_head(void)
{
    return files_list;
}

GList *get_list_end(void)
{
    return list_end;
}

/*** Additions. ***/

static gboolean is_loadable(const gchar * filename)
{
    loader_t loader;

    if (options->force)
        return TRUE;

    loader = get_loader(filename);
    return loader == LOADER_PIXBUF || loader == LOADER_DECOMP_PIXBUF;
}

/* Returns the number of files added. */
static gint add_file_to_list(const gchar * filename)
{
    if (is_loadable(filename)) {
        list_end = g_list_append(list_end, clean_filename(filename));
        list_length++;

        if (list_length == 1)
            files_list = list_end;
        else
            list_end = list_end->next;

        touch(&timestamp);
        return 1;
    }

    return 0;
}

struct files_list_state {
    GList *files_list;
    gint list_length;
    GList *list_end;
};

static struct files_list_state *save_files_list_state(void)
{
    struct files_list_state *prev = g_new(struct files_list_state, 1);

    prev->files_list = files_list;
    prev->list_length = list_length;
    prev->list_end = list_end;

    files_list = NULL;
    list_length = 0;
    list_end = NULL;

    return prev;
}

static void merge_files_list_state(struct files_list_state *prev,
                                   gboolean after_current)
{
    if (prev->list_length == 0) {
        /* The previous list was empty => nothing to merge. */
        goto end_free;
    }

    if (files_list == NULL) {
        /* No files were added. */
        files_list = prev->files_list;
        list_length = prev->list_length;
        list_end = prev->list_end;
        goto end_free;
    }

    if (after_current && current_image != NULL &&
        current_image->node->next != NULL) {
        /* Insert after the current image. */

        /* Merge the insertion end. */
        list_end->next = current_image->node->next;
        list_end->next->prev = list_end;

        /* Merge the insertion beginning. */
        current_image->node->next = files_list;
        files_list->prev = current_image->node;

        files_list = prev->files_list;
    } else {
        /* Insert at the end. */
        if (prev->list_end != NULL)
            prev->list_end->next = files_list;

        files_list->prev = prev->list_end;
        files_list = prev->files_list;
    }

    list_length += prev->list_length;

  end_free:

    g_free(prev);
}

static void reorder_list(gboolean shuffle);

static gint cmp_filename(gconstpointer a, gconstpointer b)
{
    return !g_str_equal(a, b);
}

/* Returns the number of files added. */
static gint add_dir(const gchar * dirname, const gchar * first_file)
{
    gint nb_inserted = 0;
    struct files_list_state *prev = save_files_list_state();
    GList *place_first = NULL;

    if (options->recursive) {
        /* Traverse recursively the directory. */
        nb_inserted += foreach_file(dirname, add_file_to_list);
    } else {
        /* Add every file in the directory. */
        GDir *dir;
        GError *err = NULL;
        const gchar *dir_entry;

        dir = g_dir_open(dirname, 0, &err);
        if (dir == NULL) {
            g_printerr("%s\n", err->message);
            g_error_free(err);
            return 0;
        }

        while ((dir_entry = g_dir_read_name(dir)) != NULL) {
            gchar *full_path = g_build_filename(dirname, dir_entry, NULL);

            if (!g_file_test(full_path, G_FILE_TEST_IS_DIR))
                /* We have a file. */
                nb_inserted += add_file_to_list(full_path);

            g_free(full_path);
        }

        g_dir_close(dir);
    }

    reorder_list(FALSE);

    if (first_file != NULL)
        place_first = g_list_find_custom(files_list, first_file, cmp_filename);

    if (place_first != NULL && place_first->prev != NULL) {
        place_first->prev->next = place_first->next;

        if (place_first->next != NULL)
            place_first->next->prev = place_first->prev;

        place_first->prev = NULL;
        place_first->next = files_list;

        files_list->prev = place_first;

        files_list = place_first;
    }

    merge_files_list_state(prev, FALSE);
    return nb_inserted;
}

/* Returns the number of files added. */
static gint add_to_list(const gchar * name, gboolean add_all)
{
    gint nb_inserted = 0;
    struct stat st;

    if (stat(name, &st) < 0) {
        perror(name);
        return 0;
    }

    if (S_ISDIR(st.st_mode)) {
        nb_inserted += add_dir(name, NULL);
    } else {
        loader_t loader = get_loader(name);

        if (loader == LOADER_DOT_GLIV || loader == LOADER_DECOMP_DOT_GLIV)
            /* A .gliv collection. */
            nb_inserted += load_dot_gliv(name);

        else if (add_all) {
            gchar *dirname = g_path_get_dirname(name);
            gchar *clean = clean_filename(name);

            nb_inserted += add_dir(dirname, clean);
            g_free(dirname);
            g_free(clean);
        } else {
            nb_inserted += add_file_to_list(name);
        }
    }

    return nb_inserted;
}

/*** Deletion ***/

void remove_from_list(GList * node)
{
    if (node == list_end)
        list_end = node->prev;

    if (current_image && current_image->node == node)
        current_image->node = NULL;

    unload(node);
    g_free(node->data);
    files_list = g_list_delete_link(files_list, node);
    list_length--;
    touch(&timestamp);
}

/*** Sorting ***/

/* To shuffle the list, we simply sort with a random compare func. */
static gint random_compar(gconstpointer unused1, gconstpointer unused2)
{
    return g_random_int_range(-1, 3);
}

/* We want children to be after their parent, or the alphabetical order. */
G_GNUC_PURE static gint filename_compar(gconstpointer a, gconstpointer b)
{
    const gchar *ptr1, *ptr2;
    gboolean ptr1_has_dir = FALSE, ptr2_has_dir = FALSE;
    gint prefix_length;

    ptr1 = (const gchar *) a;
    ptr2 = (const gchar *) b;

    if (ptr1[0] != ptr2[0])
        /* Comparing an absolute filename, and a relative one. */
        return ptr1[0] == '/' ? -1 : 1;

    prefix_length = common_prefix_length(ptr1, ptr2);
    ptr1 += prefix_length;
    ptr2 += prefix_length;

    if (*ptr1 == *ptr2)
        /* The filenames were equal. */
        return 0;

    /* Go back to the first different dir. */
    for (;;) {
        ptr1--;
        ptr2--;

        if (*ptr1 == '/') {
            if (*ptr2 == '/')
                break;

            ptr1_has_dir = TRUE;
        } else if (*ptr2 == '/') {
            ptr2_has_dir = TRUE;
        }
    }

    /* Skip the common '/'. */
    ptr1++;
    ptr2++;

    if (ptr1_has_dir == ptr2_has_dir)
        /*
         * Either the files are in the same directory,
         * or they are not parent.
         */
        return strnatcmp(ptr1, ptr2);

    /* One of the directory is parent of the other one. */
    return ptr1_has_dir ? -1 : 1;
}

static void reorder_list(gboolean shuffle)
{
    GCompareFunc compare_func;

    compare_func = shuffle ? random_compar : filename_compar;
    files_list = g_list_sort(files_list, compare_func);
    list_end = g_list_last(list_end);
}

/* Called by the menu. */
gboolean reorder_files(gboolean shuffle)
{
    if (files_list == NULL)
        return FALSE;

    reorder_list(shuffle);

    after_reorder();
    touch(&timestamp);
    return FALSE;
}

G_GNUC_PURE static gint compar(gconstpointer a, gconstpointer b)
{
    return filename_compar(*((const gchar **) a), *((const gchar **) b));
}

/*
 * Used to build the images menus.
 */
gchar **get_sorted_files_array(void)
{
    gchar **array, **array_ptr;
    GList *list_ptr;

    if (list_length == 0)
        return NULL;

    array_ptr = array = g_new(gchar *, list_length + 1);

    /* Fill the array. */
    for (list_ptr = files_list; list_ptr != NULL; list_ptr = list_ptr->next) {
        *array_ptr = list_ptr->data;
        array_ptr++;
    }

    *array_ptr = NULL;
    qsort(array, list_length, sizeof(gchar *), compar);

    return array;
}

/*** Initialization ***/

static void list_initialized(gboolean sort, gboolean shuffle)
{
    if (sort || shuffle)
        reorder_list(shuffle);

    touch(&timestamp);
}

gint init_from_null_filenames(gboolean sort, gboolean shuffle, gboolean add_all)
{
    gchar *filename = NULL;
    size_t len = 0;
    gint nb_inserted = 0;

    while (!feof(stdin) && getdelim(&filename, &len, '\0', stdin) > 0)
        nb_inserted += add_to_list(filename, add_all);

    list_initialized(sort, shuffle);
    g_free(filename);

    return nb_inserted;
}

gint init_list(gchar ** names, gint nb, gboolean sort, gboolean shuffle,
               gboolean add_all)
{
    gint nb_inserted;

    nb_inserted = insert_after_current(names, nb, FALSE, add_all);
    list_initialized(sort, shuffle);

    return nb_inserted;
}

/*** Misc. operations. ***/

/* Returns the number of files inserted. */
gint insert_after_current(gchar ** names, gint nb, gboolean just_file,
                          gboolean add_all)
{
    gint nb_inserted = 0;
    struct files_list_state *prev = save_files_list_state();
    struct pathset *paths = pathset_new();

    for (; nb != 0; names++, nb--) {
        if (just_file)
            nb_inserted += add_file_to_list(*names);

        else if (!pathset_add(paths, *names))
            /* Already added */
            continue;

        else if (add_all && !g_file_test(*names, G_FILE_TEST_IS_DIR)) {
            gchar *dirname = g_path_get_dirname(*names);

            if (pathset_add(paths, dirname))
                nb_inserted += add_to_list(*names, TRUE);

            g_free(dirname);

        } else
            nb_inserted += add_to_list(*names, FALSE);
    }

    pathset_free(paths);
    list_initialized(FALSE, FALSE);
    merge_files_list_state(prev, TRUE);
    return nb_inserted;
}

static gboolean show_remove_dialog(const gchar * msg)
{
    GtkMessageDialog *dialog;
    gint res;

    dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(NULL,
                                                       GTK_DIALOG_MODAL,
                                                       GTK_MESSAGE_QUESTION,
                                                       GTK_BUTTONS_OK_CANCEL,
                                                       "%s", msg));

    res = run_modal_dialog(GTK_DIALOG(dialog));
    gtk_widget_destroy(GTK_WIDGET(dialog));

    return res == GTK_RESPONSE_ACCEPT || res == GTK_RESPONSE_OK ||
        res == GTK_RESPONSE_YES;
}

gboolean confirm_remove_current(void)
{
    gchar *filename, *msg;

    if (current_image == NULL || current_image->node == NULL)
        return FALSE;

    filename = current_image->node->data;
    msg = g_strdup_printf(_("Do you really want to delete this file?\n%s\n"),
                          filename_to_utf8(filename));

    if (show_remove_dialog(msg)) {
        add_obsolete_node(current_image->node);

        if (remove(filename) < 0)
            perror(filename);
    }

    g_free(msg);
    return FALSE;
}

timestamp_t get_list_timestamp(void)
{
    return timestamp;
}

const gchar *get_nth_filename(gint n)
{
    GList *ptr;

    if (n < 0)
        return _("directory/file");

    if (n <= get_list_length() / 2)
        for (ptr = get_list_head(); n != 0; n--, ptr = ptr->next);
    else {
        n = get_list_length() - n - 1;
        for (ptr = get_list_end(); n != 0; n--, ptr = ptr->prev);
    }

    return ptr->data;
}

gint get_image_number(GlivImage * im)
{
    if (im->number < 0) {
        GList *ptr;
        gint im_nr = 0;

        if (im->node == NULL)
            return im->number;

        for (ptr = get_list_head(); ptr != NULL; ptr = ptr->next) {
            if (ptr == im->node) {
                im->number = im_nr;
                break;
            }

            im_nr++;
        }
    }

    return im->number;
}

GList *find_node_by_name(const gchar * name)
{
    GList *ptr;

    for (ptr = get_list_head(); ptr != NULL; ptr = ptr->next)
        if (ptr->data == name)
            return ptr;

    return NULL;
}

void add_obsolete_node(GList * node)
{
    obsolete_nodes = g_list_prepend(obsolete_nodes, node);
}

gboolean remove_obsolete_nodes(void)
{
    GList *ptr, *next;
    gboolean destroyed = FALSE;

    for (ptr = obsolete_nodes; ptr != NULL; ptr = next) {
        next = ptr->next;
        if (ptr->data != current_image->node) {
            remove_from_list(ptr->data);
            obsolete_nodes = g_list_delete_link(obsolete_nodes, ptr);
            destroyed = TRUE;
        }
    }

    if (destroyed)
        touch(&timestamp);

    return destroyed;
}
