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

#include "util-bits.h"
#include "util-io.h"
#include "util-macros.h"
#include "util-mem.h"
#include "util-sources.h"
#include "util-strings.h"
#include "util-structs.h"
#include "util-time.h"
#include "util-tristate.h"
#include "util-version.h"

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

DEFINE_TRISTATE(started, finished, connected);
DEFINE_UNREF_CLEANUP_FUNC(brei_result);

struct eis_unsent {
	struct list node;
	struct iobuf *buf;
};

static void
eis_unsent_free(struct eis_unsent *unsent);

static void
client_drop_seats(struct eis_client *client);

static void
eis_client_destroy(struct eis_client *client)
{
	struct eis_unsent *unsent;
	list_for_each_safe(unsent, &client->unsent_queue, node) {
		eis_unsent_free(unsent);
	}

	client_drop_seats(client);
	eis_handshake_unref(client->setup);
	eis_connection_unref(client->connection);
	free(client->name);
	brei_context_unref(client->brei);
	source_remove(client->source);
	source_unref(client->source);
	list_remove(&client->link);
}

static OBJECT_IMPLEMENT_CREATE(eis_client);
static OBJECT_IMPLEMENT_PARENT(eis_client, eis);

_public_ OBJECT_IMPLEMENT_REF(eis_client);

_public_ OBJECT_IMPLEMENT_UNREF_CLEANUP(eis_client);

_public_
OBJECT_IMPLEMENT_GETTER(eis_client, name, const char *);
_public_
OBJECT_IMPLEMENT_SETTER(eis_client, user_data, void *);
_public_
OBJECT_IMPLEMENT_GETTER(eis_client, user_data, void *);

uint32_t
eis_client_get_next_serial(struct eis_client *client)
{
	return ++client->serial;
}

void
eis_client_update_client_serial(struct eis_client *client, uint32_t serial)
{
	client->last_client_serial = serial;
}

_public_ struct eis *
eis_client_get_context(struct eis_client *client)
{
	return eis_client_parent(client);
}

const struct brei_object *
eis_client_get_proto_object(struct eis_client *client)
{
	return &client->connection->proto_object;
}

object_id_t
eis_client_get_new_id(struct eis_client *client)
{
	static const uint64_t mask = -1 >> 8;
	static const uint64_t offset = 0xff00000000000000;
	return offset | (client->next_object_id++ & mask);
}

bool
eis_client_update_client_object_id(struct eis_client *client, object_id_t id)
{
	if (id <= client->last_client_object_id)
		return false;

	client->last_client_object_id = id;
	return true;
}

void
eis_client_register_object(struct eis_client *client, struct brei_object *object)
{
	struct eis *eis = eis_client_get_context(client);
	log_debug(eis,
		  "object %#" PRIx64 " registering %s v%u",
		  object->id,
		  object->interface->name,
		  object->version);
	list_append(&client->proto_objects, &object->link);
}

void
eis_client_unregister_object(struct eis_client *client, struct brei_object *object)
{
	struct eis *eis = eis_client_get_context(client);
	log_debug(eis,
		  "object %#" PRIx64 " deregistering %s v%u",
		  object->id,
		  object->interface->name,
		  object->version);
	list_remove(&object->link);
}

struct eis_client *
eis_client_get_client(struct eis_client *client)
{
	return client;
}

_public_ bool
eis_client_is_sender(struct eis_client *client)
{
	return client->is_sender;
}

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

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

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

