/*
 * Copyright (C) 2012 One laptop per child
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Author: Carlos Garnacho  <carlos@lanedo.com>
 */

#include <gdk/gdkx.h>
#include <X11/extensions/XInput2.h>
#include "sugar-gesture-grabber.h"

typedef struct _ControllerData ControllerData;
typedef struct _TouchData TouchData;

struct _TouchData
{
	GdkDevice *device;
	GdkEventSequence *sequence;
	gboolean consumed;
};

struct _ControllerData
{
	SugarEventController *controller;
	GdkRectangle rect;
};

G_DEFINE_TYPE_WITH_PRIVATE (SugarGestureGrabber, sugar_gesture_grabber, G_TYPE_OBJECT)

static void
_sugar_gesture_grabber_notify_touch (SugarGestureGrabber *grabber,
				     GdkDevice           *device,
				     GdkEventSequence    *sequence,
				     gboolean             handled)
{
	SugarGestureGrabberPrivate *priv = grabber->priv;
	GdkDisplay *display;
	guint i;

	display = gdk_window_get_display (priv->root_window);

	for (i = 0; i < priv->touches->len; i++) {
		TouchData *data;

		data = &g_array_index (priv->touches, TouchData, i);

		if (device && data->device != device)
			continue;

		if (sequence && data->sequence != sequence)
			continue;

		if (data->consumed)
			continue;

		gdk_x11_display_error_trap_push (display);
		XIAllowTouchEvents (gdk_x11_display_get_xdisplay (display),
				    gdk_x11_device_get_id (data->device),
				    GPOINTER_TO_INT (data->sequence),
				    gdk_x11_window_get_xid (priv->root_window),
				    (handled) ? XIAcceptTouch : XIRejectTouch);

		gdk_x11_display_error_trap_pop_ignored (display);
		data->consumed = TRUE;
	}
}

static void
_sugar_gesture_grabber_add_touch (SugarGestureGrabber *grabber,
				  GdkDevice           *device,
				  GdkEventSequence    *sequence)
{
	SugarGestureGrabberPrivate *priv = grabber->priv;
	TouchData data;

	data.device = device;
	data.sequence = sequence;
	data.consumed = FALSE;
	g_array_append_val (priv->touches, data);
}

static void
_sugar_gesture_grabber_remove_touch (SugarGestureGrabber *grabber,
				     GdkDevice           *device,
				     GdkEventSequence    *sequence)
{
	SugarGestureGrabberPrivate *priv = grabber->priv;
	guint i;

	for (i = 0; i < priv->touches->len; i++) {
		TouchData *data;

		data = &g_array_index (priv->touches, TouchData, i);

		if (data->device == device &&
		    data->sequence == sequence) {
			g_array_remove_index_fast (priv->touches, i);
			break;
		}
	}
}

static gboolean
_sugar_gesture_grabber_cancel_timeout (SugarGestureGrabber *grabber)
{
	SugarGestureGrabberPrivate *priv = grabber->priv;

	_sugar_gesture_grabber_notify_touch (grabber, NULL, NULL, FALSE);
	priv->cancel_timeout_id = 0;

	return FALSE;
}

static void
sugar_gesture_grabber_finalize (GObject *object)
{
	SugarGestureGrabberPrivate *priv = SUGAR_GESTURE_GRABBER (object)->priv;
	guint i;

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

	_sugar_gesture_grabber_notify_touch (SUGAR_GESTURE_GRABBER (object),
					     NULL, NULL, FALSE);

	for (i = 0; i < priv->controllers->len; i++) {
		ControllerData *data;

		data = &g_array_index (priv->controllers, ControllerData, i);
		g_object_unref (data->controller);
	}

	g_array_free (priv->controllers, TRUE);
	g_array_free (priv->touches, TRUE);

	G_OBJECT_CLASS (sugar_gesture_grabber_parent_class)->finalize (object);
}

static void
sugar_gesture_grabber_class_init (SugarGestureGrabberClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->finalize = sugar_gesture_grabber_finalize;
}

