/*
 * 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@yahoo.fr>
 */

/********************
 * GLiv collections *
 ********************/

/*
 * +--------------------------------+
 * | The .gliv format specification |
 * +--------------------------------+
 *
 * Newlines are here just for clarity.  All numbers are represented in a string.
 * So 123 is actually represented: '1''2''3''\0'
 * PixelData is a char* so it should be endian independant, testing will tell...
 *
 *
 *
 * GlivCollectionFile ::=
 * "GLiv <COLLECTION>"
 * Version
 * FileCount
 * Entry*
 * "</COLLECTION>"
 *
 *
 * Version ::=
 * "1"    (Currently there is only one version)
 *
 *
 * Entry ::=
 * "<FILE>"
 * PathLength
 * Path
 * MaybeThumb
 * "</FILE>"
 *
 *
 * MaybeThumb ::=
 * "0" | Thumb
 *
 * Thumb ::=
 * "1"
 * HashKeyLength
 * HashKey
 * GdkColorspace
 * AlphaChannel
 * BitsPerSample
 * ThumbWidth
 * ThumbHeight
 * RowStride
 * PixelDataLength
 * PixelData
 *
 *
 *
 * Path, and HashKey are '0' terminated strings.
 * FileCount, PathLength, HashKeyLength, GdkColorspace, AlphaChannel,
 * BitsPerSample, ThumbWidth, ThumbHeight, RowStride and PixelDataLength
 * are '\0' terminated strings representing decimal numbers.
 */

#include <unistd.h>             /* isatty() */
#include <sys/types.h>          /* size_t, off_t */
#include <sys/mman.h>           /* mmap(), PROT_READ, MAP_PRIVATE, ... */
#include <stdio.h>              /* FILE, fopen(), fread(), fwrite(), ... */
#include <string.h>             /* strlen(), strrchr() */
#include <errno.h>              /* errno */
#include <ctype.h>              /* isdigit() */
#include <time.h>               /* time_t, time() */

#include "gliv.h"
#include "collection.h"
#include "math_floats.h"        /* log10f() */
#include "large_files.h"
#include "messages.h"
#include "tree.h"
#include "thumbnails.h"
#include "str_utils.h"
#include "files_list.h"
#include "next_image.h"
#include "gliv-image.h"
#include "formats.h"
#include "loading.h"
#include "decompression.h"
#include "callbacks.h"
#include "windows.h"
#include "options.h"

/* Max filename length in the progress window. */
#define MAX_DISPLAYED_NAME_LENGTH 30

extern rt_struct *rt;
extern options_struct *options;
extern GlivImage *current_image;

static gchar *get_filename(const gchar * label, gboolean save)
{
    GtkFileChooserAction action;
    const gchar *stock_button;
    GtkFileChooser *chooser;
    gint response;
    gchar *filename = NULL;

    if (save) {
        action = GTK_FILE_CHOOSER_ACTION_SAVE;
        stock_button = GTK_STOCK_SAVE;
    } else {
        action = GTK_FILE_CHOOSER_ACTION_OPEN;
        stock_button = GTK_STOCK_OPEN;
    }

    chooser = GTK_FILE_CHOOSER(gtk_file_chooser_dialog_new(label,
                                                           NULL,
                                                           action,
                                                           GTK_STOCK_CANCEL,
                                                           GTK_RESPONSE_CANCEL,
                                                           stock_button,
                                                           GTK_RESPONSE_ACCEPT,
                                                           NULL));

    response = run_modal_dialog(GTK_DIALOG(chooser));

    if (response == GTK_RESPONSE_ACCEPT)
        filename = gtk_file_chooser_get_filename(chooser);

    gtk_widget_destroy(GTK_WIDGET(chooser));
    return filename;
}

