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

/***********************
 * Textures management *
 ***********************/

#include "gliv.h"
#include "textures.h"
#include "math_floats.h"        /* ceilf(), powf() */
#include "options.h"
#include "params.h"
#include "thread.h"
#include "dithering.h"
#include "opengl.h"

extern rt_struct *rt;
extern options_struct *options;
extern GtkWidget *gl_widget;

/*
 * Lowest power of two bigger than the argument and 64.
 * glTexImage2D(3G): All implementations support texture images
 * that are at least 64 texels (wide|high).
 */
G_GNUC_CONST static gint gl_dim(gint p)
{
    gint ret = 64;

    while (ret < p)
        ret += ret;

    return ret;
}

/* Draws a piece of a multi-textures image or a whole mono-texture one. */
static void draw_rectangle(gfloat tex_x0, gfloat tex_x1,
                           gfloat tex_y0, gfloat tex_y1,
                           gfloat vert_x0, gfloat vert_x1,
                           gfloat vert_y0, gfloat vert_y1)
{
    /*
     * (tex_x0  ; tex_y0)  : Origin of the interesting part of the texture.
     * (tex_x1  ; tex_y1)  : Extremity of the interesting part of the texture.
     * (vert_x0 ; vert_y0) : Origin of the rectangle.
     * (vert_x1 ; vert_y1) : Extremity of the rectangle.
     */

    glBegin(GL_QUADS);

    glTexCoord2f(tex_x0, tex_y0);
    glVertex2f(vert_x0, vert_y0);

    glTexCoord2f(tex_x1, tex_y0);
    glVertex2f(vert_x1, vert_y0);

    glTexCoord2f(tex_x1, tex_y1);
    glVertex2f(vert_x1, vert_y1);

    glTexCoord2f(tex_x0, tex_y1);
    glVertex2f(vert_x0, vert_y1);

    glEnd();
}

/*
 * Called twice for each rectangle to get rectangle
 * coordinates to put in the display list.
 */
static void compute_coordinates(texture_map * map,
                                gfloat * vert0, gfloat * vert1,
                                gfloat * tex0, gfloat * tex1,
                                gboolean is_x, gint id)
{
    gint map_dim;
    gboolean last;

    if (is_x) {
        map_dim = map->map_init->width;
        last = (id == map->map_init->x_tiles - 1);
    } else {
        map_dim = map->map_init->height;
        last = (id == map->map_init->y_tiles - 1);
    }

    if (id == 0) {
        *vert0 = -map_dim / 2.0;
        *tex0 = 0.0;

        if (last) {
            /* Single tile. */
            *vert1 = -*vert0;
            *tex1 = (gfloat) map_dim / gl_dim(map_dim);
        } else {
            /*
             * First tile.
             * - 1.0: there is only one overlapping pixel.
             */
            *vert1 = *vert0 + rt->max_texture_size - 1.0;
            *tex1 = (rt->max_texture_size - 1.0) / rt->max_texture_size;
        }
    } else {
        /*
         * - 2.0: there are two overlapping pixels.
         * + 1.0: in the first tile there is only one.
         */
        *vert0 = -map_dim / 2.0 + id * (rt->max_texture_size - 2.0) + 1.0;

        if (last) {
            /* Last tile. */
            gint tile_dim;
            gfloat gl_tile_dim;

            tile_dim = map_dim / 2.0 - *vert0 + 1.0;
            gl_tile_dim = (gfloat) gl_dim(tile_dim);

            *vert1 = map_dim / 2.0;
            *tex0 = 1.0 / gl_tile_dim;
            *tex1 = tile_dim / gl_tile_dim;
        } else {
            /* Middle tiles. */
            *vert1 = *vert0 + rt->max_texture_size - 2.0;
            *tex0 = 1.0 / rt->max_texture_size;
            *tex1 = 1.0 - *tex0;
        }
    }
}

static void rectangle(texture_map * map, tile_dim * tile, gint i, gint j,
                      gint level)
{
    gfloat ty0, ty1, tx0, tx1;
    gfloat x0, x1, y0, y1;
    gfloat mipmap_coeff;

    mipmap_coeff = powf(MIPMAP_RATIO, -level);

    compute_coordinates(map, &x0, &x1, &tx0, &tx1, TRUE, i);
    compute_coordinates(map, &y0, &y1, &ty0, &ty1, FALSE, j);

    x0 *= mipmap_coeff;
    x1 *= mipmap_coeff;
    y0 *= mipmap_coeff;
    y1 *= mipmap_coeff;

    draw_rectangle(tx0, tx1, ty0, ty1, x0, x1, y0, y1);

    /* Used when drawing, to know which tiles are hidden. */
    tile->x0 = x0;
    tile->y0 = y0;
    tile->x1 = x1;
    tile->y1 = y1;
}

/* Shortcut to OpenGL parameters common to all textures. */
static void texture_parameter(void)
{
    /* We don't change the filter for small mip maps. */
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
}

typedef struct {
    gint i, j;
    gint w, h;
    gint gl_w, gl_h;
    gint x, y;
    gboolean last_tile;
    GdkPixbuf *pixbuf;
    GdkPixbuf *tile_max_size;
    gboolean has_alpha;
} texture_data;

