/* SPDX-License-Identifier: MIT */
/*
 * Copyright © 2020 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 <math.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/time.h>

#include <munit.h>

#include "eierpecken.h"

#include "util-bits.h"
#include "util-color.h"
#include "util-io.h"
#include "util-mem.h"
#include "util-munit.h"
#include "util-tristate.h"
#include "util-logger.h"
#include "util-object.h"
#include "util-strings.h"
#include "util-time.h"

static bool enable_sigalarm;

DEFINE_TRISTATE(yes, no, unset);

struct peck {
	struct object object;
	struct ei *ei;
	struct eis *eis;
	uint32_t eis_behavior;
	uint32_t ei_behavior;
	struct logger *logger;
	int ei_socket_fd;

	/* The default seat/devices */
	struct eis_seat *eis_seat;
	struct eis_device *eis_pointer;
	struct eis_device *eis_keyboard;
	struct eis_device *eis_abs;
	struct eis_device *eis_button;
	struct eis_device *eis_scroll;
	struct eis_device *eis_touch;

	struct ei_seat *ei_seat;
	struct ei_device *ei_pointer;
	struct ei_device *ei_keyboard;
	struct ei_device *ei_abs;
	struct ei_device *ei_button;
	struct ei_device *ei_scroll;
	struct ei_device *ei_touch;

	uint64_t now;

	struct eis_client *eis_client;

	uint32_t indent;

	struct sigaction sigact;

	bool ei_fatal_bugs;
	bool eis_fatal_bugs;

	uint64_t ei_time_offset;
	uint64_t eis_time_offset;
};

static const uint32_t INDENTATION = 4;
static void
peck_indent(struct peck *peck)
{
	peck->indent += INDENTATION;
}

static void
peck_dedent(struct peck *peck)
{
	assert(peck->indent >= INDENTATION);
	peck->indent -= INDENTATION;
}

static
OBJECT_IMPLEMENT_GETTER(peck, indent, uint32_t);

static uint64_t
peck_ei_now_func(struct ei *ei)
{
	static uint64_t offset = 0;
	struct peck *peck = ei_get_user_data(ei);

	if (peck->ei_time_offset != offset) {
		offset = peck->ei_time_offset;
		log_debug(peck, "Time is now offset by %" PRIu64"us\n", offset);
	}

	uint64_t ts = 0;
	now(&ts);

	return ts + peck->ei_time_offset;
}

static uint64_t
peck_eis_now_func(struct eis *eis)
{
	static uint64_t offset = 0;
	struct peck *peck = eis_get_user_data(eis);

	if (peck->eis_time_offset != offset) {
		offset = peck->eis_time_offset;
		log_debug(peck, "Time is now offset by %" PRIu64"us\n", offset);
	}

	uint64_t ts = 0;
	now(&ts);

	return ts + peck->ei_time_offset;
}

static void
peck_destroy(struct peck *peck)
{
	log_debug(peck, "destroying peck context\n");

	/* we don't want valgrind to complain about us not handling *all*
	 * events in a test */
	peck_drain_ei(peck->ei);
	peck_drain_eis(peck->eis);

	log_debug(peck, "final event processing done\n");

	eis_client_unref(peck->eis_client);


	eis_device_unref(peck->eis_pointer);
	eis_device_unref(peck->eis_abs);
	eis_device_unref(peck->eis_keyboard);
	eis_device_unref(peck->eis_touch);
	eis_device_unref(peck->eis_button);
	eis_device_unref(peck->eis_scroll);
	eis_seat_unref(peck->eis_seat);

	ei_device_unref(peck->ei_pointer);
	ei_device_unref(peck->ei_abs);
	ei_device_unref(peck->ei_keyboard);
	ei_device_unref(peck->ei_touch);
	ei_device_unref(peck->ei_button);
	ei_device_unref(peck->ei_scroll);
	ei_seat_unref(peck->ei_seat);

	ei_unref(peck->ei);
	eis_unref(peck->eis);
	logger_unref(peck->logger);

	if (enable_sigalarm) {
		struct itimerval timer = {0};
		setitimer(ITIMER_REAL, &timer, 0);
		sigaction(SIGALRM, &peck->sigact, NULL);
	}
}

void
peck_ei_add_time_offset(struct peck *peck, uint64_t us)
{
	log_debug(peck, "Adding ei time offset of %" PRIu64 "us\n", us);
	peck->ei_time_offset += us;
}

void
peck_eis_add_time_offset(struct peck *peck, uint64_t us)
{
	log_debug(peck, "Adding EIS time offset of %" PRIu64 "us\n", us);
	peck->eis_time_offset += us;
}

static
OBJECT_IMPLEMENT_CREATE(peck);
OBJECT_IMPLEMENT_UNREF(peck);
OBJECT_IMPLEMENT_GETTER(peck, ei, struct ei*);
OBJECT_IMPLEMENT_GETTER(peck, eis, struct eis*);

void
peck_drop_ei(struct peck *peck)
{
	peck->ei_pointer = ei_device_unref(peck->ei_pointer);
	peck->ei_keyboard = ei_device_unref(peck->ei_keyboard);
	peck->ei_abs = ei_device_unref(peck->ei_abs);
	peck->ei_touch = ei_device_unref(peck->ei_touch);
	peck->ei_seat = ei_seat_unref(peck->ei_seat);
	peck->ei = NULL;
}

struct eis_client *
peck_eis_get_default_client(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_client);
	return peck->eis_client;
};

struct eis_seat *
peck_eis_get_default_seat(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_seat);
	return peck->eis_seat;
};

struct eis_device *
peck_eis_get_default_pointer(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_pointer);
	return peck->eis_pointer;
};

struct eis_device *
peck_eis_get_default_pointer_absolute(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_abs);
	return peck->eis_abs;
};

struct eis_device *
peck_eis_get_default_button(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_button);
	return peck->eis_button;
};

struct eis_device *
peck_eis_get_default_scroll(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_scroll);
	return peck->eis_scroll;
};

struct eis_device *
peck_eis_get_default_keyboard(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_keyboard);
	return peck->eis_keyboard;
};

