/* GDM - The Gnome Display Manager
 * Copyright (C) 1999, 2000 Martin K. Petersen <mkp@mkp.net>
 *
 * This file Copyright (c) 2003 Sun Microsystems Inc.
 *
 * 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
 */

#include "config.h"

#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <syslog.h>
#include <math.h>

#include <glib.h>
#include <gmodule.h>
#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include <gtk/gtk.h>
#include <glib/gi18n.h>

#include <X11/Xlib.h>
 
#ifdef HAVE_XINPUT
#include <X11/extensions/XInput.h>
#endif
 
/*
 * Note that CONFIGFILE will have to be changed to something more generic
 * if this module is ever moved outside of gdm.
 */

#define CONFIGFILE GDMCONFDIR "/modules/AccessDwellMouseEvents"
#define iseol(ch)       ((ch) == '\r' || (ch) == '\f' || (ch) == '\0' || \
                        (ch) == '\n')

typedef enum
{
        BINDING_DWELL_BORDER_TOP        = 1 << 0,
        BINDING_DWELL_BORDER_BOTTOM     = 1 << 1,
        BINDING_DWELL_BORDER_RIGHT      = 1 << 2,
        BINDING_DWELL_BORDER_LEFT       = 1 << 3,
        BINDING_DWELL_BORDER_ERROR      = 1 << 4
} BindingType;

typedef enum
{
        BINDING_DWELL_DIRECTION_IN        = 1 << 0,
        BINDING_DWELL_DIRECTION_OUT       = 1 << 1,
        BINDING_DWELL_DIRECTION_ERROR     = 1 << 2
} BindingDirection;

typedef struct {
      int num_gestures;
      BindingType *gesture; 
      BindingDirection start_direction;
} Dwell;

typedef struct {
        Dwell input;
        char *binding_str;
        GSList *actions;
        guint timeout;
} Binding;

typedef struct {
         BindingType type;
         BindingDirection direction;
         guint32 time;
} Crossings;

static int lineno = 0;
static GSList *binding_list = NULL;

extern char **environ;

static guint enter_signal_id = 0;
static guint leave_signal_id = 0;
static int xinput_type_motion = 0;

static Crossings *crossings = NULL;
static int cross_pos = 0;
static guint max_crossings = 0;
static XID *ext_input_devices = NULL;
static gint ext_device_count = 0;
static gboolean latch_core_pointer = TRUE;
 
static void create_event_watcher (void);
static void load_bindings(gchar *path);
static gchar * screen_exec_display_string (GdkScreen *screen, const char *old);
static gchar ** get_exec_environment (GdkScreen *screen);
static Binding * parse_line(gchar *buf);
static gboolean binding_already_used (Binding *binding);
static gboolean debug_gestures = FALSE;

BindingType get_binding_type(char c);
BindingDirection get_binding_direction(char c);

static gboolean
is_ext_device (XID id)
{
	gint i;
	for (i=0; i < ext_device_count; i++)
		if (id == ext_input_devices[i])
			return TRUE;

	if (debug_gestures)
		syslog (LOG_WARNING, "is-ext-device failed for %d", (int) id);

	return FALSE;
}

static void
init_xinput (GdkDisplay *display, GdkWindow *root)
{
#ifdef HAVE_XINPUT
	XEventClass      event_list[40];
	int              i, j, number = 0, num_devices; 
	XDeviceInfo  *devices = NULL;
	XDevice      *device = NULL;

	devices = XListInputDevices (GDK_DISPLAY_XDISPLAY (display),
		&num_devices);

	if (debug_gestures)
		syslog (LOG_WARNING, "Checking %d input devices...",
			num_devices);

	for (i=0; i < num_devices; i++) {
		if (devices[i].use == IsXExtensionDevice) {
			device = XOpenDevice (GDK_DISPLAY_XDISPLAY (display),
				devices[i].id);
			for (j=0; j < device->num_classes && number < 39; j++) {
				switch (device->classes[j].input_class) 
				{
				case ValuatorClass:
					DeviceMotionNotify (device, 
							    xinput_type_motion, 
							    event_list[number]);
					number++;
				default:
					break;
				}
			}
			++ext_device_count;

			if (ext_input_devices) {
				ext_input_devices = g_realloc (ext_input_devices,
					sizeof (XID *) * ext_device_count);
			} else {
				ext_input_devices = g_malloc (sizeof (XID *));
			}
			ext_input_devices[ext_device_count - 1] = devices[i].id;
		}
	}

	if (debug_gestures)
		syslog (LOG_WARNING, "%d event types available", number);

	if (XSelectExtensionEvent (GDK_WINDOW_XDISPLAY (root), 
				   GDK_WINDOW_XWINDOW (root),
				   event_list, number)) {
		if (debug_gestures)
			syslog (LOG_WARNING,
				"Can't select input device events!");
	}
#endif
}