static gchar *get_collection_destination(void)
{
    gchar *filename;

    filename = get_filename(_("Choose a file to save the collection"), TRUE);
    if (filename != NULL && g_file_test(filename, G_FILE_TEST_EXISTS)) {
        GtkMessageDialog *overwrite =
            GTK_MESSAGE_DIALOG(gtk_message_dialog_new(NULL,
                                                      GTK_DIALOG_MODAL,
                                                      GTK_MESSAGE_QUESTION,
                                                      GTK_BUTTONS_YES_NO,
                                                      _("Overwrite \"%s\" ?"),
                                                      filename));

        gint response = run_modal_dialog(GTK_DIALOG(overwrite));
        if (response != GTK_RESPONSE_YES) {
            g_free(filename);
            filename = NULL;
        }

        gtk_widget_destroy(GTK_WIDGET(overwrite));
    }

    return filename;
}

/*** Progress dialog ***/

static GtkLabel *file_label;
static GtkLabel *ratio_label;
static GtkProgressBar *progress;
static GtkWindow *progress_window;
static GtkLabel *elapsed_label, *total_label, *remaining_label;
static time_t start_time;
static gboolean is_saving;      /* loading or saving ? */

static gboolean set_true(gboolean * ptr)
{
    *ptr = TRUE;
    return FALSE;
}

static GtkLabel *add_time_display(GtkTable * table, const gchar * text,
                                  gint pos)
{
    GtkLabel *name_label, *time_label;

    name_label = GTK_LABEL(gtk_label_new(text));
    gtk_widget_show(GTK_WIDGET(name_label));

    time_label = GTK_LABEL(gtk_label_new(NULL));
    gtk_widget_show(GTK_WIDGET(time_label));

    gtk_table_attach_defaults(table, GTK_WIDGET(name_label),
                              0, 1, pos, pos + 1);

    gtk_table_attach_defaults(table, GTK_WIDGET(time_label),
                              1, 2, pos, pos + 1);

    return time_label;
}

static void create_progress_dialog(const gchar * filename, gpointer cancel_data)
{
    GtkButton *cancel;
    GtkTable *table;
    gchar *title;

    file_label = GTK_LABEL(gtk_label_new(NULL));
    gtk_widget_show(GTK_WIDGET(file_label));

    ratio_label = GTK_LABEL(gtk_label_new(NULL));
    gtk_widget_show(GTK_WIDGET(ratio_label));


    progress = GTK_PROGRESS_BAR(gtk_progress_bar_new());
    gtk_widget_show(GTK_WIDGET(progress));

    cancel = GTK_BUTTON(gtk_button_new_from_stock(GTK_STOCK_CANCEL));
    g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(set_true),
                             cancel_data);

    gtk_widget_show(GTK_WIDGET(cancel));

    table = GTK_TABLE(gtk_table_new(7, 2, TRUE));
    gtk_table_attach_defaults(table, GTK_WIDGET(file_label), 0, 2, 0, 1);
    gtk_table_attach_defaults(table, GTK_WIDGET(ratio_label), 0, 2, 1, 2);
    gtk_table_attach_defaults(table, GTK_WIDGET(progress), 0, 2, 2, 3);

    elapsed_label = add_time_display(table, _("Elapsed time"), 3);
    remaining_label = add_time_display(table, _("Remaining time"), 4);
    total_label = add_time_display(table, _("Total time"), 5);

    gtk_table_attach_defaults(table, GTK_WIDGET(cancel), 1, 2, 6, 7);

    gtk_widget_show(GTK_WIDGET(table));

    progress_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
    g_signal_connect_swapped(progress_window, "delete-event",
                             G_CALLBACK(set_true), cancel_data);

    if (is_saving)
        title = g_strdup_printf(_("Saving collection: %s"),
                                filename_to_utf8(filename));
    else
        title = g_strdup_printf(_("Loading collection: %s"),
                                filename_to_utf8(filename));

    gtk_window_set_title(progress_window, title);
    g_free(title);

    gtk_container_add(GTK_CONTAINER(progress_window), GTK_WIDGET(table));
    gtk_widget_show(GTK_WIDGET(progress_window));
    start_time = time(NULL);
}

static gchar *time2str(time_t t)
{
    gint hours = t / 3600;
    gint minutes = (t - hours * 3600) / 60;
    gint seconds = t % 60;

    return g_strdup_printf("%02d:%02d:%02d", hours, minutes, seconds);
}