struct eis_device *
peck_eis_get_default_touch(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->eis_touch);
	return peck->eis_touch;
};

struct ei_seat *
peck_ei_get_default_seat(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->ei_seat);
	return peck->ei_seat;
}

struct ei_device *
peck_ei_get_default_pointer(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->ei_pointer);
	return peck->ei_pointer;
};

struct ei_device *
peck_ei_get_default_pointer_absolute(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->ei_abs);
	return peck->ei_abs;
};

struct ei_device *
peck_ei_get_default_button(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->ei_button);
	return peck->ei_button;
};

struct ei_device *
peck_ei_get_default_scroll(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->ei_scroll);
	return peck->ei_scroll;
};

struct ei_device *
peck_ei_get_default_keyboard(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->ei_keyboard);
	return peck->ei_keyboard;
};

struct ei_device *
peck_ei_get_default_touch(struct peck *peck)
{
	munit_assert_ptr_not_null(peck->ei_touch);
	return peck->ei_touch;
};

/* Ensures that device frames in tests always have an ascending and fixed
 * interval. Every time this is called it adds 10ms to the time offset.
 */
uint64_t
peck_ei_now(struct peck *peck)
{
	peck_ei_add_time_offset(peck, ms2us(10));
	return ei_now(peck->ei);
}

uint64_t
peck_eis_now(struct peck *peck)
{
	peck_eis_add_time_offset(peck, ms2us(10));
	return eis_now(peck->eis);
}

void
peck_ei_disable_fatal_bug(struct peck *peck)
{
	peck->ei_fatal_bugs = false;
}

void
peck_ei_enable_fatal_bug(struct peck *peck)
{
	peck->ei_fatal_bugs = true;
}

void
peck_eis_disable_fatal_bug(struct peck *peck)
{
	peck->eis_fatal_bugs = false;
}

void
peck_eis_enable_fatal_bug(struct peck *peck)
{
	peck->eis_fatal_bugs = true;
}

static void
peck_ei_log_handler(struct ei *ei,
		    enum ei_log_priority priority,
		    const char *message,
		    struct ei_log_context *ctx)
{
	bool use_color = true;
	run_only_once {
		use_color = isatty(STDERR_FILENO);
	}

	const char *prefix = NULL;
	switch (priority) {
		case EI_LOG_PRIORITY_ERROR:   prefix = "ERR "; break;
		case EI_LOG_PRIORITY_WARNING: prefix = "WRN "; break;
		case EI_LOG_PRIORITY_INFO:    prefix = "INF "; break;
		case EI_LOG_PRIORITY_DEBUG:   prefix = "    "; break;
	}

	struct peck *peck = ei_get_user_data(ei);
	fprintf(stderr, "|      | ei |     | %s |%*s🥚 %s%s%s\n",
		prefix, (int)peck_get_indent(peck), " ",
		use_color ? ANSI_FG_RGB(150, 125, 100) : "",
		message,
		use_color ? ansi_colorcode[RESET] : "");

	if (peck->ei_fatal_bugs) {
		assert(!strstr(message, "🪳"));
		assert(!strstr(message, "🪲"));
	}
}

static void
peck_eis_log_handler(struct eis *eis,
		     enum eis_log_priority priority,
		     const char *message,
		     struct eis_log_context *ctx)
{
	bool use_color = true;
	run_only_once {
		use_color = isatty(STDERR_FILENO);
	}

	const char *prefix = NULL;
	switch (priority) {
		case EIS_LOG_PRIORITY_ERROR:   prefix = "ERR "; break;
		case EIS_LOG_PRIORITY_WARNING: prefix = "WRN "; break;
		case EIS_LOG_PRIORITY_INFO:    prefix = "INF "; break;
		case EIS_LOG_PRIORITY_DEBUG:   prefix = "    "; break;
	}

	struct peck *peck = eis_get_user_data(eis);
	fprintf(stderr, "|      |    | EIS | %s |%*s🍨 %s%s%s\n",
		prefix, (int)peck_get_indent(peck), " ",
		use_color ? ANSI_FG_RGB(150, 200, 125) : "",
		message,
		use_color ? ansi_colorcode[RESET] : "");

	if (peck->eis_fatal_bugs) {
		assert(!strstr(message, "🪳"));
		assert(!strstr(message, "🪲"));
	}
}

_printf_(7, 0)
static void
peck_log_handler(struct logger *logger,
		 const char *prefix,
		 enum logger_priority priority,
		 const char *file, int lineno, const char *func,
		 const char *format, va_list args)
{
	_cleanup_free_ char *msgtype;
	bool use_color = true;
	run_only_once {
		use_color = isatty(STDERR_FILENO);
	}

	switch(priority) {
	case LOGGER_DEBUG: msgtype = xaprintf("%4d", lineno);  break;
	case LOGGER_INFO:  msgtype = xstrdup("INF");   break;
	case LOGGER_WARN:  msgtype = xstrdup("WRN");   break;
	case LOGGER_ERROR: msgtype = xstrdup("ERR");  break;
	default:
		msgtype = xstrdup("<invalid msgtype>");
		break;
	}

	struct peck *peck = logger_get_user_data(logger);

	fprintf(stderr, "| peck |    |     | %s |%*s%s",
		msgtype,
		(int)peck_get_indent(peck), " ",
		use_color ? ANSI_FG_RGB(125, 150, 255) : "");
	vfprintf(stderr, format, args);
	if (use_color)
		fprintf(stderr, "%s", ansi_colorcode[RESET]);
}

static void
handle_sigalrm(int signo)
{
	/* Nothing to do but this may cause a few EINTR */
}