static gchar *
screen_exec_display_string (GdkScreen *screen, const char *old)
{
	GString    *str;
	const gchar *old_display;
	gchar       *retval;
	gchar       *p;

	g_return_val_if_fail (GDK_IS_SCREEN (screen), NULL);

	old_display = gdk_display_get_name (gdk_screen_get_display (screen));

	str = g_string_new ("DISPLAY=");
	g_string_append (str, old_display);

	p = strrchr (str->str, '.');
	if (p && p >  strchr (str->str, ':'))
		g_string_truncate (str, p - str->str);

	g_string_append_printf (str, ".%d", gdk_screen_get_number (screen));

	retval = str->str;

	g_string_free (str, FALSE);

	return retval;
}

/**
 * get_exec_environment:
 *
 * Description: Modifies the current program environment to
 * ensure that $DISPLAY is set such that a launched application
 * inheriting this environment would appear on screen.
 *
 * Returns: a newly-allocated %NULL-terminated array of strings or
 * %NULL on error. Use g_strfreev() to free it.
 *
 * mainly ripped from egg_screen_exec_display_string in
 * gnome-panel/egg-screen-exec.c
 **/
static gchar **
get_exec_environment (GdkScreen *screen)
{
	gchar **retval = NULL;
	gint    i;
	gint    display_index = -1;

	g_assert (GDK_IS_SCREEN (screen));

	for (i=0; environ [i]; i++)
		if (strncmp (environ [i], "DISPLAY", 7) == 0)
			display_index = i;

	if (display_index == -1)
		display_index = i++;

	retval = g_new0 (char *, i + 1);

	for (i=0; environ [i]; i++)
		if (i == display_index)
			retval [i] = screen_exec_display_string (screen,
				environ[i]);
		else
			retval [i] = g_strdup (environ [i]);

	retval [i] = NULL;

	return retval;
}

BindingType
get_binding_type(char c)
{
	BindingType rc;

	if (c == toupper ('T'))
		rc = BINDING_DWELL_BORDER_TOP;
	else if (c == toupper ('B'))
		rc = BINDING_DWELL_BORDER_BOTTOM;
	else if (c == toupper ('R'))
		rc = BINDING_DWELL_BORDER_RIGHT;
	else if (c == toupper ('L'))
		rc = BINDING_DWELL_BORDER_LEFT;
	else
		rc = BINDING_DWELL_BORDER_ERROR;

	return rc;
}

BindingDirection
get_binding_direction(char c)
{
	BindingDirection rc;

	if (c == toupper ('I'))
		rc = BINDING_DWELL_DIRECTION_IN;
	else if (c == toupper ('O'))
		rc = BINDING_DWELL_DIRECTION_OUT;
	else
		rc = BINDING_DWELL_DIRECTION_ERROR;

	return rc;
}

static void
free_binding (Binding *binding)
{
	if (binding == NULL)
		return;

	g_slist_foreach (binding->actions, (GFunc)g_free, NULL);
	g_slist_free (binding->actions);
	g_free (binding->binding_str);
	g_free (binding->input.gesture);
	g_free (binding);
}

