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

/************************
 * The MODELVIEW matrix *
 ************************/

#include <string.h>             /* memcpy() */

#include "gliv.h"
#include "matrix.h"
#include "math_floats.h"        /* cosf(), sinf(), ... */
#include "options.h"
#include "opengl.h"
#include "gliv-image.h"

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

/*
 * OpenGL uses a transposed matrix, we use a 'normal' one,
 * we transpose it just before glLoadMatrix().
 *
 * Value:         Index:
 * c1  s1  0  x | 0   1   2   3
 * s2  c2  0  y | 4   5   6   7
 * 0   0   1  0 | 8   9   10  11 \ constant so
 * 0   0   0  1 | 12  13  14  15 / unused.
 */

#define MATRIX_C1 matrix[0]
#define MATRIX_S1 matrix[1]
#define MATRIX_X  matrix[3]
#define MATRIX_S2 matrix[4]
#define MATRIX_C2 matrix[5]
#define MATRIX_Y  matrix[7]

static gfloat matrix[8] = {
    /* We only use two rows. */
    1.0, 0.0, 0.0, 0.0,
    0.0, 1.0, 0.0, 0.0
};

/* Used to know if the OpenGL matrix and this one are in sync. */
static gboolean matrix_changed = TRUE;

static void touch_matrix(void)
{
    matrix_changed = TRUE;
}

G_GNUC_PURE gfloat get_matrix_zoom(void)
{
    /*
     * c1 == zoom*cos, s1 == zoom*sin
     * cos² + sin² == 1 => c1² + s1² == zoom²
     */
    return hypotf(MATRIX_C1, MATRIX_S1);
}

/* To be displayed in the status bar. */
G_GNUC_PURE gfloat get_matrix_angle(void)
{
    gfloat cosine, angle;

    cosine = MATRIX_C1 / get_matrix_zoom();
    angle = acosf(cosine);

    if (MATRIX_S1 < 0)
        /* Negative sine => negative angle. */
        angle *= -1.0;

    return angle;
}

/* OpenGL coordinates to window coordinates. */
static void point_coord(gfloat x, gfloat y, gfloat * res_x, gfloat * res_y)
{
    /* OpenGL coordinates through the modelview matrix. */
    *res_x = MATRIX_C1 * x + MATRIX_S1 * y + MATRIX_X;
    *res_y = MATRIX_S2 * x + MATRIX_C2 * y + MATRIX_Y;

    /* And now through the projection matrix. */
    *res_x += rt->wid_size->width / 2.0;
    *res_y += rt->wid_size->height / 2.0;
}

static gfloat min4(gfloat a, gfloat b, gfloat c, gfloat d)
{
    if (a > b)
        a = b;

    if (c > d)
        c = d;

    return MIN(a, c);
}

static gfloat max4(gfloat a, gfloat b, gfloat c, gfloat d)
{
    if (a < b)
        a = b;

    if (c < d)
        c = d;

    return MAX(a, c);
}

/* Convenient function: a == b ? */
G_GNUC_PURE gboolean float_equal(gfloat a, gfloat b)
{
    return fabsf(a - b) < 1e-3;
}

void get_matrix_bounding_box(gfloat * min_x, gfloat * max_x,
                             gfloat * min_y, gfloat * max_y)
{
    gfloat x0, y0, x1, y1;
    gfloat x2, y2, x3, y3;
    gfloat half_w, half_h;

    half_w = current_image->width / 2.0;
    half_h = current_image->height / 2.0;

    point_coord(-half_w, -half_h, &x0, &y0);
    point_coord(half_w, -half_h, &x1, &y1);
    point_coord(-half_w, half_h, &x2, &y2);
    point_coord(half_w, half_h, &x3, &y3);

    *min_x = min4(x0, x1, x2, x3);
    *max_x = max4(x0, x1, x2, x3);
    *min_y = min4(y0, y1, y2, y3);
    *max_y = max4(y0, y1, y2, y3);
}

/*** Input, output. ***/

void write_gl_matrix(void)
{
    /* *INDENT-OFF* */
    static gfloat transposed[16] = {
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
    };
    /* *INDENT-ON* */

    if (matrix_changed) {
        transposed[0] = MATRIX_C1;
        transposed[5] = MATRIX_C2;
        transposed[4] = MATRIX_S1;
        transposed[1] = MATRIX_S2;
        transposed[12] = MATRIX_X;
        transposed[13] = MATRIX_Y;

        glLoadMatrixf(transposed);
        matrix_changed = FALSE;
    }
}

