/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/*
 *  Copyright (C) 2004  Hidetaka Iwai
 *
 *  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, 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.
 */

/*
 * The original code is  src/galeon-embed-autoscroller.c in galeon-1.3.18
 *  Copyright (C) 2002  Ricardo Fern?ndez Pascual
 */


#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "kz-autoscroller.h"

#include <gtk/gtkimage.h>
#include <gtk/gtkwindow.h>
#include <gtk/gtkmain.h>
#include <stdlib.h>
#include <math.h>

typedef struct _KzAutoscrollerPriv KzAutoscrollerPriv;
struct _KzAutoscrollerPriv {
	KzEmbed *embed;
	GtkWidget *widget;
	guint start_x;
	guint start_y;
	gfloat step_x;
	gfloat step_y;
	gfloat roundoff_error_x;
	gfloat roundoff_error_y;
	gint msecs;
	guint timeout_id;
	gboolean active;
};

#define KZ_AUTOSCROLLER_GET_PRIVATE(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), KZ_TYPE_AUTOSCROLLER, KzAutoscrollerPriv))

static void     dispose        (GObject *object);

static gboolean kz_autoscroller_motion_cb      (GtkWidget *widget,
						GdkEventMotion *e,
						KzAutoscroller *as);
static gboolean kz_autoscroller_mouse_press_cb (GtkWidget *widget,
						GdkEventButton *e,
						KzAutoscroller *as);
static gboolean kz_autoscroller_key_press_cb   (GtkWidget *widget,
						GdkEventKey *e,
						KzAutoscroller *as);
static gint     kz_autoscroller_timeout_cb     (gpointer data);
static void     kz_autoscroller_stop           (KzAutoscroller *as);

static GtkWidget *autoscroll_icon = NULL;

G_DEFINE_TYPE(KzAutoscroller, kz_autoscroller, G_TYPE_OBJECT)

static void
kz_autoscroller_class_init (KzAutoscrollerClass *klass)
{
	GObjectClass *object_class;
	GdkPixbuf *icon_pixbuf = NULL;
	GdkPixmap *icon_pixmap = NULL;
	GdkBitmap *icon_bitmap = NULL;
	GtkWidget *icon_img;

	object_class = (GObjectClass *) klass;

	object_class->dispose = dispose;

	/* initialize the autoscroll icon */

	icon_pixbuf = gdk_pixbuf_new_from_file (KZ_DATADIR "/icons/autoscroll.xpm", NULL);
	if(icon_pixbuf)
	{
		gdk_pixbuf_render_pixmap_and_mask (icon_pixbuf, &icon_pixmap,
					   &icon_bitmap, 128);
		g_object_unref (icon_pixbuf);

	}
	else
	{
		g_warning("Fail to load an autoscroll icon\n");
	}

	/*
	  gtk_widget_push_visual (gdk_rgb_get_visual ());
	  gtk_widget_push_colormap (gdk_rgb_get_cmap ());
	*/
	if(icon_pixmap && icon_bitmap)
	{
		icon_img = gtk_image_new_from_pixmap (icon_pixmap, icon_bitmap);

		autoscroll_icon = gtk_window_new (GTK_WINDOW_POPUP);
		gtk_widget_realize (autoscroll_icon);
		gtk_container_add (GTK_CONTAINER (autoscroll_icon), icon_img);
		gtk_widget_shape_combine_mask (autoscroll_icon, icon_bitmap, 0, 0);

	/*
	  gtk_widget_pop_visual ();
	  gtk_widget_pop_colormap ();
	*/
		
		g_object_unref (icon_pixmap);
		g_object_unref (icon_bitmap);

		gtk_widget_show_all (icon_img);
	}
	else
	{
		g_warning("Fail to make an autoscroll cursor\n");
	}
	g_type_class_add_private (klass, sizeof(KzAutoscrollerPriv));
}

static void 
kz_autoscroller_init (KzAutoscroller *as)
{
	KzAutoscrollerPriv *priv = KZ_AUTOSCROLLER_GET_PRIVATE (as);

	priv->embed  = NULL;
	priv->widget = NULL;
	priv->active = FALSE;
	priv->msecs  = 33;
}

static void
dispose (GObject *object)
{
	KzAutoscroller *as = KZ_AUTOSCROLLER (object);
	KzAutoscrollerPriv *priv = KZ_AUTOSCROLLER_GET_PRIVATE (as);

	if (priv->embed)
	{
		g_object_unref (priv->embed);
		priv->embed = NULL;
	}

	if (priv->timeout_id)
	{
		g_source_remove (priv->timeout_id);
		priv->timeout_id = 0;
	}

	if (G_OBJECT_CLASS (kz_autoscroller_parent_class)->dispose)
		G_OBJECT_CLASS (kz_autoscroller_parent_class)->dispose(object);
}