static struct peck *
new_context(enum peck_ei_mode ei_mode)
{
	struct peck *peck = peck_create(NULL);

	assert(ei_mode == PECK_EI_RECEIVER || ei_mode == PECK_EI_SENDER);

	int rc, fd;

	struct eis *eis = eis_new(peck);
	eis_log_set_handler(eis, peck_eis_log_handler);
	eis_log_set_priority(eis, EIS_LOG_PRIORITY_DEBUG);
	eis_clock_set_now_func(eis, peck_eis_now_func);
	rc = eis_setup_backend_fd(eis);
	munit_assert_int(rc, ==, 0);
	fd = eis_backend_fd_add_client(eis);
	munit_assert_int(fd, >, 0);
	peck->eis = eis;
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_NONE);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_HANDLE_FRAME);
	peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_HANDLE_SYNC);

	struct ei *ei = ei_mode == PECK_EI_RECEIVER ? ei_new_receiver(peck) : ei_new_sender(peck);
	ei_set_user_data(ei, peck);
	ei_log_set_handler(ei, peck_ei_log_handler);
	ei_log_set_priority(ei, EI_LOG_PRIORITY_DEBUG);
	ei_clock_set_now_func(ei, peck_ei_now_func);
	ei_configure_name(ei, "eierpecken test context");
	rc = ei_setup_backend_fd(ei, fd);
	munit_assert_int(rc, ==, 0);
	peck->ei_socket_fd = -1;
	peck->ei = ei;
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSEAT);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSTART);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_FRAME);
	peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_SYNC);

	peck_ei_enable_fatal_bug(peck);
	peck_eis_enable_fatal_bug(peck);

	peck->logger = logger_new("peck", peck);
	logger_set_handler(peck->logger, peck_log_handler);
	logger_set_priority(peck->logger, LOGGER_DEBUG);

	if (enable_sigalarm) {
		/* We set up SIGALRM to hammer us because libei is used by Xwayland which
		 * uses that for scheduling and we need to be able to handle that */
		struct sigaction act;
		sigaddset(&act.sa_mask, SIGALRM);
		act.sa_flags = 0;
		act.sa_handler = handle_sigalrm;
		rc = xerrno(sigaction(SIGALRM, &act, &peck->sigact));
		if (rc >= 0) {
			struct itimerval timer = {
				.it_interval = {
					.tv_sec = 0,
					.tv_usec = 50,
				},
				.it_value = {
					.tv_sec = 0,
					.tv_usec = 50,
				}
			};
			rc = xerrno(setitimer(ITIMER_REAL, &timer, 0));
			if (rc < 0) {
				log_error(peck, "Failed to enable timer: %s\n", strerror(-rc));
				munit_assert_int(rc, >=, 0);
			}
		} else {
			log_error(peck, "Failed to enable SIGALRM: %s\n", strerror(-rc));
			munit_assert_int(rc, >=, 0);
		}
	}

	return peck;
}

struct peck *
peck_new_context(enum peck_ei_mode ei_mode)
{
	return new_context(ei_mode);
}

struct peck *
peck_new(void)
{
	return peck_new_context(PECK_EI_SENDER);
}

void
peck_ei_connect(struct peck *peck)
{
	assert(peck->ei_socket_fd != -1);

	struct ei *ei = peck->ei;
	int rc = ei_setup_backend_fd(ei, peck->ei_socket_fd);
	munit_assert_int(rc, ==, 0);
	peck->ei_socket_fd = -1;
}

void
peck_enable_eis_behavior(struct peck *peck, enum peck_eis_behavior behavior)
{
	switch (behavior) {
	case PECK_EIS_BEHAVIOR_NONE:
		peck->eis_behavior = 0;
		break;
	case PECK_EIS_BEHAVIOR_DEFAULT_SEAT:
	case PECK_EIS_BEHAVIOR_NO_DEFAULT_SEAT:
		flag_set(peck->eis_behavior, behavior);
		break;
	case PECK_EIS_BEHAVIOR_ACCEPT_ALL:
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_DEFAULT_SEAT);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_HANDLE_BIND_SEAT);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_RESUME_DEVICE);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_HANDLE_CLOSE_DEVICE);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_HANDLE_START_EMULATING);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_HANDLE_STOP_EMULATING);
		break;
	case PECK_EIS_BEHAVIOR_ADD_DEVICES:
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_POINTER);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_POINTER_ABSOLUTE);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_KEYBOARD);
		peck_enable_eis_behavior(peck, PECK_EIS_BEHAVIOR_ADD_TOUCH);
		break;
	case PECK_EIS_BEHAVIOR_REJECT_CLIENT:
		flag_clear(peck->eis_behavior, behavior - 1);
		flag_set(peck->eis_behavior, behavior);
		break;
	case PECK_EIS_BEHAVIOR_HANDLE_BIND_SEAT:
	case PECK_EIS_BEHAVIOR_HANDLE_CLOSE_DEVICE:
	case PECK_EIS_BEHAVIOR_HANDLE_FRAME:
	case PECK_EIS_BEHAVIOR_HANDLE_SYNC:
	case PECK_EIS_BEHAVIOR_HANDLE_START_EMULATING:
	case PECK_EIS_BEHAVIOR_HANDLE_STOP_EMULATING:
	case PECK_EIS_BEHAVIOR_ADD_POINTER:
	case PECK_EIS_BEHAVIOR_ADD_POINTER_ABSOLUTE:
	case PECK_EIS_BEHAVIOR_ADD_KEYBOARD:
	case PECK_EIS_BEHAVIOR_ADD_TOUCH:
		flag_set(peck->eis_behavior, behavior);
		break;
	case PECK_EIS_BEHAVIOR_ACCEPT_CLIENT:
		flag_clear(peck->eis_behavior, behavior + 1);
		flag_set(peck->eis_behavior, behavior);
		break;
	case PECK_EIS_BEHAVIOR_RESUME_DEVICE:
		flag_clear(peck->eis_behavior, PECK_EIS_BEHAVIOR_SUSPEND_DEVICE);
		flag_set(peck->eis_behavior, behavior);
		break;
	case PECK_EIS_BEHAVIOR_SUSPEND_DEVICE:
		flag_clear(peck->eis_behavior, PECK_EIS_BEHAVIOR_RESUME_DEVICE);
		flag_set(peck->eis_behavior, behavior);
		break;
	}
}
void
peck_enable_ei_behavior(struct peck *peck, enum peck_ei_behavior behavior)
{
	switch (behavior) {
	case PECK_EI_BEHAVIOR_NONE:
		peck->ei_behavior = 0;
		break;
	case PECK_EI_BEHAVIOR_HANDLE_CONNECT:
	case PECK_EI_BEHAVIOR_AUTOSEAT:
		flag_set(peck->ei_behavior, behavior);
		break;
	case PECK_EI_BEHAVIOR_AUTODEVICES:
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_CONNECT);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_AUTOSEAT);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_ADDED);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_RESUMED);
		break;
	case PECK_EI_BEHAVIOR_HANDLE_ADDED:
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_ADDED_POINTER);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_ADDED_POINTER_ABSOLUTE);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_ADDED_KEYBOARD);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_ADDED_TOUCH);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_ADDED_BUTTON);
		peck_enable_ei_behavior(peck, PECK_EI_BEHAVIOR_HANDLE_ADDED_SCROLL);
		break;
	case PECK_EI_BEHAVIOR_AUTOSTART:
	case PECK_EI_BEHAVIOR_HANDLE_ADDED_POINTER:
	case PECK_EI_BEHAVIOR_HANDLE_ADDED_POINTER_ABSOLUTE:
	case PECK_EI_BEHAVIOR_HANDLE_ADDED_KEYBOARD:
	case PECK_EI_BEHAVIOR_HANDLE_ADDED_TOUCH:
	case PECK_EI_BEHAVIOR_HANDLE_ADDED_BUTTON:
	case PECK_EI_BEHAVIOR_HANDLE_ADDED_SCROLL:
	case PECK_EI_BEHAVIOR_HANDLE_FRAME:
	case PECK_EI_BEHAVIOR_HANDLE_SYNC:
		flag_set(peck->ei_behavior, behavior);
		break;
	case PECK_EI_BEHAVIOR_HANDLE_RESUMED:
	case PECK_EI_BEHAVIOR_HANDLE_PAUSED:
		flag_set(peck->ei_behavior, behavior);
		break;
	}
}

