/*
 * Copyright © 2014 Red Hat, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */
#include <config.h>

#include <assert.h>
#include <cairo.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <glib-unix.h>
#include <glib.h>
#include <gtk/gtk.h>
#include <libevdev/libevdev.h>
#include <libinput.h>
#include <linux/input.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "util-list.h"
#include "util-macros.h"
#include "util-strings.h"

#include "shared.h"

#ifdef HAVE_GTK_WAYLAND
#include <wayland-client.h>

#include "pointer-constraints-unstable-v1-client-protocol.h"
#ifdef HAVE_GTK4
#include <gdk/wayland/gdkwayland.h>
#else
#include <gdk/gdkwayland.h>
#endif
#endif

#ifdef HAVE_GTK_X11
#include <X11/X.h>
#include <X11/Xlib.h>
#ifdef HAVE_GTK4
#include <gdk/x11/gdkx.h>
#else
#include <gdk/gdkx.h>
#endif
#endif

#define clip(val_, min_, max_) min((max_), max((min_), (val_)))

enum touch_state {
	TOUCH_ACTIVE,
	TOUCH_ENDED,
	TOUCH_CANCELLED,
};

struct touch {
	enum touch_state state;
	int x, y;
};

struct point {
	double x, y;
};

struct evdev_device {
	struct list node;
	struct libevdev *evdev;
	struct libinput_device *libinput_device;
	int fd;
	guint source_id;
};

struct window {
	bool grab;
	struct tools_options options;
	struct list evdev_devices;

	GMainLoop *event_loop;

	GtkWidget *win;
	GtkWidget *area;
	int width, height; /* of window */

	/* sprite position */
	struct point pointer;
	struct point unaccelerated;

	/* these are for the delta coordinates, but they're not
	 * deltas, they are converted into abs positions */
	size_t ndeltas;
	struct point deltas[64];

	/* abs position */
	struct point abs;

	/* Wayland and X11 pointer locking */
	struct {
		bool locked;

#ifdef HAVE_GTK_WAYLAND
		struct zwp_pointer_constraints_v1 *wayland_pointer_constraints;
		struct zwp_locked_pointer_v1 *wayland_locked_pointer;
#endif
	} lock_pointer;

	/* scroll bar positions */
	struct {
		double vx, vy;
		double hx, hy;

		double vx_discrete, vy_discrete;
		double hx_discrete, hy_discrete;
	} scroll;

	/* touch positions */
	struct touch touches[32];

	/* l/m/r mouse buttons */
	struct {
		bool l, m, r;
		bool other;
		const char *other_name;
	} buttons;

	/* touchpad swipe */
	struct {
		int nfingers;
		double x, y;
	} swipe;

	struct {
		int nfingers;
		double scale;
		double angle;
		double x, y;
	} pinch;

	struct {
		int nfingers;
		bool active;
	} hold;

	struct {
		double x, y;
		double x_in, y_in;
		double x_down, y_down;
		double x_up, y_up;
		double pressure;
		double distance;
		double tilt_x, tilt_y;
		double rotation;
		double size_major, size_minor;
		bool is_down;

		/* these are for the delta coordinates, but they're not
		 * deltas, they are converted into abs positions */
		size_t ndeltas;
		struct point deltas[64];
	} tool;

	struct {
		struct {
			double position;
			int number;
		} ring;
		struct {
			double position;
			int number;
		} strip;
		struct {
			double position;
			int number;
		} dial;
	} pad;

	struct {
		int rel_x, rel_y; /* REL_X/Y */
		int x, y;         /* ABS_X/Y */
		struct {
			int x, y; /* ABS_MT_POSITION_X/Y */
			bool active;
		} slots[16];
		unsigned int slot; /* ABS_MT_SLOT */
		/* So we know when to re-fetch the abs axes */
		uintptr_t device, last_device;
	} evdev;

	struct libinput_device *devices[50];
};

#ifdef HAVE_GTK_WAYLAND
static void
wayland_registry_global(void *data,
			struct wl_registry *registry,
			uint32_t name,
			const char *interface,
			uint32_t version)
{
	struct window *w = data;

	if (!g_strcmp0(interface, "zwp_pointer_constraints_v1")) {
		w->lock_pointer.wayland_pointer_constraints =
			wl_registry_bind(registry,
					 name,
					 &zwp_pointer_constraints_v1_interface,
					 1);
	}
}

static void
wayland_registry_global_remove(void *data,
			       struct wl_registry *wl_registry,
			       uint32_t name)
{
}

static struct wl_registry_listener registry_listener = {
	wayland_registry_global,
	wayland_registry_global_remove
};

static bool
wayland_lock_pointer(struct window *w)
{
	GdkDisplay *gdk_display;
	GdkSeat *gdk_seat;
	GdkDevice *gdk_device;
	struct wl_display *display;
	struct wl_registry *registry;
	struct wl_pointer *wayland_pointer;
	struct wl_surface *surface;

	w->lock_pointer.wayland_pointer_constraints = NULL;

	gdk_display = gdk_display_get_default();
	display = gdk_wayland_display_get_wl_display(gdk_display);

	gdk_seat = gdk_display_get_default_seat(gdk_display);
	gdk_device = gdk_seat_get_pointer(gdk_seat);
	wayland_pointer = gdk_wayland_device_get_wl_pointer(gdk_device);

	registry = wl_display_get_registry(display);
	wl_registry_add_listener(registry, &registry_listener, w);
	wl_display_roundtrip(display);

	if (!w->lock_pointer.wayland_pointer_constraints)
		return false;

#ifdef HAVE_GTK4
	GtkNative *window = gtk_widget_get_native(w->win);
	GdkSurface *gdk_surface = gtk_native_get_surface(window);
	surface = gdk_wayland_surface_get_wl_surface(gdk_surface);
#else
	GdkWindow *window = gtk_widget_get_window(w->win);
	surface = gdk_wayland_window_get_wl_surface(window);
#endif

	w->lock_pointer.wayland_locked_pointer =
		zwp_pointer_constraints_v1_lock_pointer(
			w->lock_pointer.wayland_pointer_constraints,
			surface,
			wayland_pointer,
			NULL,
			ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);

	return true;
}

static void
wayland_unlock_pointer(struct window *w)
{
	w->lock_pointer.wayland_pointer_constraints = NULL;
	zwp_locked_pointer_v1_destroy(w->lock_pointer.wayland_locked_pointer);
}

static inline bool
backend_is_wayland(void)
{
	return GDK_IS_WAYLAND_DISPLAY(gdk_display_get_default());
}
#endif /* HAVE_GTK_WAYLAND */

#ifdef HAVE_GTK_X11
static bool
x_lock_pointer(struct window *w)
{
	Display *x_display;
	Window x_win;
	int result;

	/* gdk_display_get_default() is deprecated but probably won't be removed
	 * before GTK X11 is removed completely so this whole section will
	 * be gone anyway. Meanwhile, disable the warning
	 */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
	x_display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
#pragma GCC diagnostic pop

#ifdef HAVE_GTK4
	GtkNative *window = gtk_widget_get_native(w->win);
	GdkSurface *surface = gtk_native_get_surface(window);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
	x_win = GDK_SURFACE_XID(surface);
#pragma GCC diagnostic pop
#else
	GdkWindow *window = gtk_widget_get_window(w->win);
	x_win = GDK_WINDOW_XID(window);
#endif

	result = XGrabPointer(x_display,
			      x_win,
			      False,
			      NoEventMask,
			      GrabModeAsync,
			      GrabModeAsync,
			      x_win,
			      None,
			      CurrentTime);
	return (result == GrabSuccess);
}

static void
x_unlock_pointer(struct window *w)
{
	Display *x_display;

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
	x_display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
#pragma GCC diagnostic pop

	XUngrabPointer(x_display, CurrentTime);
}