static void update_times(time_t elapsed_time,
                         time_t remaining_time, time_t total_time)
{
    gchar *text;

    text = time2str(elapsed_time);
    gtk_label_set_text(elapsed_label, text);
    g_free(text);

    text = time2str(remaining_time);
    gtk_label_set_text(remaining_label, text);
    g_free(text);

    text = time2str(total_time);
    gtk_label_set_text(total_label, text);
    g_free(text);
}

static gboolean need_refresh_progress(gboolean last_time)
{
    static GTimeVal last = { 0, 0 };
    GTimeVal now;
    glong usec_diff;

    if (last_time) {
        last.tv_sec = 0;
        last.tv_usec = 0;
        return TRUE;
    }

    if (last.tv_sec == 0 && last.tv_usec == 0) {
        g_get_current_time(&last);
        return TRUE;
    }

    g_get_current_time(&now);
    usec_diff = (now.tv_sec - last.tv_sec) * G_USEC_PER_SEC +
        now.tv_usec - last.tv_usec;

    if (usec_diff < G_USEC_PER_SEC / 10)
        /* Lower the redrawing load. */
        return FALSE;

    last = now;
    return TRUE;
}

static FILE *get_terminal_output(void)
{
    static FILE *print;
    static gboolean print_ok = FALSE;

    if (!print_ok) {
        if (isatty(fileno(stdout)))
            print = stdout;

        else if (isatty(fileno(stderr)))
            print = stderr;

        else
            print = NULL;

        print_ok = TRUE;
    }

    return print;
}

static void update_progress_nogui(gint id, gint count)
{
    FILE *print = get_terminal_output();

    if (print != NULL && need_refresh_progress(id == count - 1)) {
        gint percent = id * 100 / count;

        fprintf(print, "  %d/%d: %d%%\r", id + 1, count, percent);
        fflush(print);
    }
}

static void update_progress(const gchar * filename, gint id, gint count)
{
    const gchar *base;
    time_t current_time, elapsed_time, total_time, remaining_time;
    size_t len;
    gchar *ratio_text;

    if (progress_window == NULL) {
        update_progress_nogui(id, count);
        return;
    }

    if (filename == NULL) {
        /* Last update. */
        gchar *msg = _("Inserting files...");

        need_refresh_progress(TRUE);
        gtk_label_set_text(file_label, msg);

    } else {
        if (need_refresh_progress(FALSE) == FALSE)
            return;

        base = strrchr(filename, '/');
        if (base == NULL)
            base = filename;
        else
            base++;

        len = strlen(base);
        if (len > MAX_DISPLAYED_NAME_LENGTH) {
            gchar *begin, *text;
            const gchar *end;

            begin = g_strndup(base, MAX_DISPLAYED_NAME_LENGTH / 2 - 3);
            end = base + len - MAX_DISPLAYED_NAME_LENGTH / 2 + 3;
            text = g_strconcat(begin, " ... ", end, NULL);
            g_free(begin);

            gtk_label_set_text(file_label, text);
            g_free(text);
        } else
            gtk_label_set_text(file_label, base);
    }

    ratio_text = g_strdup_printf("%d / %d", id, count - 1);
    gtk_label_set_text(ratio_label, ratio_text);
    g_free(ratio_text);

    if (count == 1)
        gtk_progress_bar_set_fraction(progress, 1.0);
    else
        gtk_progress_bar_set_fraction(progress, (gdouble) id / (count - 1));

    current_time = time(NULL);
    elapsed_time = current_time - start_time;
    if (id == 0)
        total_time = 0;
    else
        total_time = elapsed_time * (count - 1) / id;

    remaining_time = total_time - elapsed_time;

    update_times(elapsed_time, remaining_time, total_time);
    process_events();
}

static void destroy_progress_dialog(void)
{
    if (progress_window != NULL) {
        if (GTK_IS_WIDGET(progress_window))
            gtk_widget_destroy(GTK_WIDGET(progress_window));
        progress_window = NULL;
    }
}

/*
 * In the serialization and deserialization,
 * returning FALSE means aborting it.
 */

/*** Serialization ***/

