/* 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 <errno.h>
#include <stdint.h>

#include "util-bits.h"
#include "util-io.h"
#include "util-macros.h"
#include "util-time.h"

#include "eis-proto.h"
#include "libeis-private.h"

static_assert((int)EIS_DEVICE_TYPE_VIRTUAL == EIS_DEVICE_DEVICE_TYPE_VIRTUAL, "ABI mismatch");
static_assert((int)EIS_DEVICE_TYPE_PHYSICAL == EIS_DEVICE_DEVICE_TYPE_PHYSICAL, "ABI mismatch");

_public_ OBJECT_IMPLEMENT_REF(eis_keymap);
_public_ OBJECT_IMPLEMENT_UNREF_CLEANUP(eis_keymap);
_public_ OBJECT_IMPLEMENT_GETTER(eis_keymap, type, enum eis_keymap_type);
_public_
OBJECT_IMPLEMENT_GETTER(eis_keymap, fd, int);
_public_
OBJECT_IMPLEMENT_GETTER(eis_keymap, size, size_t);
_public_
OBJECT_IMPLEMENT_GETTER(eis_keymap, device, struct eis_device *);
_public_
OBJECT_IMPLEMENT_GETTER(eis_keymap, user_data, void *);
_public_
OBJECT_IMPLEMENT_SETTER(eis_keymap, user_data, void *);

static void
eis_keymap_destroy(struct eis_keymap *keymap)
{
	if (!keymap->assigned)
		eis_device_unref(keymap->device);
	xclose(keymap->fd);
}

static OBJECT_IMPLEMENT_CREATE(eis_keymap);

_public_ struct eis_keymap *
eis_device_new_keymap(struct eis_device *device, enum eis_keymap_type type, int fd, size_t size)
{
	switch (type) {
	case EIS_KEYMAP_TYPE_XKB:
		break;
	default:
		return NULL;
	}

	if (fd < 0 || size == 0)
		return NULL;

	int newfd = xdup(fd);
	if (newfd < 0)
		return NULL;

	struct eis_keymap *keymap = eis_keymap_create(NULL);
	keymap->device = eis_device_ref(device);
	keymap->fd = newfd;
	keymap->type = type;
	keymap->size = size;

	return keymap;
}

_public_ struct eis *
eis_device_get_context(struct eis_device *device)
{
	return eis_client_get_context(eis_device_get_client(device));
}

_public_ void
eis_keymap_add(struct eis_keymap *keymap)
{
	struct eis_device *device = eis_keymap_get_device(keymap);

	if (device->state != EIS_DEVICE_STATE_NEW) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device already (dis)connected",
			       __func__);
		return;
	}

	if (device->keymap) {
		log_bug_client(eis_device_get_context(device),
			       "%s: only one keymap can only be assigned and only once",
			       __func__);
		return;
	}

	/* New keymap holds ref to the device, for assigned keymap the device
	 * holds the ref to the keymap instead */
	device->keymap = eis_keymap_ref(keymap);
	keymap->assigned = true;
	eis_device_unref(keymap->device);
}

_public_ struct eis_keymap *
eis_device_keyboard_get_keymap(struct eis_device *device)
{
	return device->keymap;
}

static void
eis_device_destroy(struct eis_device *device)
{
	struct eis_region *r;
	struct eis_event *event;

	list_for_each_safe(r, &device->regions, link)
		eis_region_unref(r);

	/* regions_new does not own a ref */

	eis_keymap_unref(device->keymap);

	list_for_each_safe(event, &device->pending_event_queue, link) {
		list_remove(&event->link);
		eis_event_unref(event);
	}

	eis_pointer_unref(device->pointer);
	eis_touchscreen_unref(device->touchscreen);
	eis_keyboard_unref(device->keyboard);
	eis_text_unref(device->text);

	free(device->name);
}

_public_ OBJECT_IMPLEMENT_REF(eis_device);
_public_ OBJECT_IMPLEMENT_UNREF_CLEANUP(eis_device);
static OBJECT_IMPLEMENT_CREATE(eis_device);
static OBJECT_IMPLEMENT_PARENT(eis_device, eis_seat);
_public_
OBJECT_IMPLEMENT_GETTER(eis_device, user_data, void *);
_public_
OBJECT_IMPLEMENT_SETTER(eis_device, user_data, void *);
_public_
OBJECT_IMPLEMENT_GETTER(eis_device, name, const char *);
_public_ OBJECT_IMPLEMENT_GETTER(eis_device, type, enum eis_device_type);
_public_
OBJECT_IMPLEMENT_GETTER(eis_device, width, uint32_t);
_public_
OBJECT_IMPLEMENT_GETTER(eis_device, height, uint32_t);
OBJECT_IMPLEMENT_GETTER_AS_REF(eis_device, proto_object, const struct brei_object *);

object_id_t
eis_device_get_id(struct eis_device *device)
{
	return device->proto_object.id;
}

_public_ struct eis_seat *
eis_device_get_seat(struct eis_device *device)
{
	return eis_device_parent(device);
}

_public_ struct eis_region *
eis_device_get_region(struct eis_device *device, size_t index)
{
	return list_nth_entry(struct eis_region, &device->regions, link, index);
}

_public_ struct eis_region *
eis_device_get_region_at(struct eis_device *device, double x, double y)
{
	struct eis_region *r;

	list_for_each(r, &device->regions, link) {
		if (eis_region_contains(r, x, y))
			return r;
	}
	return NULL;
}

_public_ struct eis_client *
eis_device_get_client(struct eis_device *device)
{
	return eis_seat_get_client(eis_device_get_seat(device));
}

static inline bool
eis_device_in_region(struct eis_device *device, double x, double y)
{
	struct eis_region *r;

	if (list_empty(&device->regions))
		return true;

	list_for_each(r, &device->regions, link) {
		if (eis_region_contains(r, x, y))
			return true;
	}

	return false;
}

