/* 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 <fcntl.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>

#include "util-io.h"
#include "util-macros.h"
#include "util-object.h"
#include "util-sources.h"
#include "util-strings.h"
#include "util-time.h"
#include "util-version.h"

#include "libei.h"
#include "libei-private.h"
#include "brei-shared.h"
#include "ei-proto.h"

_Static_assert(sizeof(enum ei_device_capability) == sizeof(int), "Invalid enum size");
_Static_assert(sizeof(enum ei_keymap_type) == sizeof(int), "Invalid enum size");
_Static_assert(sizeof(enum ei_event_type) == sizeof(int), "Invalid enum size");
_Static_assert(sizeof(enum ei_log_priority) == sizeof(int), "Invalid enum size");

static void
ei_unsent_free(struct ei_unsent *unsent);

static void
ei_defunct_object_free(struct ei_defunct_object *obj);

static void
ei_destroy(struct ei *ei)
{
	ei_disconnect(ei);

	struct ei_event *e;
	while ((e = ei_get_event(ei)) != NULL)
		ei_event_unref(e);

	struct ei_unsent *unsent;
	list_for_each_safe(unsent, &ei->unsent_queue, node) {
		ei_unsent_free(unsent);
	}

	if (ei->backend_interface.destroy)
		ei->backend_interface.destroy(ei, ei->backend);

	ei->backend = NULL;
	ei_handshake_unref(ei->handshake);
	ei_connection_unref(ei->connection);
	brei_context_unref(ei->brei);

	sink_unref(ei->sink);
	free(ei->name);

	struct ei_defunct_object *obj;
	list_for_each_safe(obj, &ei->defunct_objects, node) {
		ei_defunct_object_free(obj);
	}
}

static
OBJECT_IMPLEMENT_CREATE(ei);
_public_
OBJECT_IMPLEMENT_REF(ei);
_public_
OBJECT_IMPLEMENT_UNREF_CLEANUP(ei);
_public_
OBJECT_IMPLEMENT_SETTER(ei, user_data, void *);
_public_
OBJECT_IMPLEMENT_GETTER(ei, user_data, void *);
OBJECT_IMPLEMENT_GETTER(ei, connection, struct ei_connection *);
OBJECT_IMPLEMENT_GETTER(ei, serial, uint32_t);

DEFINE_UNREF_CLEANUP_FUNC(brei_result);
DEFINE_UNREF_CLEANUP_FUNC(ei_pingpong);

struct ei *
ei_get_context(struct ei *ei)
{
	return ei; /* for the protocol bindings */
}

static struct ei *
ei_create_context(bool is_sender, void *user_data)
{
	_unref_(ei) *ei = ei_create(NULL);

	ei->state = EI_STATE_NEW;
	list_init(&ei->event_queue);
	list_init(&ei->seats);
	list_init(&ei->proto_objects);
	list_init(&ei->unsent_queue);
	list_init(&ei->defunct_objects);

	ei->interface_versions = (struct ei_interface_versions){
		.ei_connection = VERSION_V(1),
		.ei_handshake = VERSION_V(1),
		.ei_callback = VERSION_V(1),
		.ei_pingpong = VERSION_V(1),
		.ei_seat = VERSION_V(1),
		.ei_device = VERSION_V(2),
		.ei_pointer = VERSION_V(1),
		.ei_pointer_absolute = VERSION_V(1),
		.ei_scroll = VERSION_V(1),
		.ei_button = VERSION_V(1),
		.ei_keyboard = VERSION_V(1),
		.ei_touchscreen = VERSION_V(1),
	};
	/* This must be v1 until the server tells us otherwise */
	ei->handshake = ei_handshake_new(ei, VERSION_V(1));
	ei->next_object_id = 1;
	ei->brei = brei_context_new(ei);
	brei_context_set_log_context(ei->brei, ei);
	brei_context_set_log_func(ei->brei, (brei_logfunc_t)ei_log_msg_va);

	ei_log_set_handler(ei, NULL);
	ei_log_set_priority(ei, EI_LOG_PRIORITY_INFO);
	ei->sink = sink_new();
	if (!ei->sink)
		return NULL;

	ei->user_data = user_data;
	ei->backend = NULL;
	ei->is_sender = is_sender;

	return steal(&ei);
}

