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

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

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

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

/* 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,
                                gint map_dim, gint pos, gint size)
{
    gboolean last = pos + size >= map_dim;

    *vert0 = -map_dim / 2.0 + pos;

    if (pos == 0) {
        *tex0 = 0.0;

        if (last) {
            /* Single tile. */
            *vert1 = -*vert0;
            *tex1 = (gfloat) map_dim / size;
        } else {
            /*
             * First tile.
             * - 1.0: there is only one overlapping pixel.
             */
            *vert1 = *vert0 + size - 1.0;
            *tex1 = (size - 0.5) / size;
        }
    } else {
        *tex0 = 0.5 / size;
        if (last) {
            /* Last tile. */
            *vert1 = map_dim / 2.0 - 1.0;
            *tex1 = (gfloat) (map_dim - pos) / size;
        } else {
            /* Middle tiles. */
            *vert1 = *vert0 + size - 1.0;
            *tex1 = 1.0 - *tex0;
        }
    }

#if 0
    printf("pos:%d map_dim:%d size:%d v0:%f v1:%f t0:%f t1:%f\n", pos, map_dim,
           size, *vert0, *vert1, *tex0, *tex1);
#endif
}

static void rectangle(texture_map * map, tile_dim * tile, gint x, gint y,
                      gint w, gint h, 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, map->width, x, w);
    compute_coordinates(map, &y0, &y1, &ty0, &ty1, map->height, y, h);

    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 x, y;
    gint w, h;
    texture_map *map;
    GdkPixbuf *tile_max_size;
    gboolean has_alpha;
} texture_data;

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

    if (data->x == 0 && data->y == 0 &&
        data->w == data->map->width && data->h == data->map->height) {
        /* Special case, the image is OK for an OpenGL texture. */
        g_object_ref(data->map->pixbuf);
        return data->map->pixbuf;
    }

    if (data->w == rt->max_texture_size && data->h == rt->max_texture_size) {
        tile = data->tile_max_size;
        g_object_ref(tile);
    } else
        tile = gdk_pixbuf_new(GDK_COLORSPACE_RGB, data->has_alpha, 8,
                              data->w, data->h);

    real_w = data->w;
    if (data->x + data->w >= data->map->width)
        real_w = data->map->width - data->x;

    real_h = data->h;
    if (data->y + data->h >= data->map->height)
        real_h = data->map->height - data->y;

    gdk_pixbuf_copy_area(data->map->pixbuf, data->x, data->y, real_w, real_h,
                         tile, 0, 0);

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

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

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

    return tile;
}

static void make_texture(GlivImage * im, gint level, gint x, gint y,
                         gint w, gint h)
{
    gint texture0, texture1;
    static GdkPixbuf *tile_max_size = NULL;
    texture_data *data;
    texture_map *map;
    GdkPixbuf *tile;

    if (tile_max_size == NULL &&
        w == rt->max_texture_size && h == rt->max_texture_size)
        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->map = map;
    data->tile_max_size = tile_max_size;
    if (data->tile_max_size)
        g_object_ref(tile_max_size);
    data->has_alpha = im->has_alpha;
    data->x = x;
    data->y = y;
    data->w = w;
    data->h = h;

    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 (data->tile_max_size)
        g_object_unref(data->tile_max_size);

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

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

    if (tile)
        g_object_unref(tile);

    if (x + w >= map->width && y + h >= map->height && tile_max_size != NULL) {
        g_object_unref(tile_max_size);
        tile_max_size = NULL;
    }

    g_free(data);
}

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)
{
    map->x_tiles = make_tiles(map->width);
    map->y_tiles = make_tiles(map->height);

    map->nb_tiles = map->x_tiles->nb_tiles * map->y_tiles->nb_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 x, y, h, w;
    gint id = 0;
    texture_map *map;
    struct tiles_iterator *x_iterator, *y_iterator;

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

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

    y = 0;
    y_iterator = tiles_iterator_new(map->y_tiles);

    while ((h = tiles_iterator_next(y_iterator)) > 0) {
        x_iterator = tiles_iterator_new(map->x_tiles);
        x = 0;
        while ((w = tiles_iterator_next(x_iterator)) > 0) {

            glBindTexture(GL_TEXTURE_2D, map->tex_ids[id]);
#if 0
            printf("%d: [x:%d y:%d w:%d h:%d]\n", id, x, y, w, h);
#endif

            make_texture(im, level, x, y, w, h);

            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, x, y, w, h, level);
            glEndList();

            id++;
            x += w - 1;
        }
        g_free(x_iterator);
        y += h - 1;
    }
    g_free(y_iterator);

    destroy_tiles(map->x_tiles);
    destroy_tiles(map->y_tiles);
}

/*** 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->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->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->width = im->width;
    map->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->pixbuf;

    create_a_map(im, 0);

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

    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;

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

        arg->image = previous;
        arg->width = width;
        arg->height = height;
        arg->interp = GDK_INTERP_BILINEAR;
        map->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->pixbuf);
        else
            previous = map->pixbuf;
    }

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

    prioritize_textures(im, FALSE);
}