typedef struct {
    FILE *file;
    gint file_id;
    gint count;
    gchar *cwd;                 /* To avoid relative filenames. */
} serialization_data;

/* Attempts to put a string in the serialized file. */
#define WRITE(file, str)                            \
    do {                                            \
        gchar * __str__ = (str);                    \
        gint len = strlen(__str__) + 1;             \
                                                    \
        if (fwrite(__str__, 1, len, (file)) != len) \
            return FALSE;                           \
    } while (0)

/* Attempts to put a number (in string form) in the serialized file. */
#define WRITE_NB(file, nb)                                \
    do {                                                  \
        gchar *str = g_strdup_printf("%d%c", (nb), '\0'); \
        gint len = strlen(str) + 1;                       \
                                                          \
        if (fwrite(str, 1, len, (file)) != len) {         \
            g_free(str);                                  \
            return FALSE;                                 \
        }                                                 \
                                                          \
        g_free(str);                                      \
    } while (0)

static gboolean print_header(serialization_data * work)
{
    WRITE(work->file, "GLiv <COLLECTION>");
    WRITE(work->file, "1");     /* Version */
    WRITE_NB(work->file, work->count);  /* FileCount */

    return TRUE;
}

static gboolean print_footer(FILE * file)
{
    WRITE(file, "</COLLECTION>");

    return TRUE;
}

/*
 * We will write the value number, then a '\0' and then the image
 * data.  The data should be aligned if we read it via mmap, as
 * GdkPixbuf uses a rowstride value so that every line beginning are
 * on a 32 bit boundary.
 * The alignment is made by padding some '0'.
 */
static gboolean align_next(FILE * file, gint value)
{
    gint length, pos, align;

    /* For the (extremly ...) rare case value == 0 */
    value += !value;

    length = (int) log10f(value) + 2;
    pos = (int) ftell(file) + length;   /* We don't need 64 bit precision */
    align = (4 - pos % 4) % 4;  /* We align on 32 bit boundaries */

    while (align > 0) {
        if (putc('0', file) < 0)
            return FALSE;

        align--;
    }

    return TRUE;
}

/* Write the MaybeThumb part of an entry. */
static gboolean write_thumb(const gchar * filename, FILE * file)
{
    gint height, rowstride, len, bps;
    gchar *thumb_key;
    GdkPixbuf *thumb;

    thumb = get_thumbnail(filename, &thumb_key);
    if (thumb == NULL) {
        WRITE(file, "0");
        return TRUE;
    }

    height = gdk_pixbuf_get_height(thumb);
    rowstride = gdk_pixbuf_get_rowstride(thumb);
    len = pixels_size(thumb);
    bps = gdk_pixbuf_get_bits_per_sample(thumb);

    WRITE(file, "1");
    WRITE_NB(file, strlen(thumb_key));  /* HashKeyLength */
    WRITE(file, thumb_key);     /* HashKey */
    WRITE_NB(file, gdk_pixbuf_get_colorspace(thumb));   /* GdkColorspace */
    WRITE_NB(file, gdk_pixbuf_get_has_alpha(thumb));    /* AlphaChannel */
    WRITE_NB(file, bps);        /* BitsPerSample */
    WRITE_NB(file, gdk_pixbuf_get_width(thumb));        /* ThumbWidth */
    WRITE_NB(file, height);     /* ThumbHeight */
    WRITE_NB(file, rowstride);  /* RowStride */

    if (align_next(file, len) == FALSE)
        return FALSE;

    WRITE_NB(file, len);        /* PixelDataLength */

    /* PixelData */
    return fwrite(gdk_pixbuf_get_pixels(thumb), 1, len, file) == len;
}

static gboolean serialize_file(const gchar * filename,
                               serialization_data * work)
{
    FILE *file = work->file;
    gchar *path;

    update_progress(filename, work->file_id, work->count);

    WRITE(file, "<FILE>");

    path = get_absolute_filename(filename);
    WRITE_NB(file, strlen(path));       /* PathLength */
    WRITE(file, path);          /* Path */
    g_free(path);

    if (write_thumb(filename, file) == FALSE)   /* MaybeThumb */
        return FALSE;

    WRITE(file, "</FILE>");

    work->file_id++;
    return TRUE;
}