object_id_t
ei_get_new_id(struct ei *ei)
{
	static const uint64_t server_range = 0xff00000000000000;
	return ei->next_object_id++ & ~server_range;
}

void
ei_update_serial(struct ei *ei, uint32_t serial)
{
	ei->serial = serial;
}

void
ei_register_object(struct ei *ei, struct brei_object *object)
{
	log_debug(ei, "registering %s v%u object %#" PRIx64 "", object->interface->name, object->version, object->id);
	list_append(&ei->proto_objects, &object->link);
}

static void
ei_defunct_object_free(struct ei_defunct_object *obj)
{
	list_remove(&obj->node);
	free(obj);
}

void
ei_unregister_object(struct ei *ei, struct brei_object *object)
{
	log_debug(ei, "deregistering %s v%u object %#" PRIx64 "", object->interface->name, object->version, object->id);
	list_remove(&object->link);

	struct ei_defunct_object *obj = xalloc(sizeof *obj);
	obj->object_id = object->id;
	obj->time = ei_now(ei);
	list_append(&ei->defunct_objects, &obj->node);
}

_public_ bool
ei_is_sender(struct ei *ei)
{
	return ei->is_sender;
}

_public_ struct ei *
ei_new(void *user_data)
{
	return ei_new_sender(user_data);
}

_public_ struct ei *
ei_new_sender(void *user_data)
{
	return ei_create_context(true, user_data);
}

_public_ struct ei *
ei_new_receiver(void *user_data)
{
	return ei_create_context(false, user_data);
}

_public_ int
ei_get_fd(struct ei *ei)
{
	return sink_get_fd(ei->sink);
}

_public_ void
ei_dispatch(struct ei *ei)
{
	sink_dispatch(ei->sink);
}

static void
update_event_timestamp(struct ei_event *event, uint64_t time)
{
	switch (event->type) {
	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_TOUCH_DOWN:
	case EI_EVENT_TOUCH_UP:
	case EI_EVENT_TOUCH_MOTION:
		if (event->timestamp != 0) {
			log_bug(ei_event_get_context(event),
				"Unexpected timestamp for event of type: %s",
				ei_event_type_to_string(event->type));
			return;
		}
		event->timestamp = time;
		break;
	default:
		log_bug(ei_event_get_context(event),
			"Unexpected event %s in pending queue event",
			ei_event_type_to_string(event->type));
		return;
	}
}

static void
queue_event(struct ei *ei, struct ei_event *event)
{
	struct ei_device *device = ei_event_get_device(event);
	struct list *queue = &ei->event_queue;
	const char *prefix = "";

	switch (event->type) {
	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_TOUCH_DOWN:
	case EI_EVENT_TOUCH_UP:
	case EI_EVENT_TOUCH_MOTION:
		prefix = "pending ";
		queue = &device->pending_event_queue;
		break;
	case EI_EVENT_FRAME:
		/* silently discard empty frames */
		if (list_empty(&device->pending_event_queue))
			return;

		struct ei_event *pending;
		list_for_each_safe(pending, &device->pending_event_queue, link) {
			update_event_timestamp(pending, event->timestamp);
			list_remove(&pending->link);
			list_append(&ei->event_queue, &pending->link);
		}
		break;
	default:
		if (device && !list_empty(&device->pending_event_queue))
				ei_queue_frame_event(device, ei_now(ei));
		break;
	}

	log_debug(ei, "queuing %sevent type %s (%u)",
		  prefix,
		  ei_event_type_to_string(event->type), event->type);

	list_append(queue, &event->link);
}

void
ei_queue_connect_event(struct ei *ei)
{
	struct ei_event *e = ei_event_new(ei);
	e->type = EI_EVENT_CONNECT;

	queue_event(ei, e);
}

void
ei_queue_disconnect_event(struct ei *ei)
{
	struct ei_event *e = ei_event_new(ei);
	e->type = EI_EVENT_DISCONNECT;

	queue_event(ei, e);
}

void
ei_queue_seat_added_event(struct ei_seat *seat)
{
	struct ei *ei= ei_seat_get_context(seat);

	struct ei_event *e = ei_event_new(ei);
	e->type = EI_EVENT_SEAT_ADDED;
	e->seat = ei_seat_ref(seat);

	queue_event(ei, e);
}

