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

/***********************************
 * Loading and unloading of images *
 ***********************************/

#include <stdio.h>              /* FILE, fopen(), fread(), fclose(), perror() */
#include <signal.h>             /* signal(), SIG_DFL, SIGSEGV */
#include <string.h>             /* strlen(), strrchr() */

#include "gliv.h"
#include "loading.h"
#include "options.h"
#include "gliv-image.h"
#include "messages.h"
#include "str_utils.h"
#include "formats.h"
#include "params.h"
#include "files_list.h"
#include "textures.h"
#include "math_floats.h"
#include "thread.h"
#include "decompression.h"
#include "thumbnails.h"
#include "rendering.h"
#include "tree_browser.h"

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

/* Used to know at any time if we are loading an image. */
static gchar *loading_filename = NULL;

/*
 * Used to be quiet when we try to load files
 * not explicitly requested by the user.
 */
static gint quiet = 0;

void start_quiet(void)
{
    quiet++;
}

void stop_quiet(void)
{
    quiet--;
}

const gchar *currently_loading(void)
{
    return loading_filename;
}

static void segv_handler(gint sig)
{
    const gchar *sig_str;

    signal(sig, SIG_DFL);
    sig_str = g_strsignal(sig);

    if (loading_filename == NULL)
        g_printerr(_("%s not while loading an image\n"), sig_str);
    else
        g_printerr(_("%s while loading %s\n"), sig_str, loading_filename);

    raise(sig);
}

/* Called when the first image is loaded. */
void install_segv_handler(void)
{
    signal(SIGSEGV, segv_handler);
}

void fill_ident(GlivImage * im)
{
    gchar *alpha_str = NULL;
    gint im_nr;

    g_free(im->ident);

    if (im->has_alpha)
        alpha_str = g_strconcat(" (", _("alpha channel"), ")", NULL);
    else
        alpha_str = g_strdup("");

    im_nr = get_image_number(im);
    im->ident = g_strdup_printf("%s (%u/%u)%s",
                                im->node ?
                                filename_to_utf8(im->node->data) : "",
                                im_nr + 1, get_list_length(), alpha_str);

    g_free(alpha_str);
}

const gchar *get_extension(const gchar * filename)
{
    const gchar *ext;
    const gchar *basename;

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

    ext = strrchr(basename, '.');
    if (ext != NULL)
        /* Skip the '.' */
        ext++;

    return ext;
}

#define LOADING_ERROR if (!quiet) g_printerr

static loader_t get_decompressor(const gchar * filename, gint ext_len)
{
    const struct format *res;

    /* We want the image extension between image_ext_start and image_ext_end. */
    const gchar *image_ext_end = filename + strlen(filename) - ext_len - 1;
    const gchar *image_ext_start = image_ext_end - 1;

    gint image_ext_length = 0;
    gchar *image_ext;

    while (image_ext_start > filename && *image_ext_start != '.') {
        image_ext_length++;
        image_ext_start--;
    }

    image_ext = g_strndup(image_ext_start + 1, image_ext_length);
    res = ext_to_loader(image_ext, image_ext_length);

    g_free(image_ext);

    if (res == NULL) {
        LOADING_ERROR(_("%s: unknown decompressed extension\n"),
                      filename_to_utf8(filename));
        return LOADER_NONE;
    }

    switch (res->loader) {
    case LOADER_DECOMP:
        LOADING_ERROR(_("%s: image cannot be compressed twice\n"),
                      filename_to_utf8(filename));
        return LOADER_NONE;

    case LOADER_PIXBUF:
        return LOADER_DECOMP_PIXBUF;

    case LOADER_DOT_GLIV:
        return LOADER_DECOMP_DOT_GLIV;

    default:
        return LOADER_NONE;
    }
}

loader_t get_loader(const gchar * filename)
{
    const struct format *res;
    const gchar *ext;
    gint ext_len;

    ext = get_extension(filename);
    if (ext == NULL) {
        LOADING_ERROR(_("%s: unknown extension (none)\n"),
                      filename_to_utf8(filename));
        return LOADER_NONE;
    }

    ext_len = strlen(ext);
    res = ext_to_loader(ext, ext_len);

    if (res == NULL) {
        LOADING_ERROR(_("%s: unknown extension\n"), filename_to_utf8(filename));
        return LOADER_NONE;
    }

    if (res->loader == LOADER_DECOMP)
        return get_decompressor(filename, ext_len);

    return res->loader;
}

#if 0
/*
 * We used to blacken image pixels with alpha == 0.
 * These pixels are invisible but may alter the bilinear filtering.
 * We have stopped doing it since the time lost is too big for the benefit.
 */
static void clean_alpha(GdkPixbuf * pixbuf)
{
    guchar *pixel, *end;

    pixel = gdk_pixbuf_get_pixels(pixbuf);
    end = pixel + pixels_size(pixels_size);

    for (; pixel < end; pixel += 4) {
        if (pixel[3] == 0)
            /*
             * alpha == 0
             * This is not valid C since a cast cannot be a lvalue but...
             */
            *((guint32 *) pixel) = 0;
    }
}
#endif