static void file_error(const gchar * filename)
{
    if (rt) {
        gchar *message = g_strconcat(filename, ": ", g_strerror(errno), NULL);
        DIALOG_MSG(message);
        g_free(message);
    }
}

gint save_collection(FILE * file, gboolean * cancel)
{
    GList *list;
    serialization_data *work;
    gboolean ok = FALSE;

    list = get_list_head();
    is_saving = TRUE;
    work = g_new(serialization_data, 1);
    work->file = file;
    work->file_id = 0;
    work->count = get_list_length();
    work->cwd = g_get_current_dir();

    if (print_header(work) == FALSE)
        goto error;

    while (*cancel == FALSE && list != NULL) {
        gboolean file_ok = serialize_file(list->data, work);
        if (file_ok == FALSE)
            goto error;

        list = list->next;
    }

    if (*cancel || print_footer(work->file) == FALSE)
        goto error;

    ok = TRUE;
    goto ok;

  error:
    ok = FALSE;

  ok:
    g_free(work->cwd);
    g_free(work);

    return ok ? 0 : 1;
}

gint serialize_collection_nogui(const gchar * filename)
{
    FILE *tty;
    FILE *file;
    gint res;
    gboolean cancel = FALSE;

    if (get_list_head() == NULL) {
        g_printerr(_("No images to put in a collection\n"));
        return 1;
    }

    if (filename == NULL) {
        file = stdout;
        if (isatty(fileno(file))) {
            g_printerr(_("GLiv won't write a collection to a terminal\n"));
            return 1;
        }
    } else {
        /* We remove the file, since we may have mmapped it */
        remove(filename);

        file = fopen(filename, "w");
        if (file == NULL) {
            perror(filename);
            return 1;
        }
    }

    tty = get_terminal_output();
    fprintf(tty, _("Saving collection: %s"), filename_to_utf8(filename));
    putc('\n', tty);

    res = save_collection(file, &cancel);

    if (res)
        perror(filename == NULL ? _("Standard output") : filename);

    if (filename != NULL && fclose(file) < 0 && res == 0)
        perror(filename);

    return res;
}

gboolean serialize_collection_gui(void)
{
    FILE *file;
    gchar *filename;
    gint res;
    gboolean cancel = FALSE;

    if (get_list_head() == NULL)
        return FALSE;

    filename = get_collection_destination();
    if (filename == NULL)
        return FALSE;

    /* We remove the file, since we may have mmapped it */
    remove(filename);

    file = fopen(filename, "w");
    if (file == NULL) {
        file_error(filename);
        return FALSE;
    }

    create_progress_dialog(filename, &cancel);

    res = save_collection(file, &cancel);
    if (res) {
        if (cancel == FALSE)
            file_error(filename);

        fclose(file);
        remove(filename);
    } else if (fclose(file) < 0)
        file_error(filename);

    destroy_progress_dialog();
    g_free(filename);

    return FALSE;
}

/*** Deserialization ***/

struct coll_src {
    gboolean is_file;
    union {
        FILE *file;
        struct {
            guchar *base;
            guchar *ptr;
            guchar *end;
        };
    };
};

static void free_buffer(struct coll_src *source, guchar * buffer)
{
    if (source->is_file)
        g_free(buffer);
}

static guchar *read_buffer(struct coll_src *source, size_t length,
                           gboolean is_string)
{
    guchar *data;

    if (source->is_file) {
        data = g_new(guchar, length);
        if (fread(data, 1, length, source->file) != length) {
            g_free(data);
            return NULL;
        }
    } else {
        if (source->ptr + length > source->end)
            return NULL;

        data = source->ptr;
        source->ptr += length;
    }

    if (is_string && data[length - 1] != '\0') {
        free_buffer(source, data);
        data = NULL;
    }

    return data;
}

static gint read_char(struct coll_src *source)
{
    if (source->is_file)
        return fgetc(source->file);

    if (source->ptr >= source->end)
        return EOF;

    return *(source->ptr++);
}