static Binding *
parse_line(gchar *buf)
{
	gchar *keystring, *keyservice;
	Binding *tmp_binding = NULL;
	static GdkDisplay *display = NULL;

	lineno++;
  
	if (!display) {
		if ((display = gdk_display_get_default ()) == NULL)
			return NULL;
	}

	if ((*buf == '#') || (iseol (*buf)) || (buf == NULL))
		return NULL;

	/* Find the binding name */
	keystring = strtok (buf, " \t\n\r\f");
	if (keystring == NULL) {
		/* TODO - Add an error message */
		return NULL;
	}

	tmp_binding = g_new0 (Binding, 1);
	tmp_binding->binding_str = g_strdup (keystring);

	if (strcmp (tmp_binding->binding_str, "<Add>") != 0) {
		BindingType bt;
		BindingDirection bd;
		guint timeout;
		gchar *tmp_string;
		int i, j;

		tmp_binding->input.gesture = g_new0 (BindingType,
			strlen (tmp_binding->binding_str));

		j=0;
		for (i=0; i < strlen(tmp_binding->binding_str); i++) {
			bt = get_binding_type (tmp_binding->binding_str[i]);

			if (bt == BINDING_DWELL_BORDER_ERROR) {
				if (debug_gestures)
					syslog (LOG_WARNING,
						"Invalid value in binding %s",
						tmp_binding->binding_str);

				continue;
			}

			tmp_binding->input.gesture[j++] = bt;
		}
		tmp_binding->input.num_gestures = j;

		if (j > max_crossings)
			max_crossings = j;

		/* [TODO] Need to clean up here. */
		tmp_string = strtok (NULL, " \t\n\r\f");
		if (tmp_string == NULL) {
			/* TODO - Add an error message */
			free_binding (tmp_binding);
			return NULL;
		}

		bd = get_binding_direction (tmp_string[0]);

		if (bd == BINDING_DWELL_DIRECTION_ERROR) {
			if (debug_gestures)
			    syslog (LOG_WARNING, "Invalid value in binding %s",
			       tmp_binding->binding_str);
		} else {
			tmp_binding->input.start_direction = bd;
		}

		/*
		 * Find the timeout duration (in ms). Timeout value is the 
		 * time within which consecutive keypress actions must be
		 * performed by the user before the sequence is discarded.
		 */

		tmp_string = strtok (NULL, " \t\n\r\f");
		if (tmp_string == NULL) {
			/* TODO - Add an error message */
			free_binding (tmp_binding);
			return NULL;
		}

		timeout = atoi (tmp_string);
		if (timeout <= 0) {
			/* TODO - Add an error message */;
			free_binding (tmp_binding);
			return NULL;
		}
		tmp_binding->timeout = timeout;
	}

	/* Find servcice. Permit blank space so arguments can be supplied. */
	keyservice = strtok (NULL, "\n\r\f");
	if (keyservice == NULL) {
		/* TODO - Add an error message */
		free_binding (tmp_binding);
		return NULL;
	 }

	/* skip over initial whitespace */
	while (*keyservice && isspace (*keyservice))
		keyservice++;

	tmp_binding->actions = g_slist_append (tmp_binding->actions,
		g_strdup (keyservice));

	return tmp_binding;
}

static gboolean
binding_already_used (Binding *binding)
{
	GSList *li;

	for (li=binding_list; li != NULL; li = li->next) {
		Binding *tmp_binding = (Binding*) li->data;

		if (tmp_binding != binding &&
		    tmp_binding->input.start_direction == binding->input.start_direction)
		{
			int i;

			for (i=0; i < tmp_binding->input.num_gestures; i++) {
				if (tmp_binding->input.gesture !=
				    binding->input.gesture)
					break;
			}

			if (i == tmp_binding->input.num_gestures)
				return TRUE;
		}
	}
	return FALSE;
}

static void
load_bindings(gchar *path)
{
	FILE *fp;
	Binding *tmp_binding;
	gchar buf[1024];

	fp = fopen (path, "r");
	if (fp == NULL) {
		/* TODO - I18n */
		if (debug_gestures)
			syslog (LOG_WARNING,
				"Cannot open bindings file: %s", path);
		return;
	}

	while (fgets (buf, sizeof (buf), fp) != NULL) {
		tmp_binding = (Binding *)parse_line (buf);

		if (tmp_binding) {
			/* Is the key already associated with an existing */
			/* binding? */
			if (strcmp (tmp_binding->binding_str, "<Add>") == 0) {
				/* Add another action to the last binding */
				Binding *last_binding;
				GSList *last_item = g_slist_last (binding_list);

				/* If there is no last_item to add onto */
				/* ignore the entry */
				if (last_item) {
				    last_binding = (Binding *)last_item->data;

				    /* Add the action to the last */
				    /*  binding's actions list */
				    last_binding->actions =
				      g_slist_append (last_binding->actions,
				      g_strdup ((gchar *)tmp_binding->actions->data));
				}
				free_binding (tmp_binding);

				/* Ignore duplicate bindings */
			} else if (!binding_already_used (tmp_binding))
				binding_list = g_slist_append (binding_list,
					tmp_binding);
			else
				free_binding (tmp_binding);
		}
	}
	fclose (fp);
}

static gboolean
change_cursor_back (gpointer data)
{
	GdkCursor *cursor = gdk_cursor_new (GDK_LEFT_PTR);
	gdk_window_set_cursor (gdk_get_default_root_window (), cursor);
	gdk_cursor_unref (cursor);

	return FALSE;
}