static int
eis_client_unsent_flush(struct eis_client *client)
{
	if (list_empty(&client->unsent_queue))
		return 0;

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

int
eis_client_send_message(struct eis_client *client,
			const struct brei_object *object,
			uint32_t opcode,
			const char *signature,
			size_t nargs,
			...)
{
	struct eis *eis = eis_client_get_context(client);

	log_debug(eis,
		  "object %#" PRIx64 " sending (%s@v%u:%s(%u)) signature '%s'",
		  object->id,
		  object->interface->name,
		  object->interface->version,
		  object->interface->events[opcode].name,
		  opcode,
		  signature);

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

	if (brei_result_get_reason(result) != 0) {
		log_warn(eis, "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(client->source);
	int rc = -EPIPE;
	if (fd != -1) {
		rc = eis_client_unsent_flush(client);
		if (rc >= 0)
			rc = iobuf_send(buf, fd);
		if (rc == -EAGAIN) {
			eis_client_queue_unsent(client, client->source, steal(&buf));
			rc = 0;
		} else if (rc < 0) {
			if (rc == -EPIPE) {
				log_debug(eis, "failed to send message: %s", strerror(-rc));
			} else {
				log_warn(eis, "failed to send message: %s", strerror(-rc));
			}
			source_remove(client->source);
		}
	}
	return rc < 0 ? rc : 0;
}

static int
client_send_seat_added(struct eis_client *client, struct eis_seat *seat)
{
	/* Client didn't announce ei_seat */
	if (client->interface_versions.ei_seat == 0)
		return 0;

	return eis_connection_event_seat(client->connection,
					 eis_seat_get_id(seat),
					 eis_seat_get_version(seat));
}

_public_ void
eis_client_connect(struct eis_client *client)
{
	switch (client->state) {
	case EIS_CLIENT_STATE_DISCONNECTED:
		return;
	case EIS_CLIENT_STATE_CONNECTING:
		break;
	default:
		log_bug_client(eis_client_get_context(client),
			       "%s: client already connected",
			       __func__);
		return;
	}
	client->state = EIS_CLIENT_STATE_CONNECTED;
}

static void
client_drop_seats(struct eis_client *client)
{
	struct eis_seat *s;
	list_for_each_safe(s, &client->seats, link) {
		eis_seat_drop(s);
	}
}

static void
client_disconnect(struct eis_client *client,
		  enum eis_connection_disconnect_reason reason,
		  const char *explanation)
{
	switch (client->state) {
	case EIS_CLIENT_STATE_DISCONNECTED:
		/* Client already disconnected? don't bother sending an
		 * event */
		return;
	case EIS_CLIENT_STATE_REQUESTED_DISCONNECT:
		/* Drop the seats again (see client_msg_disconnect) because
		 * the server may have added seats between the client requesting the
		 * disconnect and EIS actually processing that event
		 */
		client_drop_seats(client);
		/* DISCONNECTED event is already in the EIS queue */
		/* Must not send disconnected to the client */
		client->connection = eis_connection_unref(client->connection);
		client->state = EIS_CLIENT_STATE_DISCONNECTED;
		source_remove(client->source);
		break;
	case EIS_CLIENT_STATE_CONNECTING:
	case EIS_CLIENT_STATE_CONNECTED:
		client_drop_seats(client);
		eis_connection_remove_pending_callbacks(client->connection);
		eis_queue_disconnect_event(client);
		eis_connection_event_disconnected(client->connection,
						  client->last_client_serial,
						  reason,
						  explanation);
		client->connection = eis_connection_unref(client->connection);
		client->state = EIS_CLIENT_STATE_DISCONNECTED;
		source_remove(client->source);
		break;
	case EIS_CLIENT_STATE_NEW:
		client->state = EIS_CLIENT_STATE_DISCONNECTED;
		source_remove(client->source);
		break;
	}

	eis_client_unref(client);
}

_public_ void
eis_client_disconnect(struct eis_client *client)
{
	client_disconnect(client, EIS_CONNECTION_DISCONNECT_REASON_DISCONNECTED, NULL);
}

void
eis_client_disconnect_with_reason(struct eis_client *client,
				  enum eis_connection_disconnect_reason reason,
				  const char *explanation)
{
	client_disconnect(client, reason, explanation);
}

void
eis_client_setup_done(struct eis_client *client,
		      const char *name,
		      bool is_sender,
		      const struct eis_client_interface_versions *versions)
{
	client->setup = NULL; /* connection object cleans itself up */
	client->name = xstrdup(name);
	client->is_sender = is_sender;
	client->interface_versions = *versions;
	client->state = EIS_CLIENT_STATE_CONNECTING;
	/* We don't queue the connect event yet because we send an extra ping/pong
	 * out, just to make sure that part of the protocol works.
	 * See pong() in libeis-client.c
	 */
}

#define DISCONNECT_IF_INVALID_ID(client_, id_) do { \
	if (!brei_is_client_id(id_) || !eis_client_update_client_object_id(client, id_)) { \
		struct eis *_ctx = eis_client_get_context(client_); \
		log_bug_client(_ctx, "Invalid object id %#" PRIx64 ". Disconnecting client", id_); \
		return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL, "Invalid object id %#" PRIx64 ".", new_id); \
	} \
} while(0)

static struct brei_result *
client_msg_disconnect(struct eis_connection *connection)
{
	struct eis_client *client = eis_connection_get_client(connection);

	/* We need to drop the seats because that unrolls the EIS device state
	 * so that EIS gets the correct DEVICE_REMOVED sequence, etc.
	 * Then queue the DISCONNECTED event and wait for EIS to call
	 * eis_client_disconnect()
	 */
	client_drop_seats(client);
	eis_queue_disconnect_event(client);
	client->state = EIS_CLIENT_STATE_REQUESTED_DISCONNECT;
	return NULL;
}

static struct brei_result *
client_msg_sync(struct eis_connection *connection, object_id_t new_id, uint32_t version)
{
	struct eis_client *client = eis_connection_get_client(connection);

	DISCONNECT_IF_INVALID_ID(client, new_id);
	DISCONNECT_IF_INVALID_VERSION(client, ei_connection, new_id, version);

	eis_queue_sync_event(client, new_id, version);

	return 0;
}

static const struct eis_connection_interface intf_state_new = {
	.sync = client_msg_sync,
	.disconnect = client_msg_disconnect,
};

/* Client is waiting for us, shouldn't send anything except disconnect */
static const struct eis_connection_interface intf_state_connecting = {
	.sync = client_msg_sync,
	.disconnect = client_msg_disconnect,
};

static const struct eis_connection_interface intf_state_connected = {
	.sync = client_msg_sync,
	.disconnect = client_msg_disconnect,
};

static const struct eis_connection_interface *interfaces[] = {
	[EIS_CLIENT_STATE_NEW] = &intf_state_new,
	[EIS_CLIENT_STATE_CONNECTING] = &intf_state_connecting,
	[EIS_CLIENT_STATE_CONNECTED] = &intf_state_connected,
	[EIS_CLIENT_STATE_REQUESTED_DISCONNECT] = NULL,
	[EIS_CLIENT_STATE_DISCONNECTED] = NULL,
};

const struct eis_connection_interface *
eis_client_get_interface(struct eis_client *client)
{
	assert(client->state < ARRAY_LENGTH(interfaces));
	return interfaces[client->state];
}

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

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

	log_debug(eis_client_get_context(client), "Failed to find object %#" PRIx64 "", object_id);
	if (client->connection)
		eis_connection_event_invalid_object(client->connection,
						    client->last_client_serial,
						    object_id);

	return -ENOENT;
}