static inline bool
backend_is_x11(void)
{
	return GDK_IS_X11_DISPLAY(gdk_display_get_default());
}
#endif /* HAVE_GTK_X11 */

static bool
window_lock_pointer(struct window *w)
{
	if (w->lock_pointer.locked)
		return true;

#ifdef HAVE_GTK_WAYLAND
	if (backend_is_wayland())
		w->lock_pointer.locked = wayland_lock_pointer(w);
#endif

#ifdef HAVE_GTK_X11
	if (backend_is_x11())
		w->lock_pointer.locked = x_lock_pointer(w);
#endif

	return w->lock_pointer.locked;
}

static void
window_unlock_pointer(struct window *w)
{
	if (!w->lock_pointer.locked)
		return;

	w->lock_pointer.locked = false;

#ifdef HAVE_GTK_WAYLAND
	if (backend_is_wayland())
		wayland_unlock_pointer(w);
#endif

#ifdef HAVE_GTK_X11
	if (backend_is_x11())
		x_unlock_pointer(w);
#endif
}

LIBINPUT_ATTRIBUTE_PRINTF(1, 2)
static inline void
msg(const char *fmt, ...)
{
	va_list args;
	printf("info: ");

	va_start(args, fmt);
	vprintf(fmt, args);
	va_end(args);
}

static inline void
draw_evdev_rel(struct window *w, cairo_t *cr)
{
	int center_x, center_y;

	cairo_save(cr);
	cairo_set_source_rgb(cr, .2, .2, .8);
	center_x = w->width / 2 - 400;
	center_y = w->height / 2;

	cairo_arc(cr, center_x, center_y, 10, 0, 2 * M_PI);
	cairo_stroke(cr);

	if (w->evdev.rel_x) {
		int dir = w->evdev.rel_x > 0 ? 1 : -1;
		for (int i = 0; i < abs(w->evdev.rel_x); i++) {
			cairo_move_to(cr, center_x + (i + 1) * 20 * dir, center_y - 20);
			cairo_rel_line_to(cr, 0, 40);
			cairo_rel_line_to(cr, 20 * dir, -20);
			cairo_rel_line_to(cr, -20 * dir, -20);
			cairo_fill(cr);
		}
	}

	if (w->evdev.rel_y) {
		int dir = w->evdev.rel_y > 0 ? 1 : -1;
		for (int i = 0; i < abs(w->evdev.rel_y); i++) {
			cairo_move_to(cr, center_x - 20, center_y + (i + 1) * 20 * dir);
			cairo_rel_line_to(cr, 40, 0);
			cairo_rel_line_to(cr, -20, 20 * dir);
			cairo_rel_line_to(cr, -20, -20 * dir);
			cairo_fill(cr);
		}
	}

	cairo_restore(cr);
}

static inline void
draw_evdev_abs(struct window *w, cairo_t *cr)
{
	static const struct input_absinfo *ax = NULL, *ay = NULL;
	const int normalized_width = 200;
	int outline_width = normalized_width, outline_height = normalized_width * 0.75;
	int center_x, center_y;
	int width, height;
	int x, y;

	cairo_save(cr);

	center_x = w->width / 2 + 400;
	center_y = w->height / 2;

	/* Always the outline even if we didn't get any abs events yet so it
	 * doesn't just appear out of nowhere */
	if (w->evdev.device == 0)
		goto draw_outline;

	/* device has changed, so the abs proportions/dimensions have
	 * changed. */
	if (w->evdev.device != w->evdev.last_device) {
		struct evdev_device *d;

		ax = NULL;
		ay = NULL;

		list_for_each(d, &w->evdev_devices, node) {
			if ((uintptr_t)d->libinput_device != w->evdev.device)
				continue;

			ax = libevdev_get_abs_info(d->evdev, ABS_X);
			ay = libevdev_get_abs_info(d->evdev, ABS_Y);
			w->evdev.last_device = w->evdev.device;
		}
	}
	if (ax == NULL || ay == NULL)
		goto draw_outline;

	width = ax->maximum - ax->minimum;
	height = ay->maximum - ay->minimum;
	outline_height = 1.0 * height / width * normalized_width;
	outline_width = normalized_width;

	cairo_set_source_rgb(cr, .2, .2, .8);
	x = 1.0 * (w->evdev.x - ax->minimum) / width * outline_width;
	y = 1.0 * (w->evdev.y - ay->minimum) / height * outline_height;
	x += center_x - outline_width / 2;
	y += center_y - outline_height / 2;
	cairo_arc(cr, x, y, 10, 0, 2 * M_PI);
	cairo_fill(cr);

	for (size_t i = 0; i < ARRAY_LENGTH(w->evdev.slots); i++) {
		if (!w->evdev.slots[i].active)
			continue;

		cairo_set_source_rgb(cr,
				     .2 + .2 * (i % 5),
				     .2 + .2 * (i % 5),
				     .8 - .2 * (i % 5));
		x = w->evdev.slots[i].x;
		y = w->evdev.slots[i].y;
		x = 1.0 * (x - ax->minimum) / width * outline_width;
		y = 1.0 * (y - ay->minimum) / height * outline_height;
		x += center_x - outline_width / 2;
		y += center_y - outline_height / 2;
		cairo_arc(cr, x, y, 10, 0, 2 * M_PI);
		cairo_fill(cr);

		char finger_text[3];
		cairo_text_extents_t finger_text_extents;
		snprintf(finger_text, 3, "%zu", i);
		cairo_set_source_rgb(cr, 1.f, 1.f, 1.f);
		cairo_set_font_size(cr, 12.0);
		cairo_text_extents(cr, finger_text, &finger_text_extents);
		cairo_move_to(cr,
			      x - finger_text_extents.width / 2,
			      y + finger_text_extents.height / 2);
		cairo_show_text(cr, finger_text);
	}

draw_outline:
	/* The touchpad outline */
	cairo_set_source_rgb(cr, .2, .2, .8);
	cairo_rectangle(cr,
			center_x - outline_width / 2,
			center_y - outline_height / 2,
			outline_width,
			outline_height);
	cairo_stroke(cr);
	cairo_restore(cr);
}

static inline void
draw_gestures(struct window *w, cairo_t *cr)
{
	int offset;

	/* swipe */
	cairo_save(cr);
	cairo_translate(cr, w->swipe.x, w->swipe.y);
	for (int i = 0; i < w->swipe.nfingers; i++) {
		cairo_set_source_rgb(cr, .8, .8, .4);
		cairo_arc(cr, (i - 2) * 40, 0, 20, 0, 2 * M_PI);
		cairo_fill(cr);
	}

	for (int i = 0; i < 4; i++) { /* 4 fg max */
		cairo_set_source_rgb(cr, 0, 0, 0);
		cairo_arc(cr, (i - 2) * 40, 0, 20, 0, 2 * M_PI);
		cairo_stroke(cr);
	}
	cairo_restore(cr);

	/* pinch */
	cairo_save(cr);
	offset = w->pinch.scale * 100;
	cairo_translate(cr, w->pinch.x, w->pinch.y);
	cairo_rotate(cr, w->pinch.angle * M_PI / 180.0);
	if (w->pinch.nfingers > 0) {
		cairo_set_source_rgb(cr, .4, .4, .8);
		cairo_arc(cr, offset, -offset, 20, 0, 2 * M_PI);
		cairo_arc(cr, -offset, offset, 20, 0, 2 * M_PI);
		cairo_fill(cr);
	}

	cairo_set_source_rgb(cr, 0, 0, 0);
	cairo_arc(cr, offset, -offset, 20, 0, 2 * M_PI);
	cairo_stroke(cr);
	cairo_arc(cr, -offset, offset, 20, 0, 2 * M_PI);
	cairo_stroke(cr);

	cairo_restore(cr);

	/* hold */
	cairo_save(cr);
	cairo_translate(cr, w->width / 2, w->height / 2 + 100);

	for (int i = 4; i > 0; i--) { /* 4 fg max */
		double r, g, b, hold_alpha;

		r = .4 + .2 * (i % 2);
		g = .2;
		b = .2;
		hold_alpha = (w->hold.active && i <= w->hold.nfingers) ? 1 : .5;

		cairo_set_source_rgba(cr, r, g, b, hold_alpha);
		cairo_arc(cr, 0, 0, 20 * i, 0, 2 * M_PI);
		cairo_fill(cr);

		cairo_set_source_rgba(cr, 0, 0, 0, hold_alpha);
		cairo_arc(cr, 0, 0, 20 * i, 0, 2 * M_PI);
		cairo_stroke(cr);
	}

	cairo_restore(cr);
}