static struct brei_result *
client_msg_release(struct eis_device *device)
{
	eis_device_closed_by_client(device);
	return NULL;
}

#define DISCONNECT_IF_RECEIVER_CONTEXT(device_) do { \
	struct eis_client *client_ = eis_device_get_client(device_); \
	if (!eis_client_is_sender(client_)) { \
		struct eis *_ctx = eis_client_get_context(client_); \
		log_bug_client(_ctx, "Invalid event from receiver ei context. Disconnecting client"); \
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_MODE, "Invalid event from receiver ei context"); \
	} \
} while(0)

static struct brei_result *
client_msg_start_emulating(struct eis_device *device, uint32_t serial, uint32_t sequence)
{
	struct brei_result *result = NULL;

	eis_client_update_client_serial(eis_device_get_client(device), serial);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	switch (device->state) {
	case EIS_DEVICE_STATE_DEAD:
	case EIS_DEVICE_STATE_CLOSED_BY_CLIENT:
	case EIS_DEVICE_STATE_NEW:
	case EIS_DEVICE_STATE_AWAITING_READY:
	case EIS_DEVICE_STATE_EMULATING:
		result = brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
					 "Invalid device state %u for a start_emulating event",
					 device->state);
		break;
	case EIS_DEVICE_STATE_RESUMED:
		eis_queue_device_start_emulating_event(device, sequence);
		device->state = EIS_DEVICE_STATE_EMULATING;
		break;
	case EIS_DEVICE_STATE_PAUSED:
		/* race condition, that's fine */
		break;
	}

	return result;
}

static struct brei_result *
client_msg_stop_emulating(struct eis_device *device, uint32_t serial)
{
	struct brei_result *result = NULL;

	eis_client_update_client_serial(eis_device_get_client(device), serial);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	switch (device->state) {
	case EIS_DEVICE_STATE_DEAD:
	case EIS_DEVICE_STATE_CLOSED_BY_CLIENT:
	case EIS_DEVICE_STATE_NEW:
	case EIS_DEVICE_STATE_AWAITING_READY:
	case EIS_DEVICE_STATE_RESUMED:
		result = brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
					 "Invalid device state %u for a stop_emulating event",
					 device->state);
		break;
	case EIS_DEVICE_STATE_EMULATING:
		eis_queue_device_stop_emulating_event(device);
		device->state = EIS_DEVICE_STATE_RESUMED;
		break;
	case EIS_DEVICE_STATE_PAUSED:
		/* race condition, that's fine */
		break;
	}

	return result;
}

static struct brei_result *
maybe_error_on_device_state(struct eis_device *device, const char *event_type)
{
	switch (device->state) {
	case EIS_DEVICE_STATE_RESUMED:
		/* could be a race condition, but it's unlikely unless the
		 * EIS implementation pauses and resumes immediately without
		 * giving the client a chance to catch up. So let's
		 * treat this as error until we see real issues.
		 */
		break;
	case EIS_DEVICE_STATE_PAUSED:
		/* we paused the device but the client sent us an event
		 * - most likely a race condition, so let's ignore it */
		return NULL;
	case EIS_DEVICE_STATE_EMULATING:
		return NULL;
	case EIS_DEVICE_STATE_NEW:
	case EIS_DEVICE_STATE_AWAITING_READY:
	case EIS_DEVICE_STATE_CLOSED_BY_CLIENT:
	case EIS_DEVICE_STATE_DEAD:
		break;
	}

	return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
			       "Invalid device state %u for a %s event",
			       device->state,
			       event_type);
}

static struct brei_result *
client_msg_frame(struct eis_device *device, uint32_t serial, uint64_t time)
{
	eis_client_update_client_serial(eis_device_get_client(device), serial);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_frame_event(device, time);
		return NULL;
	}

	return maybe_error_on_device_state(device, "frame");
}

static struct brei_result *
client_msg_ready(struct eis_device *device)
{
	if (device->state != EIS_DEVICE_STATE_AWAITING_READY)
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Invalid device state %u for a ready event",
				       device->state);

	device->state = EIS_DEVICE_STATE_PAUSED;
	eis_queue_device_ready_event(device);

	return NULL;
}

static const struct eis_device_interface interface = {
	.release = client_msg_release,
	.start_emulating = client_msg_start_emulating,
	.stop_emulating = client_msg_stop_emulating,
	.frame = client_msg_frame,
	.ready = client_msg_ready,
};

const struct eis_device_interface *
eis_device_get_interface(struct eis_device *device)
{
	return &interface;
}

static struct brei_result *
client_msg_pointer_rel(struct eis_pointer *pointer, float x, float y)
{
	struct eis_device *device = eis_pointer_get_device(pointer);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_POINTER)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Pointer rel event for non-pointer device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_pointer_rel_event(device, x, y);
		return NULL;
	}

	return maybe_error_on_device_state(device, "pointer rel");
}

static struct brei_result *
client_msg_pointer_abs(struct eis_pointer_absolute *pointer, float x, float y)
{
	struct eis_device *device = eis_pointer_absolute_get_device(pointer);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_POINTER_ABSOLUTE)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Pointer abs event for non-pointer device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		if (eis_device_in_region(device, x, y))
			eis_queue_pointer_abs_event(device, x, y);
		return NULL;
	}

	return maybe_error_on_device_state(device, "pointer abs");
}

static struct brei_result *
client_msg_button(struct eis_button *button, uint32_t btn, uint32_t state)
{
	struct eis_device *device = eis_button_get_device(button);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_BUTTON)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Button event for non-button device");
	}

	if (btn >= KEY_CNT)
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Button event for invalid button %x (KEY_CNT is %#x)",
				       btn,
				       KEY_CNT);

	if (!eis_device_update_key_button_state(device, btn, state))
		return NULL;

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_pointer_button_event(device, btn, !!state);
		return NULL;
	}

	return maybe_error_on_device_state(device, "pointer button");
}

