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

/****************************************************************
 * Callbacks to control the gl_widget through its parent window *
 ****************************************************************/

#include <gdk/gdkkeysyms.h>     /* GDK_* */

#include "gliv.h"
#include "callbacks.h"
#include "math_floats.h"        /* atan2f(), powf() */
#include "options.h"
#include "rendering.h"
#include "params.h"
#include "main.h"
#include "zoom_frame.h"
#include "move_pointer.h"
#include "matrix.h"
#include "cursors.h"
#include "next_image.h"
#include "gliv-image.h"

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

/* Left button saved position in the root window. */
static gfloat old1_root_x, old1_root_y;

/* Where the left button press occurred in the window. */
static gfloat press1_win_x, press1_win_y;

/* Where the middle button press occurred in the window. */
static gfloat press2_win_x, press2_win_y;

/* Right button saved position in the window. */
static gint old3_win_x, old3_win_y;

/* If the middle button is released. */
static gboolean mouse_wheel_zoom = TRUE;

static gboolean button_press_event(GtkWidget * unused, GdkEventButton * event)
{
    if (current_image == NULL)
        return FALSE;

    switch (event->button) {
    case 1:
        old1_root_x = event->x_root;
        old1_root_y = event->y_root;

        press1_win_x = event->x;
        press1_win_y = event->y;
        break;

    case 2:
        mouse_wheel_zoom = FALSE;
        press2_win_x = event->x;
        press2_win_y = event->y;
        break;

    case 3:
        /* We now have one of the zoom rectangle vertex. */
        old3_win_x = (gint) event->x;
        old3_win_y = (gint) event->y;
        break;
    }

    return FALSE;
}

#define CHANGE_IMAGE_SCREEN_RATIO 20

static void detect_image_change(GdkEventButton * event)
{
    int dx = event->x - press2_win_x;
    int dy = event->y - press2_win_y;
    int dir = 0;

    if (!(event->state & GDK_CONTROL_MASK))
        return;

    if (dx > rt->scr_width / CHANGE_IMAGE_SCREEN_RATIO ||
        dy > rt->scr_height / CHANGE_IMAGE_SCREEN_RATIO)
        dir = 1;
    else if (-dx > rt->scr_width / CHANGE_IMAGE_SCREEN_RATIO ||
             -dy > rt->scr_height / CHANGE_IMAGE_SCREEN_RATIO)
        dir = -1;

    if (dir)
        load_direction(dir);
}

static gboolean button_release_event(GtkWidget * unused, GdkEventButton * event)
{
    if (current_image == NULL)
        return FALSE;

    switch (event->button) {
    case 1:
        refresh(APPEND_HISTORY);
        break;

    case 2:
        mouse_wheel_zoom = TRUE;
        detect_image_change(event);
        break;

    case 3:
        zoom_frame();
    }

    return FALSE;
}

/* Checks whether the pointer is on a border then moves it and returns TRUE. */
static gboolean warp_pointer(gfloat x, gfloat y)
{
    gboolean warped;
    gint new_x, new_y;

    if (x <= 0.0) {
        new_x = rt->scr_width - 1;
        warped = TRUE;

    } else if (x >= rt->scr_width - 1.0) {
        new_x = 0;
        warped = TRUE;

    } else {
        new_x = (gint) x;
        warped = FALSE;
    }

    if (y <= 0.0) {
        new_y = rt->scr_height - 1;
        warped = TRUE;

    } else if (y >= rt->scr_height - 1.0) {
        new_y = 0;
        warped = TRUE;

    } else
        new_y = (gint) y;

    if (warped)
        move_pointer(new_x, new_y);

    return warped;
}

/*
 * Remember the pointer position to know the sliding distance, the angle,
 * or the zoom when the first button is pressed.
 */
static void update_coordinates1(GdkEventMotion * event)
{
    old1_root_x = event->x_root;
    old1_root_y = event->y_root;
}

static gint process_move(GdkEventMotion * event)
{
    gfloat root_x, root_y;

    root_x = event->x_root;
    root_y = event->y_root;

    matrix_move(root_x - old1_root_x, root_y - old1_root_y);

    return REFRESH_BURST;
}