static inline void
draw_scrollbars(struct window *w, cairo_t *cr)
{

	/* normal scrollbars */
	cairo_save(cr);
	cairo_set_source_rgb(cr, .4, .8, 0);
	cairo_rectangle(cr, w->scroll.vx - 10, w->scroll.vy - 20, 20, 40);
	cairo_rectangle(cr, w->scroll.hx - 20, w->scroll.hy - 10, 40, 20);
	cairo_fill(cr);

	/* discrete scrollbars */
	cairo_set_source_rgb(cr, .8, .4, 0);
	cairo_rectangle(cr,
			w->scroll.vx_discrete - 5,
			w->scroll.vy_discrete - 10,
			10,
			20);
	cairo_rectangle(cr,
			w->scroll.hx_discrete - 10,
			w->scroll.hy_discrete - 5,
			20,
			10);
	cairo_fill(cr);

	cairo_restore(cr);
}

static inline void
draw_touchpoints(struct window *w, cairo_t *cr)
{
	cairo_save(cr);
	ARRAY_FOR_EACH(w->touches, t) {
		if (t->state == TOUCH_ACTIVE)
			cairo_set_source_rgb(cr, .8, .2, .2);
		else
			cairo_set_source_rgb(cr, .8, .4, .4);
		cairo_arc(cr, t->x, t->y, 10, 0, 2 * M_PI);
		if (t->state == TOUCH_CANCELLED)
			cairo_stroke(cr);
		else
			cairo_fill(cr);
	}
	cairo_restore(cr);
}

static inline void
draw_abs_pointer(struct window *w, cairo_t *cr)
{

	cairo_save(cr);
	cairo_set_source_rgb(cr, .2, .4, .8);
	cairo_arc(cr, w->abs.x, w->abs.y, 10, 0, 2 * M_PI);
	cairo_fill(cr);
	cairo_restore(cr);
}

static inline void
draw_text(cairo_t *cr, const char *text, double x, double y)
{
	cairo_text_extents_t te;
	cairo_font_extents_t fe;

	cairo_text_extents(cr, text, &te);
	cairo_font_extents(cr, &fe);
	/* center of the rectangle */
	cairo_move_to(cr, x, y);
	cairo_rel_move_to(cr, -te.width / 2, -fe.descent + te.height / 2);
	cairo_show_text(cr, text);
}

static inline void
draw_other_button(struct window *w, cairo_t *cr)
{
	const char *name = w->buttons.other_name;

	cairo_save(cr);

	if (!w->buttons.other)
		goto outline;

	if (!name)
		name = "undefined";

	cairo_set_source_rgb(cr, .2, .8, .8);
	cairo_rectangle(cr, w->width / 2 - 40, w->height - 150, 80, 30);
	cairo_fill(cr);

	cairo_set_source_rgb(cr, 0, 0, 0);

	draw_text(cr, name, w->width / 2, w->height - 150 + 15);

outline:
	cairo_set_source_rgb(cr, 0, 0, 0);
	cairo_rectangle(cr, w->width / 2 - 40, w->height - 150, 80, 30);
	cairo_stroke(cr);
	cairo_restore(cr);
}

static inline void
draw_buttons(struct window *w, cairo_t *cr)
{
	cairo_save(cr);

	if (w->buttons.l || w->buttons.m || w->buttons.r) {
		cairo_set_source_rgb(cr, .2, .8, .8);
		if (w->buttons.l)
			cairo_rectangle(cr,
					w->width / 2 - 100,
					w->height - 200,
					70,
					30);
		if (w->buttons.m)
			cairo_rectangle(cr, w->width / 2 - 20, w->height - 200, 40, 30);
		if (w->buttons.r)
			cairo_rectangle(cr, w->width / 2 + 30, w->height - 200, 70, 30);
		cairo_fill(cr);
	}

	cairo_set_source_rgb(cr, 0, 0, 0);
	cairo_rectangle(cr, w->width / 2 - 100, w->height - 200, 70, 30);
	cairo_rectangle(cr, w->width / 2 - 20, w->height - 200, 40, 30);
	cairo_rectangle(cr, w->width / 2 + 30, w->height - 200, 70, 30);
	cairo_stroke(cr);
	cairo_restore(cr);

	draw_other_button(w, cr);
}

static inline void
draw_pad(struct window *w, cairo_t *cr)
{
	double rx, ry;
	double pos;
	char number[3];

	rx = w->width / 2 - 200;
	ry = w->height / 2 + 100;

	cairo_save(cr);
	/* outer ring (for ring) */
	cairo_set_source_rgb(cr, .7, .7, .0);
	cairo_arc(cr, rx, ry, 50, 0, 2 * M_PI);
	cairo_fill(cr);

	/* inner ring (for dial) */
	cairo_set_source_rgb(cr, 1., 1., 1.);
	cairo_arc(cr, rx, ry, 30, 0, 2 * M_PI);
	cairo_fill(cr);

	/* marker */
	/* libinput has degrees and 0 is north, cairo has radians and 0 is
	 * east */
	if (w->pad.ring.position != -1) {
		pos = (w->pad.ring.position + 270) * M_PI / 180.0;
		cairo_set_source_rgb(cr, .0, .0, .0);
		cairo_set_line_width(cr, 20);
		cairo_arc(cr, rx, ry, 40, pos - M_PI / 8, pos + M_PI / 8);
		cairo_stroke(cr);

		snprintf(number, sizeof(number), "%d", w->pad.ring.number);
		cairo_set_source_rgb(cr, .0, .0, .0);
		draw_text(cr, number, rx, ry);
	}

	if (w->pad.dial.position != -1) {
		const int degrees_per_click = 15.0;
		double degrees =
			fmod(w->pad.dial.position / 120 * degrees_per_click, 360);
		pos = (degrees + 270) * M_PI / 180.0;
		cairo_set_source_rgb(cr, .0, .0, .0);
		cairo_set_line_width(cr, 20);
		cairo_arc(cr, rx, ry, 20, pos - M_PI / 12, pos + M_PI / 12);
		cairo_stroke(cr);

		snprintf(number, sizeof(number), "%d", w->pad.dial.number);
		cairo_set_source_rgb(cr, .0, .0, .0);
		draw_text(cr, number, rx, ry);
	}

	cairo_restore(cr);

	rx = w->width / 2 - 300;
	ry = w->height / 2 + 50;

	cairo_save(cr);
	cairo_set_source_rgb(cr, .7, .7, .0);
	cairo_rectangle(cr, rx, ry, 20, 100);
	cairo_fill(cr);

	if (w->pad.strip.position != -1) {
		pos = w->pad.strip.position * 80;
		cairo_set_source_rgb(cr, .0, .0, .0);
		cairo_rectangle(cr, rx, ry + pos, 20, 20);
		cairo_fill(cr);

		snprintf(number, sizeof(number), "%d", w->pad.strip.number);
		cairo_set_source_rgb(cr, .0, .0, .0);
		draw_text(cr, number, rx + 10, ry - 10);
	}

	cairo_restore(cr);
}