static gboolean
leave_enter_emission_hook (GSignalInvocationHint        *ihint,
                           guint                        n_param_values,
                           const GValue                 *param_values,
                           gpointer                     data)
{
	GObject *object;
	GtkWidget *widget;
	GtkWindow *window;
	GdkEventCrossing *event;
	GdkRectangle rect;
	GSList *li;
	double mid_x, mid_y;
	int i;

	object = g_value_get_object (param_values + 0);
	event  = g_value_get_boxed (param_values + 1);
	widget = GTK_WIDGET (object);
	window = GTK_WINDOW (object);

	if (event->detail == GDK_NOTIFY_INFERIOR ||
	    !GTK_IS_WINDOW (object) || !GTK_WIDGET_TOPLEVEL (object)) {
		return TRUE;
	}

	gdk_window_get_frame_extents (widget->window, &rect);

	mid_x = rect.x + (rect.width  / 2);
	mid_y = rect.y + (rect.height / 2);

	/* avoid division by 0 */
	if (fabs (event->x_root - mid_x) <= 0.001) {
		if (event->x_root < mid_x)
			crossings[cross_pos].type = BINDING_DWELL_BORDER_LEFT;
		else
			crossings[cross_pos].type = BINDING_DWELL_BORDER_RIGHT;
	} else {
		double slope = (event->y_root - mid_y) / (event->x_root - mid_x);

		if (event->y_root < mid_y) {
			if (slope > 1 || slope < -1)
				crossings[cross_pos].type = BINDING_DWELL_BORDER_TOP;
			else if (slope >= 0)
				crossings[cross_pos].type = BINDING_DWELL_BORDER_LEFT;
			else
				crossings[cross_pos].type = BINDING_DWELL_BORDER_RIGHT;
		} else {
			if (slope > 1 || slope < -1)
				crossings[cross_pos].type = BINDING_DWELL_BORDER_BOTTOM;
			else if (slope >= 0)
				crossings[cross_pos].type = BINDING_DWELL_BORDER_RIGHT;
			else
				crossings[cross_pos].type = BINDING_DWELL_BORDER_LEFT;
		}
	}

	if (ihint->signal_id == enter_signal_id)
		crossings[cross_pos].direction = BINDING_DWELL_DIRECTION_IN;
	else if (ihint->signal_id == leave_signal_id)
		crossings[cross_pos].direction = BINDING_DWELL_DIRECTION_OUT;

	if (debug_gestures) {
		if (crossings[cross_pos].type == BINDING_DWELL_BORDER_BOTTOM) 
			syslog (LOG_WARNING, "Crossing bottom.");
		else if (crossings[cross_pos].type == BINDING_DWELL_BORDER_TOP) 
			syslog (LOG_WARNING, "Crossing top.");
		else if (crossings[cross_pos].type == BINDING_DWELL_BORDER_LEFT) 
			syslog (LOG_WARNING, "Crossing left.");
		else if (crossings[cross_pos].type == BINDING_DWELL_BORDER_RIGHT) 
			syslog (LOG_WARNING, "Crossing right.");

		if (crossings[cross_pos].direction == BINDING_DWELL_DIRECTION_IN) 
			syslog (LOG_WARNING, "Crossing in.");
		else if (crossings[cross_pos].direction == BINDING_DWELL_DIRECTION_OUT) 
			syslog (LOG_WARNING, "Crossing out.");
	}

	crossings[cross_pos].time = event->time;

	/* Check to see if a gesture has been completed */
	for (li=binding_list; li != NULL; li = li->next) {
		Binding *curr_binding = (Binding *) li->data;
		GSList *act_li;
		gboolean retval;
		gchar **argv  = NULL;
		gchar **envp  = NULL;
		int start_pos = (cross_pos - curr_binding->input.num_gestures + 1 +
			max_crossings) % max_crossings;

		if (debug_gestures) {
			syslog (LOG_WARNING, "Checking against registered gestures");
		}

		/* being anal here */
		if (start_pos < 0)
			start_pos = 0;

		/* check direction */
		if (curr_binding->input.start_direction != crossings[start_pos].direction) 
			continue;

		/* check borders */
		for (i=0; i < curr_binding->input.num_gestures; i++) {
			if (curr_binding->input.gesture[i] !=
			    crossings[(start_pos + i) % max_crossings].type)
				break; 
		}

		if (i != curr_binding->input.num_gestures)
			continue;

		/* check timeout values */
		for (i=1; i < curr_binding->input.num_gestures; i++) {
			int cur_pos  = (start_pos + i) % max_crossings;
			int prev_pos = (start_pos + i - 1) % max_crossings; 
			guint32 diff_time = crossings[cur_pos].time -
					    crossings[prev_pos].time;
  
			if (curr_binding->timeout != 0 &&
			    curr_binding->timeout < diff_time)
				break; 
		}

		if (i != curr_binding->input.num_gestures)
			continue;

		/* gesture recognized */
		if (debug_gestures) {
			syslog (LOG_WARNING, "Found gesture");
		}

		for (act_li=curr_binding->actions; act_li != NULL; act_li=act_li->next) {
			gchar *action = (gchar *)act_li->data;

			g_return_val_if_fail (action != NULL, TRUE);

			if (!g_shell_parse_argv (action, NULL, &argv, NULL))
				continue;

			envp = get_exec_environment (gtk_window_get_screen(window));

			if (debug_gestures)
				syslog (LOG_WARNING, "Action is %s", action);

			retval = g_spawn_async (NULL, argv, envp,
				G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL);

			g_strfreev (argv);
			g_strfreev (envp);

			if ( ! retval) {
				GtkWidget *dialog = gtk_message_dialog_new (NULL,
						0, GTK_MESSAGE_ERROR,
						GTK_BUTTONS_OK,
						_("Error while trying to run (%s)\n"\
						"which is linked to (%s)"),
						action, curr_binding->binding_str);
				gtk_dialog_set_has_separator (GTK_DIALOG (dialog),
					FALSE);
				g_signal_connect (dialog, "response",
					G_CALLBACK (gtk_widget_destroy), NULL);
				gtk_widget_show (dialog);
			} else {
				GdkCursor *cursor = gdk_cursor_new (GDK_WATCH);
				gdk_window_set_cursor (gdk_get_default_root_window (),
					cursor);
				gdk_cursor_unref (cursor);
				g_timeout_add (2000, change_cursor_back, NULL);
				latch_core_pointer = FALSE;
				/* once we've recognized a gesture, we need to *
				 * leave the pointer alone */
			}
		}
	}

	cross_pos = (cross_pos + 1) % max_crossings;

	return TRUE;
}