void
ei_queue_seat_removed_event(struct ei_seat *seat)
{
	struct ei *ei= ei_seat_get_context(seat);

	struct ei_event *e = ei_event_new(ei);
	e->type = EI_EVENT_SEAT_REMOVED;
	e->seat = ei_seat_ref(seat);

	queue_event(ei, e);
}

static void
queue_device_added_event(struct ei_device *device)
{
	struct ei *ei= ei_device_get_context(device);
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_DEVICE_ADDED;

	queue_event(ei, e);
}

static void
queue_device_removed_event(struct ei_device *device)
{
	struct ei *ei= ei_device_get_context(device);
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_DEVICE_REMOVED;

	queue_event(ei, e);
}

void
ei_queue_device_paused_event(struct ei_device *device)
{
	struct ei *ei= ei_device_get_context(device);
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_DEVICE_PAUSED;

	queue_event(ei, e);
}

void
ei_queue_device_resumed_event(struct ei_device *device)
{
	struct ei *ei= ei_device_get_context(device);
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_DEVICE_RESUMED;

	queue_event(ei, e);
}

void
ei_queue_keyboard_modifiers_event(struct ei_device *device,
				  const struct ei_xkb_modifiers *mods)
{
	struct ei *ei= ei_device_get_context(device);
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_KEYBOARD_MODIFIERS;
	e->modifiers = *mods;

	queue_event(ei, e);
}

void
ei_queue_frame_event(struct ei_device *device, uint64_t time)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_FRAME;
	e->timestamp = time;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_device_start_emulating_event(struct ei_device *device, uint32_t sequence)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_DEVICE_START_EMULATING;
	e->start_emulating.sequence = sequence;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_device_stop_emulating_event(struct ei_device *device)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_DEVICE_STOP_EMULATING;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_pointer_rel_event(struct ei_device *device,
			    double dx, double dy)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_POINTER_MOTION;
	e->pointer.dx = dx;
	e->pointer.dy = dy;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_pointer_abs_event(struct ei_device *device,
			    double x, double y)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_POINTER_MOTION_ABSOLUTE;
	e->pointer.absx = x;
	e->pointer.absy = y;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_pointer_button_event(struct ei_device *device, uint32_t button,
			       bool is_press)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_BUTTON_BUTTON;
	e->pointer.button = button;
	e->pointer.button_is_press = is_press;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_pointer_scroll_event(struct ei_device *device,
			       double x, double y)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_SCROLL_DELTA;
	e->pointer.sx = x;
	e->pointer.sy = y;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_pointer_scroll_discrete_event(struct ei_device *device,
					int32_t x, int32_t y)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_SCROLL_DISCRETE;
	e->pointer.sdx = x;
	e->pointer.sdy = y;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_pointer_scroll_stop_event(struct ei_device *device, bool x, bool y)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_SCROLL_STOP;
	e->pointer.stop_x = x;
	e->pointer.stop_y = y;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_pointer_scroll_cancel_event(struct ei_device *device, bool x, bool y)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_SCROLL_CANCEL;
	e->pointer.stop_x = x;
	e->pointer.stop_y = y;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_keyboard_key_event(struct ei_device *device, uint32_t key,
			     bool is_press)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_KEYBOARD_KEY;
	e->keyboard.key = key;
	e->keyboard.key_is_press = is_press;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_touch_down_event(struct ei_device *device, uint32_t touchid,
			   double x, double y)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_TOUCH_DOWN;
	e->touch.touchid = touchid,
	e->touch.x = x;
	e->touch.y = y;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_touch_motion_event(struct ei_device *device, uint32_t touchid,
			     double x, double y)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_TOUCH_MOTION;
	e->touch.touchid = touchid,
	e->touch.x = x;
	e->touch.y = y;

	queue_event(ei_device_get_context(device), e);
}

void
ei_queue_touch_up_event(struct ei_device *device, uint32_t touchid)
{
	struct ei_event *e = ei_event_new_for_device(device);

	e->type = EI_EVENT_TOUCH_UP;
	e->touch.touchid = touchid,

	queue_event(ei_device_get_context(device), e);
}