static inline void
draw_tablet(struct window *w, cairo_t *cr)
{
	double x, y;
	int first, last;
	size_t mask;
	int rx, ry;

	/* pressure/distance bars */
	rx = w->width / 2 + 100;
	ry = w->height / 2 + 50;
	cairo_save(cr);
	cairo_set_source_rgb(cr, .2, .6, .6);
	cairo_rectangle(cr, rx, ry, 20, 100);
	cairo_stroke(cr);

	if (w->tool.distance > 0) {
		double pos = w->tool.distance * 100;
		cairo_rectangle(cr, rx, ry + 100 - pos, 20, 5);
		cairo_fill(cr);
	}
	if (w->tool.pressure > 0) {
		double pos = w->tool.pressure * 100;
		if (w->tool.is_down)
			cairo_rectangle(cr, rx + 25, ry + 95, 5, 5);
		cairo_rectangle(cr, rx, ry + 100 - pos, 20, pos);
		cairo_fill(cr);
	}
	cairo_restore(cr);

	/* tablet tool, square for prox-in location */
	cairo_save(cr);
	cairo_set_source_rgb(cr, .2, .6, .6);
	if (w->tool.x_in && w->tool.y_in) {
		cairo_rectangle(cr, w->tool.x_in - 15, w->tool.y_in - 15, 30, 30);
		cairo_stroke(cr);
	}

	if (w->tool.x_down && w->tool.y_down) {
		cairo_rectangle(cr, w->tool.x_down - 10, w->tool.y_down - 10, 20, 20);
		cairo_stroke(cr);
	}

	if (w->tool.x_up && w->tool.y_up) {
		cairo_rectangle(cr, w->tool.x_up - 10, w->tool.y_up - 10, 20, 20);
		cairo_stroke(cr);
	}

	if (w->tool.pressure)
		cairo_set_source_rgb(cr, .2, .8, .8);

	cairo_translate(cr, w->tool.x, w->tool.y);
	/* scale of 2.5 is large enough to make the marker visible around the
	   physical totem */
	cairo_scale(cr, 1.0 + w->tool.size_major * 2.5, 1.0 + w->tool.size_minor * 2.5);
	cairo_scale(cr, 1.0 + w->tool.tilt_x / 30.0, 1.0 + w->tool.tilt_y / 30.0);
	if (w->tool.rotation)
		cairo_rotate(cr, w->tool.rotation * M_PI / 180.0);
	if (w->tool.pressure)
		cairo_set_source_rgb(cr, .8, .8, .2);
	cairo_arc(cr,
		  0,
		  0,
		  1 + 10 * max(w->tool.pressure, w->tool.distance),
		  0,
		  2 * M_PI);
	cairo_fill(cr);
	cairo_restore(cr);

	/* The line to indicate the origin */
	if (w->tool.size_major) {
		cairo_save(cr);
		cairo_scale(cr, 1.0, 1.0);
		cairo_translate(cr, w->tool.x, w->tool.y);
		if (w->tool.rotation)
			cairo_rotate(cr, w->tool.rotation * M_PI / 180.0);
		cairo_set_source_rgb(cr, .0, .0, .0);
		cairo_move_to(cr, 0, 0);
		cairo_rel_line_to(cr, 0, -w->tool.size_major * 2.5);
		cairo_stroke(cr);
		cairo_restore(cr);
	}

	/* tablet deltas */
	mask = ARRAY_LENGTH(w->tool.deltas);
	first = max(w->tool.ndeltas + 1, mask) - mask;
	last = w->tool.ndeltas;

	cairo_save(cr);
	cairo_set_source_rgb(cr, .8, .8, .2);

	x = w->tool.deltas[first % mask].x;
	y = w->tool.deltas[first % mask].y;
	cairo_move_to(cr, x, y);

	for (int i = first + 1; i < last; i++) {
		x = w->tool.deltas[i % mask].x;
		y = w->tool.deltas[i % mask].y;
		cairo_line_to(cr, x, y);
	}

	cairo_stroke(cr);
	cairo_restore(cr);
}

static inline void
draw_pointer(struct window *w, cairo_t *cr)
{
	double x, y;
	int first, last;
	size_t mask;

	/* draw pointer sprite */
	cairo_set_source_rgb(cr, 0, 0, 0);
	cairo_save(cr);
	cairo_move_to(cr, w->pointer.x, w->pointer.y);
	cairo_rel_line_to(cr, 10, 15);
	cairo_rel_line_to(cr, -10, 0);
	cairo_rel_line_to(cr, 0, -15);
	cairo_fill(cr);

	/* draw unaccelerated sprite */
	cairo_set_source_rgb(cr, 0.7, 0.7, 0.7);
	cairo_save(cr);
	cairo_move_to(cr, w->unaccelerated.x, w->unaccelerated.y);
	cairo_rel_line_to(cr, -5, -10);
	cairo_rel_line_to(cr, 10, 0);
	cairo_rel_line_to(cr, -5, 10);
	cairo_fill(cr);

	/* pointer deltas */
	mask = ARRAY_LENGTH(w->deltas);
	first = max(w->ndeltas + 1, mask) - mask;
	last = w->ndeltas;

	cairo_set_source_rgb(cr, .8, .5, .2);

	x = w->deltas[first % mask].x;
	y = w->deltas[first % mask].y;
	cairo_move_to(cr, x, y);

	for (int i = first + 1; i < last; i++) {
		x = w->deltas[i % mask].x;
		y = w->deltas[i % mask].y;
		cairo_line_to(cr, x, y);
	}

	cairo_stroke(cr);
	cairo_restore(cr);
}

static inline void
draw_background(struct window *w, cairo_t *cr)
{
	int x1, x2, y1, y2, x3, y3, x4, y4;
	int cols;

	/* 10px and 5px grids */
	cairo_save(cr);
	cairo_set_source_rgb(cr, 0.8, 0.8, 0.8);
	x1 = w->width / 2 - 200;
	y1 = w->height / 2 - 200;
	x2 = w->width / 2 + 200;
	y2 = w->height / 2 - 200;
	for (cols = 1; cols < 10; cols++) {
		cairo_move_to(cr, x1 + 10 * cols, y1);
		cairo_rel_line_to(cr, 0, 100);
		cairo_move_to(cr, x1, y1 + 10 * cols);
		cairo_rel_line_to(cr, 100, 0);

		cairo_move_to(cr, x2 + 5 * cols, y2);
		cairo_rel_line_to(cr, 0, 50);
		cairo_move_to(cr, x2, y2 + 5 * cols);
		cairo_rel_line_to(cr, 50, 0);
	}

	/* 3px horiz/vert bar codes */
	x3 = w->width / 2 - 200;
	y3 = w->height / 2 + 200;
	x4 = w->width / 2 + 200;
	y4 = w->height / 2 + 100;
	for (cols = 0; cols < 50; cols++) {
		cairo_move_to(cr, x3 + 3 * cols, y3);
		cairo_rel_line_to(cr, 0, 20);

		cairo_move_to(cr, x4, y4 + 3 * cols);
		cairo_rel_line_to(cr, 20, 0);
	}
	cairo_stroke(cr);

	/* round targets */
	for (int i = 0; i <= 3; i++) {
		x1 = w->width * i / 4.0;
		x2 = w->width * i / 4.0;

		y1 = w->height * 1.0 / 4.0;
		y2 = w->height * 3.0 / 4.0;

		cairo_arc(cr, x1, y1, 10, 0, 2 * M_PI);
		cairo_stroke(cr);
		cairo_arc(cr, x2, y2, 10, 0, 2 * M_PI);
		cairo_stroke(cr);
	}

	cairo_restore(cr);
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr, gpointer data)
{
	struct window *w = data;

	cairo_set_font_size(cr, 12.0);
	cairo_set_source_rgb(cr, 1, 1, 1);
	cairo_rectangle(cr, 0, 0, w->width, w->height);
	cairo_fill(cr);

	draw_background(w, cr);
	draw_evdev_rel(w, cr);
	draw_evdev_abs(w, cr);

	draw_pad(w, cr);
	draw_tablet(w, cr);
	draw_gestures(w, cr);
	draw_scrollbars(w, cr);
	draw_touchpoints(w, cr);
	draw_abs_pointer(w, cr);
	draw_buttons(w, cr);
	draw_pointer(w, cr);

	return TRUE;
}