static struct brei_result *
client_msg_scroll(struct eis_scroll *scroll, float x, float y)
{
	struct eis_device *device = eis_scroll_get_device(scroll);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Scroll event for non-scroll device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_pointer_scroll_event(device, x, y);
		return NULL;
	}

	return maybe_error_on_device_state(device, "pointer scroll");
}

static struct brei_result *
client_msg_scroll_discrete(struct eis_scroll *scroll, int32_t x, int32_t y)
{
	struct eis_device *device = eis_scroll_get_device(scroll);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Scroll discrete event for non-scroll device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_pointer_scroll_discrete_event(device, x, y);
		return NULL;
	}

	return maybe_error_on_device_state(device, "pointer scroll discrete");
}

static struct brei_result *
client_msg_scroll_stop(struct eis_scroll *scroll, uint32_t x, uint32_t y, uint32_t is_cancel)
{
	struct eis_device *device = eis_scroll_get_device(scroll);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Scroll stop event for non-scroll device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		if (is_cancel)
			eis_queue_pointer_scroll_cancel_event(device, !!x, !!y);
		else
			eis_queue_pointer_scroll_stop_event(device, !!x, !!y);
		return NULL;
	}

	return maybe_error_on_device_state(device, "pointer scroll stop");
}

static struct brei_result *
client_msg_pointer_release(struct eis_pointer *pointer)
{
	struct eis_device *device = eis_pointer_get_device(pointer);
	eis_pointer_event_destroyed(device->pointer,
				    eis_client_get_next_serial(eis_device_get_client(device)));
	eis_pointer_unref(steal(&device->pointer));
	return 0;
}

static struct brei_result *
client_msg_pointer_absolute_release(struct eis_pointer_absolute *pointer)
{
	struct eis_device *device = eis_pointer_absolute_get_device(pointer);
	eis_pointer_absolute_event_destroyed(
		device->pointer_absolute,
		eis_client_get_next_serial(eis_device_get_client(device)));
	eis_pointer_absolute_unref(steal(&device->pointer_absolute));
	return 0;
}

static struct brei_result *
client_msg_scroll_release(struct eis_scroll *scroll)
{
	struct eis_device *device = eis_scroll_get_device(scroll);
	eis_scroll_event_destroyed(device->scroll,
				   eis_client_get_next_serial(eis_device_get_client(device)));
	eis_scroll_unref(steal(&device->scroll));
	return 0;
}

static struct brei_result *
client_msg_button_release(struct eis_button *button)
{
	struct eis_device *device = eis_button_get_device(button);
	eis_button_event_destroyed(device->button,
				   eis_client_get_next_serial(eis_device_get_client(device)));
	eis_button_unref(steal(&device->button));
	return 0;
}

static const struct eis_pointer_interface pointer_interface = {
	.release = client_msg_pointer_release,
	.motion_relative = client_msg_pointer_rel,
};

static const struct eis_pointer_absolute_interface pointer_absolute_interface = {
	.release = client_msg_pointer_absolute_release,
	.motion_absolute = client_msg_pointer_abs,
};

static const struct eis_scroll_interface scroll_interface = {
	.release = client_msg_scroll_release,
	.scroll = client_msg_scroll,
	.scroll_discrete = client_msg_scroll_discrete,
	.scroll_stop = client_msg_scroll_stop,
};

static const struct eis_button_interface button_interface = {
	.release = client_msg_button_release,
	.button = client_msg_button,
};

const struct eis_pointer_interface *
eis_device_get_pointer_interface(struct eis_device *device)
{
	return &pointer_interface;
}

const struct eis_pointer_absolute_interface *
eis_device_get_pointer_absolute_interface(struct eis_device *device)
{
	return &pointer_absolute_interface;
}

const struct eis_scroll_interface *
eis_device_get_scroll_interface(struct eis_device *device)
{
	return &scroll_interface;
}

const struct eis_button_interface *
eis_device_get_button_interface(struct eis_device *device)
{
	return &button_interface;
}

static struct brei_result *
client_msg_keyboard_key(struct eis_keyboard *keyboard, uint32_t key, uint32_t state)
{
	struct eis_device *device = eis_keyboard_get_device(keyboard);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_KEYBOARD)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Key event for non-keyboard device");
	}

	if (key >= KEY_CNT)
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Key event for invalid key %x (KEY_CNT is %#x)",
				       key,
				       KEY_CNT);

	if (!eis_device_update_key_button_state(device, key, state))
		return NULL;

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_keyboard_key_event(device, key, !!state);
		return NULL;
	}

	return maybe_error_on_device_state(device, "key");
}

static struct brei_result *
client_msg_keyboard_release(struct eis_keyboard *keyboard)
{
	struct eis_device *device = eis_keyboard_get_device(keyboard);
	eis_keyboard_event_destroyed(device->keyboard,
				     eis_client_get_next_serial(eis_device_get_client(device)));
	eis_keyboard_unref(steal(&device->keyboard));
	return 0;
}

static const struct eis_keyboard_interface keyboard_interface = {
	.release = client_msg_keyboard_release,
	.key = client_msg_keyboard_key,
};

const struct eis_keyboard_interface *
eis_device_get_keyboard_interface(struct eis_device *device)
{
	return &keyboard_interface;
}

/* Returns true and the position of the touch with the given ID, or
 * false and the first position that is available
 */
static bool
find_touch(struct eis_device *device, uint32_t touchid, size_t *index)
{
	ssize_t first_available = -1;
	for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
		if (device->touch_state.down[i] != UINT64_MAX) {
			if (device->touch_state.down[i] == touchid) {
				if (index)
					*index = i;
				return true;
			}
		} else if (first_available < 0) {
			first_available = i;
		}
	}

	if (index) {
		if (first_available < 0)
			*index = EIS_MAX_TOUCHES;
		else
			*index = (size_t)first_available;
	}

	return false;
}