/* Runs in a separate thread. */
static GdkPixbuf *make_tile(texture_data * data)
{
    GdkPixbuf *tile;

    if (data->i == 0 && data->j == 0 && data->last_tile &&
        data->w == data->gl_w && data->h == data->gl_h)
        /* Special case, the image is OK for an OpenGL texture. */
        return data->pixbuf;

    if (data->gl_w == rt->max_texture_size &&
        data->gl_h == rt->max_texture_size && data->tile_max_size != NULL)

        tile = data->tile_max_size;
    else
        tile = gdk_pixbuf_new(GDK_COLORSPACE_RGB, data->has_alpha, 8,
                              data->gl_w, data->gl_h);

    gdk_pixbuf_copy_area(data->pixbuf, data->x, data->y, data->w, data->h,
                         tile, 0, 0);

    if (data->gl_w != data->w)
        /* Right border: copy the last column. */
        gdk_pixbuf_copy_area(tile, data->w - 1, 0, 1, data->h,
                             tile, data->w, 0);

    if (data->gl_h != data->h) {
        /* Lower corner: copy the last line. */
        gdk_pixbuf_copy_area(tile, 0, data->h - 1, data->w, 1,
                             tile, 0, data->h);

        if (data->gl_w != data->w)
            /* Lower-right corner: copy the last pixel. */
            gdk_pixbuf_copy_area(tile, data->w - 1, data->h - 1, 1, 1,
                                 tile, data->w, data->h);
    }

    return tile;
}

static void make_texture(GlivImage * im, gint level, gint i, gint j)
{
    guint texture0, texture1;
    static GdkPixbuf *tile_max_size = NULL;
    texture_data *data;
    texture_map *map;
    GdkPixbuf *tile;

    if (tile_max_size == NULL &&
        im->width >= rt->max_texture_size &&
        im->height >= rt->max_texture_size &&
        (im->width != gl_dim(im->width) ||
         im->height != gl_dim(im->height) ||
         im->width > rt->max_texture_size || im->height > rt->max_texture_size))
        /* First tile. */
        tile_max_size = gdk_pixbuf_new(GDK_COLORSPACE_RGB, im->has_alpha, 8,
                                       rt->max_texture_size,
                                       rt->max_texture_size);

    map = im->maps + level;

    data = g_new(texture_data, 1);
    data->pixbuf = map->map_init->pixbuf;
    data->tile_max_size = tile_max_size;
    data->has_alpha = im->has_alpha;

    data->i = i;
    data->j = j;

    data->x = i * (rt->max_texture_size - 2);
    data->y = j * (rt->max_texture_size - 2);

    if (i + 1 == map->map_init->x_tiles) {
        /* Last horizontal tile in this level. */
        data->w = map->map_init->width - data->x;
        data->gl_w = gl_dim(data->w);

        /* Last level? */
        data->last_tile = (level + 1 == im->nb_maps);
    } else {
        data->w = data->gl_w = rt->max_texture_size;
        data->last_tile = FALSE;
    }

    if (j + 1 == map->map_init->y_tiles) {
        /* Last vertical tile in this level. */
        data->h = map->map_init->height - data->y;
        data->gl_h = gl_dim(data->h);
    } else {
        data->h = data->gl_h = rt->max_texture_size;
        data->last_tile = FALSE;
    }

    texture_parameter();

    /* We may change the current texture while waiting for the thread. */
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture0);
    tile = do_threaded((GThreadFunc) make_tile, data);
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture1);

    if (texture0 != texture1)
        glBindTexture(GL_TEXTURE_2D, texture0);

    glTexImage2D(GL_TEXTURE_2D, 0, 3 + im->has_alpha, data->gl_w, data->gl_h, 0,
                 im->has_alpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE,
                 gdk_pixbuf_get_pixels(tile));

    if (tile != tile_max_size && tile != map->map_init->pixbuf)
        g_object_unref(tile);

    if (data->last_tile && tile_max_size != NULL) {
        g_object_unref(tile_max_size);
        tile_max_size = NULL;
    }

    g_free(data);
}

static void tiles_parameters(texture_map * map, gboolean is_x)
{
    gint *tiles;
    gint dim;

    if (is_x) {
        tiles = &map->map_init->x_tiles;
        dim = map->map_init->width;
    } else {
        tiles = &map->map_init->y_tiles;
        dim = map->map_init->height;
    }

    *tiles = 1;
    if (dim > rt->max_texture_size)
        *tiles += (gint) ceilf((gfloat) (dim - rt->max_texture_size) /
                               (rt->max_texture_size - 2.0));
}

void prioritize_textures(GlivImage * im, gboolean is_prio)
{
    GLclampf *priorities;
    gint i;

    if (im == NULL)
        return;

    /*
     * im->maps[0] has the biggest nb_tiles, so we don't change the
     * priorities array for each map, we simply use the biggest for all.
     */
    priorities = g_new(GLclampf, im->maps[0].nb_tiles);
    for (i = 0; i < im->maps[0].nb_tiles; i++)
        priorities[i] = (gfloat) is_prio;

    for (i = 0; i < im->nb_maps; i++)
        glPrioritizeTextures(im->maps[i].nb_tiles, im->maps[i].tex_ids,
                             priorities);

    g_free(priorities);
}