#ifdef HAVE_GTK4
static void
draw_gtk4(GtkDrawingArea *widget, cairo_t *cr, int width, int height, gpointer data)
{
	draw(GTK_WIDGET(widget), cr, data);
}
#endif

static void
window_place_ui_elements(GtkWidget *widget, struct window *w)
{
#ifdef HAVE_GTK4
	w->width = gtk_widget_get_width(w->area);
	w->height = gtk_widget_get_height(w->area);
#else
	gtk_window_get_size(GTK_WINDOW(widget), &w->width, &w->height);
#endif

	w->pointer.x = w->width / 2;
	w->pointer.y = w->height / 2;
	w->unaccelerated.x = w->width / 2;
	w->unaccelerated.y = w->height / 2;
	w->deltas[0].x = w->pointer.x;
	w->deltas[0].y = w->pointer.y;

	w->scroll.vx = w->width / 2;
	w->scroll.vy = w->height / 2;
	w->scroll.hx = w->width / 2;
	w->scroll.hy = w->height / 2;
	w->scroll.vx_discrete = w->width / 2;
	w->scroll.vy_discrete = w->height / 2;
	w->scroll.hx_discrete = w->width / 2;
	w->scroll.hy_discrete = w->height / 2;

	w->swipe.x = w->width / 2;
	w->swipe.y = w->height / 2;

	w->pinch.scale = 1.0;
	w->pinch.x = w->width / 2;
	w->pinch.y = w->height / 2;
}

#ifdef HAVE_GTK4
static void
map_event_cb(GtkDrawingArea *widget, int width, int height, gpointer data)
{
	struct window *w = data;

	window_place_ui_elements(GTK_WIDGET(widget), w);

	gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(w->area), draw_gtk4, w, NULL);

	gtk_widget_set_cursor_from_name(w->win, "none");

	window_lock_pointer(w);
}
#else
static void
map_event_cb(GtkWidget *widget, GdkEvent *event, gpointer data)
{
	struct window *w = data;
	GdkDisplay *display;
	GdkWindow *window;

	window_place_ui_elements(widget, w);

	g_signal_connect(G_OBJECT(w->area), "draw", G_CALLBACK(draw), w);

	window = gdk_event_get_window(event);
	display = gdk_window_get_display(window);

	gdk_window_set_cursor(gtk_widget_get_window(w->win),
			      gdk_cursor_new_for_display(display, GDK_BLANK_CURSOR));

	window_lock_pointer(w);
}
#endif

static void
window_quit(struct window *w)
{
	g_main_loop_quit(w->event_loop);
}

#ifdef HAVE_GTK4
static gboolean
window_delete_event_cb(GtkWindow *window, gpointer data)
{
	struct window *w = data;

	window_quit(w);

	return TRUE;
}
#else
static void
window_delete_event_cb(GtkWidget *widget, GdkEvent *event, gpointer data)
{
	struct window *w = data;

	window_quit(w);
}
#endif

static void
window_init(struct window *w)
{
	list_init(&w->evdev_devices);

#ifdef HAVE_GTK4
	w->win = gtk_window_new();
#else
	w->win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
#endif

	if (getenv("LIBINPUT_RUNNING_TEST_SUITE")) {
#ifdef HAVE_GTK4
		gtk_window_minimize(GTK_WINDOW(w->win));
#else
		gtk_window_iconify(GTK_WINDOW(w->win));
#endif
	}

	gtk_window_set_title(GTK_WINDOW(w->win), "libinput debugging tool");
	gtk_window_set_default_size(GTK_WINDOW(w->win), 1024, 768);
	gtk_window_maximize(GTK_WINDOW(w->win));
	gtk_window_set_resizable(GTK_WINDOW(w->win), TRUE);
	gtk_widget_realize(w->win);

	w->area = gtk_drawing_area_new();

#ifdef HAVE_GTK4
	g_signal_connect(G_OBJECT(w->area), "resize", G_CALLBACK(map_event_cb), w);
	g_signal_connect(G_OBJECT(w->win),
			 "close-request",
			 G_CALLBACK(window_delete_event_cb),
			 w);

	gtk_window_set_child(GTK_WINDOW(w->win), w->area);
	gtk_widget_set_visible(w->win, TRUE);
#else
	g_signal_connect(G_OBJECT(w->win), "map-event", G_CALLBACK(map_event_cb), w);
	g_signal_connect(G_OBJECT(w->win),
			 "delete-event",
			 G_CALLBACK(window_delete_event_cb),
			 w);

	gtk_widget_set_events(w->win, 0);
	gtk_widget_set_events(w->area, 0);

	gtk_container_add(GTK_CONTAINER(w->win), w->area);
	gtk_widget_show_all(w->win);
#endif

	w->pad.ring.position = -1;
	w->pad.strip.position = -1;
	w->pad.dial.position = -1;
}

static void
window_cleanup(struct window *w)
{
	ARRAY_FOR_EACH(w->devices, dev) {
		if (*dev)
			libinput_device_unref(*dev);
	}
}

static void
change_ptraccel(struct window *w, double amount)
{
	ARRAY_FOR_EACH(w->devices, dev) {
		double speed;
		enum libinput_config_status status;

		if (*dev == NULL)
			continue;

		if (!libinput_device_config_accel_is_available(*dev))
			continue;

		speed = libinput_device_config_accel_get_speed(*dev);
		speed = clip(speed + amount, -1, 1);

		status = libinput_device_config_accel_set_speed(*dev, speed);

		if (status != LIBINPUT_CONFIG_STATUS_SUCCESS) {
			msg("%s: failed to change accel to %.2f (%s)\n",
			    libinput_device_get_name(*dev),
			    speed,
			    libinput_config_status_to_str(status));
		} else {
			printf("%s: speed is %.2f\n",
			       libinput_device_get_name(*dev),
			       speed);
		}
	}
}

static int
handle_event_evdev(GIOChannel *source, GIOCondition condition, gpointer data)
{
	struct libinput_device *dev = data;
	struct libinput *li = libinput_device_get_context(dev);
	struct window *w = libinput_get_user_data(li);
	struct evdev_device *d, *device = NULL;
	struct input_event e;
	int rc;

	list_for_each(d, &w->evdev_devices, node) {
		if (d->libinput_device == dev) {
			device = d;
			break;
		}
	}

	if (device == NULL) {
		msg("Unknown device: %s\n", libinput_device_get_name(dev));
		return FALSE;
	}

	do {
		rc = libevdev_next_event(device->evdev, LIBEVDEV_READ_FLAG_NORMAL, &e);
		if (rc == -EAGAIN) {
			break;
		} else if (rc == LIBEVDEV_READ_STATUS_SYNC) {
			msg("SYN_DROPPED received\n");
			goto out;
		} else if (rc != LIBEVDEV_READ_STATUS_SUCCESS) {
			msg("Error reading event: %s\n", strerror(-rc));
			goto out;
		}

#define EVENT(t_, c_) (t_ << 16 | c_)
		switch (EVENT(e.type, e.code)) {
		case EVENT(EV_REL, REL_X):
			w->evdev.rel_x = e.value;
			break;
		case EVENT(EV_REL, REL_Y):
			w->evdev.rel_y = e.value;
			break;
		case EVENT(EV_ABS, ABS_MT_SLOT):
			w->evdev.slot = min((unsigned int)e.value,
					    ARRAY_LENGTH(w->evdev.slots) - 1);
			w->evdev.device = (uintptr_t)dev;
			break;
		case EVENT(EV_ABS, ABS_MT_TRACKING_ID):
			w->evdev.slots[w->evdev.slot].active = (e.value != -1);
			w->evdev.device = (uintptr_t)dev;
			break;
		case EVENT(EV_ABS, ABS_X):
			w->evdev.x = e.value;
			w->evdev.device = (uintptr_t)dev;
			break;
		case EVENT(EV_ABS, ABS_Y):
			w->evdev.y = e.value;
			w->evdev.device = (uintptr_t)dev;
			break;
		case EVENT(EV_ABS, ABS_MT_POSITION_X):
			w->evdev.slots[w->evdev.slot].x = e.value;
			w->evdev.device = (uintptr_t)dev;
			break;
		case EVENT(EV_ABS, ABS_MT_POSITION_Y):
			w->evdev.slots[w->evdev.slot].y = e.value;
			w->evdev.device = (uintptr_t)dev;
			break;
		}
	} while (rc == LIBEVDEV_READ_STATUS_SUCCESS);

	gtk_widget_queue_draw(w->area);
out:
	return TRUE;
}