static GdkPixbufDestroyNotify destroy_func(struct coll_src *source)
{
    return (GdkPixbufDestroyNotify) (source->is_file ? g_free : NULL);
}

/* Checks that file starts with magic. */
static gboolean check_magic(struct coll_src *source, const gchar * magic)
{
    gboolean res;
    gint size = strlen(magic) + 1;
    guchar *buffer = read_buffer(source, size, TRUE);

    if (buffer == NULL)
        return FALSE;

    res = g_str_equal(magic, buffer);
    free_buffer(source, buffer);

    return res;
}



/* Reads a number represented by a string. -1 => ERROR */
static gint read_number(struct coll_src *source)
{
    gint number = 0;
    gint next_char = read_char(source);

    while (isdigit(next_char)) {
        number = number * 10 + next_char - '0';
        next_char = read_char(source);
    }

    if (next_char == '\0')
        return number;

    return -1;
}

#define READ_THUMB_NB(source, var) \
    do {                           \
        var = read_number(source); \
        if (var < 0)               \
            return FALSE;          \
    } while (0)

static gboolean read_pixbuf(struct coll_src *source, GdkPixbuf ** thumb)
{
    GdkColorspace colorspace;
    gboolean has_alpha;
    gint bits_per_sample, width, height, rowstride;
    gint pixel_length;
    guchar *pixel;

    READ_THUMB_NB(source, colorspace);
    READ_THUMB_NB(source, has_alpha);
    if (has_alpha != FALSE && has_alpha != TRUE)
        return FALSE;

    READ_THUMB_NB(source, bits_per_sample);
    READ_THUMB_NB(source, width);
    READ_THUMB_NB(source, height);
    READ_THUMB_NB(source, rowstride);
    READ_THUMB_NB(source, pixel_length);

    pixel = read_buffer(source, pixel_length, FALSE);
    if (pixel == NULL)
        return FALSE;

    *thumb = gdk_pixbuf_new_from_data(pixel, colorspace, has_alpha,
                                      bits_per_sample, width, height, rowstride,
                                      destroy_func(source), NULL);

    if (*thumb == NULL) {
        free_buffer(source, pixel);
        return FALSE;
    }

    return TRUE;
}

static gboolean read_thumb(struct coll_src *source, gchar ** hash_key,
                           GdkPixbuf ** thumb)
{
    gint has_thumb, hash_key_length;

    has_thumb = read_number(source);
    if (has_thumb == 0)
        return TRUE;

    if (has_thumb != 1)
        return FALSE;

    hash_key_length = read_number(source);
    if (hash_key_length <= 0)
        return FALSE;

    hash_key_length++;
    *hash_key = read_buffer(source, hash_key_length, TRUE);
    if (*hash_key == NULL)
        return FALSE;

    return read_pixbuf(source, thumb);
}

static tree_item *read_entry(struct coll_src *source)
{
    tree_item *item;
    gint len;
    gchar *path, *hash_key = NULL;
    GdkPixbuf *thumb = NULL;

    if (check_magic(source, "<FILE>") == FALSE)
        return NULL;

    len = read_number(source);
    if (len <= 0)
        return NULL;

    len++;
    path = read_buffer(source, len, TRUE);
    if (path == NULL)
        return NULL;

    if (read_thumb(source, &hash_key, &thumb) == FALSE) {
        free_buffer(source, path);
        return NULL;
    }

    if (check_magic(source, "</FILE>") == FALSE) {
        free_buffer(source, path);
        free_buffer(source, hash_key);
        if (hash_key != NULL)
            g_object_unref(thumb);

        return NULL;
    }

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

    return item;
}

#define CHECK_MAGIC(source, str)                 \
    do {                                         \
        gboolean res = check_magic(source, str); \
        if (res == FALSE)                        \
            goto error;                          \
    } while (0)

static struct coll_src *build_source(FILE * file)
{
    off_t length;
    struct coll_src *source = g_new(struct coll_src, 1);

    if (fseeko(file, 0, SEEK_END) < 0)
        goto no_mmap;