static struct brei_result *
client_msg_touch_down(struct eis_touchscreen *touchscreen, uint32_t touchid, float x, float y)
{
	struct eis_device *device = eis_touchscreen_get_device(touchscreen);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TOUCH)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Touch down event for non-touch device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		size_t first_available;
		if (find_touch(device, touchid, &first_available)) {
			return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
					       "Touch down event for duplicated touch ID");
		}

		if (first_available >= EIS_MAX_TOUCHES)
			return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_ERROR,
					       "Too many simultaneous touch events");

		device->touch_state.down[first_available] = touchid;
		eis_queue_touch_down_event(device, touchid, x, y);
		return NULL;
	}

	return maybe_error_on_device_state(device, "touch down");
}

static struct brei_result *
client_msg_touch_motion(struct eis_touchscreen *touchscreen, uint32_t touchid, float x, float y)
{
	struct eis_device *device = eis_touchscreen_get_device(touchscreen);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TOUCH)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Touch motion event for non-touch device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		/* Silently ignore motion for non-existing touches */
		if (find_touch(device, touchid, NULL))
			eis_queue_touch_motion_event(device, touchid, x, y);
		return NULL;
	}

	return maybe_error_on_device_state(device, "touch motion");
}

static bool
release_touch(struct eis_device *device, uint32_t touchid)
{
	size_t index;
	bool rc = find_touch(device, touchid, &index);
	if (rc)
		device->touch_state.down[index] = UINT64_MAX;

	return rc;
}

static struct brei_result *
client_msg_touch_up(struct eis_touchscreen *touchscreen, uint32_t touchid)
{
	struct eis_device *device = eis_touchscreen_get_device(touchscreen);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TOUCH)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Touch up event for non-touch device");
	}

	/* End the touch locally even if we're not emulating, but
	 * silently ignore touch end/cancel for non-existing touches */
	if (release_touch(device, touchid)) {
		if (device->state == EIS_DEVICE_STATE_EMULATING)
			eis_queue_touch_up_event(device, touchid);
		return NULL;
	}

	return maybe_error_on_device_state(device, "touch up");
}

static struct brei_result *
client_msg_touch_cancel(struct eis_touchscreen *touchscreen, uint32_t touchid)
{
	struct eis_device *device = eis_touchscreen_get_device(touchscreen);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TOUCH)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Touch cancel event for non-touch device");
	}

	struct eis_client *client = eis_device_get_client(device);
	if (client->interface_versions.ei_touchscreen <
	    EIS_TOUCHSCREEN_EVENT_CANCEL_SINCE_VERSION) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "Touch cancel event for touchscreen version v1");
	}

	/* End the touch locally even if we're not emulating, but
	 * silently ignore touch end/cancel for non-existing touches */
	if (release_touch(device, touchid)) {
		if (device->state == EIS_DEVICE_STATE_EMULATING)
			eis_queue_touch_cancel_event(device, touchid);
		return NULL;
	}

	return maybe_error_on_device_state(device, "touch cancel");
}

static struct brei_result *
client_msg_touchscreen_release(struct eis_touchscreen *touchscreen)
{
	struct eis_device *device = eis_touchscreen_get_device(touchscreen);
	eis_touchscreen_event_destroyed(device->touchscreen,
					eis_client_get_next_serial(eis_device_get_client(device)));
	eis_touchscreen_unref(steal(&device->touchscreen));
	return NULL;
}

static const struct eis_touchscreen_interface touchscreen_interface = {
	.release = client_msg_touchscreen_release,
	.down = client_msg_touch_down,
	.motion = client_msg_touch_motion,
	.up = client_msg_touch_up,
	.cancel = client_msg_touch_cancel,
};

const struct eis_touchscreen_interface *
eis_device_get_touchscreen_interface(struct eis_device *device)
{
	return &touchscreen_interface;
}

static struct brei_result *
client_msg_text_release(struct eis_text *text)
{
	struct eis_device *device = eis_text_get_device(text);
	eis_text_event_destroyed(device->text,
				 eis_client_get_next_serial(eis_device_get_client(device)));
	eis_text_unref(steal(&device->text));
	return NULL;
}

static struct brei_result *
client_msg_text_keysym(struct eis_text *text, uint32_t keysym, uint32_t state)
{
	struct eis_device *device = eis_text_get_device(text);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "keysym event for non-text device");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_text_keysym_event(device, keysym, !!state);
		return NULL;
	}

	return maybe_error_on_device_state(device, "text keysym");
}

static struct brei_result *
client_msg_text_utf8(struct eis_text *text, const char *utf8)
{
	struct eis_device *device = eis_text_get_device(text);

	DISCONNECT_IF_RECEIVER_CONTEXT(device);

	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "utf8 event for non-text device");
	}

	if (!utf8 || utf8[0] == '\0') {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "utf8 text is NULL or the empty string");
	}

	if (strlen(utf8) > 254) {
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL,
				       "utf8 string exceeds max length of 255 bytes");
	}

	if (device->state == EIS_DEVICE_STATE_EMULATING) {
		eis_queue_text_utf8_event(device, utf8);
		return NULL;
	}

	return maybe_error_on_device_state(device, "text utf8");
}

static const struct eis_text_interface text_interface = {
	.release = client_msg_text_release,
	.keysym = client_msg_text_keysym,
	.utf8 = client_msg_text_utf8,
};

const struct eis_text_interface *
eis_device_get_text_interface(struct eis_device *device)
{
	return &text_interface;
}

_public_ struct eis_device *
eis_seat_new_device(struct eis_seat *seat)
{
	struct eis_device *device = eis_device_create(&seat->object);
	struct eis_client *client = eis_seat_get_client(seat);

	device->proto_object.id = eis_client_get_new_id(client);
	device->proto_object.implementation = device;
	device->proto_object.interface = &eis_device_proto_interface;
	device->proto_object.version = client->interface_versions.ei_device;
	assert(device->proto_object.version != 0);
	list_init(&device->proto_object.link);

	device->name = xstrdup("unnamed device");
	device->capabilities = 0;
	device->state = EIS_DEVICE_STATE_NEW;
	device->type = EIS_DEVICE_TYPE_VIRTUAL;
	list_init(&device->regions);
	list_init(&device->regions_new);
	list_init(&device->pending_event_queue);

	list_append(&seat->devices, &device->link);

	for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
		device->touch_state.down[i] = UINT64_MAX;
	}

	return eis_device_ref(device);
}