static void
register_evdev_device(struct window *w, struct libinput_device *dev)
{
	GIOChannel *c;
	struct udev_device *ud;
	struct libevdev *evdev;
	const char *device_node;
	int fd;
	struct evdev_device *d;

	ud = libinput_device_get_udev_device(dev);
	device_node = udev_device_get_devnode(ud);

	fd = open(device_node, O_RDONLY | O_NONBLOCK);
	if (fd == -1) {
		msg("failed to open %s, evdev events unavailable\n", device_node);
		goto out;
	}

	if (libevdev_new_from_fd(fd, &evdev) != 0) {
		msg("failed to create context for %s, evdev events unavailable\n",
		    device_node);
		goto out;
	}

	d = zalloc(sizeof *d);
	list_append(&w->evdev_devices, &d->node);
	d->fd = fd;
	d->evdev = evdev;
	d->libinput_device = libinput_device_ref(dev);

	c = g_io_channel_unix_new(fd);
	g_io_channel_set_encoding(c, NULL, NULL);
	d->source_id =
		g_io_add_watch(c, G_IO_IN, handle_event_evdev, d->libinput_device);
	fd = -1;
out:
	close(fd);
	udev_device_unref(ud);
}

static void
unregister_evdev_device(struct window *w, struct libinput_device *dev)
{
	struct evdev_device *d;

	list_for_each_safe(d, &w->evdev_devices, node) {
		if (d->libinput_device != dev)
			continue;

		list_remove(&d->node);
		g_source_remove(d->source_id);
		free(libinput_device_get_user_data(d->libinput_device));
		libinput_device_unref(d->libinput_device);
		libevdev_free(d->evdev);
		close(d->fd);
		free(d);
		w->evdev.last_device = 0;
		break;
	}
}

static void
handle_event_device_notify(struct libinput_event *ev)
{
	struct libinput_device *dev = libinput_event_get_device(ev);
	struct libinput *li;
	struct window *w;
	const char *type;

	li = libinput_event_get_context(ev);
	w = libinput_get_user_data(li);

	if (libinput_event_get_type(ev) == LIBINPUT_EVENT_DEVICE_ADDED) {
		type = "added";
		register_evdev_device(w, dev);
		tools_device_apply_config(libinput_event_get_device(ev), &w->options);
	} else {
		type = "removed";
		unregister_evdev_device(w, dev);
	}

	msg("%s %-30s %s\n",
	    libinput_device_get_sysname(dev),
	    libinput_device_get_name(dev),
	    type);

	if (libinput_event_get_type(ev) == LIBINPUT_EVENT_DEVICE_ADDED) {
		ARRAY_FOR_EACH(w->devices, device) {
			if (*device == NULL) {
				*device = libinput_device_ref(dev);
				break;
			}
		}
	} else {
		ARRAY_FOR_EACH(w->devices, device) {
			if (*device == dev) {
				libinput_device_unref(*device);
				*device = NULL;
				break;
			}
		}
	}
}

static void
handle_event_motion(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(ev);
	double dx = libinput_event_pointer_get_dx(p),
	       dy = libinput_event_pointer_get_dy(p);
	double dx_unaccel = libinput_event_pointer_get_dx_unaccelerated(p),
	       dy_unaccel = libinput_event_pointer_get_dy_unaccelerated(p);
	struct point point;
	const int mask = ARRAY_LENGTH(w->deltas);
	size_t idx;

	w->pointer.x = clip(w->pointer.x + dx, 0.0, w->width);
	w->pointer.y = clip(w->pointer.y + dy, 0.0, w->height);
	w->unaccelerated.x = clip(w->unaccelerated.x + dx_unaccel, 0.0, w->width);
	w->unaccelerated.y = clip(w->unaccelerated.y + dy_unaccel, 0.0, w->height);

	idx = w->ndeltas % mask;
	point = w->deltas[idx];
	idx = (w->ndeltas + 1) % mask;
	point.x += dx_unaccel;
	point.y += dy_unaccel;
	w->deltas[idx] = point;
	w->ndeltas++;
}

static void
handle_event_absmotion(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(ev);
	double x = libinput_event_pointer_get_absolute_x_transformed(p, w->width),
	       y = libinput_event_pointer_get_absolute_y_transformed(p, w->height);

	w->abs.x = x;
	w->abs.y = y;
}

static void
handle_event_touch(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_touch *t = libinput_event_get_touch_event(ev);
	int slot = libinput_event_touch_get_seat_slot(t);
	struct touch *touch;
	double x, y;

	if (slot == -1 || slot >= (int)ARRAY_LENGTH(w->touches))
		return;

	touch = &w->touches[slot];

	switch (libinput_event_get_type(ev)) {
	case LIBINPUT_EVENT_TOUCH_UP:
		touch->state = TOUCH_ENDED;
		return;
	case LIBINPUT_EVENT_TOUCH_CANCEL:
		touch->state = TOUCH_CANCELLED;
		return;
	default:
		break;
	}

	x = libinput_event_touch_get_x_transformed(t, w->width),
	y = libinput_event_touch_get_y_transformed(t, w->height);

	touch->state = TOUCH_ACTIVE;
	touch->x = (int)x;
	touch->y = (int)y;
}

static void
handle_event_axis(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(ev);
	double value;
	enum libinput_pointer_axis axis;
	enum libinput_event_type type;

	type = libinput_event_get_type(ev);

	axis = LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL;
	if (libinput_event_pointer_has_axis(p, axis)) {
		value = libinput_event_pointer_get_scroll_value(p, axis);
		w->scroll.vy += value;
		w->scroll.vy = clip(w->scroll.vy, 0, w->height);

		if (type == LIBINPUT_EVENT_POINTER_SCROLL_WHEEL) {
			w->scroll.vy_discrete += value;
			w->scroll.vy_discrete =
				clip(w->scroll.vy_discrete, 0, w->height);
		}
	}

	axis = LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL;
	if (libinput_event_pointer_has_axis(p, axis)) {
		value = libinput_event_pointer_get_scroll_value(p, axis);
		w->scroll.hx += value;
		w->scroll.hx = clip(w->scroll.hx, 0, w->width);

		if (type == LIBINPUT_EVENT_POINTER_SCROLL_WHEEL) {
			w->scroll.hx_discrete += value;
			w->scroll.hx_discrete =
				clip(w->scroll.hx_discrete, 0, w->width);
		}
	}
}

static int
handle_event_keyboard(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_keyboard *k = libinput_event_get_keyboard_event(ev);
	unsigned int key = libinput_event_keyboard_get_key(k);

	if (libinput_event_keyboard_get_key_state(k) == LIBINPUT_KEY_STATE_RELEASED)
		return 0;

	switch (key) {
	case KEY_ESC:
		return 1;
	case KEY_UP:
		change_ptraccel(w, 0.1);
		break;
	case KEY_DOWN:
		change_ptraccel(w, -0.1);
		break;
	default:
		break;
	}

	return 0;
}