static void
_grab_touch_events (GdkWindow *window)
{
        XIGrabModifiers mods = { 1 };
        unsigned char mask[4] = { 0 };
        GdkDisplay *display;
        XIEventMask evmask;

        XISetMask (mask, XI_TouchBegin);
        XISetMask (mask, XI_TouchUpdate);
        XISetMask (mask, XI_TouchEnd);

        evmask.deviceid = XIAllMasterDevices;
        evmask.mask_len = sizeof (mask);
        evmask.mask = mask;

        mods.modifiers = XIAnyModifier;
        display = gdk_window_get_display (window);

        XIGrabTouchBegin (gdk_x11_display_get_xdisplay (display),
                          XIAllMasterDevices,
                          gdk_x11_window_get_xid (window),
                          XINoOwnerEvents, &evmask, 1, &mods);
}

static GdkWindow *
_get_default_root_window (void)
{
        GdkDisplay *display;
        GdkScreen *screen;

        display = gdk_display_get_default ();
        screen = gdk_display_get_default_screen (display);

        return gdk_screen_get_root_window (screen);
}

static gboolean
_sugar_gesture_grabber_run_controllers (SugarGestureGrabber *grabber,
					GdkEvent            *event)
{
	SugarGestureGrabberPrivate *priv = grabber->priv;
	gboolean handled = FALSE;
	guint i;

	for (i = 0; i < priv->controllers->len; i++) {
		ControllerData *data;

		data = &g_array_index (priv->controllers, ControllerData, i);

		if (event->type == GDK_TOUCH_BEGIN &&
                    (event->touch.x_root < data->rect.x ||
                     event->touch.x_root > data->rect.x + data->rect.width ||
                     event->touch.y_root < data->rect.y ||
                     event->touch.y_root > data->rect.y + data->rect.height))
			continue;

		handled = sugar_event_controller_handle_event (data->controller,
							       event);

		if (handled) {
			guint state;

			state = sugar_event_controller_get_state (data->controller);

			if (state == SUGAR_EVENT_CONTROLLER_STATE_RECOGNIZED) {
				_sugar_gesture_grabber_notify_touch (grabber,
								     event->touch.device,
								     event->touch.sequence,
								     TRUE);
			}
		}
	}

	return handled;
}

static GdkFilterReturn
filter_function (GdkXEvent *xevent,
                 GdkEvent  *gdkevent,
                 gpointer   user_data)
{
        XGenericEventCookie *xge = xevent;
        SugarGestureGrabber *grabber;
        SugarGestureGrabberPrivate *priv;
        gboolean handled = FALSE;
        XIDeviceEvent *ev;
        GdkDisplay *display;
        GdkEvent *event;

        if (xge->type != GenericEvent)
                return GDK_FILTER_CONTINUE;

        grabber = user_data;
        priv = grabber->priv;

        display = gdk_window_get_display (priv->root_window);
        ev = (XIDeviceEvent *) xge->data;

        switch (ev->evtype) {
        case XI_TouchBegin:
                event = gdk_event_new (GDK_TOUCH_BEGIN);
                break;
        case XI_TouchEnd:
                event = gdk_event_new (GDK_TOUCH_END);
                break;
        case XI_TouchUpdate:
                event = gdk_event_new (GDK_TOUCH_UPDATE);
                break;
        default:
                return GDK_FILTER_CONTINUE;
        }

        if (ev->event != gdk_x11_window_get_xid (priv->root_window))
                return GDK_FILTER_CONTINUE;

        event->touch.window = g_object_ref (priv->root_window);
        event->touch.time = ev->time;
        event->touch.x = ev->event_x;
        event->touch.y = ev->event_y;
        event->touch.x_root = ev->root_x;
        event->touch.y_root = ev->root_y;
        event->touch.sequence = GINT_TO_POINTER (ev->detail);
        event->touch.emulating_pointer = (ev->flags & XITouchEmulatingPointer);

        handled = _sugar_gesture_grabber_run_controllers (grabber, event);

        if (!handled) {
                gdk_x11_display_error_trap_push (display);
                XIAllowTouchEvents (gdk_x11_display_get_xdisplay (display),
                                    ev->deviceid, ev->detail,
                                    gdk_x11_window_get_xid (priv->root_window),
                                    XIRejectTouch);
                gdk_x11_display_error_trap_pop_ignored (display);
        } else if (event->type == GDK_TOUCH_BEGIN) {
                _sugar_gesture_grabber_add_touch (grabber,
                                                  event->touch.device,
                                                  event->touch.sequence);
        } else if (event->type == GDK_TOUCH_END) {
                _sugar_gesture_grabber_notify_touch (grabber,
                                                     event->touch.device,
                                                     event->touch.sequence,
                                                     FALSE);
                _sugar_gesture_grabber_remove_touch (grabber,
                                                     event->touch.device,
                                                     event->touch.sequence);
        }

        if (handled) {
                if (priv->cancel_timeout_id)
                        g_source_remove (priv->cancel_timeout_id);

                priv->cancel_timeout_id =
                        gdk_threads_add_timeout (150,
                                                 (GSourceFunc) _sugar_gesture_grabber_cancel_timeout,
                                                 grabber);
        }

        gdk_event_free (event);

        return GDK_FILTER_REMOVE;
}