static void
client_dispatch(struct source *source, void *userdata)
{
	_unref_(eis_client) *client = eis_client_ref(userdata);
	enum eis_client_state old_state = client->state;

	/* Flush any pending writes, if we have them */
	int rc = eis_client_unsent_flush(client);
	if (rc < 0 && rc != -EAGAIN) {
		log_warn(eis_client_get_context(client),
			 "Error flushing unsent queue: %s",
			 strerror(-rc));
		eis_client_disconnect(client);
	} else {
		_unref_(brei_result) *result =
			brei_dispatch(client->brei, source_get_fd(source), lookup_object, client);
		if (result) {
			if (old_state != EIS_CLIENT_STATE_REQUESTED_DISCONNECT ||
			    brei_result_get_reason(result) !=
				    BREI_CONNECTION_DISCONNECT_REASON_TRANSPORT)
				log_warn(eis_client_get_context(client),
					 "Client error: %s",
					 brei_result_get_explanation(result));

			brei_drain_fd(source_get_fd(source));
			eis_client_disconnect_with_reason(client,
							  brei_result_get_reason(result),
							  brei_result_get_explanation(result));
		}
	}

	static const char *client_states[] = {
		"NEW", "CONNECTING", "CONNECTED", "REQUESTED_DISCONNECT", "DISCONNECTED",
	};
	if (old_state != client->state) {
		assert(old_state < ARRAY_LENGTH(client_states));
		assert(client->state < ARRAY_LENGTH(client_states));
		log_debug(eis_client_get_context(client),
			  "Client dispatch: %s -> %s",
			  client_states[old_state],
			  client_states[client->state]);
	}
}

struct eis_client *
eis_client_new(struct eis *eis, int fd)
{
	static uint32_t client_id;
	struct eis_client *client = eis_client_create(&eis->object);

	client->brei = brei_context_new(client);
	brei_context_set_log_context(client->brei, eis);
	brei_context_set_log_func(client->brei, (brei_logfunc_t)eis_log_msg_va);

	client->is_sender = true;
	client->id = ++client_id;
	list_init(&client->seats);
	list_init(&client->seats_pending);
	list_init(&client->proto_objects);
	list_init(&client->unsent_queue);

	client->interface_versions = (struct eis_client_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(2),
		.ei_device = flag_is_set(eis->flags, EIS_FLAG_DEVICE_READY) ? VERSION_V(3)
									    : 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_text = VERSION_V(1),
		.ei_touchscreen = VERSION_V(2),
	};
	struct source *s = source_new(fd, client_dispatch, client);
	int rc = sink_add_source(eis->sink, s);

	if (rc != 0) {
		source_unref(s);
		return NULL;
	}

	client->source = source_ref(s);
	client->state = EIS_CLIENT_STATE_NEW;

	eis_add_client(eis, eis_client_ref(client));

	source_unref(s);

	client->setup = eis_handshake_new(client, &client->interface_versions);

	return client;
}

void
eis_client_add_seat(struct eis_client *client, struct eis_seat *seat)
{
	/* remove from the pending list first */
	list_remove(&seat->link);
	/* We own this seat now */
	eis_seat_ref(seat);
	list_append(&client->seats, &seat->link);
	client_send_seat_added(client, seat);
}