    length = ftello(file);
    if (length < 0) {
        perror("ftello");
        goto no_mmap;
    }

    source->base = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fileno(file), 0);
    if (source->base == MAP_FAILED) {
        perror("mmap");
        goto no_mmap;
    }

    source->is_file = FALSE;
    source->ptr = source->base;
    source->end = source->base + length;
    goto ok;

  no_mmap:
    fseeko(file, 0, SEEK_SET);

    source->is_file = TRUE;
    source->file = file;

  ok:
    return source;
}

static void destroy_source(struct coll_src *source, gboolean ok)
{
    if (ok == FALSE && source->is_file == FALSE)
        if (munmap(source->base, source->end - source->base) < 0)
            perror("munmap");
}

gint load_dot_gliv_from_file(FILE * file)
{
    gint count, i = 0;
    tree_item **items = NULL;
    gchar **filenames = NULL;
    gboolean ok = TRUE, cancel = FALSE;
    gint nb_inserted = -1;
    struct coll_src *source;
    gboolean saved_force_load;

    is_saving = FALSE;
    source = build_source(file);

    CHECK_MAGIC(source, "GLiv <COLLECTION>");
    CHECK_MAGIC(source, "1");

    count = read_number(source);
    if (count < 0)
        goto error;

    if (rt)
        create_progress_dialog(NULL, &cancel);

    items = g_new(tree_item *, count);
    for (i = 0; i < count; i++) {
        tree_item *item;

        item = read_entry(source);
        if (item == NULL)
            goto error;

        items[i] = item;
        update_progress(item->path, i, count);

        if (cancel) {
            i++;
            nb_inserted = 0;
            goto error;
        }
    }

    CHECK_MAGIC(source, "</COLLECTION>");

    update_progress(NULL, count - 1, count);
    nb_inserted = 0;

    filenames = g_new(gchar *, count);

    for (i = 0; i < count; i++) {
        if (items[i]->thumb_key != NULL)
            collection_add_thumbnail(items[i]->thumb_key, items[i]->thumb);

        filenames[i] = items[i]->path;
    }

    /* We don't want to stat() every file in the collection */
    saved_force_load = options->force;
    options->force = TRUE;

    nb_inserted = insert_after_current(filenames, count, TRUE, FALSE);

    options->force = saved_force_load;

    goto ok;

  error:
    ok = FALSE;

  ok:

    while (i) {
        i--;
        free_buffer(source, items[i]->path);
        if (ok == FALSE && items[i]->thumb_key != NULL) {
            free_buffer(source, items[i]->thumb_key);
            g_object_unref(items[i]->thumb);
        }
        g_free(items[i]);
    }
    g_free(items);
    g_free(filenames);

    if (rt)
        destroy_progress_dialog();
    destroy_source(source, ok);
    return nb_inserted;
}

gint load_dot_gliv(const gchar * filename)
{
    FILE *file;
    FILE *tty;
    gint nb_inserted;
    loader_t loader;

    tty = get_terminal_output();
    fprintf(tty, _("Loading collection: %s"), filename_to_utf8(filename));
    putc('\n', tty);

    loader = get_loader(filename);
    if (loader == LOADER_DECOMP_DOT_GLIV)
        nb_inserted = load_compressed_collection(filename);

    else {
        file = fopen(filename, "r");
        if (file == NULL) {
            file_error(filename);
            return 0;
        }

        nb_inserted = load_dot_gliv_from_file(file);
        fclose(file);
    }

    if (nb_inserted < 0) {
        if (rt)
            DIALOG_MSG(_("%s is not a GLiv collection"),
                       filename_to_utf8(filename));
        nb_inserted = 0;
    }

    return nb_inserted;
}

gboolean deserialize_collection(void)
{
    gchar *filename;
    gint nb_inserted;

    filename = get_filename(_("Choose a collection to load"), FALSE);
    if (filename == NULL)
        return FALSE;

    nb_inserted = load_dot_gliv(filename);
    g_free(filename);
    if (nb_inserted <= 0)
        return FALSE;

    open_next_image(nb_inserted == 1);
    return FALSE;
}
