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

/****************************
 * Which image to load next *
 ****************************/

#include <sys/types.h>          /* off_t */
#include <sys/stat.h>           /* struct stat, stat() */
#include <time.h>               /* time_t */
#include <stdio.h>              /* perror() */
#include <string.h>             /* memcmp() */
#include <stdlib.h>             /* random() */

#include "gliv.h"
#include "next_image.h"
#include "options.h"
#include "gliv-image.h"
#include "rendering.h"
#include "loading.h"
#include "files_list.h"
#include "textures.h"
#include "messages.h"
#include "menus.h"
#include "callbacks.h"
#include "transition.h"
#include "windows.h"
#include "tree_browser.h"
#include "images_menus.h"

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

/*
 * We can have previous_image == next_image.
 * Thanks to ref counting, we don't care.
 */
static GlivImage *previous_image = NULL;
static GlivImage *next_image = NULL;

static GtkMenuItem *slide_show_menu;
static guint slide_show_timer = 0;
static gint slide_show_direction = 1;

static void forget_image(GlivImage ** im_ptr)
{
    if (*im_ptr != NULL) {
        g_object_unref(*im_ptr);
        *im_ptr = NULL;
    }
}

/*
 * Once the images list has been reordered we must take care
 * of the current image position and destroy the previous and
 * next images since they changed.
 */
void after_reorder(void)
{
    if (previous_image != NULL &&
        current_image->node->prev != previous_image->node) {

        forget_image(&previous_image);
    }

    if (next_image != NULL && current_image->node->next != next_image->node) {
        forget_image(&next_image);
    }

    update_current_image_status(TRUE);
}

/*** Last image notice ***/

typedef enum {
    NOTICE_FIRST,
    NOTICE_LAST,
    NOTICE_NOTHING
} notice_t;

static notice_t notice = NOTICE_NOTHING;
static guint notice_timer = 0;

/* Called by the timer. */
static gboolean hide_image_notice(void)
{
    gboolean need_refresh;

    need_refresh = notice != NOTICE_NOTHING;
    notice = NOTICE_NOTHING;

    if (need_refresh)
        refresh(REFRESH_IMAGE);

    notice_timer = 0;
    return FALSE;
}

static void image_notice(gboolean last_image)
{
    notice_t new_notice;
    gboolean need_refresh;

    if (options->notice_time <= 0)
        return;

    new_notice = last_image ? NOTICE_LAST : NOTICE_FIRST;
    need_refresh = new_notice != notice;
    notice = new_notice;

    if (notice_timer != 0)
        g_source_remove(notice_timer);

    notice_timer = g_timeout_add(options->notice_time,
                                 (GSourceFunc) hide_image_notice, NULL);

    if (need_refresh)
        refresh(REFRESH_IMAGE);
}

const gchar *get_image_notice(void)
{
    switch (notice) {
    case NOTICE_FIRST:
        return _("First image");

    case NOTICE_LAST:
        return _("Last image");

    default:
        return NULL;
    }
}

/*** Switching images ***/

static void slide_show_advance(void);

static void render_next_image(GlivImage * im)
{
    gboolean first_image = current_image == NULL;
    gboolean make_transition = options->transitions &&
        options->trans_time > 0 && current_image != NULL && im != NULL;

    if (options->fullscreen == FALSE) {
        gint i;
        goto_window(im, FALSE);
        /*
         * This seems to work to make sure the window is actually resized and
         * the corresponding events have been handled.
         */
        for (i = 0; i < 2; i++) {
            gtk_widget_queue_draw(GTK_WIDGET(get_current_window()));
            gdk_window_process_all_updates();
            process_events();
            gdk_flush();
        }
    }

    if (make_transition)
        transition(im);

    if (current_image != NULL) {
        prioritize_textures(current_image, FALSE);
        g_object_unref(current_image);
    }

    current_image = im;
    g_object_ref(im);
    render();

    if (im->node->next == NULL)
        image_notice(TRUE);

    else if (im->node->prev == NULL && first_image == FALSE)
        image_notice(FALSE);

    if (slide_show_started())
        slide_show_advance();

    highlight_current_image();
}

/*** Next/previous image ***/

static GList *next_node(GList * node, gint dir)
{
    if (node == NULL)
        return (dir == 1) ? get_list_head() : get_list_end();

    if (dir == 1)
        return node->next;

    /* dir == -1 */
    return node->prev;
}