static void compute_gl_dimensions(texture_map * map)
{
    tiles_parameters(map, TRUE);
    tiles_parameters(map, FALSE);

    map->nb_tiles = map->map_init->x_tiles * map->map_init->y_tiles;

    map->tex_ids = g_new(guint, map->nb_tiles);
    glGenTextures(map->nb_tiles, map->tex_ids);

    map->list = glGenLists(map->nb_tiles);
}

static void create_a_map(GlivImage * im, gint level)
{
    gint i, j;
    gint id = 0;
    texture_map *map;

    map = im->maps + level;
    compute_gl_dimensions(map);

    map->tiles = g_new(tile_dim, map->nb_tiles);

    for (j = 0; j < map->map_init->y_tiles; j++)
        for (i = 0; i < map->map_init->x_tiles; i++) {

            glBindTexture(GL_TEXTURE_2D, map->tex_ids[id]);
            make_texture(im, level, i, j);

            glNewList(map->list + id, GL_COMPILE);

            /* Redundant but need to be in the display list. */
            glBindTexture(GL_TEXTURE_2D, map->tex_ids[id]);

            rectangle(map, map->tiles + id, i, j, level);
            glEndList();

            id++;
        }
}

/*** Dithering ***/

typedef struct {
    texture_map *map;
    gboolean destroy_old;
} dithering;

/* Runs in a separate thread. */
static GdkPixbuf *_dither(dithering * todo)
{
    GdkPixbuf *old, *dithered;

    old = todo->map->map_init->pixbuf;

    if (todo->destroy_old)
        dithered = old;
    else
        dithered = gdk_pixbuf_new(gdk_pixbuf_get_colorspace(old),
                                  gdk_pixbuf_get_has_alpha(old),
                                  gdk_pixbuf_get_bits_per_sample(old),
                                  gdk_pixbuf_get_width(old),
                                  gdk_pixbuf_get_height(old));

    dither_pixbuf(dithered, old);

    todo->map->map_init->pixbuf = dithered;

    return old;
}

/* Returns the original pixbuf if not asked to be destroyed. */
static GdkPixbuf *dither(texture_map * map, gboolean destroy_old)
{
    dithering todo;

    todo.map = map;
    todo.destroy_old = destroy_old;

    return do_threaded((GThreadFunc) _dither, &todo);
}

/* Returns the original pixbuf. */
static GdkPixbuf *create_first_map(GlivImage * im)
{
    texture_map *map;
    GdkPixbuf *orig;

    map = im->maps;
    map->map_init->width = im->width;
    map->map_init->height = im->height;

    if (options->dither)
        /* We keep the pixbuf only if mipmaps are used. */
        orig = dither(map, (im->nb_maps == 1));
    else
        orig = map->map_init->pixbuf;

    create_a_map(im, 0);

    if (options->dither)
        g_object_unref(map->map_init->pixbuf);

    g_free(map->map_init);
    return orig;
}

/* Wrapper for do_threaded(). */
typedef struct {
    GdkPixbuf *image;
    gint width;
    gint height;
    GdkInterpType interp;
} rescale_arg;

static GdkPixbuf *rescale(rescale_arg * arg)
{
    return gdk_pixbuf_scale_simple(arg->image, arg->width, arg->height,
                                   arg->interp);
}

void create_maps(GlivImage * im)
{
    gint level;
    texture_map *map;
    GdkPixbuf *previous;
    gint width, height;
    rescale_arg *arg;

    previous = create_first_map(im);

    if (im->nb_maps == 1) {
        /* No mipmaps. */

        if (options->dither == FALSE)
            g_object_unref(previous);

        prioritize_textures(im, FALSE);
        return;
    }

    width = im->width;
    height = im->height;
    arg = g_new(rescale_arg, 1);

    for (level = 1; level < im->nb_maps; level++) {

        map = im->maps + level;

        map->map_init = g_new(loading_data, 1);

        width = map->map_init->width = width * MIPMAP_RATIO;
        height = map->map_init->height = height * MIPMAP_RATIO;

        arg->image = previous;
        arg->width = width;
        arg->height = height;
        arg->interp = GDK_INTERP_BILINEAR;
        map->map_init->pixbuf = do_threaded((GThreadFunc) rescale, arg);

        g_object_unref(previous);

        if (options->dither)
            /*
             * We must dither each map since rescaling does not preserve it.
             * We keep the original image except for the last level.
             */
            previous = dither(map, (level == im->nb_maps - 1));

        create_a_map(im, level);

        if (options->dither)
            g_object_unref(map->map_init->pixbuf);
        else
            previous = map->map_init->pixbuf;

        g_free(map->map_init);
    }

    if (options->dither == FALSE)
        g_object_unref(previous);

    prioritize_textures(im, FALSE);
}