static void
handle_event_button(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_pointer *p = libinput_event_get_pointer_event(ev);
	unsigned int button = libinput_event_pointer_get_button(p);
	bool is_press;

	is_press = libinput_event_pointer_get_button_state(p) ==
		   LIBINPUT_BUTTON_STATE_PRESSED;

	switch (button) {
	case BTN_LEFT:
		w->buttons.l = is_press;
		break;
	case BTN_RIGHT:
		w->buttons.r = is_press;
		break;
	case BTN_MIDDLE:
		w->buttons.m = is_press;
		break;
	default:
		w->buttons.other = is_press;
		w->buttons.other_name = libevdev_event_code_get_name(EV_KEY, button);
	}
}

static void
handle_event_swipe(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_gesture *g = libinput_event_get_gesture_event(ev);
	int nfingers;
	double dx, dy;

	nfingers = libinput_event_gesture_get_finger_count(g);

	switch (libinput_event_get_type(ev)) {
	case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN:
		w->swipe.nfingers = nfingers;
		w->swipe.x = w->width / 2;
		w->swipe.y = w->height / 2;
		break;
	case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE:
		dx = libinput_event_gesture_get_dx(g);
		dy = libinput_event_gesture_get_dy(g);
		w->swipe.x += dx;
		w->swipe.y += dy;
		break;
	case LIBINPUT_EVENT_GESTURE_SWIPE_END:
		w->swipe.nfingers = 0;
		w->swipe.x = w->width / 2;
		w->swipe.y = w->height / 2;
		break;
	default:
		abort();
	}
}

static void
handle_event_pinch(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_gesture *g = libinput_event_get_gesture_event(ev);
	int nfingers;
	double dx, dy;

	nfingers = libinput_event_gesture_get_finger_count(g);

	switch (libinput_event_get_type(ev)) {
	case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN:
		w->pinch.nfingers = nfingers;
		w->pinch.x = w->width / 2;
		w->pinch.y = w->height / 2;
		break;
	case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE:
		dx = libinput_event_gesture_get_dx(g);
		dy = libinput_event_gesture_get_dy(g);
		w->pinch.x += dx;
		w->pinch.y += dy;
		w->pinch.scale = libinput_event_gesture_get_scale(g);
		w->pinch.angle += libinput_event_gesture_get_angle_delta(g);
		break;
	case LIBINPUT_EVENT_GESTURE_PINCH_END:
		w->pinch.nfingers = 0;
		w->pinch.x = w->width / 2;
		w->pinch.y = w->height / 2;
		w->pinch.angle = 0.0;
		w->pinch.scale = 1.0;
		break;
	default:
		abort();
	}
}

static void
handle_event_hold(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_gesture *g = libinput_event_get_gesture_event(ev);
	int nfingers;

	nfingers = libinput_event_gesture_get_finger_count(g);

	switch (libinput_event_get_type(ev)) {
	case LIBINPUT_EVENT_GESTURE_HOLD_BEGIN:
		w->hold.nfingers = nfingers;
		w->hold.active = true;
		break;
	case LIBINPUT_EVENT_GESTURE_HOLD_END:
		w->hold.nfingers = nfingers;
		w->hold.active = false;
		break;
	default:
		abort();
	}
}

static void
handle_event_tablet(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_tablet_tool *t = libinput_event_get_tablet_tool_event(ev);
	struct libinput_tablet_tool *tool = libinput_event_tablet_tool_get_tool(t);
	double x, y;
	struct point point;
	int idx;
	const int mask = ARRAY_LENGTH(w->tool.deltas);
	bool is_press;
	unsigned int button;

	x = libinput_event_tablet_tool_get_x_transformed(t, w->width);
	y = libinput_event_tablet_tool_get_y_transformed(t, w->height);

	switch (libinput_event_get_type(ev)) {
	case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY:
		tools_tablet_tool_apply_config(tool, &w->options);
		if (libinput_event_tablet_tool_get_proximity_state(t) ==
		    LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_OUT) {
			w->tool.x_in = 0;
			w->tool.y_in = 0;
			w->tool.x_down = 0;
			w->tool.y_down = 0;
			w->tool.x_up = 0;
			w->tool.y_up = 0;
		} else {
			w->tool.x_in = x;
			w->tool.y_in = y;
			w->tool.ndeltas = 0;
			w->tool.deltas[0].x = w->width / 2;
			w->tool.deltas[0].y = w->height / 2;
		}
		break;
	case LIBINPUT_EVENT_TABLET_TOOL_TIP:
		w->tool.pressure = libinput_event_tablet_tool_get_pressure(t);
		w->tool.distance = libinput_event_tablet_tool_get_distance(t);
		w->tool.tilt_x = libinput_event_tablet_tool_get_tilt_x(t);
		w->tool.tilt_y = libinput_event_tablet_tool_get_tilt_y(t);
		if (libinput_event_tablet_tool_get_tip_state(t) ==
		    LIBINPUT_TABLET_TOOL_TIP_DOWN) {
			w->tool.x_down = x;
			w->tool.y_down = y;
			w->tool.is_down = true;
		} else {
			w->tool.x_up = x;
			w->tool.y_up = y;
			w->tool.is_down = false;
		}
		_fallthrough_;
	case LIBINPUT_EVENT_TABLET_TOOL_AXIS:
		w->tool.x = x;
		w->tool.y = y;
		w->tool.pressure = libinput_event_tablet_tool_get_pressure(t);
		w->tool.distance = libinput_event_tablet_tool_get_distance(t);
		w->tool.tilt_x = libinput_event_tablet_tool_get_tilt_x(t);
		w->tool.tilt_y = libinput_event_tablet_tool_get_tilt_y(t);
		w->tool.rotation = libinput_event_tablet_tool_get_rotation(t);
		w->tool.size_major = libinput_event_tablet_tool_get_size_major(t);
		w->tool.size_minor = libinput_event_tablet_tool_get_size_minor(t);

		/* Add the delta to the last position and store them as abs
		 * coordinates */
		idx = w->tool.ndeltas % mask;
		point = w->tool.deltas[idx];

		idx = (w->tool.ndeltas + 1) % mask;
		point.x += libinput_event_tablet_tool_get_dx(t);
		point.y += libinput_event_tablet_tool_get_dy(t);
		w->tool.deltas[idx] = point;
		w->tool.ndeltas++;
		break;
	case LIBINPUT_EVENT_TABLET_TOOL_BUTTON:
		is_press = libinput_event_tablet_tool_get_button_state(t) ==
			   LIBINPUT_BUTTON_STATE_PRESSED;
		button = libinput_event_tablet_tool_get_button(t);

		w->buttons.other = is_press;
		w->buttons.other_name = libevdev_event_code_get_name(EV_KEY, button);
		break;
	default:
		abort();
	}
}

static void
handle_event_tablet_pad(struct libinput_event *ev, struct window *w)
{
	struct libinput_event_tablet_pad *p = libinput_event_get_tablet_pad_event(ev);
	bool is_press;
	unsigned int button;
	static const char *pad_buttons[] = { "Pad 0", "Pad 1", "Pad 2",    "Pad 3",
					     "Pad 4", "Pad 5", "Pad 6",    "Pad 7",
					     "Pad 8", "Pad 9", "Pad >= 10" };
	double position, delta;
	double number;

	switch (libinput_event_get_type(ev)) {
	case LIBINPUT_EVENT_TABLET_PAD_BUTTON:
		is_press = libinput_event_tablet_pad_get_button_state(p) ==
			   LIBINPUT_BUTTON_STATE_PRESSED;
		button = libinput_event_tablet_pad_get_button_number(p);
		w->buttons.other = is_press;
		w->buttons.other_name = pad_buttons[min(button, 10)];
		break;
	case LIBINPUT_EVENT_TABLET_PAD_RING:
		position = libinput_event_tablet_pad_get_ring_position(p);
		number = libinput_event_tablet_pad_get_ring_number(p);
		w->pad.ring.number = number;
		w->pad.ring.position = position;
		break;
	case LIBINPUT_EVENT_TABLET_PAD_STRIP:
		position = libinput_event_tablet_pad_get_strip_position(p);
		number = libinput_event_tablet_pad_get_strip_number(p);
		w->pad.strip.number = number;
		w->pad.strip.position = position;
		break;
	case LIBINPUT_EVENT_TABLET_PAD_DIAL:
		delta = libinput_event_tablet_pad_get_dial_delta_v120(p);
		number = libinput_event_tablet_pad_get_dial_number(p);
		if (w->pad.dial.number != number)
			w->pad.dial.position = -delta;
		w->pad.dial.number = number;
		w->pad.dial.position += delta;
		break;
	default:
		abort();
	}
}