static inline void
peck_create_eis_seat(struct peck *peck, struct eis_client *client)
{
	_unref_(eis_seat) *seat = eis_client_new_seat(client, "peck default seat");

	eis_seat_configure_capability(seat, EIS_DEVICE_CAP_POINTER);
	eis_seat_configure_capability(seat, EIS_DEVICE_CAP_POINTER_ABSOLUTE);
	eis_seat_configure_capability(seat, EIS_DEVICE_CAP_KEYBOARD);
	eis_seat_configure_capability(seat, EIS_DEVICE_CAP_TOUCH);
	eis_seat_configure_capability(seat, EIS_DEVICE_CAP_BUTTON);
	eis_seat_configure_capability(seat, EIS_DEVICE_CAP_SCROLL);

	log_debug(peck, "EIS adding seat: '%s'\n", eis_seat_get_name(seat));
	eis_seat_add(seat);

	peck->eis_seat = eis_seat_ref(seat);
}

static inline void
peck_handle_eis_connect(struct peck *peck, struct eis_event *e)
{
	struct eis_client *client = eis_event_get_client(e);

	if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT)) {
		log_debug(peck, "EIS accepting client: '%s'\n", eis_client_get_name(client));
		peck->eis_client = eis_client_ref(client);
		eis_client_connect(client);
		if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_DEFAULT_SEAT))
			peck_create_eis_seat(peck, client);
	} else {
		log_debug(peck, "EIS disconnecting client: '%s'\n", eis_client_get_name(client));
		eis_client_disconnect(client);
	}
}

static inline struct eis_device *
peck_eis_create_pointer(struct peck *peck, struct eis_seat *seat, const char *name)
{
	struct eis_device *device = eis_seat_new_device(seat);

	eis_device_configure_name(device, name);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_POINTER);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_BUTTON);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_SCROLL);
	eis_device_add(device);

	return device;
}

static inline struct eis_device *
peck_eis_create_pointer_absolute(struct peck *peck, struct eis_seat *seat, const char *name)
{
	struct eis_device *device = eis_seat_new_device(seat);
	_unref_(eis_region) *region = eis_device_new_region(device);

	eis_region_set_offset(region, 0, 0);
	eis_region_set_size(region, 1920, 1080);

	eis_device_configure_name(device, name);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_POINTER_ABSOLUTE);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_BUTTON);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_SCROLL);
	eis_region_add(region);
	eis_device_add(device);

	return device;
}

static inline struct eis_device *
peck_eis_create_keyboard(struct peck *peck, struct eis_seat *seat, const char *name)
{
	struct eis_device *device = eis_seat_new_device(seat);

	eis_device_configure_name(device, name);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_KEYBOARD);
	eis_device_add(device);

	return device;
}

static inline struct eis_device *
peck_eis_create_touch(struct peck *peck, struct eis_seat *seat, const char *name)
{
	struct eis_device *device = eis_seat_new_device(seat);
	_unref_(eis_region) *region = eis_device_new_region(device);

	eis_region_set_offset(region, 0, 0);
	eis_region_set_size(region, 1920, 1080);

	eis_device_configure_name(device, name);
	eis_device_configure_capability(device, EIS_DEVICE_CAP_TOUCH);
	eis_region_add(region);
	eis_device_add(device);

	return device;
}