void
ei_disconnect(struct ei *ei)
{
	if (ei->state == EI_STATE_DISCONNECTED ||
	    ei->state == EI_STATE_DISCONNECTING)
		return;

	enum ei_state state = ei->state;

	/* We need the disconnecting state to be re-entrant
	   ei_device_remove() may call ei_disconnect() on a socket error */
	ei->state = EI_STATE_DISCONNECTING;

	struct ei_seat *seat;
	list_for_each_safe(seat, &ei->seats, link) {
		ei_seat_remove(seat);
	}

	if (state != EI_STATE_NEW) {
		ei_connection_request_disconnect(ei->connection);
	}
	ei_queue_disconnect_event(ei);
	ei->state = EI_STATE_DISCONNECTED;
	if (ei->source)
		source_remove(ei->source);
	ei->source = source_unref(ei->source);
}

#define DISCONNECT_IF_INVALID_ID(connection_, id_) do { \
	if (!brei_is_server_id(id_)) { \
		struct ei *ei_ = ei_connection_get_context(connection_); \
		log_bug(ei_, "Received invalid object id %#" PRIx64 ". Disconnecting", id_); \
		return brei_result_new(EI_CONNECTION_DISCONNECT_REASON_PROTOCOL, "Received invalid object id %#" PRIx64 ".", id_); \
	} \
} while(0)

static struct brei_result *
handle_msg_seat(struct ei_connection *connection, object_id_t seat_id, uint32_t version)
{
	DISCONNECT_IF_INVALID_ID(connection, seat_id);

	struct ei *ei = ei_connection_get_context(connection);

	DISCONNECT_IF_INVALID_VERSION(ei, ei_seat, seat_id, version);

	struct ei_seat *seat = ei_seat_new(ei, seat_id, version);

	/* We might get the seat event before our callback finished, so make sure
	 * we know we're connected
	*/
	ei_connected(ei);

	/* seats list owns the ref */
	list_append(&ei->seats, &seat->link);

	return NULL;
}

void
ei_queue_device_removed_event(struct ei_device *device)
{
	queue_device_removed_event(device);
}

void
ei_queue_device_added_event(struct ei_device *device)
{
	queue_device_added_event(device);
}

_public_ struct ei_event*
ei_get_event(struct ei *ei)
{
	if (list_empty(&ei->event_queue))
		return NULL;

	struct ei_event *e = list_first_entry(&ei->event_queue, e, link);
	list_remove(&e->link);

	return e;
}

_public_ struct ei_event*
ei_peek_event(struct ei *ei)
{
	if (list_empty(&ei->event_queue))
		return NULL;

	struct ei_event *e = list_first_entry(&ei->event_queue, e, link);
	return ei_event_ref(e);
}

void
ei_connected(struct ei *ei)
{
	if (ei->state == EI_STATE_CONNECTING) {
		ei->state = EI_STATE_CONNECTED;
		ei_queue_connect_event(ei);
	}
}

static struct brei_result *
handle_msg_disconnected(struct ei_connection *connection, uint32_t last_serial,
			uint32_t reason, const char *explanation)
{
	struct ei *ei = ei_connection_get_context(connection);

	if (reason == EI_CONNECTION_DISCONNECT_REASON_DISCONNECTED) {
		log_info(ei, "Disconnected by EIS");
		/* We got disconnected, disconnect our source because whatever
		 * we'd receive after this is garbage and the server won't
		*  want to hear anything from us anyway. */
		source_remove(ei->source);
		ei_disconnect(ei);
		return NULL;
	} else {
		log_info(ei, "Disconnected after error: %s", explanation);
		return brei_result_new(reason, "%s", explanation);
	}

}

static struct brei_result *
handle_msg_invalid_object(struct ei_connection *connection, uint32_t last_serial, object_id_t object_id)
{
	struct ei *ei = ei_connection_get_context(connection);

	/* The protocol is async, so what can happen is:
	 *
	 * server sends A->destroyed()
	 *     client sends A->foo()
	 *     client receives A->destroyed()
	 * server receives A->foo()
	 * server sends invalid_object_id(A)
	 *     client receives invalid_object_id(A)
	 *
	 * This is expected and we shouldn't have a problem with that.
	 */

	struct ei_defunct_object *defunct;
	list_for_each_safe(defunct, &ei->defunct_objects, node) {
		if (defunct->object_id == object_id)
			return NULL;
	}

	log_bug(ei, "Invalid object %#" PRIx64 " after serial %u, I don't yet know how to handle that", object_id, last_serial);

	return NULL;
}

