/*
 * Copyright (c) 2022-2023 Jim Ramsay
 * Copyright (c) 2023 Andri Yngvason
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <netdb.h>
#include <neatvnc.h>
#include <aml.h>
#include <jansson.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include "output.h"
#include "ctl-commands.h"
#include "ctl-server.h"
#include "json-ipc.h"
#include "util.h"
#include "strlcpy.h"
#include "image-source.h"

#define FAILED_TO(action) \
	nvnc_log(NVNC_LOG_ERROR, "Failed to " action ": %m");

enum send_priority {
	SEND_FIFO,
	SEND_IMMEDIATE,
};

struct cmd {
	enum cmd_type type;
};

struct cmd_attach {
	struct cmd cmd;
	char display[128];
	enum image_source_type image_source_type;
	char image_source_name[128];
};

struct cmd_help {
	struct cmd cmd;
	char id[64];
	bool id_is_command;
};

struct cmd_set_output {
	struct cmd cmd;
	char target[64];
	enum output_cycle_direction cycle;
};

struct cmd_disconnect_client {
	struct cmd cmd;
	char id[64];
};

struct cmd_set_desktop_name {
	struct cmd cmd;
	char desktop_name[256];
};

struct cmd_response {
	int code;
	json_t* data;
};

struct ctl_client {
	int fd;
	struct wl_list link;
	struct ctl* server;
	struct aml_handler* handler;
	char read_buffer[512];
	size_t read_len;
	json_t* response_queue;
	char* write_buffer;
	char* write_ptr;
	size_t write_len;
	bool drop_after_next_send;
	bool accept_events;
};

struct ctl {
	char socket_path[255];
	struct ctl_server_actions actions;
	int fd;
	struct aml_handler* handler;
	struct wl_list clients;
};

static struct cmd_response* cmd_response_new(int code, json_t* data)
{
	struct cmd_response* new = calloc(1, sizeof(struct cmd_response));
	new->code = code;
	new->data = data;
	return new;
}

static void cmd_response_destroy(struct cmd_response* self)
{
	json_decref(self->data);
	free(self);
}

static struct cmd_attach* cmd_attach_new(json_t* args,
		struct jsonipc_error* err)
{
	const char* display = NULL;
	const char* is_desktop = false; // TODO: This should NOT be a string
	const char* output = NULL;
	const char* toplevel = NULL;
	if (json_unpack(args, "{s:s, s?s, s?s, s?s}",
				"display", &display,
				"desktop", &is_desktop,
				"output", &output,
				"toplevel", &toplevel) == -1) {
		jsonipc_error_printf(err, EINVAL, "Missing display name");
		return NULL;
	}
	struct cmd_attach* cmd = calloc(1, sizeof(*cmd));
	strlcpy(cmd->display, display, sizeof(cmd->display));
	if (is_desktop) {
		cmd->image_source_type = IMAGE_SOURCE_TYPE_DESKTOP;
	} else if (output) {
		cmd->image_source_type = IMAGE_SOURCE_TYPE_OUTPUT;
		strlcpy(cmd->image_source_name, output,
				sizeof(cmd->image_source_name));
	} else if (toplevel) {
		cmd->image_source_type = IMAGE_SOURCE_TYPE_TOPLEVEL;
		strlcpy(cmd->image_source_name, toplevel,
				sizeof(cmd->image_source_name));
	}
	return cmd;
}

static struct cmd_help* cmd_help_new(json_t* args,
		struct jsonipc_error* err)
{
	const char* command = NULL;
	const char* event = NULL;
	if (args && json_unpack(args, "{s?s, s?s}",
				"command", &command,
				"event", &event) == -1) {
		jsonipc_error_printf(err, EINVAL,
				"expecting \"command\" or \"event\" (optional)");
		return NULL;
	}
	if (command && event) {
		jsonipc_error_printf(err, EINVAL,
				"expecting exacly one of \"command\" or \"event\"");
		return NULL;
	}
	struct cmd_help* cmd = calloc(1, sizeof(*cmd));
	if (command) {
		strlcpy(cmd->id, command, sizeof(cmd->id));
		cmd->id_is_command = true;
	} else if (event) {
		strlcpy(cmd->id, event, sizeof(cmd->id));
		cmd->id_is_command = false;
	}
	return cmd;
}

static struct cmd_set_output* cmd_set_output_new(json_t* args,
		struct jsonipc_error* err)
{
	const char* target = NULL;
	if (json_unpack(args, "{s:s}", "output-name", &target) == -1) {
		jsonipc_error_printf(err, EINVAL, "Missing output name");
		return NULL;
	}
	struct cmd_set_output* cmd = calloc(1, sizeof(*cmd));
	strlcpy(cmd->target, target, sizeof(cmd->target));
	return cmd;
}

static struct cmd_disconnect_client* cmd_disconnect_client_new(json_t* args,
		struct jsonipc_error* err)
{
	const char* id = NULL;
	if (json_unpack(args, "{s:s}", "id", &id) == -1) {
		jsonipc_error_printf(err, EINVAL, "Missing client id");
		return NULL;
	}
	struct cmd_disconnect_client* cmd = calloc(1, sizeof(*cmd));
	strlcpy(cmd->id, id, sizeof(cmd->id));
	return cmd;
}

static struct cmd_set_desktop_name* cmd_set_desktop_name_new(json_t* args,
		struct jsonipc_error* err)
{
	const char* desktop_name = NULL;
	if (json_unpack(args, "{s:s}", "desktop-name", &desktop_name) == -1) {
		jsonipc_error_printf(err, EINVAL, "Missing desktop name");
		return NULL;
	}
	struct cmd_set_desktop_name* cmd = calloc(1, sizeof(*cmd));
	strlcpy(cmd->desktop_name, desktop_name, sizeof(cmd->desktop_name));
	return cmd;
}

static json_t* list_allowed(struct cmd_info (*list)[], size_t len)
{
	json_t* allowed = json_array();
	for (size_t i = 0; i < len; ++i) {
		json_array_append_new(allowed, json_string((*list)[i].name));
	}
	return allowed;
}

static json_t* list_allowed_commands()
{
	return list_allowed(&ctl_command_list, CMD_LIST_LEN);
}

static json_t* list_allowed_events()
{
	return list_allowed(&ctl_event_list, EVT_LIST_LEN);
}

static struct cmd* parse_command(struct jsonipc_request* ipc,
		struct jsonipc_error* err)
{
	nvnc_trace("Parsing command %s", ipc->method);
	enum cmd_type cmd_type = ctl_command_parse_name(ipc->method);
	struct cmd* cmd = NULL;
	switch (cmd_type) {
	case CMD_ATTACH:
		cmd = (struct cmd*)cmd_attach_new(ipc->params, err);
		break;
	case CMD_HELP:
		cmd = (struct cmd*)cmd_help_new(ipc->params, err);
		break;
	case CMD_OUTPUT_SET:
		cmd = (struct cmd*)cmd_set_output_new(ipc->params, err);
		break;
	case CMD_CLIENT_DISCONNECT:
		cmd = (struct cmd*)cmd_disconnect_client_new(ipc->params, err);
		break;
	case CMD_SET_DESKTOP_NAME:
		cmd = (struct cmd*)cmd_set_desktop_name_new(ipc->params, err);
		break;
	case CMD_DETACH:
	case CMD_VERSION:
	case CMD_EVENT_RECEIVE:
	case CMD_CLIENT_LIST:
	case CMD_OUTPUT_LIST:
	case CMD_OUTPUT_CYCLE:
	case CMD_WAYVNC_EXIT:
		cmd = calloc(1, sizeof(*cmd));
		break;
	case CMD_UNKNOWN:
		jsonipc_error_set_new(err, ENOENT,
				json_pack("{s:o, s:o}",
					"error",
					jprintf("Unknown command \"%s\"",
						ipc->method),
					"commands", list_allowed_commands()));
		break;
	}
	if (cmd)
		cmd->type = cmd_type;
	return cmd;
}

static void client_destroy(struct ctl_client* self)
{
	nvnc_trace("Destroying client %p", self);
	aml_stop(aml_get_default(), self->handler);
	aml_unref(self->handler);
	close(self->fd);
	json_array_clear(self->response_queue);
	json_decref(self->response_queue);
	wl_list_remove(&self->link);
	free(self);
}

static void set_internal_error(struct cmd_response** err, int code,
		const char* fmt, ...)
{
	char msg[256];
	va_list ap;
	va_start(ap, fmt);
	vsnprintf(msg, sizeof(msg), fmt, ap);
	va_end(ap);
	nvnc_log(NVNC_LOG_WARNING, msg);
	*err = cmd_response_new(code, json_pack("{s:s}", "error", msg));
}

// Return values:
// >0: Number of bytes read
// 0: No bytes read (EAGAIN)
// -1: Fatal error.  Check 'err' for details, or if 'err' is null, terminate the connection.
static ssize_t client_read(struct ctl_client* self, struct cmd_response** err)
{
	size_t bufferspace = sizeof(self->read_buffer) - self->read_len;
	if (bufferspace == 0) {
		set_internal_error(err, EIO, "Buffer overflow");
		return -1;
	}
	ssize_t n = recv(self->fd, self->read_buffer + self->read_len, bufferspace,
			MSG_DONTWAIT);
	if (n == -1) {
		if (errno == EAGAIN || errno == EWOULDBLOCK) {
			nvnc_trace("recv: EAGAIN");
			return 0;
		}
		set_internal_error(err, EIO, "Read failed: %m");
		return -1;
	} else if (n == 0) {
		nvnc_log(NVNC_LOG_INFO, "Control socket client disconnected: %p", self);
		errno = ENOTCONN;
		return -1;
	}
	self->read_len += n;
	nvnc_trace("Read %d bytes, total is now %d", n, self->read_len);
	return n;
}

static json_t* client_next_object(struct ctl_client* self, struct cmd_response** ierr)
{
	if (self->read_len == 0)
		return NULL;

	json_error_t err;
	json_t* root = json_loadb(self->read_buffer, self->read_len,
			JSON_DISABLE_EOF_CHECK, &err);
	if (root) {
		nvnc_log(NVNC_LOG_DEBUG, "<< %.*s", err.position, self->read_buffer);
		advance_read_buffer(&self->read_buffer, &self->read_len, err.position);
	} else if (json_error_code(&err) == json_error_premature_end_of_input) {
		nvnc_trace("Awaiting more data");
	} else {
		set_internal_error(ierr, EINVAL, err.text);
	}
	return root;
}

static struct cmd_response* generate_help_object(const char* id, bool id_is_command)
{
	struct cmd_info* info = id_is_command ?
		ctl_command_by_name(id) :
		ctl_event_by_name(id);
	json_t* data;
	if (!info) {
		data = json_pack("{s:o, s:o}",
				"commands", list_allowed_commands(),
				"events", list_allowed_events());
	} else {
		json_t* param_list = NULL;
		if (info->params[0].name) {
			param_list = json_object();
			for (struct cmd_param_info* param = info->params;
					param->name; ++param)
				json_object_set_new(param_list, param->name,
						json_string(param->description));
		}
		data = json_pack("{s:{s:s, s:o*}}",
				info->name,
				"description", info->description,
				"params", param_list);
	}
	struct cmd_response* response = cmd_ok();
	response->data = data;
	return response;
}

static struct cmd_response* generate_version_object()
{
	struct cmd_response* response = cmd_ok();
	response->data = json_pack("{s:s, s:s, s:s}",
			"wayvnc", wayvnc_version,
			"neatvnc", nvnc_version,
			"aml", aml_version);
	return response;
}

static struct ctl_server_client* ctl_server_client_first(struct ctl* self)
{
	return self->actions.client_next(self, NULL);
}

static struct ctl_server_client* ctl_server_client_next(struct ctl* self,
		struct ctl_server_client* prev)
{
	return self->actions.client_next(self, prev);
}

static int sockaddr_to_string(char* dst, size_t sz, const struct sockaddr* addr)
{
	struct sockaddr_in* sa_in = (struct sockaddr_in*)addr;
	struct sockaddr_in6* sa_in6 = (struct sockaddr_in6*)addr;

	switch (addr->sa_family) {
	case AF_INET:
		inet_ntop(addr->sa_family, &sa_in->sin_addr, dst, sz);
		return 0;
	case AF_INET6:
		inet_ntop(addr->sa_family, &sa_in6->sin6_addr, dst, sz);
		return 0;
	}
	nvnc_log(NVNC_LOG_DEBUG,
			"Don't know how to convert sa_family %d to string",
			addr->sa_family);
	return -1;
}

static void ctl_server_client_get_info(struct ctl* self,
		const struct ctl_server_client* client,
		struct ctl_server_client_info* info)
{
	return self->actions.client_info(client, info);
}

static struct cmd_response* generate_vnc_client_list(struct ctl* self)
{
	struct cmd_response* response = cmd_ok();
	response->data = json_array();

	struct ctl_server_client* client;
	for (client = ctl_server_client_first(self); client;
			client = ctl_server_client_next(self, client)) {
		struct ctl_server_client_info info = {};
		ctl_server_client_get_info(self, client, &info);

		char id_str[64];
		snprintf(id_str, sizeof(id_str), "%d", info.id);
		json_t* packed = json_pack("{s:s}", "id", id_str);

		char address_string[256];
		if (sockaddr_to_string(address_string, sizeof(address_string),
					&info.address) == 0) {
			json_object_set_new(packed, "address",
					json_string(address_string));
		}

		if (info.username)
			json_object_set_new(packed, "username",
					json_string(info.username));

		if (info.seat)
			json_object_set_new(packed, "seat",
					json_string(info.seat));

		json_array_append_new(response->data, packed);
	}

	return response;
}

static struct cmd_response* generate_output_list(struct ctl* self)
{
	struct ctl_server_output* outputs;
	size_t num_outputs = self->actions.get_output_list(self, &outputs);
	struct cmd_response* response = cmd_ok();

	response->data = json_array();
	for (size_t i = 0; i < num_outputs; ++i)
		json_array_append_new(response->data, json_pack(
					"{s:s, s:s, s:i, s:i, s:b, s:s}",
				"name", outputs[i].name,
				"description", outputs[i].description,
				"height", outputs[i].height,
				"width", outputs[i].width,
				"captured", outputs[i].captured,
				"power", outputs[i].power));
	free(outputs);
	return response;
}

static struct cmd_response* ctl_server_dispatch_cmd(struct ctl* self,
		struct ctl_client* client, struct cmd* cmd)
{
	const struct cmd_info* info = ctl_command_by_type(cmd->type);
	assert(info);
	nvnc_log(NVNC_LOG_INFO, "Dispatching control client command '%s'", info->name);
	struct cmd_response* response = NULL;
	switch (cmd->type) {
	case CMD_ATTACH:{
		struct cmd_attach* c = (struct cmd_attach*)cmd;
		response = self->actions.on_attach(self, c->display,
				c->image_source_type, c->image_source_name);
		break;
		}
	case CMD_HELP:{
		struct cmd_help* c = (struct cmd_help*)cmd;
		response = generate_help_object(c->id, c->id_is_command);
		break;
		}
	case CMD_OUTPUT_SET: {
		struct cmd_set_output* c = (struct cmd_set_output*)cmd;
		response = self->actions.on_output_switch(self, c->target);
		break;
		}
	case CMD_CLIENT_DISCONNECT: {
		struct cmd_disconnect_client* c =
			(struct cmd_disconnect_client*)cmd;
		response = self->actions.on_disconnect_client(self, c->id);
		break;
		}
	case CMD_SET_DESKTOP_NAME: {
		struct cmd_set_desktop_name* c =
			(struct cmd_set_desktop_name*)cmd;
		response = self->actions.on_set_desktop_name(self, c->desktop_name);
		break;
		}
	case CMD_DETACH:
		response = self->actions.on_detach(self);
		break;
	case CMD_WAYVNC_EXIT:
		response = self->actions.on_wayvnc_exit(self);
		break;
	case CMD_VERSION:
		response = generate_version_object();
		break;
	case CMD_EVENT_RECEIVE:
		client->accept_events = true;
		response = cmd_ok();
		break;
	case CMD_CLIENT_LIST:
		response = generate_vnc_client_list(self);
		break;
	case CMD_OUTPUT_LIST:
		response = generate_output_list(self);
		break;
	case CMD_OUTPUT_CYCLE:
		response = self->actions.on_output_cycle(self, OUTPUT_CYCLE_FORWARD);
		break;
	case CMD_UNKNOWN:
		break;
	}
	return response;
}

static void client_set_aml_event_mask(struct ctl_client* self)
{
	int mask = AML_EVENT_READ;
	if (json_array_size(self->response_queue) > 0 ||
			self->write_len)
		mask |= AML_EVENT_WRITE;
	aml_set_event_mask(self->handler, mask);
}

static int client_enqueue(struct ctl_client* self, json_t* message,
		enum send_priority priority)
{
	int result;
	switch(priority) {
	case SEND_IMMEDIATE:
		result = json_array_insert(self->response_queue, 0, message);
		break;
	case SEND_FIFO:
		result = json_array_append(self->response_queue, message);
		break;
	}
	client_set_aml_event_mask(self);
	return result;
}

static int client_enqueue_jsonipc(struct ctl_client* self,
		struct jsonipc_response* resp, enum send_priority priority)
{
	int result = 0;
	json_error_t err;
	json_t* packed_response = jsonipc_response_pack(resp, &err);
	if (!packed_response) {
		nvnc_log(NVNC_LOG_WARNING, "Pack failed: %s", err.text);
		result = -1;
		goto failure;
	}
	result = client_enqueue(self, packed_response, priority);
	json_decref(packed_response);
	if (result != 0)
		nvnc_log(NVNC_LOG_WARNING, "Append failed");
failure:
	jsonipc_response_destroy(resp);
	return result;
}

static int client_enqueue_error(struct ctl_client* self,
		struct jsonipc_error* err, json_t* id)
{
	struct jsonipc_response* resp = jsonipc_error_response_new(err, id);
	return client_enqueue_jsonipc(self, resp, SEND_FIFO);
}

static int client_enqueue__response(struct ctl_client* self,
		struct cmd_response* response, json_t* id,
		enum send_priority priority)
{
	nvnc_log(NVNC_LOG_INFO, "Enqueueing response: %s (%d)",
			response->code == 0 ? "OK" : "FAILED", response->code);
	char* str = NULL;
	if (response->data)
		str = json_dumps(response->data, 0);
	nvnc_log(NVNC_LOG_DEBUG, "Response data: %s", str);
	if(str)
		free(str);
	struct jsonipc_response* resp =
		jsonipc_response_new(response->code, response->data, id);
	cmd_response_destroy(response);
	return client_enqueue_jsonipc(self, resp, priority);
}

static int client_enqueue_response(struct ctl_client* self,
		struct cmd_response* response, json_t* id)
{
	return client_enqueue__response(self, response, id, SEND_FIFO);
}

static int client_enqueue_internal_error(struct ctl_client* self,
		struct cmd_response* err)
{
	int result = client_enqueue__response(self, err, NULL, SEND_IMMEDIATE);
	if (result != 0)
		client_destroy(self);
	self->drop_after_next_send = true;
	return result;
}

static void send_ready(struct ctl_client* client)
{
	if (client->write_buffer) {
		nvnc_trace("Continuing partial write (%d left)", client->write_len);
	} else if (json_array_size(client->response_queue) > 0){
		nvnc_trace("Sending new queued message");
		json_t* item = json_array_get(client->response_queue, 0);
		client->write_len = json_dumpb(item, NULL, 0, JSON_COMPACT);
		client->write_buffer = calloc(1, client->write_len);
		client->write_ptr = client->write_buffer;
		json_dumpb(item, client->write_buffer, client->write_len,
				JSON_COMPACT);
		nvnc_log(NVNC_LOG_DEBUG, ">> %.*s", client->write_len, client->write_buffer);
		json_array_remove(client->response_queue, 0);
	} else {
		nvnc_trace("Nothing to send");
	}
	if (!client->write_ptr)
		goto no_data;
	ssize_t n = send(client->fd, client->write_ptr, client->write_len,
			MSG_NOSIGNAL|MSG_DONTWAIT);
	if (n == -1) {
		if (errno == EAGAIN || errno == EWOULDBLOCK) {
			nvnc_trace("send: EAGAIN");
			goto send_eagain;
		}
		nvnc_log(NVNC_LOG_ERROR, "Could not send response: %m");
		client_destroy(client);
		return;
	}
	nvnc_trace("sent %d/%d bytes", n, client->write_len);
	client->write_ptr += n;
	client->write_len -= n;
send_eagain:
	if (client->write_len == 0) {
		nvnc_trace("Write buffer empty!");
		free(client->write_buffer);
		client->write_buffer = NULL;
		client->write_ptr = NULL;
		if (client->drop_after_next_send) {
			nvnc_log(NVNC_LOG_WARNING, "Intentional disconnect");
			client_destroy(client);
			return;
		}
	} else {
		nvnc_trace("Write buffer has %d remaining", client->write_len);
	}
no_data:
	client_set_aml_event_mask(client);
}

static void recv_ready(struct ctl_client* client)
{
	struct ctl* server = client->server;
	struct cmd_response* details = NULL;
	switch (client_read(client, &details)) {
	case 0: // Needs more data
		return;
	case -1: // Fatal error
		if (details)
			client_enqueue_internal_error(client, details);
		else
			client_destroy(client);
		return;
	default: // Read some data; check it
		break;
	}

	json_t* root;
	while (true) {
		root = client_next_object(client, &details);
		if (root == NULL)
			break;

		struct jsonipc_error jipc_err = JSONIPC_ERR_INIT;

		struct jsonipc_request* request =
			jsonipc_request_parse_new(root, &jipc_err);
		if (!request) {
			client_enqueue_error(client, &jipc_err,
					NULL);
			goto request_parse_failed;
		}

		struct cmd* cmd = parse_command(request, &jipc_err);
		if (!cmd) {
			client_enqueue_error(client, &jipc_err,
					request->id);
			goto cmdparse_failed;
		}

		// TODO: Enqueue the command (and request ID) to be
		// handled by the main loop instead of doing the
		// dispatch here
		struct cmd_response* response =
			ctl_server_dispatch_cmd(server, client, cmd);
		if (!response)
			goto no_response;
		client_enqueue_response(client, response, request->id);
no_response:
		free(cmd);
cmdparse_failed:
		jsonipc_request_destroy(request);
request_parse_failed:
		jsonipc_error_cleanup(&jipc_err);
		json_decref(root);
	}
	if (details)
		client_enqueue_internal_error(client, details);
}

static void on_ready(struct aml_handler* handler)
{
	struct ctl_client* client = aml_get_userdata(handler);
	uint32_t events = aml_get_revents(handler);
	nvnc_trace("Client %p ready: 0x%x", client, events);

	if (events & AML_EVENT_WRITE)
		send_ready(client);
	else if (events & AML_EVENT_READ)
		recv_ready(client);
}

static void on_connection(struct aml_handler* handler)
{
	nvnc_log(NVNC_LOG_DEBUG, "New connection");
	struct ctl* server = aml_get_userdata(handler);

	struct ctl_client* client = calloc(1, sizeof(*client));
	if (!client) {
		FAILED_TO("allocate a client object");
		return;
	}

	client->server = server;
	client->response_queue = json_array();

	client->fd = accept(server->fd, NULL, 0);
	if (client->fd < 0) {
		FAILED_TO("accept a connection");
		goto accept_failure;
	}

	client->handler = aml_handler_new(client->fd, on_ready, client, NULL);
	if (!client->handler) {
		FAILED_TO("create a loop handler");
		goto handle_failure;
	}

	if (aml_start(aml_get_default(), client->handler) < 0) {
		FAILED_TO("register for client events");
		goto poll_start_failure;
	}

	wl_list_insert(&server->clients, &client->link);
	nvnc_log(NVNC_LOG_INFO, "New control socket client connected: %p", client);
	return;

poll_start_failure:
	aml_unref(client->handler);
handle_failure:
	close(client->fd);
accept_failure:
	json_decref(client->response_queue);
	free(client);
}

static int cleanup_old_socket(struct ctl* self, struct sockaddr* addr,
		size_t addr_size)
{
	struct stat sb;
	if (stat(self->socket_path, &sb) == -1)
		// Doesn't exist: safe to proceed.
		return 0;

	if (!S_ISSOCK(sb.st_mode)) {
		nvnc_log(NVNC_LOG_ERROR, "Socket path '%s’ exists already and is not a socket.");
		goto manual_intervention;
	}

	int fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (fd == -1) {
		FAILED_TO("open a temporary socket");
		goto manual_intervention;
	}

	nvnc_log(NVNC_LOG_DEBUG, "Connecting to existing socket in case it's stale");
	if (connect(fd, addr, addr_size) == 0) {
		close(fd);
		nvnc_log(NVNC_LOG_ERROR, "Another wayvnc process is already running.");
		nvnc_log(NVNC_LOG_ERROR, "Use the '-S' option to choose an alternate control socket location");
		return -1;
	}
	nvnc_log(NVNC_LOG_DEBUG, "Connect failed: %m");

	close(fd);
	nvnc_log(NVNC_LOG_WARNING, "Deleting stale control socket path \"%s\"", self->socket_path);
	if (unlink(self->socket_path) == -1) {
		FAILED_TO("remove stale unix socket");
		goto manual_intervention;
	}
	return 0;

manual_intervention:
	nvnc_log(NVNC_LOG_ERROR, "Manually remove \"%s\" or use the '-S' option to choose an alternate socket location", self->socket_path);
	return -1;
}


int ctl_server_init(struct ctl* self, const char* socket_path)
{
	if (!socket_path) {
		socket_path = default_ctl_socket_path();
		if (!getenv("XDG_RUNTIME_DIR"))
			nvnc_log(NVNC_LOG_WARNING, "$XDG_RUNTIME_DIR is not set. Falling back to control socket \"%s\"", socket_path);
	}
	strlcpy(self->socket_path, socket_path, sizeof(self->socket_path));
	nvnc_log(NVNC_LOG_DEBUG, "Initializing wayvncctl socket: %s", self->socket_path);

	wl_list_init(&self->clients);

	struct sockaddr_un addr = {
		.sun_family = AF_UNIX,
	};

	if (strlen(self->socket_path) >= sizeof(addr.sun_path)) {
		errno = ENAMETOOLONG;
		FAILED_TO("create unix socket");
		goto socket_failure;
	}
	strcpy(addr.sun_path, self->socket_path);

	self->fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (self->fd < 0) {
		FAILED_TO("create unix socket");
		goto socket_failure;
	}

	if (cleanup_old_socket(self, (struct sockaddr*)&addr, sizeof(addr)) != 0)
		goto bind_failure;

	if (bind(self->fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
		FAILED_TO("bind unix socket");
		goto bind_failure;
	}

	if (listen(self->fd, 16) < 0) {
		FAILED_TO("listen to unix socket");
		goto listen_failure;
	}

	self->handler = aml_handler_new(self->fd, on_connection, self, NULL);
	if (!self->handler) {
		FAILED_TO("create a main loop handler");
		goto handle_failure;
	}

	if (aml_start(aml_get_default(), self->handler) < 0) {
		FAILED_TO("Register for server events");
		goto poll_start_failure;
	}
	return 0;

poll_start_failure:
	aml_unref(self->handler);
handle_failure:
listen_failure:
	unlink(self->socket_path);
bind_failure:
	close(self->fd);
socket_failure:
	return -1;
}

static void ctl_server_stop(struct ctl* self)
{
	aml_stop(aml_get_default(), self->handler);
	aml_unref(self->handler);
	struct ctl_client* client;
	struct ctl_client* tmp;
	wl_list_for_each_safe(client, tmp, &self->clients, link)
		client_destroy(client);
	close(self->fd);
	unlink(self->socket_path);
}

struct ctl* ctl_server_new(const char* socket_path,
		const struct ctl_server_actions* actions)
{
	struct ctl* ctl = calloc(1, sizeof(*ctl));
	memcpy(&ctl->actions, actions, sizeof(*actions));
	if (ctl_server_init(ctl, socket_path) != 0) {
		free(ctl);
		return NULL;
	}
	return ctl;
}

void ctl_server_destroy(struct ctl* self)
{
	ctl_server_stop(self);
	free(self);
}

void* ctl_server_userdata(struct ctl* self)
{
	return self->actions.userdata;
}

struct cmd_response* cmd_ok()
{
	return cmd_response_new(0, NULL);
}

struct cmd_response* cmd_failed(const char* fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	struct cmd_response* resp = cmd_response_new(1, json_pack("{s:o}",
				"error", jvprintf(fmt, ap)));
	va_end(ap);
	return resp;
}

json_t* pack_connection_event_params(
		const struct ctl_server_client_info *info,
		int new_connection_count)
{
	// TODO: Why is the id a string?
	char id_str[64];
	snprintf(id_str, sizeof(id_str), "%d", info->id);

	char address_string[256];
	bool have_addr = sockaddr_to_string(address_string,
			sizeof(address_string), &info->address) == 0;

	return json_pack("{s:s, s:s?, s:s?, s:s?, s:i}",
			"id", id_str,
			"address", have_addr ? address_string : NULL,
			"username", info->username,
			"seat", info->seat,
			"connection_count", new_connection_count);
}

int ctl_server_enqueue_event(struct ctl* self, enum event_type evt_type,
		json_t* params)
{
	const char* event_name = ctl_event_list[evt_type].name;
	char* param_str = json_dumps(params, JSON_COMPACT);
	nvnc_log(NVNC_LOG_DEBUG, "Enqueueing %s event: %s", event_name, param_str);
	free(param_str);
	struct jsonipc_request* event = jsonipc_event_new(event_name, params);
	json_decref(params);
	json_error_t err;
	json_t* packed_event = jsonipc_request_pack(event, &err);
	jsonipc_request_destroy(event);
	if (!packed_event) {
		nvnc_log(NVNC_LOG_WARNING, "Could not pack %s event json: %s", event_name, err.text);
		return -1;
	}

	int enqueued = 0;
	struct ctl_client* client;
	wl_list_for_each(client, &self->clients, link) {
		if (!client->accept_events) {
			nvnc_trace("Skipping event send to control client %p", client);
			continue;
		}
		if (client_enqueue(client, packed_event, false) == 0) {
			nvnc_trace("Enqueued event for control client %p", client);
			enqueued++;
		} else {
			nvnc_trace("Failed to enqueue event for control client %p", client);
		}
	}
	json_decref(packed_event);
	nvnc_log(NVNC_LOG_DEBUG, "Enqueued %s event for %d clients", event_name, enqueued);
	return enqueued;
}

static void ctl_server_event_connect(struct ctl* self,
		enum event_type evt_type,
		const struct ctl_server_client_info *info,
		int new_connection_count)
{
	json_t* params =
		pack_connection_event_params(info, new_connection_count);
	ctl_server_enqueue_event(self, evt_type, params);
}

void ctl_server_event_connected(struct ctl* self,
		const struct ctl_server_client_info *info,
		int new_connection_count)
{
	ctl_server_event_connect(self, EVT_CLIENT_CONNECTED, info,
			new_connection_count);
}

void ctl_server_event_disconnected(struct ctl* self,
		const struct ctl_server_client_info *info,
		int new_connection_count)
{
	ctl_server_event_connect(self, EVT_CLIENT_DISCONNECTED, info,
			new_connection_count);
}

void ctl_server_event_capture_changed(struct ctl* self,
		const char* captured_output)
{
	ctl_server_enqueue_event(self, EVT_CAPTURE_CHANGED,
			json_pack("{s:s}", "output", captured_output));
}

void ctl_server_event_detached(struct ctl* self)
{
	ctl_server_enqueue_event(self, EVT_DETACHED, json_object());
}

void ctl_server_event_output_added(struct ctl* self, const char* name)
{
	ctl_server_enqueue_event(self, EVT_OUTPUT_ADDED,
			json_pack("{s:s}", "name", name));
}

void ctl_server_event_output_removed(struct ctl* self, const char* name)
{
	ctl_server_enqueue_event(self, EVT_OUTPUT_REMOVED,
			json_pack("{s:s}", "name", name));
}