_public_ void
eis_device_configure_type(struct eis_device *device, enum eis_device_type type)
{
	if (device->state != EIS_DEVICE_STATE_NEW)
		return;

	switch (type) {
	case EIS_DEVICE_TYPE_VIRTUAL:
	case EIS_DEVICE_TYPE_PHYSICAL:
		break;
	default:
		log_bug_client(eis_device_get_context(device), "Invalid device type %u", type);
		return;
	}

	device->type = type;
}

_public_ void
eis_device_configure_name(struct eis_device *device, const char *name)
{
	if (device->state != EIS_DEVICE_STATE_NEW)
		return;

	free(device->name);
	device->name = xstrdup(name);
}

_public_ void
eis_device_configure_capability(struct eis_device *device, enum eis_device_capability cap)
{
	if (device->state != EIS_DEVICE_STATE_NEW)
		return;

	if (!eis_seat_has_capability(eis_device_get_seat(device), cap))
		return;

	switch (cap) {
	case EIS_DEVICE_CAP_POINTER:
	case EIS_DEVICE_CAP_POINTER_ABSOLUTE:
	case EIS_DEVICE_CAP_KEYBOARD:
	case EIS_DEVICE_CAP_TOUCH:
	case EIS_DEVICE_CAP_BUTTON:
	case EIS_DEVICE_CAP_SCROLL:
	case EIS_DEVICE_CAP_TEXT:
		mask_add(device->capabilities, cap);
		break;
	}
}

_public_ void
eis_device_configure_size(struct eis_device *device, uint32_t width, uint32_t height)
{
	if (device->type != EIS_DEVICE_TYPE_PHYSICAL) {
		log_bug_client(eis_device_get_context(device),
			       "Device type physical required for size");
		return;
	}

	if (width > 2000 || height > 2000)
		log_warn(eis_device_get_context(device),
			 "Suspicious device size: %ux%umm",
			 width,
			 height);

	device->width = width;
	device->height = height;
}

_public_ void
eis_device_add(struct eis_device *device)
{
	struct eis_client *client = eis_device_get_client(device);
	struct eis_seat *seat = eis_device_get_seat(device);

	if (device->state != EIS_DEVICE_STATE_NEW) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device already (dis)connected",
			       __func__);
		return;
	}

	if (!device->capabilities) {
		log_bug_client(eis_device_get_context(device),
			       "%s: adding device without capabilities",
			       __func__);
	}

	device->state = EIS_DEVICE_STATE_AWAITING_READY;
	eis_client_register_object(client, &device->proto_object);
	eis_seat_event_device(seat, device->proto_object.id, device->proto_object.version);
	int rc = eis_device_event_name(device, device->name);
	if (rc < 0)
		goto out;

	rc = eis_device_event_device_type(device, device->type);
	if (rc < 0)
		goto out;

	if (device->type == EIS_DEVICE_TYPE_PHYSICAL) {
		rc = eis_device_event_dimensions(device, device->width, device->height);
		if (rc < 0)
			goto out;
	}
	if (device->type == EIS_DEVICE_TYPE_VIRTUAL) {
		struct eis_region *r;
		list_for_each(r, &device->regions, link) {
			if (r->mapping_id) {
				if (client->interface_versions.ei_device >=
				    EIS_DEVICE_EVENT_REGION_MAPPING_ID_SINCE_VERSION) {
					rc = eis_device_event_region_mapping_id(device,
										r->mapping_id);
					if (rc < 0)
						goto out;
				} else {
					/* If our client doesn't support mapping_id, drop it */
					free(r->mapping_id);
					r->mapping_id = NULL;
				}
			}
			rc = eis_device_event_region(device,
						     r->x,
						     r->y,
						     r->width,
						     r->height,
						     r->physical_scale);
			if (rc < 0)
				goto out;
		}
	}
	if (eis_device_has_capability(device, EIS_DEVICE_CAP_POINTER)) {
		device->pointer = eis_pointer_new(device);
		rc = eis_device_event_interface(device,
						eis_pointer_get_id(device->pointer),
						EIS_POINTER_INTERFACE_NAME,
						eis_pointer_get_version(device->pointer));
		if (rc < 0)
			goto out;
	}
	if (eis_device_has_capability(device, EIS_DEVICE_CAP_POINTER_ABSOLUTE)) {
		device->pointer_absolute = eis_pointer_absolute_new(device);
		rc = eis_device_event_interface(
			device,
			eis_pointer_absolute_get_id(device->pointer_absolute),
			EIS_POINTER_ABSOLUTE_INTERFACE_NAME,
			eis_pointer_absolute_get_version(device->pointer_absolute));
		if (rc < 0)
			goto out;
	}
	if (eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		device->scroll = eis_scroll_new(device);
		rc = eis_device_event_interface(device,
						eis_scroll_get_id(device->scroll),
						EIS_SCROLL_INTERFACE_NAME,
						eis_scroll_get_version(device->scroll));
		if (rc < 0)
			goto out;
	}
	if (eis_device_has_capability(device, EIS_DEVICE_CAP_BUTTON)) {
		device->button = eis_button_new(device);
		rc = eis_device_event_interface(device,
						eis_button_get_id(device->button),
						EIS_BUTTON_INTERFACE_NAME,
						eis_button_get_version(device->button));
		if (rc < 0)
			goto out;
	}
	if (eis_device_has_capability(device, EIS_DEVICE_CAP_KEYBOARD)) {
		device->keyboard = eis_keyboard_new(device);
		rc = eis_device_event_interface(device,
						eis_keyboard_get_id(device->keyboard),
						EIS_KEYBOARD_INTERFACE_NAME,
						eis_keyboard_get_version(device->keyboard));
		if (rc < 0)
			goto out;

		if (device->keymap)
			rc = eis_keyboard_event_keymap(device->keyboard,
						       device->keymap->type,
						       device->keymap->size,
						       device->keymap->fd);
		if (rc < 0)
			goto out;
	}
	if (eis_device_has_capability(device, EIS_DEVICE_CAP_TOUCH)) {
		device->touchscreen = eis_touchscreen_new(device);
		rc = eis_device_event_interface(device,
						eis_touchscreen_get_id(device->touchscreen),
						EIS_TOUCHSCREEN_INTERFACE_NAME,
						eis_touchscreen_get_version(device->touchscreen));
		if (rc < 0)
			goto out;
	}
	if (eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) {
		device->text = eis_text_new(device);
		rc = eis_device_event_interface(device,
						eis_text_get_id(device->text),
						EIS_TEXT_INTERFACE_NAME,
						eis_text_get_version(device->text));
		if (rc < 0)
			goto out;
	}

	rc = eis_device_event_done(device);
	if (rc < 0)
		goto out;

	struct eis *eis = eis_device_get_context(device);
	if (client->interface_versions.ei_device < EIS_DEVICE_REQUEST_READY_SINCE_VERSION) {
		device->state = EIS_DEVICE_STATE_PAUSED;
		if (flag_is_set(eis->flags, EIS_FLAG_DEVICE_READY))
			eis_queue_device_ready_event(device);
	}