static gint process_rotation(GdkEventMotion * event)
{
    gfloat center_x, center_y, x0, y0, x1, y1, angle;
    gint win_x, win_y;

    gdk_window_get_root_origin(gl_widget->window, &win_x, &win_y);

    /* Window center in root coordinates. */
    center_x = win_x + rt->wid_size->width / 2.0;
    center_y = win_y + rt->wid_size->height / 2.0;

    /* First rotation vector. */
    x0 = old1_root_x - center_x;
    y0 = old1_root_y - center_y;

    /* Second rotation vector. */
    x1 = event->x_root - center_x;
    y1 = event->y_root - center_y;

    angle = atan2f(y0, x0) + atan2f(x1, y1) - PI / 2.0;

    matrix_rotate(angle);

    return REFRESH_BURST | REFRESH_STATUS;
}

static gint process_zoom(GdkEventMotion * event)
{
    gfloat center_x, center_y, delta, ratio;

    delta = event->y_root - old1_root_y;
    ratio = powf(ZOOM_FACTOR, delta / 10.0);

    if (options->zoom_pointer) {
        center_x = press1_win_x;
        center_y = press1_win_y;
    } else {
        center_x = rt->wid_size->width / 2.0;
        center_y = rt->wid_size->height / 2.0;
    }

    matrix_zoom(ratio, center_x, center_y);

    return REFRESH_BURST | REFRESH_STATUS;
}

static gboolean motion_notify_event(GtkWidget * unused, GdkEventMotion * event)
{
    /* move_pointer() generates a motion_notify_event, useless here. */
    static gboolean skip = FALSE;
    gint what = 0;

    if (skip) {
        /* We come just after a move_pointer(). */
        skip = FALSE;
        update_coordinates1(event);
        return TRUE;
    }

    if (rt->cursor_hidden)
        show_cursor();
    else
        schedule_hide_cursor();

    if (current_image == NULL)
        return FALSE;

    if (event->state & GDK_BUTTON1_MASK) {

        if (event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) {
            if (event->state & GDK_CONTROL_MASK)
                what = process_rotation(event);

            if (event->state & GDK_SHIFT_MASK)
                what |= process_zoom(event);
        } else
            what = process_move(event);

        refresh(what);

        update_coordinates1(event);
        skip = warp_pointer(event->x_root, event->y_root);

    } else if (event->state & GDK_BUTTON3_MASK) {

        /* We draw the zoom rectangle. */
        set_zoom_frame(old3_win_x, old3_win_y,
                       (gint) event->x - old3_win_x,
                       (gint) event->y - old3_win_y);

        draw_zoom_frame();
    }

    return TRUE;
}

/* Called when pressing an arrow key. */
static void move_or_rotate(guint state, gfloat x, gfloat y, gfloat angle)
{
    guint what = REFRESH_IMAGE | APPEND_HISTORY;

    if (state & GDK_CONTROL_MASK) {
        matrix_rotate(angle);
        what |= REFRESH_STATUS;
    } else
        matrix_move(x, y);

    refresh(what);
}

/* Mask to keep track of which arrow keys are pressed. */
#define ARROW_UP    (1 << 0)
#define ARROW_LEFT  (1 << 1)
#define ARROW_RIGHT (1 << 2)
#define ARROW_DOWN  (1 << 3)

/* Called twice to know how far we move in the two directions. */
static gfloat get_arrow_move(guint state, guint flag_pos, guint flag_neg)
{
    guint flags;

    flags = state & (flag_pos | flag_neg);

    if (flags == (flag_pos | flag_neg) || flags == 0)
        /* Both keys are pressed, or none => don't move. */
        return 0.0;

    if (state & flag_pos)
        /* Left or up. */
        return MOVE_OFFSET;

    /* Right or down. */
    return -MOVE_OFFSET;
}

/* An arrow key or something else was either pressed or released. */
static gboolean process_arrow_key(GdkEventKey * event, gboolean press)
{
    static guint arrow_state = 0;
    guint arrow;
    gfloat x, y, angle;

    if (current_image == NULL)
        return FALSE;

    switch (event->keyval) {
    case GDK_Up:
    case GDK_KP_Up:
        arrow = ARROW_UP;
        angle = BIG_ROTATION;
        break;

    case GDK_Left:
    case GDK_KP_Left:
        arrow = ARROW_LEFT;
        angle = SMALL_ROTATION;
        break;

    case GDK_Right:
    case GDK_KP_Right:
        arrow = ARROW_RIGHT;
        angle = -SMALL_ROTATION;
        break;

    case GDK_Down:
    case GDK_KP_Down:
        arrow = ARROW_DOWN;
        angle = -BIG_ROTATION;
        break;

    default:
        /* Key not found, try a keyboard accelerator. */
        return FALSE;
    }

    if (press)
        arrow_state |= arrow;
    else
        arrow_state &= ~arrow;

    if (press) {
        x = get_arrow_move(arrow_state, ARROW_LEFT, ARROW_RIGHT);
        y = get_arrow_move(arrow_state, ARROW_UP, ARROW_DOWN);

        move_or_rotate(event->state, x, y, angle);
    }

    return TRUE;
}