static inline void
peck_handle_eis_seat_bind(struct peck *peck, struct eis_event *e)
{
	struct eis_seat *seat = eis_event_get_seat(e);

	log_debug(peck, "EIS binding seat: '%s'\n", eis_seat_get_name(seat));

	if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER)) {
		if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_ADD_POINTER) && !peck->eis_pointer) {
			log_debug(peck, "EIS creating default pointer\n");
			_unref_(eis_device) *ptr = peck_eis_create_pointer(peck, seat, "default pointer");

			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_RESUME_DEVICE))
				eis_device_resume(ptr);

			peck->eis_pointer = eis_device_ref(ptr);
		}
	} else {
		if (peck->eis_pointer) {
			log_debug(peck, "EIS removing default pointer\n");

			_unref_(eis_device) *ptr = steal(&peck->eis_pointer);
			eis_device_remove(ptr);
		}
	}

	if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER_ABSOLUTE)) {
		if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_ADD_POINTER_ABSOLUTE) && !peck->eis_abs) {
			log_debug(peck, "EIS creating default abs pointer\n");
			_unref_(eis_device) *abs = peck_eis_create_pointer_absolute(peck, seat, "default abs");

			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_RESUME_DEVICE))
				eis_device_resume(abs);

			peck->eis_abs = eis_device_ref(abs);
		}
	} else {
		if (peck->eis_abs) {
			log_debug(peck, "EIS removing default abs\n");

			_unref_(eis_device) *abs = steal(&peck->eis_abs);
			eis_device_remove(abs);
		}
	}

	if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_KEYBOARD)) {
	    if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_ADD_KEYBOARD) && !peck->eis_keyboard) {
		    log_debug(peck, "EIS creating default keyboard\n");
		    _unref_(eis_device) *kbd = peck_eis_create_keyboard(peck, seat, "default keyboard");

		    if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_RESUME_DEVICE))
			    eis_device_resume(kbd);

		    peck->eis_keyboard = eis_device_ref(kbd);
	    }
	} else {
		if (peck->eis_keyboard) {
			log_debug(peck, "EIS removing default keyboard\n");

			_unref_(eis_device) *kbd = steal(&peck->eis_keyboard);
			eis_device_remove(kbd);
		}
	}

	if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_TOUCH)) {
		if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_ADD_TOUCH) && !peck->eis_touch) {
			log_debug(peck, "EIS creating default touch\n");
			_unref_(eis_device) *touch = peck_eis_create_touch(peck, seat, "default touch");

			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_RESUME_DEVICE))
				eis_device_resume(touch);

			peck->eis_touch = eis_device_ref(touch);
		}
	} else {
		if (peck->eis_touch) {
			log_debug(peck, "EIS removing default touch\n");

			_unref_(eis_device) *touch = steal(&peck->eis_touch);
			eis_device_remove(touch);
		}
	}

	/* Removing all caps means removing the seat */
	if (!eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER) &&
	    !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER_ABSOLUTE) &&
	    !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_KEYBOARD) &&
	    !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_BUTTON) &&
	    !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_SCROLL) &&
	    !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_TOUCH))
		eis_seat_remove(seat);
}

static inline void
peck_eis_device_remove(struct peck *peck, struct eis_device *device)
{
	eis_device_remove(device);
	if (device == peck->eis_pointer)
		peck->eis_pointer = eis_device_unref(device);
	if (device == peck->eis_abs)
		peck->eis_abs = eis_device_unref(device);
	if (device == peck->eis_keyboard)
		peck->eis_keyboard = eis_device_unref(device);
	if (device == peck->eis_touch)
		peck->eis_touch = eis_device_unref(device);
	if (device == peck->eis_button)
		peck->eis_button = eis_device_unref(device);
	if (device == peck->eis_scroll)
		peck->eis_scroll = eis_device_unref(device);
}

bool
_peck_dispatch_eis(struct peck *peck, int lineno)
{
	struct eis *eis = peck->eis;
	bool had_event = false;
	bool need_frame = false;
	static uint64_t last_timestamp;

	log_debug(peck, "EIS Dispatch, line %d\n", lineno);
	peck_indent(peck);

	while (eis) {
		eis_dispatch(eis);
		tristate process_event = tristate_unset;

		_unref_(eis_event) *e = eis_peek_event(eis);
		if (!e)
			break;

		switch (eis_event_get_type(e)) {
		case EIS_EVENT_CLIENT_CONNECT:
		case EIS_EVENT_CLIENT_DISCONNECT:
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_ACCEPT_CLIENT) ||
			    flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_REJECT_CLIENT))
				process_event = tristate_yes;
			break;
		case EIS_EVENT_SEAT_BIND:
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_BIND_SEAT))
				process_event = tristate_yes;
			else
				process_event = tristate_no;
			break;
		case EIS_EVENT_DEVICE_CLOSED:
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_CLOSE_DEVICE))
				process_event = tristate_yes;
			else
				process_event = tristate_no;
			break;
		case EIS_EVENT_PONG:
			break;
		case EIS_EVENT_SYNC:
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_SYNC))
				process_event = tristate_yes;
			break;
		case EIS_EVENT_FRAME:
			/* Ensure we only expect frames when we expect them */
			munit_assert_true(need_frame);
			need_frame = false;
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_FRAME))
				process_event = tristate_yes;
			break;
		case EIS_EVENT_DEVICE_START_EMULATING:
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_START_EMULATING))
				process_event = tristate_yes;
			break;
		case EIS_EVENT_DEVICE_STOP_EMULATING:
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_STOP_EMULATING))
				process_event = tristate_yes;
			break;
		case EIS_EVENT_POINTER_MOTION:
		case EIS_EVENT_POINTER_MOTION_ABSOLUTE:
		case EIS_EVENT_BUTTON_BUTTON:
		case EIS_EVENT_SCROLL_DELTA:
		case EIS_EVENT_SCROLL_STOP:
		case EIS_EVENT_SCROLL_CANCEL:
		case EIS_EVENT_SCROLL_DISCRETE:
		case EIS_EVENT_KEYBOARD_KEY:
		case EIS_EVENT_TOUCH_DOWN:
		case EIS_EVENT_TOUCH_UP:
		case EIS_EVENT_TOUCH_MOTION:
			need_frame = true;
			break;
		}

		log_debug(peck, "EIS received %s.... %s\n",
			  peck_eis_event_name(e),
			  tristate_is_yes(process_event) ?
				  "handling ourselves" : "punting to caller");

		if (!tristate_is_yes(process_event))
			break;

		had_event = true;

		/* manual unref, _cleanup_ will take care of the real event */
		eis_event_unref(e);
		e = eis_get_event(eis);

		switch (eis_event_get_type(e)) {
		case EIS_EVENT_CLIENT_CONNECT:
			peck_handle_eis_connect(peck, e);
			break;
		case EIS_EVENT_CLIENT_DISCONNECT:
			log_debug(peck, "EIS disconnecting client: %s\n",
				  eis_client_get_name(eis_event_get_client(e)));
			eis_client_disconnect(eis_event_get_client(e));
			break;
		case EIS_EVENT_SEAT_BIND:
			peck_handle_eis_seat_bind(peck, e);
			break;
		case EIS_EVENT_DEVICE_CLOSED:
			peck_eis_device_remove(peck, eis_event_get_device(e));
			break;
		case EIS_EVENT_FRAME: {
			uint64_t timestamp = eis_event_get_time(e);
			uint64_t ts_now = 0;

			munit_assert_int(now(&ts_now), ==, 0);
			munit_assert_int64(last_timestamp, <, timestamp);
			munit_assert_int64(last_timestamp, <=, ts_now);
			last_timestamp = timestamp;
			break;
		}
		default:
			break;
		}
	}

	peck_dedent(peck);

	return had_event;
}