/* In parameters dest and src, NULL is the current matrix. */
void matrix_cpy(gfloat * dest, gfloat * src)
{
    if (dest == NULL) {
        dest = matrix;
        touch_matrix();
    } else if (src == NULL)
        src = matrix;

    memcpy(dest, src, sizeof(matrix));
}

/*** Informations gathering about the matrix. ***/

G_GNUC_PURE static gboolean angle_is_right(void)
{
    gfloat modulo;

    modulo = fmodf(get_matrix_angle(), PI / 2.0);

    return float_equal(modulo, 0.0);
}

gboolean matrix_tile_visible(tile_dim * tile)
{
    static guint select_buffer[4];

    glSelectBuffer(4, select_buffer);
    glRenderMode(GL_SELECT);

    glInitNames();
    glPushName(0);

    glRectf(tile->x0, tile->y0, tile->x1, tile->y1);

    return glRenderMode(GL_RENDER) > 0;
}

G_GNUC_PURE gboolean is_matrix_symmetry(void)
{
    /*
     * c1 == c2  => rotation, c1 == -c2 => symmetry.
     * s1 == -s2 => rotation, s1 == s2 => symmetry.
     *
     * Since (c1/zoom)² + (s1/zoom)² == 1, we use c1 only if far enough from 0.
     */

    return fabsf(MATRIX_C1) > fabsf(MATRIX_S1) ?
        !float_equal(MATRIX_C1, MATRIX_C2) :
        !float_equal(MATRIX_S1, -MATRIX_S2);
}

G_GNUC_PURE gboolean get_matrix_has_changed(void)
{
    return matrix_changed;
}

G_GNUC_PURE gboolean is_filtering_needed(void)
{
    return (float_equal(get_matrix_zoom(), 1.0) == FALSE) ||
        (angle_is_right() == FALSE);
}

/*** Operations on the matrix. ***/

/* Returns FALSE if the image is already maximised. */
gboolean matrix_set_max_zoom(gint width, gint height, gboolean do_it)
{
    gfloat min_x, max_x, min_y, max_y, zoom;

    if (current_image == NULL)
        return TRUE;

    if (do_it == FALSE && (float_equal(MATRIX_X, 0.0) == FALSE ||
                           float_equal(MATRIX_Y, 0.0) == FALSE))
        /* Image not centered. */
        return TRUE;

    if (width < 0)
        width = rt->wid_size->width;

    if (height < 0)
        height = rt->wid_size->height;

    if ((options->maximize == FALSE &&
         (current_image->width < width && current_image->height < height)) ||
        (options->scaledown == FALSE &&
         (current_image->width > width || current_image->height > height)))
        return TRUE;

    get_matrix_bounding_box(&min_x, &max_x, &min_y, &max_y);
    zoom = MIN(width / (max_x - min_x), height / (max_y - min_y));

    if (float_equal(zoom, 1.0) == FALSE ||
        float_equal(MATRIX_X, 0.0) == FALSE ||
        float_equal(MATRIX_Y, 0.0) == FALSE) {

        if (do_it) {
            matrix_zoom(zoom, 0.0, 0.0);
            MATRIX_X = MATRIX_Y = 0.0;
        }
        return TRUE;
    }

    return FALSE;
}

void matrix_fit(gboolean fit_w, gboolean fit_h)
{
    gfloat min_x, max_x, min_y, max_y, zoom = INFINITY;

    get_matrix_bounding_box(&min_x, &max_x, &min_y, &max_y);

    if (fit_w) {
        MATRIX_X = 0.0;
        zoom = MIN(zoom, (gfloat) rt->wid_size->width / (max_x - min_x));
    }

    if (fit_h) {
        MATRIX_Y = 0.0;
        zoom = MIN(zoom, (gfloat) rt->wid_size->height / (max_y - min_y));
    }

    matrix_zoom(zoom, rt->wid_size->width / 2.0, rt->wid_size->height / 2.0);
}


static void reset_matrix(gfloat * mat)
{
    mat[0] = 1.0;
    mat[1] = 0.0;
    mat[2] = 0.0;
    mat[3] = 0.0;
    mat[4] = 0.0;
    mat[5] = 1.0;
    mat[6] = 0.0;
    mat[7] = 0.0;
}

void matrix_reset(void)
{
    reset_matrix(matrix);
    touch_matrix();
}

/*
 * Rotation:         Product:
 * cos   sin  0  0 | c1*cos+s2*sin   s1*cos+c2*sin   0  x*cos+y*sin
 * -sin  cos  0  0 | -c1*sin+s2*cos  -s1*sin+c2*cos  0  -x*sin+y*cos
 * 0     0    1  0 | 0               0               1  0
 * 0     0    0  1 | 0               0               0  1
 */