static gboolean
handle_event_libinput(GIOChannel *source, GIOCondition condition, gpointer data)
{
	struct libinput *li = data;
	struct window *w = libinput_get_user_data(li);
	struct libinput_event *ev;

	tools_dispatch(li);

	while ((ev = libinput_get_event(li))) {
		switch (libinput_event_get_type(ev)) {
		case LIBINPUT_EVENT_NONE:
			abort();
		case LIBINPUT_EVENT_DEVICE_ADDED:
		case LIBINPUT_EVENT_DEVICE_REMOVED:
			handle_event_device_notify(ev);
			break;
		case LIBINPUT_EVENT_POINTER_MOTION:
			handle_event_motion(ev, w);
			break;
		case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
			handle_event_absmotion(ev, w);
			break;
		case LIBINPUT_EVENT_TOUCH_DOWN:
		case LIBINPUT_EVENT_TOUCH_MOTION:
		case LIBINPUT_EVENT_TOUCH_UP:
		case LIBINPUT_EVENT_TOUCH_CANCEL:
			handle_event_touch(ev, w);
			break;
		case LIBINPUT_EVENT_TOUCH_FRAME:
			break;
		case LIBINPUT_EVENT_POINTER_AXIS:
			/* ignore */
			break;
		case LIBINPUT_EVENT_POINTER_SCROLL_WHEEL:
		case LIBINPUT_EVENT_POINTER_SCROLL_FINGER:
		case LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS:
			handle_event_axis(ev, w);
			break;
		case LIBINPUT_EVENT_POINTER_BUTTON:
			handle_event_button(ev, w);
			break;
		case LIBINPUT_EVENT_KEYBOARD_KEY:
			if (handle_event_keyboard(ev, w)) {
				libinput_event_destroy(ev);
				window_quit(w);
				return FALSE;
			}
			break;
		case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN:
		case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE:
		case LIBINPUT_EVENT_GESTURE_SWIPE_END:
			handle_event_swipe(ev, w);
			break;
		case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN:
		case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE:
		case LIBINPUT_EVENT_GESTURE_PINCH_END:
			handle_event_pinch(ev, w);
			break;
		case LIBINPUT_EVENT_GESTURE_HOLD_BEGIN:
		case LIBINPUT_EVENT_GESTURE_HOLD_END:
			handle_event_hold(ev, w);
			break;
		case LIBINPUT_EVENT_TABLET_TOOL_AXIS:
		case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY:
		case LIBINPUT_EVENT_TABLET_TOOL_TIP:
		case LIBINPUT_EVENT_TABLET_TOOL_BUTTON:
			handle_event_tablet(ev, w);
			break;
		case LIBINPUT_EVENT_TABLET_PAD_BUTTON:
		case LIBINPUT_EVENT_TABLET_PAD_RING:
		case LIBINPUT_EVENT_TABLET_PAD_STRIP:
		case LIBINPUT_EVENT_TABLET_PAD_DIAL:
			handle_event_tablet_pad(ev, w);
			break;
		case LIBINPUT_EVENT_TABLET_PAD_KEY:
			break;
		case LIBINPUT_EVENT_SWITCH_TOGGLE:
			break;
		}

		libinput_event_destroy(ev);
	}
	gtk_widget_queue_draw(w->area);

	return TRUE;
}

static void
sockets_init(struct libinput *li)
{
	GIOChannel *c = g_io_channel_unix_new(libinput_get_fd(li));

	g_io_channel_set_encoding(c, NULL, NULL);
	g_io_add_watch(c, G_IO_IN, handle_event_libinput, li);
}

static void
usage(struct option *opts)
{
	printf("Usage: libinput debug-gui [options] [--udev <seat>|[--device] /dev/input/event0]\n");

	if (opts)
		tools_print_usage_option_list(opts);
}

static gboolean
signal_handler(void *data)
{
	struct libinput *li = data;
	struct window *w = libinput_get_user_data(li);

	window_quit(w);

	return FALSE;
}

int
main(int argc, char **argv)
{
	struct window w = { 0 };
	struct tools_options options;
	struct libinput *li;
	enum tools_backend backend = BACKEND_NONE;
	const char *seat_or_device[2] = { "seat0", NULL };
	bool verbose = false;
	bool gtk_init = false;

#ifdef HAVE_GTK4
	gtk_init = gtk_init_check();
#else
	gtk_init = gtk_init_check(&argc, &argv);
#endif

	if (!gtk_init)
		return 77;

	tools_init_options(&options);

	while (1) {
		int c;
		int option_index = 0;
		enum {
			OPT_DEVICE = 1,
			OPT_UDEV,
			OPT_GRAB,
			OPT_VERBOSE,
		};
		/* clang-format off */
		static struct option opts[] = {
			CONFIGURATION_OPTIONS,
			{ "help",                      no_argument,       0, 'h' },
			{ "device",                    required_argument, 0, OPT_DEVICE },
			{ "udev",                      required_argument, 0, OPT_UDEV },
			{ "grab",                      no_argument,       0, OPT_GRAB },
			{ "verbose",                   no_argument,       0, OPT_VERBOSE },
			{ 0, 0, 0, 0},
		};
		/* clang-format on */

		c = getopt_long(argc, argv, "h", opts, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case '?':
			exit(EXIT_INVALID_USAGE);
			break;
		case 'h':
			usage(opts);
			exit(0);
			break;
		case OPT_DEVICE:
			backend = BACKEND_DEVICE;
			seat_or_device[0] = optarg;
			break;
		case OPT_UDEV:
			backend = BACKEND_UDEV;
			seat_or_device[0] = optarg;
			break;
		case OPT_GRAB:
			w.grab = true;
			break;
		case OPT_VERBOSE:
			verbose = true;
			break;
		default:
			if (tools_parse_option(c, optarg, &options) != 0) {
				usage(NULL);
				return EXIT_INVALID_USAGE;
			}
			break;
		}
	}

	if (optind < argc) {
		if (optind < argc - 1 || backend != BACKEND_NONE) {
			usage(NULL);
			return EXIT_INVALID_USAGE;
		}
		backend = BACKEND_DEVICE;
		seat_or_device[0] = argv[optind];
	} else if (backend == BACKEND_NONE) {
		backend = BACKEND_UDEV;
	}

	bool with_plugins = (options.plugins == 1);
	li = tools_open_backend(backend,
				seat_or_device,
				verbose,
				&w.grab,
				with_plugins,
				steal(&options.plugin_paths));
	if (!li)
		return EXIT_FAILURE;

	libinput_set_user_data(li, &w);

	g_unix_signal_add(SIGINT, signal_handler, li);

	window_init(&w);
	w.options = options;
	sockets_init(li);
	handle_event_libinput(NULL, 0, li);

	w.event_loop = g_main_loop_new(NULL, FALSE);
	g_main_loop_run(w.event_loop);

	window_unlock_pointer(&w);
	window_cleanup(&w);
	libinput_unref(li);

	return EXIT_SUCCESS;
}