static inline tristate
peck_check_ei_added(struct peck *peck, struct ei_event *e)
{
	struct ei_device *device = ei_event_get_device(e);

	if (ei_device_has_capability(device, EI_DEVICE_CAP_POINTER) &&
	    flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_ADDED_POINTER))
		return tristate_yes;

	if (ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE) &&
	    flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_ADDED_POINTER_ABSOLUTE))
		return tristate_yes;

	if (ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD) &&
	    flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_ADDED_KEYBOARD))
		return tristate_yes;

	if (ei_device_has_capability(device, EI_DEVICE_CAP_TOUCH) &
	    flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_ADDED_TOUCH))
		return tristate_yes;

	if (ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON) &
	    flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_ADDED_BUTTON))
		return tristate_yes;

	if (ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL) &
	    flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_ADDED_SCROLL))
		return tristate_yes;

	return tristate_unset;
}

bool
_peck_dispatch_ei(struct peck *peck, int lineno)
{
	struct ei *ei = peck->ei;
	bool had_event = false;
	bool need_frame = false;

	log_debug(peck, "ei dispatch, line %d\n", lineno);
	peck_indent(peck);

	while (ei) {
		ei_dispatch(ei);
		tristate process_event = tristate_no;

		_unref_(ei_event) *e = ei_peek_event(ei);
		if (!e)
			break;

		switch (ei_event_get_type(e)) {
		case EI_EVENT_CONNECT:
			if (flag_is_set(peck->ei_behavior,
					PECK_EI_BEHAVIOR_HANDLE_CONNECT))
				process_event = tristate_yes;
			break;
		case EI_EVENT_DISCONNECT:
			break;
		case EI_EVENT_SEAT_ADDED:
			if (peck->ei_seat == NULL &&
			    flag_is_set(peck->ei_behavior,
					PECK_EI_BEHAVIOR_AUTOSEAT))
				process_event = tristate_yes;
			break;
		case EI_EVENT_SEAT_REMOVED:
			break;
		case EI_EVENT_DEVICE_ADDED:
			process_event = peck_check_ei_added(peck, e);
			break;
		case EI_EVENT_DEVICE_REMOVED:
			break;
		case EI_EVENT_DEVICE_RESUMED:
			if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_RESUMED))
				process_event = tristate_yes;
			break;
		case EI_EVENT_DEVICE_PAUSED:
			if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_PAUSED))
				process_event = tristate_yes;
			break;
		case EI_EVENT_PONG:
			break;
		case EI_EVENT_SYNC:
			if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_SYNC))
				process_event = tristate_yes;
			break;
		case EI_EVENT_FRAME:
			/* Ensure we only expect frames when we expect them */
			munit_assert_true(need_frame);
			need_frame = false;

			if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_FRAME))
				process_event = tristate_yes;
			break;
		case EI_EVENT_DEVICE_START_EMULATING:
		case EI_EVENT_DEVICE_STOP_EMULATING:
			break;
		case EI_EVENT_POINTER_MOTION:
		case EI_EVENT_POINTER_MOTION_ABSOLUTE:
		case EI_EVENT_BUTTON_BUTTON:
		case EI_EVENT_SCROLL_DELTA:
		case EI_EVENT_SCROLL_STOP:
		case EI_EVENT_SCROLL_CANCEL:
		case EI_EVENT_SCROLL_DISCRETE:
		case EI_EVENT_KEYBOARD_KEY:
		case EI_EVENT_KEYBOARD_MODIFIERS:
		case EI_EVENT_TOUCH_DOWN:
		case EI_EVENT_TOUCH_UP:
		case EI_EVENT_TOUCH_MOTION:
			need_frame = true;
			break;
		}

		log_debug(peck, "ei event %s.... %s\n",
			  peck_ei_event_name(e),
			  tristate_is_yes(process_event) ?
				  "handling ourselves" : "punting to caller");

		if (!tristate_is_yes(process_event))
			break;

		had_event = true;

		/* manual unref, _cleanup_ will take care of the real event */
		ei_event_unref(e);
		e = ei_get_event(ei);

		switch (ei_event_get_type(e)) {
		case EI_EVENT_CONNECT:
			log_debug(peck, "ei is connected\n");
			/* Nothing to do here */
			break;
		case EI_EVENT_SEAT_ADDED:
			{
			struct ei_seat *seat = ei_event_get_seat(e);
			munit_assert_ptr_null(peck->ei_seat);
			peck->ei_seat = ei_seat_ref(seat);
			log_debug(peck, "default seat: %s\n", ei_seat_get_name(peck->ei_seat));
			ei_seat_bind_capabilities(seat,
						  EI_DEVICE_CAP_POINTER,
						  EI_DEVICE_CAP_POINTER_ABSOLUTE,
						  EI_DEVICE_CAP_KEYBOARD,
						  EI_DEVICE_CAP_TOUCH,
						  EI_DEVICE_CAP_BUTTON,
						  EI_DEVICE_CAP_SCROLL, NULL);
			break;
			}
		case EI_EVENT_DEVICE_ADDED:
			{
			struct ei_device *device = ei_event_get_device(e);
			if (!peck->ei_pointer && ei_device_has_capability(device, EI_DEVICE_CAP_POINTER))
				peck->ei_pointer = ei_device_ref(device);
			if (!peck->ei_abs && ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE))
				peck->ei_abs = ei_device_ref(device);
			if (!peck->ei_keyboard && ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD))
				peck->ei_keyboard = ei_device_ref(device);
			if (!peck->ei_touch && ei_device_has_capability(device, EI_DEVICE_CAP_TOUCH))
				peck->ei_touch = ei_device_ref(device);
			if (!peck->ei_button && ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON))
				peck->ei_button = ei_device_ref(device);
			if (!peck->ei_scroll && ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL))
				peck->ei_scroll = ei_device_ref(device);
			break;
			}
		case EI_EVENT_DEVICE_RESUMED:
			if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_AUTOSTART)) {
				static uint32_t sequence;
				struct ei_device *device = ei_event_get_device(e);
				if (ei_is_sender(ei_device_get_context(device)))
					ei_device_start_emulating(device, ++sequence);
			}
			break;
		case EI_EVENT_DEVICE_PAUSED:
			/* Nothing to do here */
			break;
		default:
			break;
		}
	}

	peck_dedent(peck);
	return had_event;
}