static struct brei_result *
handle_msg_ping(struct ei_connection *connection, object_id_t id,  uint32_t version)
{
	DISCONNECT_IF_INVALID_ID(connection, id);

	struct ei *ei = ei_connection_get_context(connection);

	DISCONNECT_IF_INVALID_VERSION(ei, ei_pingpong, id, version);

	_unref_(ei_pingpong) *pingpong = ei_pingpong_new_for_id(ei, id, version);

	ei_pingpong_request_done(pingpong, 0);

	return NULL;
}

static const struct ei_connection_interface interface = {
	.disconnected = handle_msg_disconnected,
	.seat = handle_msg_seat,
	.invalid_object = handle_msg_invalid_object,
	.ping = handle_msg_ping,
};

const struct ei_connection_interface *
ei_get_interface(struct ei *ei)
{
	return &interface;
}

static int
lookup_object(object_id_t object_id, struct brei_object **object, void *userdata)
{
	struct ei *ei = userdata;

	struct brei_object *obj;
	list_for_each(obj, &ei->proto_objects, link) {
		if (obj->id == object_id) {
			*object = obj;
			return 0;
		}
	}

	log_debug(ei, "Failed to find object %#" PRIx64 "", object_id);

	return -ENOENT;
}

static void
connection_dispatch(struct source *source, void *userdata)
{
	static uint8_t cleanup;
	struct ei *ei = userdata;
	enum ei_state old_state = ei->state;

	/* Flush any pending writes, if we have them */
	int rc = ei_unsent_flush(ei);
	if (rc < 0 && rc != -EAGAIN) {
		log_warn(ei, "Error flushing unsent queue: %s", strerror(-rc));
		ei_disconnect(ei);
	} else {
		_unref_(brei_result) *result = brei_dispatch(ei->brei, source_get_fd(source),
							     lookup_object, ei);
		if (result) {
			log_warn(ei, "Connection error: %s", brei_result_get_explanation(result));
			brei_drain_fd(source_get_fd(source));
			ei_disconnect(ei);
		} else if (++cleanup % 20 == 0) {
			uint64_t now = ei_now(ei);
			struct ei_defunct_object *defunct;

			list_for_each_safe(defunct, &ei->defunct_objects, node) {
				/* Drop defunct objects after 5s */
				if (now - defunct->time < s2us(5))
					break;
				ei_defunct_object_free(defunct);
			}
		}
	}

	static const char *states[] = {
		"NEW",
		"BACKEND",
		"CONNECTING",
		"CONNECTED",
		"DISCONNECTED",
		"DISCONNECTING",
	};

	if (old_state != ei->state)
		log_debug(ei, "Connection dispatch: %s -> %s",
			  states[old_state],
			  states[ei->state]);
}

static void
ei_queue_unsent(struct ei *ei, struct source *source, struct iobuf *buf)
{
	if (list_empty(&ei->unsent_queue)) {
		source_enable_write(source, true);
	}

	struct ei_unsent *unsent = xalloc(sizeof *unsent);
	unsent->buf = buf;
	list_append(&ei->unsent_queue, &unsent->node);
}

static void
ei_unsent_free(struct ei_unsent *unsent)
{
	list_remove(&unsent->node);
	iobuf_free(unsent->buf);
	free(unsent);
}

int
ei_unsent_flush(struct ei* ei)
{
	if (list_empty(&ei->unsent_queue))
		return 0;

	struct source *source = ei->source;
	int fd = source_get_fd(source);
	struct ei_unsent *unsent;
	list_for_each_safe(unsent, &ei->unsent_queue, node) {
		int rc = iobuf_send(unsent->buf, fd);
		if (rc < 0)
			return rc;
		ei_unsent_free(unsent);
	}
	source_enable_write(source, false);
	return 0;
}

