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

/***********************
 * Rendering functions *
 ***********************/

#include <stdlib.h>             /* abs() */

#include "gliv.h"
#include "rendering.h"
#include "options.h"
#include "gliv-image.h"
#include "matrix.h"
#include "params.h"
#include "zoom_frame.h"
#include "scrollbars.h"
#include "textures.h"
#include "files_list.h"
#include "loading.h"
#include "windows.h"
#include "history.h"
#include "next_image.h"
#include "opengl.h"
#include "transition.h"
#include "images_menus.h"

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

static GTimeVal last_redraw_scheduled;
static gint last_redraw_delay;

static void set_filter(gint filter)
{
    gint id;
    texture_map *map;

    /*
     * Only the textures in the first map change their filter,
     * since in the others maps the image is zoomed out.
     */
    map = current_image->maps;

    for (id = 0; id < map->nb_tiles; id++) {
        glBindTexture(GL_TEXTURE_2D, map->tex_ids[id]);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
    }
}

static gboolean is_filtering_enabled(void)
{
    texture_map *map;
    gint filter;

    /* Only the first map changes its filter. */
    map = current_image->maps;

    glBindTexture(GL_TEXTURE_2D, map->tex_ids[0]);
    glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &filter);

    return filter == GL_LINEAR;
}

static void call_lists(gboolean smooth, gint level)
{
    gint id;
    texture_map *map;

    if (level == 0 && is_filtering_enabled() != smooth)
        set_filter(smooth ? GL_LINEAR : GL_NEAREST);

    map = current_image->maps + level;

    for (id = 0; id < map->nb_tiles; id++)
        if (matrix_tile_visible(map->tiles + id))
            glCallList(map->list + id);
}

static void draw_checker(void)
{
    static guint tex_id = 0;
    gint i;
    gfloat half_w, half_h;
    gboolean matrix_changed;

    glDisable(GL_DITHER);
    glClear(GL_COLOR_BUFFER_BIT);
    matrix_changed = get_matrix_has_changed();

    if (rt->alpha_checks_changed) {
        gushort texture[12];
        gushort alpha1[3] = {
            options->alpha1.red,
            options->alpha1.green,
            options->alpha1.blue
        };
        gushort alpha2[3] = {
            options->alpha2.red,
            options->alpha2.green,
            options->alpha2.blue
        };

        if (tex_id == 0)
            glGenTextures(1, &tex_id);

        for (i = 0; i < 3; i++) {
            texture[i] = texture[9 + i] = alpha1[i];
            texture[3 + i] = texture[6 + i] = alpha2[i];
        }

        glBindTexture(GL_TEXTURE_2D, tex_id);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB,
                     GL_UNSIGNED_SHORT, texture);

        rt->alpha_checks_changed = FALSE;
    } else
        glBindTexture(GL_TEXTURE_2D, tex_id);

    if (matrix_changed == FALSE)
        /* glMatrixMode(GL_MODELVIEW); */
        /* Save the matrix only if we will not replace it when redrawing. */
        glPushMatrix();

    glLoadIdentity();

    half_w = rt->wid_size->width / 2.0;
    half_h = rt->wid_size->height / 2.0;

    glBegin(GL_QUADS);

    glTexCoord2f(0.0, 0.0);
    glVertex2f(-half_w, -half_h);

    glTexCoord2f(half_w / 16.0, 0.0);
    glVertex2f(half_w, -half_h);

    glTexCoord2f(half_w / 16.0, half_h / 16.0);
    glVertex2f(half_w, half_h);

    glTexCoord2f(0.0, half_h / 16.0);
    glVertex2f(-half_w, half_h);

    glEnd();

    if (matrix_changed == FALSE)
        /* glMatrixMode(GL_MODELVIEW); */
        glPopMatrix();

    glEnable(GL_DITHER);
}