out:
	if (rc < 0) {
		log_error(eis_client_get_context(client),
			  "Failed to add device, disconnecting client");
		eis_client_disconnect(client);
	}
	return;
}

_public_ void
eis_device_remove(struct eis_device *device)
{
	struct eis_client *client = eis_device_get_client(device);

	if (device->state == EIS_DEVICE_STATE_DEAD)
		return;

	if (device->state == EIS_DEVICE_STATE_EMULATING &&
	    !eis_client_is_sender(eis_device_get_client(device)))
		eis_device_stop_emulating(device);

	if (device->pointer) {
		eis_pointer_event_destroyed(device->pointer, eis_client_get_next_serial(client));
		eis_pointer_unref(steal(&device->pointer));
	}
	if (device->pointer_absolute) {
		eis_pointer_absolute_event_destroyed(device->pointer_absolute,
						     eis_client_get_next_serial(client));
		eis_pointer_absolute_unref(steal(&device->pointer_absolute));
	}
	if (device->button) {
		eis_button_event_destroyed(device->button, eis_client_get_next_serial(client));
		eis_button_unref(steal(&device->button));
	}
	if (device->scroll) {
		eis_scroll_event_destroyed(device->scroll, eis_client_get_next_serial(client));
		eis_scroll_unref(steal(&device->scroll));
	}
	if (device->touchscreen) {
		eis_touchscreen_event_destroyed(device->touchscreen,
						eis_client_get_next_serial(client));
		eis_touchscreen_unref(steal(&device->touchscreen));
	}
	if (device->keyboard) {
		eis_keyboard_event_destroyed(device->keyboard, eis_client_get_next_serial(client));
		eis_keyboard_unref(steal(&device->keyboard));
	}
	if (device->text) {
		eis_text_event_destroyed(device->text, eis_client_get_next_serial(client));
		eis_text_unref(steal(&device->text));
	}

	if (device->state != EIS_DEVICE_STATE_NEW)
		eis_device_event_destroyed(device, eis_client_get_next_serial(client));

	struct eis_event *event;
	list_for_each_safe(event, &device->pending_event_queue, link) {
		list_remove(&event->link);
		eis_event_unref(event);
	}

	device->state = EIS_DEVICE_STATE_DEAD;
	eis_client_unregister_object(client, &device->proto_object);
	list_remove(&device->link);
	eis_device_unref(device);
}

_public_ bool
eis_device_has_capability(struct eis_device *device, enum eis_device_capability cap)
{
	switch (cap) {
	case EIS_DEVICE_CAP_POINTER:
	case EIS_DEVICE_CAP_POINTER_ABSOLUTE:
	case EIS_DEVICE_CAP_KEYBOARD:
	case EIS_DEVICE_CAP_TOUCH:
	case EIS_DEVICE_CAP_BUTTON:
	case EIS_DEVICE_CAP_SCROLL:
	case EIS_DEVICE_CAP_TEXT:
		return mask_all(device->capabilities, cap);
	}
	return false;
}

static void
eis_device_frame_now(struct eis_device *device)
{
	uint64_t now = eis_now(eis_device_get_context(device));

	eis_device_frame(device, now);
}

static void
_flush_frame(struct eis_device *device, const char *func)
{
	if (device->send_frame_event) {
		log_bug_client(eis_device_get_context(device),
			       "%s: missing call to eis_device_frame()",
			       func);
		eis_device_frame_now(device);
	}
}
#define eis_device_flush_frame(d_) _flush_frame(d_, __func__)

_public_ void
eis_device_start_emulating(struct eis_device *device, uint32_t sequence)
{
	struct eis_client *client = eis_device_get_client(device);

	if (device->state != EIS_DEVICE_STATE_RESUMED)
		return;

	assert(!device->send_frame_event);

	device->state = EIS_DEVICE_STATE_EMULATING;

	eis_device_event_start_emulating(device, eis_client_get_next_serial(client), sequence);
}

_public_ void
eis_device_stop_emulating(struct eis_device *device)
{
	struct eis_client *client = eis_device_get_client(device);

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	eis_device_flush_frame(device);

	device->state = EIS_DEVICE_STATE_RESUMED;

	eis_device_event_stop_emulating(device, eis_client_get_next_serial(client));
}

_public_ void
eis_device_pointer_motion(struct eis_device *device, double x, double y)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_POINTER)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a pointer",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	device->send_frame_event = true;

	eis_pointer_event_motion_relative(device->pointer, x, y);
}