void
_peck_dispatch_until_stable(struct peck *peck, const char *func, int lineno)
{
	int eis = 0, ei = 0;
	int loop_counter = 0;

	log_debug(peck, "dispatching until stable (%s:%d) {\n", func, lineno);
	peck_indent(peck);

	/* we go two dispatch loops for both ei and EIS before we say it's
	 * stable. With the DEVICE_REGION/KEYMAP/DONE event split we may
	 * get events on the wire that don't result in an actual event
	 * yet, so we can get stuck if we just wait for one.
	 */
	do {
		log_debug(peck, "entering dispatch loop %d {\n", loop_counter++);
		peck_indent(peck);
		eis = _peck_dispatch_eis(peck, lineno) ? 0 : eis + 1;
		ei = _peck_dispatch_ei(peck, lineno) ? 0 : ei + 1;
		peck_dedent(peck);
		log_debug(peck, "} done: ei %d|eis %d\n", ei, eis);
	} while (ei <= 2 || eis <= 2);

	peck_dedent(peck);
	log_debug(peck, "} stable (%s:%d)\n", func, lineno);
}

void
peck_drain_eis(struct eis *eis)
{
	if (!eis)
		return;

	eis_dispatch(eis);

	while (true) {
		_unref_(eis_event) *e = eis_get_event(eis);
		if (e == NULL)
			break;
	}
}

void
peck_drain_ei(struct ei *ei)
{
	if (!ei)
		return;

	ei_dispatch(ei);
	while (true) {
		_unref_(ei_event) *e = ei_get_event(ei);
		if (e == NULL)
			break;
	}
}

void
_peck_assert_no_ei_events(struct ei *ei, int lineno)
{
	struct peck *peck = ei_get_user_data(ei);

	ei_dispatch(ei);
	while (true) {
		_unref_(ei_event) *e = ei_get_event(ei);
		if (!e)
			return;

		if (peck && flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_FRAME) &&
		    ei_event_get_type(e) == EI_EVENT_FRAME) {
			log_debug(peck, "Skipping over frame event\n");
			continue;
		}

		if (peck && flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_SYNC) &&
		    ei_event_get_type(e) == EI_EVENT_SYNC) {
			log_debug(peck, "Skipping over sync event\n");
			continue;
		}

		munit_errorf("Expected empty event queue, have: %s, line %d\n",
			     peck_ei_event_name(e), lineno);
	}
}

void
_peck_assert_no_eis_events(struct eis *eis, int lineno)
{
	struct peck *peck = eis_get_user_data(eis);

	eis_dispatch(eis);
	while (true) {
		_unref_(eis_event) *e = eis_get_event(eis);
		if (!e)
			return;

		if (peck) {
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_FRAME) &&
			    eis_event_get_type(e) == EIS_EVENT_FRAME) {
				log_debug(peck, "Skipping over frame event\n");
				continue;
			}
			if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_SYNC) &&
			    eis_event_get_type(e) == EIS_EVENT_SYNC) {
				log_debug(peck, "Skipping over sync event\n");
				continue;
			}
		}

		munit_errorf("Expected empty event queue, have: %s, line %d\n",
			     peck_eis_event_name(e), lineno);
	}
}

struct ei_event *
_peck_ei_next_event(struct ei *ei, enum ei_event_type type, int lineno)
{
	struct ei_event *event = ei_get_event(ei);
	struct peck *peck = ei_get_user_data(ei);

	if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_FRAME)) {
		while (event && ei_event_get_type(event) == EI_EVENT_FRAME) {
			ei_event_unref(event);
			log_debug(peck, "Skipping over frame event\n");
			event = ei_get_event(ei);
		}
	}

	if (flag_is_set(peck->ei_behavior, PECK_EI_BEHAVIOR_HANDLE_SYNC)) {
		while (event && ei_event_get_type(event) == EI_EVENT_SYNC) {
			ei_event_unref(event);
			log_debug(peck, "Skipping over sync event\n");
			event = ei_get_event(ei);
		}
	}

	if (!event)
		munit_errorf("Expected ei event type %s, got none, line %d\n",
			     peck_ei_event_type_name(type),
			     lineno);
	if (!streq(peck_ei_event_name(event),
		   peck_ei_event_type_name(type)))
		munit_errorf("Expected ei event type %s, got %s, line %d\n",
			     peck_ei_event_type_name(type),
			     peck_ei_event_name(event),
			     lineno);

	log_debug(peck, "ei passing %s to caller, line %d\n", peck_ei_event_name(event), lineno);

	return event;
}

struct eis_event *
_peck_eis_next_event(struct eis *eis, enum eis_event_type type, int lineno)
{
	struct eis_event *event = eis_get_event(eis);
	struct peck *peck = eis_get_user_data(eis);

	if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_FRAME)) {
		while (event && eis_event_get_type(event) == EIS_EVENT_FRAME) {
			eis_event_unref(event);
			log_debug(peck, "Skipping over frame event\n");
			event = eis_get_event(eis);
		}
	}

	if (flag_is_set(peck->eis_behavior, PECK_EIS_BEHAVIOR_HANDLE_SYNC)) {
		while (event && eis_event_get_type(event) == EIS_EVENT_SYNC) {
			eis_event_unref(event);
			log_debug(peck, "Skipping over sync event\n");
			event = eis_get_event(eis);
		}
	}

	if (!event)
		munit_errorf("Expected EIS event type %s, got none, line %d\n",
			     peck_eis_event_type_name(type),
			     lineno);
	if (eis_event_get_type(event) != type) {
		munit_errorf("Expected EIS event type %s, got %s, line %d\n",
			     peck_eis_event_type_name(type),
			     peck_eis_event_type_name(eis_event_get_type(event)),
			     lineno);
	}

	log_debug(peck, "EIS passing %s to caller, line %d\n", peck_eis_event_name(event), lineno);

	return event;
}

