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

/****************************
 * 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 "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 "formats.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(gint new_id)
{
    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);
    }

    current_image->number = new_id;
    fill_ident(current_image);

    refresh(REFRESH_STATUS);

    g_free(current_image->ident);
    current_image->ident = NULL;
}

/*** 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)
{
    gfloat *old_matrix;
    gboolean first_image = current_image == NULL;

    old_matrix = prepare_rendering(im);

    if (current_image != NULL && im != NULL)
        transition(old_matrix, im);

    g_free(old_matrix);

    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();
}

/*** 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 GlivImage *load_next_node(GList ** node, gint dir)
{
    GlivImage *im = NULL;
    GList *next;

    while ((next = next_node(*node, dir)) != NULL) {
        im = load_file(next->data);
        if (im == NULL)
            /* Delete bad images from the list. */
            remove_from_list(next);
        else {
            *node = next;
            break;
        }
    }

    return im;
}

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

    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 = current_image->number + 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;
    }

    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);
}

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

    static volatile gint next_direction;

    /*
     * Before loading the next image in a prebuffer we let GTK process
     * remaining events and if there is a request for the next
     * image in the events pending then -> crash.
     * With this boolean we process loading request only if we know
     * all loadings are finished.
     * IOW: we are not reentrant.
     */
    static gboolean load_finished = TRUE;

    if (load_finished == FALSE) {
        /*
         * We come from process_events() called here, so we inform
         * that we want to load the next image.
         */
        next_direction = dir;
        return TRUE;
    }

    if (is_loading() || current_image == NULL)
        return TRUE;

    load_finished = FALSE;

    /* We process subsequent next image calls in the same loop. */
    while (dir * dir == 1) {

        if (dir < 0) {
            /* dir == -1 */
            backup = &next_image;
            next = &previous_image;
        } else {
            /* dir == 1 */
            backup = &previous_image;
            next = &next_image;
        }

        /*
         * next_direction can be changed by us when called
         * by process_events().
         */
        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. */
                load_finished = TRUE;
                image_notice(dir == 1);

                return FALSE;
            }
        }

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

        render_next_image(im);
        g_object_unref(im);

        process_events();

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

        slide_show_direction = dir;

        /* Usually next_direction == 0, and the while loop ends. */
        dir = next_direction;
    }

    load_finished = TRUE;
    return TRUE;
}

/*
 * 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.
 */
void open_next_image(gboolean keep_next)
{
    GlivImage *next;

    if (is_loading())
        return;

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

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

    next = load_in_direction(1);
    if (next == NULL) {
        unload_images();
        return;
    }

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

GlivImage *load_first_image(void)
{
    GlivImage *im;

    install_segv_handler();

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

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

    return im;
}

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();
}

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

static void reload_current_image(void)
{
    GlivImage *new;

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

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

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

    g_object_unref(current_image);

    if (current_image != new) {
        current_image = new;

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

void reload_images(void)
{
    if (is_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)
{
    st->present = FALSE;

    if (im != NULL) {
        struct stat real_stat;

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

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

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

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

    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();

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

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

    if (DIFFERENT(current))
        reload_current_image();

    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) {
        gint number;
        GlivImage *new;

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

        number = current_image->number + 2 * dir;

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

        new->node = next_node;
        new->number = number;

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

    return FALSE;
}

gboolean menu_load(const gchar * filename)
{
    GList *node;
    gint number = 0;
    GlivImage *im;
    static gboolean loading = FALSE;

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

    loading = TRUE;

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

    for (node = get_list_head(); node != NULL; node = node->next) {
        if (filename == node->data)
            break;

        number++;
    }

    if (node == NULL) {
        /* The image is not in the list anymore. */
        loading = FALSE;
        return FALSE;
    }

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

    unload_images();

    im->node = node;
    im->number = number;

    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)
{
    if (previous_image != NULL) {
        g_object_unref(previous_image);
        previous_image = NULL;
    }

    if (next_image != NULL) {
        g_object_unref(next_image);
        next_image = NULL;
    }
}

/*** 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)
{
    if (load_direction(slide_show_direction) == FALSE) {
        /* No more images. */
        stop_slide_show();
    }

    return FALSE;
}

static void slide_show_advance(void)
{
    slide_show_remove_timer();
    slide_show_timer = g_timeout_add(options->duration * 1000,
                                     (GSourceFunc) slide_show_next, 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;
}