KzAutoscroller *
kz_autoscroller_new (void)
{
	KzAutoscroller *as = g_object_new (KZ_TYPE_AUTOSCROLLER, NULL);
	return as;
}

void
kz_autoscroller_set_embed (KzAutoscroller *as,
			   KzEmbed *embed)
{
	KzAutoscrollerPriv *priv = KZ_AUTOSCROLLER_GET_PRIVATE (as);

	if (priv->embed)
	{
		g_object_unref (priv->embed);
	}
	
	priv->embed = g_object_ref (embed);
}

void
kz_autoscroller_start_scroll (KzAutoscroller *as,
			      GtkWidget *widget, gint x, gint y)
{
	static GdkCursor *cursor = NULL;
	KzAutoscrollerPriv *priv = KZ_AUTOSCROLLER_GET_PRIVATE (as);

	g_return_if_fail (priv->embed);
	g_return_if_fail (autoscroll_icon);
	
	if (priv->active)
	{
		return;
	}
	priv->active = TRUE;
	
	g_object_ref (as);

	priv->widget = g_object_ref (widget);

	/* get a new cursor, if necessary */
	if (!cursor) cursor = gdk_cursor_new (GDK_FLEUR);

	/* show icon */
	gtk_window_move (GTK_WINDOW (autoscroll_icon), x - 12, y - 12);
	gtk_widget_show (autoscroll_icon);

	/* set positions */
	priv->start_x = x;
	priv->start_y = y;
	priv->step_x = 0;
	priv->step_y = 0;
	priv->roundoff_error_x = 0;
	priv->roundoff_error_y = 0;

	/* attach signals */
	g_signal_connect (widget, "motion_notify_event",
			    G_CALLBACK (kz_autoscroller_motion_cb), as);
	g_signal_connect (widget, "button_press_event",
			  G_CALLBACK (kz_autoscroller_mouse_press_cb), as);
	g_signal_connect (widget, "key_press_event",
			  G_CALLBACK (kz_autoscroller_key_press_cb), as);
	priv->timeout_id =
		g_timeout_add (priv->msecs,
			       kz_autoscroller_timeout_cb, as);

	/* grab the pointer */
	gtk_grab_add (widget);
	gdk_pointer_grab (widget->window, FALSE,
			  GDK_POINTER_MOTION_MASK |
			  GDK_BUTTON_PRESS_MASK,
			  NULL, cursor, GDK_CURRENT_TIME);
	gdk_keyboard_grab (widget->window, FALSE, GDK_CURRENT_TIME);
}

static gboolean
kz_autoscroller_motion_cb (GtkWidget *widget, GdkEventMotion *e,
			   KzAutoscroller *as)
{
	KzAutoscrollerPriv *priv = KZ_AUTOSCROLLER_GET_PRIVATE (as);
	gint x_dist, x_dist_abs, y_dist, y_dist_abs;

	if (!priv->active)
	{
		return FALSE;
	}

	/* get distance between scroll center and cursor */
	x_dist = e->x_root - priv->start_x;
	x_dist_abs = abs (x_dist);
	y_dist = e->y_root - priv->start_y;
	y_dist_abs = abs (y_dist);

	/* calculate scroll step */
	if (y_dist_abs <= 48.0)
	{
		priv->step_y = (float) (y_dist / 4) / 6.0;
	}
	else if (y_dist > 0)
	{
		priv->step_y = (y_dist - 48.0) / 2.0 + 2.0;
	}
	else 
	{
		priv->step_y = (y_dist + 48.0) / 2.0 - 2.0;
	}

	if (x_dist_abs <= 48.0)
	{
		priv->step_x = (float) (x_dist / 4) / 6.0;
	}
	else if (x_dist > 0)
	{
		priv->step_x = (x_dist - 48.0) / 2.0 + 2.0;
	}
	else 
	{
		priv->step_x = (x_dist + 48.0) / 2.0 - 2.0;
	}

	return TRUE;
}

static gboolean
kz_autoscroller_mouse_press_cb (GtkWidget *widget, GdkEventButton *e,
				KzAutoscroller *as)
{
        /* ungrab and disconnect */
	kz_autoscroller_stop (as);

	return TRUE;
}