static gint choose_mipmap_level(void)
{
    gfloat zoom, mipmap_ratio = MIPMAP_RATIO;
    gint level = 1;

    if (options->mipmap) {
        zoom = get_matrix_zoom();

        while (mipmap_ratio > zoom && level < current_image->nb_maps) {
            mipmap_ratio *= MIPMAP_RATIO;
            level++;
        }
    }

    /* Mipmaps should only be scaled down. */
    return level - 1;
}

static void display_last_image_notice(void)
{
    static GdkGC *gc = NULL;
    const gchar *msg;
    PangoContext *pc;
    PangoLayout *pl;
    gint x = 10, y = 10, dx = 5, dy = 5;
    gint w, h;

    msg = get_image_notice();
    if (msg == NULL)
        return;

    pc = gtk_widget_get_pango_context(gl_widget);
    pl = pango_layout_new(pc);

    if (gc == NULL)
        /* First time. */
        gc = gdk_gc_new(gl_widget->window);

    pango_layout_set_text(pl, msg, -1);

    gdk_gc_set_foreground(gc, &(gl_widget->style->white));
    pango_layout_get_pixel_size(pl, &w, &h);
    gdk_draw_rectangle(gl_widget->window, gc, TRUE,
                       x - dx, y - dy, w + 2 * dx, h + 2 * dy);

    gdk_gc_set_foreground(gc, &(gl_widget->style->black));
    gdk_draw_layout(gl_widget->window, gc, x, y, pl);

    g_object_unref(pl);
}

static gboolean need_alpha_checks(void)
{
    return current_image != NULL && current_image->has_alpha &&
        options->alpha_checks;
}

void draw_current_image(void)
{
    gint mipmap_level;
    gboolean filtering;
    gboolean alpha_checks = need_alpha_checks();

    if (alpha_checks)
        draw_checker();
    else {
        /*
         * I don't know why but this seems to be required to see images instead
         * black rectangles with Mesa's software rendering.
         */
        glBegin(GL_QUADS);
        glVertex2f(0, 0);
        glEnd();
    }

    write_gl_matrix();

    mipmap_level = choose_mipmap_level();
    if (options->filtering == FALSE)
        filtering = FALSE;
    else if (mipmap_level == 0)
        filtering = is_filtering_needed();
    else
        filtering = TRUE;

    if (alpha_checks) {
        glPushAttrib(GL_ENABLE_BIT);
        glEnable(GL_BLEND);
    }

    call_lists(filtering, mipmap_level);

    if (alpha_checks)
        glPopAttrib();
}

/* Called by the timer when a redraw is needed. */
static void redraw(void)
{
    static GdkGLDrawable *gldrawable = NULL;
    GTimeVal now;

    if (is_in_transition())
        return;

    if (gldrawable == NULL)
        /* First time. */
        gldrawable = gtk_widget_get_gl_drawable(gl_widget);

    gdk_gl_drawable_wait_gdk(gldrawable);
    clear_zoom_frame();

    if (need_alpha_checks() == FALSE) {
        glDisable(GL_DITHER);
        glClear(GL_COLOR_BUFFER_BIT);
        glEnable(GL_DITHER);
    }

    if (current_image != NULL)
        draw_current_image();

    gdk_gl_drawable_swap_buffers(gldrawable);
    gdk_gl_drawable_wait_gl(gldrawable);

    display_last_image_notice();
    refresh(REFRESH_SCROLL);

    g_get_current_time(&now);
    last_redraw_delay = diff_timeval_us(&now, &last_redraw_scheduled);
}

/* Called the first time an image is displayed. */
void render(void)
{
    gboolean list_changed;

    configure_matrix(current_image);
    prioritize_textures(current_image, TRUE);

    refresh(REFRESH_NOW | APPEND_HISTORY | REFRESH_STATUS);

    /* A bit dumb, but needed on some cards. */
    refresh(REFRESH_NOW);

    /* Post rendering */
    list_changed = remove_obsolete_nodes();
    if (list_changed)
        do_later(G_PRIORITY_LOW, cond_rebuild_menus);
    update_current_image_status(list_changed);

    refresh(REFRESH_TITLE);
    current_image->first_image = FALSE;
}