static void show_errors(GSList * errors)
{
    GtkDialog *dialog;
    GtkTextView *text;
    GtkTextBuffer *buffer;
    GtkScrolledWindow *scroll;

    /* Create the widgets */

    dialog =
        GTK_DIALOG(gtk_dialog_new_with_buttons(_("Loading errors"),
                                               get_current_window(),
                                               GTK_DIALOG_DESTROY_WITH_PARENT,
                                               GTK_STOCK_OK,
                                               GTK_RESPONSE_NONE, NULL));

    text = GTK_TEXT_VIEW(gtk_text_view_new());
    gtk_text_view_set_wrap_mode(text, GTK_WRAP_WORD_CHAR);
    gtk_text_view_set_editable(text, FALSE);
    gtk_text_view_set_cursor_visible(text, FALSE);

    buffer = gtk_text_view_get_buffer(text);
    gtk_text_buffer_insert_at_cursor(buffer,
                                     _("The following errors occurred"
                                       " while loading the next image:\n"), -1);

    while (errors) {
        gtk_text_buffer_insert_at_cursor(buffer, errors->data, -1);
        gtk_text_buffer_insert_at_cursor(buffer, "\n", -1);
        errors = errors->next;
    }

    scroll = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new(NULL, NULL));
    gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(text));

    gtk_container_add(GTK_CONTAINER(dialog->vbox), GTK_WIDGET(scroll));
    gtk_widget_show_all(GTK_WIDGET(scroll));
    gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 400);
    run_modal_dialog(dialog);
    gtk_widget_destroy(GTK_WIDGET(dialog));
}

static GlivImage *load_next_node(GList ** node, gint dir)
{
    GlivImage *im = NULL;
    GList *next;
    GError *error;
    GSList *failed = NULL;
    gint nb_errors = 0;

    while ((next = next_node(*node, dir)) != NULL) {
        gchar *filename = next->data;

        error = NULL;
        im = load_file(filename, &error);
        if (im == NULL) {
            gchar *msg;

            /* Delete bad images from the list. */
            remove_from_list(next);

            if (++nb_errors < 1024) {
                msg = error ?
                    g_strdup(error->message) :
                    g_strdup_printf(_("Cannot load %s"), filename);
                failed = g_slist_prepend(failed, msg);
            }
            g_clear_error(&error);
        } else {
            *node = next;
            if (error != NULL && error->message != NULL)
                g_printerr("%s\n", error->message);
            break;
        }
    }

    if (nb_errors == 1) {
        gchar *filename = failed->data;
        DIALOG_MSG("%s", filename);
    } else if (nb_errors > 1) {
        /* Many errors */
        GSList *ptr;

        failed = g_slist_reverse(failed);
        show_errors(failed);
        for (ptr = failed; ptr != NULL; ptr = ptr->next)
            g_free(ptr->data);

        g_slist_free(failed);
    }

    return im;
}

static GlivImage *load_in_direction(gint dir)
{
    GlivImage *im = NULL;
    GList *node;
    gint id;
    timestamp_t before_load = get_list_timestamp();

    if (get_list_length() == 2 && options->loop) {
        /*
         * When there are only two images, and we are looping,
         * next_image and previous_image are equal.
         */

        if (previous_image != NULL) {
            g_object_ref(previous_image);
            return previous_image;
        }

        if (next_image != NULL) {
            g_object_ref(next_image);
            return next_image;
        }
    }

    if (current_image == NULL) {
        /* First time. */
        node = NULL;
        id = 0;
    } else {
        node = current_image->node;
        id = get_image_number(current_image) + dir;
    }

    im = load_next_node(&node, dir);

    if (im == NULL && options->loop && get_list_length() > 1) {
        /* Loop at the end. */
        node = NULL;
        id = (dir == 1) ? 0 : (get_list_length() - 1);
        im = load_next_node(&node, dir);
    }

    if (im != NULL) {
        im->node = node;
        im->number = id;
    } else if (get_list_head() == NULL && current_image)
        /* No more images, not even the current one */
        current_image->node = NULL;

    if (before_load != get_list_timestamp())
        update_current_image_status(TRUE);

    return im;
}

/* Called when switching images. */
static void destroy_unused(GlivImage ** backup)
{
    if (*backup != NULL) {
        g_object_unref(*backup);
        *backup = NULL;
    }

    *backup = current_image;
    g_object_ref(*backup);
}

static void stop_slide_show(void);