struct eis_event *
_peck_eis_touch_event(struct eis *eis, enum eis_event_type type, double x, double y, int lineno)
{
	assert(type == EIS_EVENT_TOUCH_DOWN || type == EIS_EVENT_TOUCH_MOTION);

	struct eis_event *event = _peck_eis_next_event(eis, type, lineno);
	double ex = eis_event_touch_get_x(event);
	double ey = eis_event_touch_get_y(event);

	if (fabs(ex - x) > 1e-3 || fabs(ey - y) > 1e-3) {
		munit_errorf("Touch coordinate mismatch: have (%.2f/%.2f) need (%.2f/%.2f)\n",
			     ex, ey, x, y);
	}

	return event;
}

struct ei_event *
_peck_ei_touch_event(struct ei *ei, enum ei_event_type type, double x, double y, int lineno)
{
	assert(type == EI_EVENT_TOUCH_DOWN || type == EI_EVENT_TOUCH_MOTION);

	struct ei_event *event = _peck_ei_next_event(ei, type, lineno);
	double ex = ei_event_touch_get_x(event);
	double ey = ei_event_touch_get_y(event);

	if (fabs(ex - x) > 1e-3 || fabs(ey - y) > 1e-3) {
		munit_errorf("Touch coordinate mismatch: have (%.2f/%.2f) need (%.2f/%.2f)\n",
			     ex, ey, x, y);
	}

	return event;
}

const char *
peck_ei_event_name(struct ei_event *e)
{
	return peck_ei_event_type_name(ei_event_get_type(e));
}

const char *
peck_ei_event_type_name(enum ei_event_type type)
{
#define CASE_STRING(_name) \
	case EI_EVENT_##_name: return #_name

	switch (type) {
		CASE_STRING(CONNECT);
		CASE_STRING(DISCONNECT);
		CASE_STRING(SEAT_ADDED);
		CASE_STRING(SEAT_REMOVED);
		CASE_STRING(DEVICE_ADDED);
		CASE_STRING(DEVICE_REMOVED);
		CASE_STRING(DEVICE_PAUSED);
		CASE_STRING(DEVICE_RESUMED);
		CASE_STRING(KEYBOARD_MODIFIERS);
		CASE_STRING(PONG);
		CASE_STRING(SYNC);
		CASE_STRING(FRAME);
		CASE_STRING(DEVICE_START_EMULATING);
		CASE_STRING(DEVICE_STOP_EMULATING);
		CASE_STRING(POINTER_MOTION);
		CASE_STRING(POINTER_MOTION_ABSOLUTE);
		CASE_STRING(BUTTON_BUTTON);
		CASE_STRING(SCROLL_DELTA);
		CASE_STRING(SCROLL_STOP);
		CASE_STRING(SCROLL_CANCEL);
		CASE_STRING(SCROLL_DISCRETE);
		CASE_STRING(KEYBOARD_KEY);
		CASE_STRING(TOUCH_DOWN);
		CASE_STRING(TOUCH_UP);
		CASE_STRING(TOUCH_MOTION);
	}
#undef CASE_STRING
	assert(!"Unhandled ei event type");
}

const char *
peck_eis_event_name(struct eis_event *e)
{
	return peck_eis_event_type_name(eis_event_get_type(e));
}

const char *
peck_eis_event_type_name(enum eis_event_type type)
{
#define CASE_STRING(_name) \
	case EIS_EVENT_##_name: return #_name

	switch (type) {
		CASE_STRING(CLIENT_CONNECT);
		CASE_STRING(CLIENT_DISCONNECT);
		CASE_STRING(SEAT_BIND);
		CASE_STRING(DEVICE_CLOSED);
		CASE_STRING(PONG);
		CASE_STRING(SYNC);
		CASE_STRING(DEVICE_START_EMULATING);
		CASE_STRING(DEVICE_STOP_EMULATING);
		CASE_STRING(POINTER_MOTION);
		CASE_STRING(POINTER_MOTION_ABSOLUTE);
		CASE_STRING(BUTTON_BUTTON);
		CASE_STRING(SCROLL_DELTA);
		CASE_STRING(SCROLL_STOP);
		CASE_STRING(SCROLL_CANCEL);
		CASE_STRING(SCROLL_DISCRETE);
		CASE_STRING(KEYBOARD_KEY);
		CASE_STRING(TOUCH_DOWN);
		CASE_STRING(TOUCH_UP);
		CASE_STRING(TOUCH_MOTION);
		CASE_STRING(FRAME);
	}
#undef CASE_STRING
	assert(!"Unhandled EIS event type");
}

void
_peck_mark(struct peck *peck, const char *func, int line)
{
	static uint32_t mark = 0;
	log_debug(peck, "mark %3d: line %d in %s()\n", mark++, line, func);
}

void _peck_debug(struct peck *peck, const char *func, int line, const char *format, ...)
{
	va_list args;

	va_start(args, format);
	log_msg_va(peck->logger, LOGGER_DEBUG, "unused", line, func, format, args);
	va_end(args);
}

MUNIT_GLOBAL_SETUP(init_sigalarm)
{
	int argc = setup->argc;
	char **argv = setup->argv;

	for (int i = 0; i < argc; i++) {
		if (streq(argv[i], "--enable-sigalrm") || streq(argv[i], "--enable-sigalarm")) {
			enable_sigalarm = true;
			for (int j = i + 1; j < setup->argc; j++) {
				setup->argv[i] = setup->argv[j];
		        }
			setup->argc--;
			break;
		}
	}
}