int
ei_send_message(struct ei *ei, const struct brei_object *object,
		uint32_t opcode, const char *signature, size_t nargs, ...)
{
	log_debug(ei, "sending: object %#" PRIx64 " (%s@v%u:%s(%u)) signature '%s'",
	   object->id,
	   object->interface->name,
	   object->interface->version,
	   object->interface->requests[opcode].name,
	   opcode,
	   signature);

	va_list args;
	va_start(args, nargs);
	_unref_(brei_result) *result = brei_marshal_message(ei->brei,
							    object->id,
							    opcode, signature,
							    nargs, args);
	va_end(args);

	if (brei_result_get_reason(result) != 0) {
		log_warn(ei, "failed to marshal message: %s",
			 brei_result_get_explanation(result));
		return -EBADMSG;
	}

	_cleanup_iobuf_ struct iobuf *buf = brei_result_get_data(result);
	assert(buf);
	int fd = source_get_fd(ei->source);
	int rc = -EPIPE;
	if (fd != -1) {
		rc = ei_unsent_flush(ei);
		if (rc >= 0)
			rc = iobuf_send(buf, fd);
		if (rc == -EAGAIN) {
			ei_queue_unsent(ei, ei->source, steal(&buf));
			rc = 0;
		} else if (rc < 0){
			log_warn(ei, "failed to send message: %s", strerror(-rc));
			source_remove(ei->source);
		}
	}
	return rc < 0 ? rc : 0;
}

int
ei_set_socket(struct ei *ei, int fd)
{
	struct source *source = source_new(fd, connection_dispatch, ei);
	int rc = sink_add_source(ei->sink, source);
	if (rc == 0) {
		ei->source = source_ref(source);
		ei->state = EI_STATE_BACKEND;

		/* The server SHOULD have already sent the handshake
		 * version, let's process that. If not ready, it'll happen
		 * in the next dispatch.
		 *
		 * FIXME: this will block if O_NONBLOCK is missing
		 */
		ei_dispatch(ei);
	}

	source_unref(source);

	return rc < 0 ? rc : 0;
}

_public_ void
ei_configure_name(struct ei *ei, const char *name)
{
	if (ei->state != EI_STATE_NEW) {
		log_bug_client(ei,"Client is already connected");
		return;
	}

	if (strlen(name) > 1024) {
		log_bug_client(ei, "Client name too long");
		return;
	}

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

_public_ void
ei_clock_set_now_func(struct ei *ei, ei_clock_now_func func)
{
	ei->clock_now = func;
}

_public_ uint64_t
ei_now(struct ei *ei)
{
	uint64_t ts = 0;

	if (ei->clock_now)
		ts = ei->clock_now(ei);
	else {
		int rc = now(&ts);
		if (rc < 0) {
			/* We should probably disconnect here but the chances of this
			 * happening are so slim it's not worth worrying about. Plus,
			 * if this fails we're likely to be inside eis_device_frame()
			 * so we should flush a frame event before disconnecting and... */
			log_error(ei, "clock_gettime failed: %s", strerror(-rc));
		}
	}

	return ts;
}

#ifdef _enable_tests_
#include "util-munit.h"

MUNIT_TEST(test_init_unref)
{
	struct ei *ei = ei_new(NULL);

	munit_assert_int(ei->state, ==, EI_STATE_NEW);
	munit_assert(list_empty(&ei->event_queue));
	munit_assert(list_empty(&ei->seats));

	munit_assert_not_null(ei->sink);

	struct ei *refd = ei_ref(ei);
	munit_assert_ptr_equal(ei, refd);
	munit_assert_int(ei->object.refcount, ==, 2);

	struct ei *unrefd = ei_unref(ei);
	munit_assert_null(unrefd);

	unrefd = ei_unref(ei);
	munit_assert_null(unrefd);

	return MUNIT_OK;
}

MUNIT_TEST(test_configure_name)
{
	struct ei *ei = ei_new(NULL);

	ei_configure_name(ei, "foo");
	munit_assert_string_equal(ei->name, "foo");
	ei_configure_name(ei, "bar");
	munit_assert_string_equal(ei->name, "bar");

	/* ignore names that are too long */
	char buf[1200] = {0};
	memset(buf, 'a', sizeof(buf) - 1);
	ei_configure_name(ei, buf);
	munit_assert_string_equal(ei->name, "bar");

	/* ignore names in all other states */
	for (enum ei_state state = EI_STATE_NEW + 1;
	     state <= EI_STATE_DISCONNECTED;
	     state++) {
		ei->state = state;
		ei_configure_name(ei, "expect ignored");
		munit_assert_string_equal(ei->name, "bar");
	}

	ei_unref(ei);

	return MUNIT_OK;
}
#endif