/* Return FALSE if there are no more images. */
void load_direction(gint dir)
{
    GlivImage **backup;
    GlivImage **next;
    GlivImage *im;

    /* We are not reentrant */
    static gboolean in_load_direction = FALSE;
    static gint next_direction = 0;

    if (in_load_direction) {
        /* We delay the reentrance */
        next_direction = dir;
        return;
    }

    if (currently_loading() || current_image == NULL)
        return;

    for (;;) {
        if (dir == -1) {
            backup = &next_image;
            next = &previous_image;
        } else if (dir == 1) {
            backup = &previous_image;
            next = &next_image;
        } else
            return;

        in_load_direction = TRUE;
        next_direction = 0;

        /* Be sure there is a next image. */
        if (*next == NULL) {
            *next = load_in_direction(dir);
            if (*next == NULL) {
                /* There is no next image to load. */
                image_notice(dir == 1);
                stop_slide_show();
                goto out;
            }
        }

        destroy_unused(backup);
        im = *next;
        *next = NULL;

        render_next_image(im);
        g_object_unref(im);

        if (options->one_image) {
            g_object_unref(*backup);
            *backup = NULL;
        } else
            *next = load_in_direction(dir);

        slide_show_direction = dir;
      out:
        in_load_direction = FALSE;
        dir = next_direction;
    }
}

/* Load a randomly selected image from the list. */
void load_random_image()
{
    gint nr;
    const gchar *filename;

    nr = random() % get_list_length();
    filename = get_nth_filename(nr);
    menu_load(filename);
}

/* Load the first image from the list. */
void load_1st_image()
{
    const gchar *filename;

    filename = get_nth_filename(0);
    menu_load(filename);
}

/* Load the last image in the list. */
void load_last_image()
{
    const gchar *filename;

    filename = get_nth_filename(get_list_length() - 1);
    menu_load(filename);
}

/*
 * After many images are opened we want to load
 * the next image but not the preloaded one.
 * Maybe the image after the one we will load does
 * not change.
 */
static void open_next_image(gboolean keep_next)
{
    GlivImage *next;
    GlivImage *future_image;

    if (currently_loading())
        return;

    if (keep_next) {
        forget_image(&previous_image);
    } else
        unload_images();

    /* load_in_direction() would be too smart when looping with 2 images */
    future_image = next_image;
    next_image = NULL;

    next = load_in_direction(1);
    if (next == NULL)
        return;

    if (current_image != NULL) {
        previous_image = current_image;
        g_object_ref(previous_image);
    }

    next_image = future_image;

    render_next_image(next);
    g_object_unref(next);

    if (options->one_image == FALSE && next_image == NULL)
        next_image = load_in_direction(1);
}

/*** First/second image ***/

static void load_second_image(void)
{
    if (next_image == NULL && options->one_image == FALSE)
        next_image = load_in_direction(1);

    if (options->start_show)
        start_slide_show();

    if (options->build_menus)
        do_later(G_PRIORITY_LOW, (GSourceDummyMarshal) rebuild_images_menus);
}

void load_first_image(void)
{
    GlivImage *first_image;

    install_segv_handler();

    if (get_list_head() == NULL) {
        if (options->build_menus)
            /* Initialize the menus' timestamps for cond_rebuild to work */
            rebuild_images_menus();
        return;
    }

    first_image = load_in_direction(1);
    if (first_image == NULL)
        DIALOG_MSG(_("No image found"));
    else
        render_next_image(first_image);

    if (first_image != NULL) {
        if (options->fullscreen == FALSE && options->resize_win == FALSE)
            goto_window(first_image, TRUE);

        g_object_unref(first_image);

    } else if (options->fullscreen == FALSE)
        goto_window(NULL, TRUE);

    do_later(G_PRIORITY_LOW, load_second_image);
}

/*
 * Reloading.
 * Used for example, when an option is changed such as dithering or mipmap.
 */

void reload_current_image(void)
{
    GlivImage *new;

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

    new = load_file(current_image->node->data, NULL);
    if (new == NULL)
        return;

    new->node = current_image->node;
    new->number = current_image->number;

    g_object_unref(current_image);
    current_image = new;

    prioritize_textures(current_image, TRUE);
    refresh(REFRESH_NOW);
    update_current_image_status(FALSE);
}

void reload_images(void)
{
    if (currently_loading())
        return;

    forget_image(&previous_image);
    forget_image(&next_image);
    reload_current_image();
}

struct image_stat {
    off_t size;
    time_t ctime;
    gboolean present;
};

struct image_stat3 {
    struct image_stat previous;
    struct image_stat current;
    struct image_stat next;
};

static void stat_image(GlivImage * im, struct image_stat *st, gboolean quiet)
{
    st->present = FALSE;

    if (im != NULL && im->node != NULL) {
        struct stat real_stat;

        if (stat(im->node->data, &real_stat) < 0) {
            if (!quiet)
                perror(im->node->data);
            return;
        }

        st->size = real_stat.st_size;
        st->ctime = real_stat.st_ctime;
        st->present = TRUE;
    }
}

gpointer stat_loaded_files(gboolean quiet)
{
    struct image_stat3 *stat_data = g_new(struct image_stat3, 1);

    stat_image(previous_image, &stat_data->previous, quiet);
    stat_image(current_image, &stat_data->current, quiet);
    stat_image(next_image, &stat_data->next, quiet);

    return stat_data;
}