static void
sugar_gesture_grabber_init (SugarGestureGrabber *grabber)
{
	SugarGestureGrabberPrivate *priv;

	grabber->priv = priv = sugar_gesture_grabber_get_instance_private (grabber);
	priv->root_window = _get_default_root_window ();
	_grab_touch_events (priv->root_window);
	gdk_window_add_filter (NULL, filter_function, grabber);

	priv->touches = g_array_new (FALSE, FALSE, sizeof (TouchData));
	priv->controllers = g_array_new (FALSE, FALSE, sizeof (ControllerData));
}

SugarGestureGrabber *
sugar_gesture_grabber_new (void)
{
	return g_object_new (SUGAR_TYPE_GESTURE_GRABBER, NULL);
}

static ControllerData *
_sugar_gesture_grabber_find_controller (SugarGestureGrabber  *grabber,
					SugarEventController *controller,
					gint		     *pos)
{
	SugarGestureGrabberPrivate *priv;
	guint i;

	priv = grabber->priv;

	for (i = 0; i < priv->controllers->len; i++) {
		ControllerData *data;

		data = &g_array_index (priv->controllers, ControllerData, i);

		if (data->controller == controller) {
			if (pos)
				*pos = i;

			return data;
		}
	}

	return NULL;
}

void
sugar_gesture_grabber_add (SugarGestureGrabber  *grabber,
			   SugarEventController *controller,
			   const GdkRectangle   *rect)
{
	SugarGestureGrabberPrivate *priv;
	ControllerData data;

	g_return_if_fail (SUGAR_IS_GESTURE_GRABBER (grabber));
	g_return_if_fail (SUGAR_IS_EVENT_CONTROLLER (controller));

	if (_sugar_gesture_grabber_find_controller (grabber, controller, NULL)) {
		g_warning ("Controller is already on the gesture grabber"
			   " list. Controllers can only be added once.");
		return;
	}

	priv = grabber->priv;

	data.controller = g_object_ref (controller);
	data.rect = *rect;
	g_array_append_val (priv->controllers, data);
}

void
sugar_gesture_grabber_remove (SugarGestureGrabber  *grabber,
			      SugarEventController *controller)
{
	SugarGestureGrabberPrivate *priv;
	ControllerData *data;
	gint pos;

	g_return_if_fail (SUGAR_IS_GESTURE_GRABBER (grabber));
	g_return_if_fail (SUGAR_IS_EVENT_CONTROLLER (controller));

	priv = grabber->priv;
	data = _sugar_gesture_grabber_find_controller (grabber, controller, &pos);

	if (data) {
		g_array_remove_index_fast (priv->controllers, pos);
		sugar_event_controller_reset (data->controller);
		g_object_unref (data->controller);
	}
}