static GdkFilterReturn
gestures_filter (GdkXEvent *gdk_xevent,
		 GdkEvent *event,
		 gpointer data)
{
	XEvent *xevent = (XEvent *)gdk_xevent;

	if (xevent->type == xinput_type_motion) {
		XDeviceMotionEvent *motion = (XDeviceMotionEvent *) xevent;
		if ((motion->axes_count < 2) || !is_ext_device (motion->deviceid))
			return GDK_FILTER_CONTINUE;
		if (latch_core_pointer)
		    XWarpPointer (motion->display, None, 
			      motion->root,
			      0, 0, 0, 0, motion->axis_data[0], motion->axis_data[1]);
	}
       	return GDK_FILTER_CONTINUE;
}

static void
create_event_watcher (void)
{
	GdkDisplay *display;
	gint i;

	display = gdk_display_get_default ();
	if (!display)
		return;

	load_bindings(CONFIGFILE);

	crossings = g_new0(Crossings, max_crossings);

	for (i=0; i < max_crossings; i++) {
		crossings[i].type      = BINDING_DWELL_BORDER_ERROR;
		crossings[i].direction = BINDING_DWELL_DIRECTION_ERROR;
		crossings[i].time      = 0;
	}

	init_xinput (display, 
		gdk_screen_get_root_window (
		gdk_display_get_default_screen (display)));

	gdk_window_add_filter (NULL, gestures_filter, NULL);

	/* set up emission hook */
	gtk_type_class (GTK_TYPE_WIDGET);
	enter_signal_id = g_signal_lookup ("enter-notify-event", GTK_TYPE_WIDGET);
	leave_signal_id = g_signal_lookup ("leave-notify-event", GTK_TYPE_WIDGET);

	g_signal_add_emission_hook (enter_signal_id, 0, 
		leave_enter_emission_hook, NULL, (GDestroyNotify) NULL); 
	g_signal_add_emission_hook (leave_signal_id, 0,
		leave_enter_emission_hook, NULL, (GDestroyNotify) NULL); 
}

/* The init function for this gtk module */
G_MODULE_EXPORT void gtk_module_init (int *argc, char* argv[]);

void gtk_module_init (int *argc, char* argv[])
{
	if (g_getenv ("GDM_DEBUG_GESTURES") != NULL)
		debug_gestures = TRUE;

	if (debug_gestures) {
		/* If not running under GDM, then need to openlog ourselves */
		if (g_getenv ("RUNNING_UNDER_GDM") == NULL)
			openlog ("gesturelistener", LOG_PID, LOG_DAEMON);

		syslog (LOG_WARNING, "dwellmouselistener loaded.");
	}

	create_event_watcher ();
}

/* EOF */