#define DIFFERENT(name) \
    memcmp(&before->name, &now->name, sizeof(struct image_stat))

void reload_changed_files(gpointer * stat_data)
{
    struct image_stat3 *before = (struct image_stat3 *) stat_data;
    struct image_stat3 *now = stat_loaded_files(TRUE);

    if (DIFFERENT(previous))
        forget_image(&previous_image);

    if (DIFFERENT(next))
        forget_image(&next_image);

    if (DIFFERENT(current)) {
        if (now->current.present)
            reload_current_image();
        else
            add_obsolete_node(current_image->node);
    }

    g_free(before);
    g_free(now);
}


/*** Menu ***/

/* Check if we can optimize the loading asked by the images menu. */
static gboolean check_direction(const gchar * filename, gint dir)
{
    GlivImage **prev, **next;
    GList *next_node;

    if (dir == 1) {
        prev = &previous_image;
        next = &next_image;

        next_node = current_image->node->next;
        if (next_node != NULL)
            next_node = next_node->next;
    } else {
        /* dir == -1 */
        prev = &next_image;
        next = &previous_image;

        next_node = current_image->node->prev;
        if (next_node != NULL)
            next_node = next_node->prev;
    }

    if (*next == NULL)
        return FALSE;

    /* Check if the requested image is just following the current one. */
    if (filename == (*next)->node->data) {
        load_direction(dir);
        return TRUE;
    }

    /*
     * Check if the requested image is just before the previous one,
     * or just after the next one.
     */
    if (next_node != NULL && filename == next_node->data) {
        GlivImage *new;

        if (*prev != NULL)
            g_object_unref(*prev);
        *prev = *next;
        *next = NULL;

        new = load_file(filename, NULL);
        if (new == NULL) {
            remove_from_list(next_node);
            return TRUE;
        }

        new->node = next_node;

        render_next_image(new);
        g_object_unref(new);
        return TRUE;
    }

    return FALSE;
}

gboolean menu_load(const gchar * filename)
{
    GlivImage *im;
    static gboolean loading = FALSE;

    if (loading || currently_loading()) {
        /* We are not reentrant. */
        return FALSE;
    }

    loading = TRUE;

    if (current_image->node != NULL &&
        (filename == current_image->node->data ||
         check_direction(filename, -1) || check_direction(filename, 1))) {
        loading = FALSE;
        return FALSE;
    }

    im = load_file(filename, NULL);
    if (im == NULL) {
        loading = FALSE;
        return FALSE;
    }

    im->node = find_node_by_name(filename);
    unload_images();

    render_next_image(im);
    loading = FALSE;
    g_object_unref(im);
    return FALSE;
}

/* Called when destroying a file. */
void unload(GList * node)
{
    if (previous_image != NULL && previous_image->node == node) {
        g_object_unref(previous_image);
        previous_image = NULL;
    }

    if (next_image != NULL && next_image->node == node) {
        g_object_unref(next_image);
        next_image = NULL;
    }
}

/* Called when we want only one image in memory. */
void unload_images(void)
{
    forget_image(&previous_image);
    forget_image(&next_image);
}

/*** Slide show ***/

static void slide_show_remove_timer(void)
{
    if (slide_show_timer != 0) {
        g_source_remove(slide_show_timer);
        slide_show_timer = 0;
    }
}

void set_slide_show_menu(GtkMenuItem * item)
{
    slide_show_menu = item;
}

static void stop_slide_show(void)
{
    slide_show_remove_timer();
    set_menu_label(slide_show_menu, _("Start the slide show"), TRUE);
}

static gboolean slide_show_next(void)
{
    load_direction(slide_show_direction);
    if (slide_show_started())
        slide_show_advance();
    return FALSE;
}

static void slide_show_advance(void)
{
    slide_show_remove_timer();
    slide_show_timer = g_timeout_add_full(G_PRIORITY_LOW,
                                          options->duration * 1000,
                                          (GSourceFunc) slide_show_next, NULL,
                                          NULL);
}

void start_slide_show(void)
{
    stop_slide_show();
    slide_show_direction = 1;

    if (options->duration < 0 || current_image == NULL)
        return;

    slide_show_advance();
    set_menu_label(slide_show_menu, _("Stop the slide show"), TRUE);
}

gboolean slide_show_started(void)
{
    return slide_show_timer != 0;
}

gboolean toggle_slide_show(void)
{
    if (slide_show_started())
        stop_slide_show();
    else
        start_slide_show();

    return FALSE;
}

void new_images(gint nb_inserted)
{
    if (nb_inserted == 0)
        return;

    open_next_image(nb_inserted == 1);
    do_later(G_PRIORITY_LOW, cond_rebuild_menus);
}