static gboolean
kz_autoscroller_key_press_cb (GtkWidget *widget, GdkEventKey *e,
			      KzAutoscroller *as)
{
	/* ungrab and disconnect */
	kz_autoscroller_stop (as);

	return TRUE;
}

static gint
kz_autoscroller_timeout_cb (gpointer data)
{
	GTimeVal start_time, finish_time;
	long elapsed_msecs;
	KzAutoscroller *as = data;
	KzAutoscrollerPriv *priv = KZ_AUTOSCROLLER_GET_PRIVATE (as);
	gfloat scroll_step_y_adj;
	gint scroll_step_y_int;
	gfloat scroll_step_x_adj;
	gint scroll_step_x_int;

	g_return_val_if_fail (KZ_IS_AUTOSCROLLER (as), FALSE);
	g_return_val_if_fail (KZ_IS_EMBED (priv->embed), FALSE);

        /* return if we're not supposed to scroll */
	if (!priv->step_y && !priv->step_x)
	{
		return TRUE;
	}

	/* calculate the number of pixels to scroll */
	scroll_step_y_adj = priv->step_y * priv->msecs / 33;
	scroll_step_y_int = scroll_step_y_adj;
	priv->roundoff_error_y += (scroll_step_y_adj - scroll_step_y_int);

	if (fabs (priv->roundoff_error_y) >= 1.0)
	{
		scroll_step_y_int += priv->roundoff_error_y;
		priv->roundoff_error_y -= (gint) priv->roundoff_error_y;
	}

	scroll_step_x_adj = priv->step_x * priv->msecs / 33;
	scroll_step_x_int = scroll_step_x_adj;
	priv->roundoff_error_x += (scroll_step_x_adj - scroll_step_x_int);

	if (fabs (priv->roundoff_error_x) >= 1.0)
	{
		scroll_step_x_int += priv->roundoff_error_x;
		priv->roundoff_error_x -= (gint) priv->roundoff_error_x;
	}

	/* exit if we're not supposed to scroll yet */
	if (!scroll_step_y_int && !scroll_step_x_int) return TRUE;
	
	/* get the time before we tell the embed to scroll */
	g_get_current_time(&start_time);

	/* do scrolling, moving at a constart speed regardless of the
	 * scrolling delay */

	/* FIXME: if mozilla is able to do diagonal scrolling in a
	 * reasonable manner at some point, this should be changed to
	 * calculate x and pass both values instead of just y */
	kz_embed_fine_scroll (priv->embed, scroll_step_x_int, scroll_step_y_int);

	/* find out how long the scroll took */
	g_get_current_time(&finish_time);
	elapsed_msecs = (1000000L * finish_time.tv_sec + finish_time.tv_usec -
			 1000000L * start_time.tv_sec - start_time.tv_usec) /
			1000;

	/* check if we should update the scroll delay */
	if ((elapsed_msecs >= priv->msecs + 5) ||
	    ((priv->msecs > 20) && (elapsed_msecs < priv->msecs - 10)))
	{
		/* update the scrolling delay, with a
		 * minimum delay of 20 ms */
		priv->msecs = (elapsed_msecs + 10 >= 20) ? elapsed_msecs + 10 : 20;

		/* create new timeout with adjusted delay */
		priv->timeout_id = g_timeout_add (priv->msecs, kz_autoscroller_timeout_cb, as);

		/* kill the old timeout */
		return FALSE;
	}

	/* don't kill timeout */
	return TRUE;
}

static void
kz_autoscroller_stop (KzAutoscroller *as)
{
	KzAutoscrollerPriv *priv = KZ_AUTOSCROLLER_GET_PRIVATE (as);

	g_return_if_fail (autoscroll_icon);

	/* ungrab the pointer if it's grabbed */
	if (gdk_pointer_is_grabbed ())
	{
		gdk_pointer_ungrab (GDK_CURRENT_TIME);
	}

	gdk_keyboard_ungrab (GDK_CURRENT_TIME);

	/* hide the icon */
	gtk_widget_hide (autoscroll_icon);

	g_return_if_fail (priv->widget);

	gtk_grab_remove (priv->widget);

	/* disconnect all of the signals */
	g_signal_handlers_disconnect_matched (priv->widget, G_SIGNAL_MATCH_DATA, 0, 0, 
					      NULL, NULL, as);
	if (priv->timeout_id)
	{
		g_source_remove (priv->timeout_id);
		priv->timeout_id = 0;
	}

	g_object_unref (priv->widget);
	priv->widget = NULL;

	priv->active = FALSE;
	g_object_unref (as);
}