void matrix_rotate(gfloat angle)
{
    gfloat cosine, sine;
    gfloat c1, s1, c2, s2, x, y;
    gboolean zoom;

    /* Do we maximize after rotating? */
    zoom = (options->maximize || options->scaledown) &&
        (matrix_set_max_zoom(-1, -1, FALSE) == FALSE);

    cosine = cosf(angle);
    sine = sinf(angle);

    /* Backup, as we'll modify them. */
    c1 = MATRIX_C1;
    c2 = MATRIX_C2;
    s1 = MATRIX_S1;
    s2 = MATRIX_S2;
    x = MATRIX_X;
    y = MATRIX_Y;

    MATRIX_C1 = c1 * cosine + s2 * sine;
    MATRIX_S1 = s1 * cosine + c2 * sine;
    MATRIX_S2 = -c1 * sine + s2 * cosine;
    MATRIX_C2 = -s1 * sine + c2 * cosine;

    MATRIX_X = x * cosine + y * sine;
    MATRIX_Y = -x * sine + y * cosine;

    if (zoom)
        matrix_set_max_zoom(-1, -1, TRUE);

    touch_matrix();
}

void matrix_move(gfloat x, gfloat y)
{
    MATRIX_X += x;
    MATRIX_Y += y;

    touch_matrix();
}

/* (x, y): zoom center. */
void matrix_zoom(gfloat ratio, gfloat x, gfloat y)
{
    gfloat offset_x, offset_y;

    offset_x = rt->wid_size->width / 2.0 - x;
    offset_y = rt->wid_size->height / 2.0 - y;

    matrix_move(offset_x, offset_y);

    MATRIX_C1 *= ratio;
    MATRIX_S1 *= ratio;
    MATRIX_X *= ratio;
    MATRIX_S2 *= ratio;
    MATRIX_C2 *= ratio;
    MATRIX_Y *= ratio;

    matrix_move(-offset_x, -offset_y);
}

void flip_matrix(gfloat * mat, gboolean h_flip)
{
    /* Flip either the x or y row. */
    gint id = h_flip ? 4 : 0;

    mat[id] *= -1.0;
    mat[id + 1] *= -1.0;
    /* mat[id + 2] is 0.0. */
    mat[id + 3] *= -1.0;
}

void matrix_flip_h(void)
{
    /* Flip y. */
    flip_matrix(matrix, TRUE);
    touch_matrix();
}

void matrix_flip_v(void)
{
    /* Flip x. */
    flip_matrix(matrix, FALSE);
    touch_matrix();
}

void matrix_set_initial_position(void)
{
    gfloat min_x, max_x, min_y, max_y;
    enum image_position pos = options->initial_pos;

    if (pos == POSITION_KEEP)
        return;

    get_matrix_bounding_box(&min_x, &max_x, &min_y, &max_y);

    if (pos == POSITION_CENTER) {
        matrix_move((rt->wid_size->width - min_x - max_x) / 2.0,
                    (rt->wid_size->height - min_y - max_y) / 2.0);
        return;
    }

    if (min_x < 0.0 || max_x > (gfloat) rt->wid_size->width) {
        if (pos == POSITION_TOP_LEFT || pos == POSITION_BOTTOM_LEFT)
            matrix_move(-min_x, 0.0);
        else
            matrix_move((gfloat) rt->wid_size->width - max_x, 0.0);
    }

    if (min_y < 0.0 || max_y > (gfloat) rt->wid_size->height) {
        if (pos == POSITION_TOP_LEFT || pos == POSITION_TOP_RIGHT)
            matrix_move(0.0, -min_y);
        else
            matrix_move(0.0, (gfloat) rt->wid_size->height - max_y);
    }
}

void configure_matrix(GlivImage * im)
{
    GlivImage *old = current_image;

    current_image = im;
    if (im->first_image || options->keep_transfo == FALSE) {
        matrix_reset();
        matrix_rotate(im->initial_angle);
        if (im->initial_h_flip)
            matrix_flip_h();
        if (options->maximize || options->scaledown)
            matrix_set_max_zoom(-1, -1, TRUE);
    }

    matrix_set_initial_position();

    current_image = old;
}

gfloat *new_matrix(void)
{
    gfloat *new_mat = g_new(gfloat, 8);

    reset_matrix(new_mat);
    return new_mat;
}

gfloat *get_matrix_for_image(GlivImage * im)
{
    gfloat current_matrix[8];
    gfloat *new_mat;

    matrix_cpy(current_matrix, NULL);
    configure_matrix(im);

    new_mat = new_matrix();
    matrix_cpy(new_mat, NULL);

    matrix_cpy(NULL, current_matrix);

    return new_mat;
}