void set_loading(const gchar * filename)
{
    /* Used if there is a segfault. */
    g_free(loading_filename);
    loading_filename = g_strdup(filename);
    update_current_image_status(FALSE);

    if (filename == NULL)
        /*
         * Do it, now that we are no more in
         * the currently_loading() case
         */
        load_later();
}

/* Wrapper: two args -> one arg. */
typedef struct {
    const gchar *filename;
    GError **error;
} fname_error;

static GdkPixbuf *load_gdk_pixbuf(fname_error * param)
{
    GdkPixbuf *pixbuf = NULL;
    loader_t loader;

    loader = get_loader(param->filename);
    if (loader == LOADER_DECOMP_PIXBUF)
        pixbuf = load_compressed_pixbuf(param->filename, param->error);

    if (*param->error)
        return pixbuf;

    if (pixbuf == NULL)
        pixbuf = gdk_pixbuf_new_from_file(param->filename, param->error);

    return pixbuf;
}

static GdkPixbuf *load_gdk_pixbuf_threaded(const gchar * filename,
                                           GError ** error)
{
    fname_error *param;
    GdkPixbuf *pixbuf;

    param = g_new(fname_error, 1);
    param->filename = filename;
    param->error = (error == NULL) ? g_new(GError *, 1) : error;

    *param->error = NULL;
    pixbuf = do_threaded((GThreadFunc) load_gdk_pixbuf, param);

    if (pixbuf == NULL) {
        if (error == NULL) {
            /*
             * The caller won't handle the error, handle it here
             */
            if (*param->error != NULL)
                DIALOG_MSG("%s", (*param->error)->message);
            else
                DIALOG_MSG(_("Cannot load %s"), filename);
        }
    } else
        add_thumbnail(filename, pixbuf);

    g_free(param);
    return pixbuf;
}

gint pixels_size(GdkPixbuf * pixbuf)
{
    /* The last rowstride may not be full. */
    gint w = gdk_pixbuf_get_width(pixbuf);
    gint h = gdk_pixbuf_get_height(pixbuf);
    gint rs = gdk_pixbuf_get_rowstride(pixbuf);
    gint nb_chan = gdk_pixbuf_get_n_channels(pixbuf);
    gint bps = gdk_pixbuf_get_bits_per_sample(pixbuf);

    return ((h - 1) * rs + w * ((nb_chan * bps + 7) / 8));
}

G_GNUC_PURE static gint nb_maps(gint dim)
{
    gint ret = 1;

    while (dim > rt->max_texture_size) {
        dim *= MIPMAP_RATIO;
        ret++;
    }

    return ret;
}

static void parse_exif_orientation(GdkPixbuf * pixbuf, GlivImage *im)
{
    const gchar *orientation_str = gdk_pixbuf_get_option(pixbuf, "orientation");

    if (orientation_str)
        switch (orientation_str[0]) {
            case '2':
                im->initial_angle = PI;
                im->initial_h_flip = TRUE;
                break;

            case '3':
                im->initial_angle = PI;
                break;

            case '4':
                im->initial_h_flip = TRUE;
                break;

            case '5':
                im->initial_angle = PI / 2.0;
                im->initial_h_flip = TRUE;
                break;

            case '6':
                im->initial_angle = -PI / 2.0;
                break;

            case '7':
                im->initial_angle = -PI / 2.0;
                im->initial_h_flip = TRUE;
                break;

            case '8':
                im->initial_angle = PI / 2.0;
                break;
        }
}

GlivImage *make_gliv_image(GdkPixbuf * pixbuf)
{
    GlivImage *im;

    im = gliv_image_new();
    im->ident = NULL;
    im->number = -1;
    im->node = NULL;
    im->first_image = (current_image == NULL);

    im->width = gdk_pixbuf_get_width(pixbuf);
    im->height = gdk_pixbuf_get_height(pixbuf);
    im->has_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
    parse_exif_orientation(pixbuf, im);

    if (options->mipmap) {
        gint nb_w = nb_maps(im->width);
        gint nb_h = nb_maps(im->height);
        im->nb_maps = MIN(nb_w, nb_h);
    } else
        im->nb_maps = 1;

    im->maps = g_new(texture_map, im->nb_maps);
    im->maps[0].pixbuf = pixbuf;

#if 0
    if (im->has_alpha)
        clean_alpha(pixbuf);
#endif

    create_maps(im);
    return im;
}

GlivImage *load_file(const gchar * filename, GError ** error)
{
    GdkPixbuf *loaded_image;
    GlivImage *im = NULL;

    set_loading(filename_to_utf8(filename));

    loaded_image = load_gdk_pixbuf_threaded(filename, error);
    if (loaded_image != NULL)
        im = make_gliv_image(loaded_image);

    set_loading(NULL);

    return im;
}