_public_ void
eis_device_pointer_motion_absolute(struct eis_device *device, double x, double y)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_POINTER_ABSOLUTE)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not an absolute pointer",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	if (!eis_device_in_region(device, x, y))
		return;

	device->send_frame_event = true;

	eis_pointer_absolute_event_motion_absolute(device->pointer_absolute, x, y);
}

_public_ void
eis_device_button_button(struct eis_device *device, uint32_t button, bool is_press)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_BUTTON)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a button device",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	/* Ignore anything < BTN_MOUSE. Avoids the common error of sending
	 * numerical buttons instead of BTN_LEFT and friends. */
	if (button < 0x110) {
		log_bug_client(eis_device_get_context(device),
			       "%s: button code must be one of BTN_*",
			       __func__);
		return;
	}

	device->send_frame_event = true;

	eis_button_event_button(device->button, button, is_press);
}

static inline void
eis_device_resume_scrolling(struct eis_device *device, double x, double y)
{
	if (x) {
		device->scroll_state.x_is_stopped = false;
		device->scroll_state.x_is_cancelled = false;
	}
	if (y) {
		device->scroll_state.y_is_stopped = false;
		device->scroll_state.y_is_cancelled = false;
	}
}

_public_ void
eis_device_scroll_delta(struct eis_device *device, double x, double y)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a scroll device",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	eis_device_resume_scrolling(device, x, y);

	device->send_frame_event = true;

	eis_scroll_event_scroll(device->scroll, x, y);
}

_public_ void
eis_device_scroll_stop(struct eis_device *device, bool x, bool y)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a scroll device",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	/* Filter out duplicate scroll stop requests */
	if (x && !device->scroll_state.x_is_stopped)
		device->scroll_state.x_is_stopped = true;
	else
		x = false;

	if (y && !device->scroll_state.y_is_stopped)
		device->scroll_state.y_is_stopped = true;
	else
		y = false;

	if (x || y) {
		device->send_frame_event = true;
		eis_scroll_event_scroll_stop(device->scroll, x, y, false);
	}
}

_public_ void
eis_device_scroll_cancel(struct eis_device *device, bool x, bool y)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a scroll device",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	/* Filter out duplicate scroll cancelled requests */
	if (x && !device->scroll_state.x_is_cancelled) {
		device->scroll_state.x_is_stopped = true;
		device->scroll_state.x_is_cancelled = true;
	} else {
		x = false;
	}

	if (y && !device->scroll_state.y_is_cancelled) {
		device->scroll_state.y_is_stopped = true;
		device->scroll_state.y_is_cancelled = true;
	} else {
		y = false;
	}

	if (x || y) {
		device->send_frame_event = true;
		eis_scroll_event_scroll_stop(device->scroll, x, y, true);
	}
}

_public_ void
eis_device_scroll_discrete(struct eis_device *device, int32_t x, int32_t y)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_SCROLL)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a scroll device",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	if (abs(x) == 1 || abs(y) == 1) {
		log_bug_client(eis_device_get_context(device),
			       "%s: suspicious discrete event value 1, did you mean 120?",
			       __func__);
	}

	eis_device_resume_scrolling(device, x, y);

	device->send_frame_event = true;

	eis_scroll_event_scroll_discrete(device->scroll, x, y);
}

_public_ void
eis_device_keyboard_key(struct eis_device *device, uint32_t key, bool is_press)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_KEYBOARD)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a keyboard",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	device->send_frame_event = true;

	eis_keyboard_event_key(device->keyboard, key, is_press);
}

_public_ OBJECT_IMPLEMENT_REF(eis_touch);
_public_ OBJECT_IMPLEMENT_UNREF_CLEANUP(eis_touch);
_public_
OBJECT_IMPLEMENT_GETTER(eis_touch, device, struct eis_device *);
_public_
OBJECT_IMPLEMENT_GETTER(eis_touch, user_data, void *);
_public_
OBJECT_IMPLEMENT_SETTER(eis_touch, user_data, void *);

static void
eis_touch_destroy(struct eis_touch *touch)
{
	if (touch->state == TOUCH_IS_DOWN)
		eis_touch_up(touch);
	/* Enforce a frame, otherwise we're just pending. If the client
	 * doesn't want this, it needs to eis_touch_up() */
	eis_device_frame_now(touch->device);
	eis_device_unref(touch->device);
}

static OBJECT_IMPLEMENT_CREATE(eis_touch);

_public_ struct eis_touch *
eis_device_touch_new(struct eis_device *device)
{
	static uint32_t tracking_id = 0;

	/* Not using the device as parent object because we need a ref
	 * to it */
	struct eis_touch *touch = eis_touch_create(NULL);

	touch->device = eis_device_ref(device);
	touch->state = TOUCH_IS_NEW;
	touch->tracking_id = ++tracking_id;

	return touch;
}

_public_ void
eis_touch_down(struct eis_touch *touch, double x, double y)
{
	struct eis_device *device = eis_touch_get_device(touch);

	if (touch->state != TOUCH_IS_NEW) {
		log_bug_client(eis_device_get_context(device),
			       "%s: touch %u already down or up",
			       __func__,
			       touch->tracking_id);
		return;
	}

	if (!eis_device_in_region(device, x, y)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: touch %u has invalid x/y coordinates",
			       __func__,
			       touch->tracking_id);
		touch->state = TOUCH_IS_UP;
		return;
	}

	touch->state = TOUCH_IS_DOWN;
	device->send_frame_event = true;

	eis_touchscreen_event_down(device->touchscreen, touch->tracking_id, x, y);
}

_public_ void
eis_touch_motion(struct eis_touch *touch, double x, double y)
{
	if (touch->state != TOUCH_IS_DOWN)
		return;

	struct eis_device *device = eis_touch_get_device(touch);
	if (!eis_device_in_region(device, x, y)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: touch %u has invalid x/y coordinates",
			       __func__,
			       touch->tracking_id);
		eis_touch_up(touch);
		return;
	}

	device->send_frame_event = true;

	eis_touchscreen_event_motion(device->touchscreen, touch->tracking_id, x, y);
}