static gboolean key_press_event(GtkWidget * unused, GdkEventKey * event)
{
    /* Most keys are handled via keyboard accelerators. */
    switch (event->keyval) {
    case GDK_Escape:
        gui_quit();
        break;

    case GDK_KP_Space:
    case GDK_space:
    case GDK_Page_Down:
    case GDK_KP_Page_Down:
        load_direction(1);
        break;

    case GDK_BackSpace:
    case GDK_Page_Up:
    case GDK_KP_Page_Up:
        load_direction(-1);
        break;

    case GDK_KP_Subtract:
        zoom_in(1.0 / ZOOM_FACTOR);
        break;

    case GDK_KP_Add:
    case GDK_equal:
        zoom_in(ZOOM_FACTOR);
        break;

    default:
        /* Key not found, try an arrow key, */
        return process_arrow_key(event, TRUE);
    }

    return TRUE;
}

static gboolean key_release_event(GtkWidget * unused, GdkEventKey * event)
{
    return process_arrow_key(event, FALSE);
}

static gboolean scroll_event(GtkWidget * unused, GdkEventScroll * event)
{
    if (current_image == NULL)
        return FALSE;

    if (mouse_wheel_zoom) {

        if (event->direction == GDK_SCROLL_UP)
            zoom_in(1.0 / ZOOM_FACTOR);

        else if (event->direction == GDK_SCROLL_DOWN)
            zoom_in(ZOOM_FACTOR);

    } else {
        /* mouse_wheel_zoom == FALSE */

        if (event->direction == GDK_SCROLL_UP)
            load_direction(-1);

        else if (event->direction == GDK_SCROLL_DOWN)
            load_direction(1);
    }

    return TRUE;
}

void install_callbacks(gpointer object)
{
    g_signal_connect(object, "button-press-event",
                     G_CALLBACK(button_press_event), NULL);

    g_signal_connect(object, "button-release-event",
                     G_CALLBACK(button_release_event), NULL);

    g_signal_connect(object, "motion-notify-event",
                     G_CALLBACK(motion_notify_event), NULL);

    g_signal_connect(object, "key-press-event",
                     G_CALLBACK(key_press_event), NULL);

    g_signal_connect(object, "key-release-event",
                     G_CALLBACK(key_release_event), NULL);

    g_signal_connect(object, "delete-event", G_CALLBACK(gui_quit), NULL);

    g_signal_connect(object, "scroll-event", G_CALLBACK(scroll_event), NULL);

    g_signal_connect_after(object, "button-press-event",
                           G_CALLBACK(set_correct_cursor), NULL);

    g_signal_connect_after(object, "button-release-event",
                           G_CALLBACK(set_correct_cursor), NULL);

    g_signal_connect_after(object, "key-press-event",
                           G_CALLBACK(set_correct_cursor), NULL);

    g_signal_connect_after(object, "key-release-event",
                           G_CALLBACK(set_correct_cursor), NULL);

    g_signal_connect_after(object, "focus-in-event",
                           G_CALLBACK(set_correct_cursor), NULL);
}

static gboolean finish(gpointer data)
{
    gboolean *b = data;
    *b = TRUE;
    return FALSE;
}

/*
 * We call process_events() when rebuilding the images menus and to wait for a
 * refresh.
 */
void process_events(void)
{
    GTimeVal beginning, now;
    gint delay = 0;
    gboolean stop = FALSE;

    if (!gtk_events_pending())
        return;

    g_idle_add(finish, &stop);
    g_get_current_time(&beginning);

    while (!stop && delay < G_USEC_PER_SEC / 10 && gtk_events_pending()) {
        gtk_main_iteration_do(FALSE);
        g_get_current_time(&now);
        delay = (now.tv_sec - beginning.tv_sec) * G_USEC_PER_SEC +
            now.tv_usec - beginning.tv_usec;
    }

    if (!stop)
        g_idle_remove_by_data(&stop);
}