void zoom_in(gfloat ratio)
{
    gint pointer_x, pointer_y;
    gfloat x, y;

    if (options->zoom_pointer) {
        /* The pointer is the zoom center. */
        gdk_window_get_pointer(gl_widget->window, &pointer_x, &pointer_y, NULL);
        x = (gfloat) pointer_x;
        y = (gfloat) pointer_y;
    } else {
        /* The zoom center is the midle of the window. */
        x = rt->wid_size->width / 2.0;
        y = rt->wid_size->height / 2.0;
    }

    matrix_zoom(ratio, x, y);
    refresh(REFRESH_IMAGE | REFRESH_STATUS | APPEND_HISTORY);
}

static gboolean has_do_later(GSourceDummyMarshal idle_func);
static void add_do_later(GSourceDummyMarshal idle_func);
static void remove_do_later(GSourceDummyMarshal idle_func);

static gboolean refresh_burst(void)
{
    remove_do_later(redraw);
    refresh(REFRESH_BURST);

    return FALSE;
}

gint diff_timeval_us(GTimeVal * after, GTimeVal * before)
{
    GTimeVal diff;

    diff_timeval(&diff, after, before);
    return diff.tv_sec * G_USEC_PER_SEC + diff.tv_usec;
}

gint get_fps_limiter_delay(GTimeVal * previous_frame)
{
    GTimeVal now;
    gint diff;

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

    g_get_current_time(&now);
    diff =
        G_USEC_PER_SEC / options->fps - diff_timeval_us(&now, previous_frame);

    return diff;
}

void refresh(gint what)
{
    /* GTK does that automatically but with more overhead. */
    if (what & REFRESH_IMAGE) {
        if (!has_do_later(redraw)) {
            g_get_current_time(&last_redraw_scheduled);
            do_later(GDK_PRIORITY_REDRAW, redraw);
        }
    } else if (what & REFRESH_BURST) {
        if (!has_do_later(redraw)) {
            gint diff = get_fps_limiter_delay(&last_redraw_scheduled);
            diff -= last_redraw_delay;

            if (diff > 0) {
                add_do_later(redraw);
                g_timeout_add(diff / 4000, (GtkFunction) refresh_burst, NULL);
            } else
                refresh(REFRESH_IMAGE);
        }
    } else if (what & REFRESH_NOW) {
        remove_do_later(redraw);
        redraw();
    }

    if (what & REFRESH_STATUS)
        do_later(G_PRIORITY_HIGH, update_status_bar);

    if (what & APPEND_HISTORY)
        append_history();

    if (what & REFRESH_TITLE)
        do_later(G_PRIORITY_DEFAULT, update_window_title);

    if (what & REFRESH_SCROLL)
        update_scrollbars();
}

void update_current_image_status(gboolean find_number)
{
    if (current_image != NULL) {
        if (find_number)
            current_image->number = -1;

        fill_ident(current_image);
    }

    refresh(REFRESH_STATUS);
}

static GHashTable *idle_functions = NULL;

static void init_do_later(void)
{
    if (idle_functions == NULL)
        /* First time */
        idle_functions = g_hash_table_new(g_direct_hash, NULL);
}

static gboolean has_do_later(GSourceDummyMarshal idle_func)
{
    init_do_later();
    return g_hash_table_lookup(idle_functions, idle_func) != NULL;
}

static void add_do_later(GSourceDummyMarshal idle_func)
{
    init_do_later();
    g_hash_table_insert(idle_functions, idle_func, idle_func);
}

static void remove_do_later(GSourceDummyMarshal idle_func)
{
    init_do_later();
    g_hash_table_remove(idle_functions, idle_func);
}


static gboolean wrapper(gpointer data)
{
    GSourceDummyMarshal func = data;
    remove_do_later(func);
    func();
    return FALSE;
}

void do_later(gint priority, GSourceDummyMarshal func)
{
    if (has_do_later(func))
        /* Already scheduled */
        return;

    add_do_later(func);
    g_idle_add_full(priority, wrapper, func, NULL);
}