_public_ void
eis_touch_up(struct eis_touch *touch)
{
	struct eis_device *device = eis_touch_get_device(touch);

	if (touch->state != TOUCH_IS_DOWN) {
		log_bug_client(eis_device_get_context(device),
			       "%s: touch %u is not currently down",
			       __func__,
			       touch->tracking_id);
		return;
	}

	touch->state = TOUCH_IS_UP;
	device->send_frame_event = true;

	eis_touchscreen_event_up(device->touchscreen, touch->tracking_id);
}

_public_ void
eis_touch_cancel(struct eis_touch *touch)
{
	struct eis_device *device = eis_touch_get_device(touch);

	if (touch->state != TOUCH_IS_DOWN) {
		log_bug_client(eis_device_get_context(device),
			       "%s: touch %u is not currently down",
			       __func__,
			       touch->tracking_id);
		return;
	}

	touch->state = TOUCH_IS_UP;
	device->send_frame_event = true;

	struct eis_client *client = eis_device_get_client(device);
	if (client->interface_versions.ei_touchscreen >= EIS_TOUCHSCREEN_EVENT_CANCEL_SINCE_VERSION)
		eis_touchscreen_event_cancel(device->touchscreen, touch->tracking_id);
	else
		eis_touchscreen_event_up(device->touchscreen, touch->tracking_id);
}

_public_ void
eis_device_text_keysym(struct eis_device *device, uint32_t keysym, bool is_press)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a text device",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	device->send_frame_event = true;

	eis_text_event_keysym(device->text, keysym, is_press);
}

_public_ void
eis_device_text_utf8(struct eis_device *device, const char *utf8)
{
	eis_device_text_utf8_with_length(device, utf8, utf8 ? strlen(utf8) : 0);
}

_public_ void
eis_device_text_utf8_with_length(struct eis_device *device, const char *utf8, size_t length)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a text device",
			       __func__);
		return;
	}

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	if (!utf8) {
		log_bug_client(eis_device_get_context(device), "%s: utf8 is NULL", __func__);
		return;
	}

	if (length == 0) {
		log_bug_client(eis_device_get_context(device),
			       "%s: zero-length utf8 string",
			       __func__);
		return;
	}

	if (length > 254) {
		log_bug_client(eis_device_get_context(device),
			       "%s: utf8 string exceeds max length of 255 bytes",
			       __func__);
		return;
	}

	device->send_frame_event = true;

	char buf[255] = { 0 };
	memcpy(buf, utf8, length);
	eis_text_event_utf8(device->text, buf);
}

_public_ void
eis_device_frame(struct eis_device *device, uint64_t time)
{
	struct eis_client *client = eis_device_get_client(device);

	if (device->state != EIS_DEVICE_STATE_EMULATING)
		return;

	if (!device->send_frame_event)
		return;

	device->send_frame_event = false;

	eis_device_event_frame(device, eis_client_get_next_serial(client), time);
}

void
eis_device_closed_by_client(struct eis_device *device)
{
	switch (device->state) {
	case EIS_DEVICE_STATE_DEAD:
	case EIS_DEVICE_STATE_CLOSED_BY_CLIENT:
		/* libei bug, ignore */
		break;
	case EIS_DEVICE_STATE_EMULATING:
		if (!eis_client_is_sender(eis_device_get_client(device)))
			eis_queue_device_stop_emulating_event(device);
		_fallthrough_;
	case EIS_DEVICE_STATE_AWAITING_READY:
		eis_queue_device_ready_event(device);
		_fallthrough_;
	case EIS_DEVICE_STATE_NEW:
	case EIS_DEVICE_STATE_PAUSED:
	case EIS_DEVICE_STATE_RESUMED:
		eis_queue_device_closed_event(device);
		device->state = EIS_DEVICE_STATE_CLOSED_BY_CLIENT;
		break;
	}
}

_public_ void
eis_device_pause(struct eis_device *device)
{
	struct eis_client *client = eis_device_get_client(device);

	switch (device->state) {
	case EIS_DEVICE_STATE_RESUMED:
	case EIS_DEVICE_STATE_EMULATING:
		break;
	default:
		return;
	}

	device->state = EIS_DEVICE_STATE_PAUSED;
	eis_device_event_paused(device, eis_client_get_next_serial(client));

	memset(device->key_button_state.down, 0, sizeof(device->key_button_state.down));

	for (size_t i = 0; i < ARRAY_LENGTH(device->touch_state.down); i++) {
		device->touch_state.down[i] = UINT64_MAX;
	}
}

_public_ void
eis_device_resume(struct eis_device *device)
{
	struct eis_client *client = eis_device_get_client(device);

	if (device->state == EIS_DEVICE_STATE_AWAITING_READY) {
		log_bug_client(eis_client_get_context(client),
			       "Attempting to resume a device before DEVICE_READY");
		return;
	}

	if (device->state != EIS_DEVICE_STATE_PAUSED)
		return;

	device->state = EIS_DEVICE_STATE_RESUMED;
	eis_device_event_resumed(device, eis_client_get_next_serial(client));
}

_public_ void
eis_device_keyboard_send_xkb_modifiers(struct eis_device *device,
				       uint32_t depressed,
				       uint32_t latched,
				       uint32_t locked,
				       uint32_t group)
{
	if (!eis_device_has_capability(device, EIS_DEVICE_CAP_KEYBOARD)) {
		log_bug_client(eis_device_get_context(device),
			       "%s: device is not a keyboard",
			       __func__);
		return;
	}

	eis_keyboard_event_modifiers(device->keyboard,
				     eis_client_get_next_serial(eis_device_get_client(device)),
				     depressed,
				     locked,
				     latched,
				     group);
}
