/*
 * common.c: common functionality
 *
 * Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com>
 *
 * 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.
 *
 */

#define LIBERTY_WANT_SSL
#define LIBERTY_WANT_POLLER
#define LIBERTY_WANT_PROTO_IRC

#ifdef WANT_SYSLOG_LOGGING
	#define print_fatal_data    ((void *) LOG_ERR)
	#define print_error_data    ((void *) LOG_ERR)
	#define print_warning_data  ((void *) LOG_WARNING)
	#define print_status_data   ((void *) LOG_INFO)
	#define print_debug_data    ((void *) LOG_DEBUG)
#endif // WANT_SYSLOG_LOGGING

#include "liberty/liberty.c"
#include <setjmp.h>
#include <inttypes.h>
#include <arpa/inet.h>

/// Shorthand to set an error and return failure from the function
#define FAIL(...)                                                              \
	BLOCK_START                                                                \
		error_set (e, __VA_ARGS__);                                            \
		return false;                                                          \
	BLOCK_END

// A few other debugging shorthands
#define LOG_FUNC_FAILURE(name, desc)                                           \
	print_debug ("%s: %s: %s", __func__, (name), (desc))
#define LOG_LIBC_FAILURE(name)                                                 \
	print_debug ("%s: %s: %s", __func__, (name), strerror (errno))

// --- To be moved to liberty --------------------------------------------------

static void
split_str (const char *s, char delimiter, struct str_vector *out)
{
	const char *begin = s, *end;
	while ((end = strchr (begin, delimiter)))
	{
		str_vector_add_owned (out, xstrndup (begin, end - begin));
		begin = ++end;
	}
	str_vector_add (out, begin);
}

static ssize_t
str_vector_find (const struct str_vector *v, const char *s)
{
	for (size_t i = 0; i < v->len; i++)
		if (!strcmp (v->vector[i], s))
			return i;
	return -1;
}

// --- Logging -----------------------------------------------------------------

static void
log_message_syslog (void *user_data, const char *quote, const char *fmt,
	va_list ap)
{
	int prio = (int) (intptr_t) user_data;

	va_list va;
	va_copy (va, ap);
	int size = vsnprintf (NULL, 0, fmt, va);
	va_end (va);
	if (size < 0)
		return;

	char buf[size + 1];
	if (vsnprintf (buf, sizeof buf, fmt, ap) >= 0)
		syslog (prio, "%s%s", quote, buf);
}

// --- Connector ---------------------------------------------------------------

// This is a helper that tries to establish a connection with any address on
// a given list.  Sadly it also introduces a bit of a callback hell.

struct connector_target
{
	LIST_HEADER (struct connector_target)

	char *hostname;                     ///< Target hostname or address
	char *service;                      ///< Target service name or port

	struct addrinfo *results;           ///< Resolved target
	struct addrinfo *iter;              ///< Current endpoint
};

static struct connector_target *
connector_target_new (void)
{
	struct connector_target *self = xmalloc (sizeof *self);
	return self;
}

static void
connector_target_destroy (struct connector_target *self)
{
	free (self->hostname);
	free (self->service);
	freeaddrinfo (self->results);
	free (self);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct connector
{
	int socket;                         ///< Socket FD for the connection
	struct poller_fd connected_event;   ///< We've connected or failed
	struct connector_target *targets;   ///< Targets
	struct connector_target *targets_t; ///< Tail of targets

	void *user_data;                    ///< User data for callbacks

	// You may destroy the connector object in these two main callbacks:

	/// Connection has been successfully established
	void (*on_connected) (void *user_data, int socket);
	/// Failed to establish a connection to either target
	void (*on_failure) (void *user_data);

	// Optional:

	/// Connecting to a new address
	void (*on_connecting) (void *user_data, const char *address);
	/// Connecting to the last address has failed
	void (*on_error) (void *user_data, const char *error);
};

static void
connector_notify_connecting (struct connector *self,
	struct connector_target *target, struct addrinfo *gai_iter)
{
	if (!self->on_connecting)
		return;

	const char *real_host = target->hostname;

	// We don't really need this, so we can let it quietly fail
	char buf[NI_MAXHOST];
	int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
		buf, sizeof buf, NULL, 0, NI_NUMERICHOST);
	if (err)
		LOG_FUNC_FAILURE ("getnameinfo", gai_strerror (err));
	else
		real_host = buf;

	char *address = format_host_port_pair (real_host, target->service);
	self->on_connecting (self->user_data, address);
	free (address);
}

static void
connector_notify_error (struct connector *self, const char *error)
{
	if (self->on_error)
		self->on_error (self->user_data, error);
}

static void
connector_prepare_next (struct connector *self)
{
	struct connector_target *target = self->targets;
	if (!(target->iter = target->iter->ai_next))
	{
		LIST_UNLINK_WITH_TAIL (self->targets, self->targets_t, target);
		connector_target_destroy (target);
	}
}

static void
connector_step (struct connector *self)
{
	struct connector_target *target = self->targets;
	if (!target)
	{
		// Total failure, none of the targets has succeeded
		self->on_failure (self->user_data);
		return;
	}

	struct addrinfo *gai_iter = target->iter;
	hard_assert (gai_iter != NULL);

	connector_notify_connecting (self, target, gai_iter);

	int fd = self->socket = socket (gai_iter->ai_family,
		gai_iter->ai_socktype, gai_iter->ai_protocol);
	if (fd == -1)
	{
		connector_notify_error (self, strerror (errno));

		connector_prepare_next (self);
		connector_step (self);
		return;
	}

	set_cloexec (fd);
	set_blocking (fd, false);

	int yes = 1;
	soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE,
		&yes, sizeof yes) != -1);

	if (!connect (fd, gai_iter->ai_addr, gai_iter->ai_addrlen))
	{
		set_blocking (fd, true);
		self->on_connected (self->user_data, fd);
		return;
	}
	else if (errno != EINPROGRESS)
	{
		connector_notify_error (self, strerror (errno));
		xclose (fd);

		connector_prepare_next (self);
		connector_step (self);
		return;
	}

	self->connected_event.fd = self->socket = fd;
	poller_fd_set (&self->connected_event, POLLOUT);

	connector_prepare_next (self);
}

static void
connector_on_ready (const struct pollfd *pfd, struct connector *self)
{
	// See http://cr.yp.to/docs/connect.html if this doesn't work.
	// The second connect() method doesn't work with DragonflyBSD.

	int error = 0;
	socklen_t error_len = sizeof error;
	hard_assert (!getsockopt (pfd->fd,
		SOL_SOCKET, SO_ERROR, &error, &error_len));

	if (error)
	{
		connector_notify_error (self, strerror (error));

		poller_fd_reset (&self->connected_event);
		xclose (self->socket);
		self->socket = -1;

		connector_step (self);
	}
	else
	{
		poller_fd_reset (&self->connected_event);
		self->socket = -1;

		set_blocking (pfd->fd, true);
		self->on_connected (self->user_data, pfd->fd);
	}
}

static void
connector_init (struct connector *self, struct poller *poller)
{
	memset (self, 0, sizeof *self);
	self->socket = -1;
	poller_fd_init (&self->connected_event, poller, self->socket);
	self->connected_event.user_data = self;
	self->connected_event.dispatcher = (poller_fd_fn) connector_on_ready;
}

static void
connector_free (struct connector *self)
{
	poller_fd_reset (&self->connected_event);
	if (self->socket != -1)
		xclose (self->socket);

	LIST_FOR_EACH (struct connector_target, iter, self->targets)
		connector_target_destroy (iter);
}

static bool
connector_add_target (struct connector *self,
	const char *hostname, const char *service, struct error **e)
{
	struct addrinfo hints, *results;
	memset (&hints, 0, sizeof hints);
	hints.ai_socktype = SOCK_STREAM;

	// TODO: even this should be done asynchronously, most likely in
	//   a thread pool, similarly to how libuv does it
	int err = getaddrinfo (hostname, service, &hints, &results);
	if (err)
	{
		error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err));
		return false;
	}

	struct connector_target *target = connector_target_new ();
	target->hostname = xstrdup (hostname);
	target->service = xstrdup (service);
	target->results = results;
	target->iter = target->results;

	LIST_APPEND_WITH_TAIL (self->targets, self->targets_t, target);
	return true;
}

// --- SOCKS 5/4a (blocking implementation) ------------------------------------

// These are awkward protocols.  Note that the `username' is used differently
// in SOCKS 4a and 5.  In the former version, it is the username that you can
// get ident'ed against.  In the latter version, it forms a pair with the
// password field and doesn't need to be an actual user on your machine.

// TODO: make a non-blocking poller-based version of this;
//   either use c-ares or (even better) start another thread to do resolution

struct socks_addr
{
	enum socks_addr_type
	{
		SOCKS_IPV4 = 1,                 ///< IPv4 address
		SOCKS_DOMAIN = 3,               ///< Domain name to be resolved
		SOCKS_IPV6 = 4                  ///< IPv6 address
	}
	type;                               ///< The type of this address
	union
	{
		uint8_t ipv4[4];                ///< IPv4 address, network octet order
		const char *domain;             ///< Domain name
		uint8_t ipv6[16];               ///< IPv6 address, network octet order
	}
	data;                               ///< The address itself
};

struct socks_data
{
	struct socks_addr address;          ///< Target address
	uint16_t port;                      ///< Target port
	const char *username;               ///< Authentication username
	const char *password;               ///< Authentication password

	struct socks_addr bound_address;    ///< Bound address at the server
	uint16_t bound_port;                ///< Bound port at the server
};

static bool
socks_get_socket (struct addrinfo *addresses, int *fd, struct error **e)
{
	int sockfd;
	for (; addresses; addresses = addresses->ai_next)
	{
		sockfd = socket (addresses->ai_family,
			addresses->ai_socktype, addresses->ai_protocol);
		if (sockfd == -1)
			continue;
		set_cloexec (sockfd);

		int yes = 1;
		soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE,
			&yes, sizeof yes) != -1);

		if (!connect (sockfd, addresses->ai_addr, addresses->ai_addrlen))
			break;
		xclose (sockfd);
	}
	if (!addresses)
	{
		error_set (e, "couldn't connect to the SOCKS server");
		return false;
	}
	*fd = sockfd;
	return true;
}

#define SOCKS_FAIL(...)                                                        \
	BLOCK_START                                                                \
		error_set (e, __VA_ARGS__);                                            \
		goto fail;                                                             \
	BLOCK_END
#define SOCKS_RECV(buf, len)                                                   \
	BLOCK_START                                                                \
		if ((n = recv (sockfd, (buf), (len), 0)) == -1)                        \
			SOCKS_FAIL ("%s: %s", "recv", strerror (errno));                   \
		if (n != (len))                                                        \
			SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF");         \
	BLOCK_END

static bool
socks_4a_connect (struct addrinfo *addresses, struct socks_data *data,
	int *fd, struct error **e)
{
	int sockfd;
	if (!socks_get_socket (addresses, &sockfd, e))
		return false;

	const void *dest_ipv4 = "\x00\x00\x00\x01";
	const char *dest_domain = NULL;

	char buf[INET6_ADDRSTRLEN];
	switch (data->address.type)
	{
	case SOCKS_IPV4:
		dest_ipv4 = data->address.data.ipv4;
		break;
	case SOCKS_IPV6:
		// About the best thing we can do, not sure if it works anywhere at all
		if (!inet_ntop (AF_INET6, &data->address.data.ipv6, buf, sizeof buf))
			SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno));
		dest_domain = buf;
		break;
	case SOCKS_DOMAIN:
		dest_domain = data->address.data.domain;
	}

	struct str req;
	str_init (&req);
	str_append_c (&req, 4);                // version
	str_append_c (&req, 1);                // connect

	str_append_c (&req, data->port >> 8);  // higher bits of port
	str_append_c (&req, data->port);       // lower bits of port
	str_append_data (&req, dest_ipv4, 4);  // destination address

	if (data->username)
		str_append (&req, data->username);
	str_append_c (&req, '\0');

	if (dest_domain)
	{
		str_append (&req, dest_domain);
		str_append_c (&req, '\0');
	}

	ssize_t n = send (sockfd, req.str, req.len, 0);
	str_free (&req);
	if (n == -1)
		SOCKS_FAIL ("%s: %s", "send", strerror (errno));

	uint8_t resp[8];
	SOCKS_RECV (resp, sizeof resp);
	if (resp[0] != 0)
		SOCKS_FAIL ("protocol error");

	switch (resp[1])
	{
	case 90:
		break;
	case 91:
		SOCKS_FAIL ("request rejected or failed");
	case 92:
		SOCKS_FAIL ("%s: %s", "request rejected",
			"SOCKS server cannot connect to identd on the client");
	case 93:
		SOCKS_FAIL ("%s: %s", "request rejected",
			"identd reports different user-id");
	default:
		SOCKS_FAIL ("protocol error");
	}

	*fd = sockfd;
	return true;

fail:
	xclose (sockfd);
	return false;
}

#undef SOCKS_FAIL
#define SOCKS_FAIL(...)                                                        \
	BLOCK_START                                                                \
		error_set (e, __VA_ARGS__);                                            \
		return false;                                                          \
	BLOCK_END

static bool
socks_5_userpass_auth (int sockfd, struct socks_data *data, struct error **e)
{
	size_t ulen = strlen (data->username);
	if (ulen > 255)
		ulen = 255;

	size_t plen = strlen (data->password);
	if (plen > 255)
		plen = 255;

	uint8_t req[3 + ulen + plen], *p = req;
	*p++ = 0x01;  // version
	*p++ = ulen;  // username length
	memcpy (p, data->username, ulen);
	p += ulen;
	*p++ = plen;  // password length
	memcpy (p, data->password, plen);
	p += plen;

	ssize_t n = send (sockfd, req, p - req, 0);
	if (n == -1)
		SOCKS_FAIL ("%s: %s", "send", strerror (errno));

	uint8_t resp[2];
	SOCKS_RECV (resp, sizeof resp);
	if (resp[0] != 0x01)
		SOCKS_FAIL ("protocol error");
	if (resp[1] != 0x00)
		SOCKS_FAIL ("authentication failure");
	return true;
}

static bool
socks_5_auth (int sockfd, struct socks_data *data, struct error **e)
{
	bool can_auth = data->username && data->password;

	uint8_t hello[4];
	hello[0] = 0x05;          // version
	hello[1] = 1 + can_auth;  // number of authentication methods
	hello[2] = 0x00;          // no authentication required
	hello[3] = 0x02;          // username/password

	ssize_t n = send (sockfd, hello, 3 + can_auth, 0);
	if (n == -1)
		SOCKS_FAIL ("%s: %s", "send", strerror (errno));

	uint8_t resp[2];
	SOCKS_RECV (resp, sizeof resp);
	if (resp[0] != 0x05)
		SOCKS_FAIL ("protocol error");

	switch (resp[1])
	{
	case 0x02:
		if (!can_auth)
			SOCKS_FAIL ("protocol error");
		if (!socks_5_userpass_auth (sockfd, data, e))
			return false;
	case 0x00:
		break;
	case 0xFF:
		SOCKS_FAIL ("no acceptable authentication methods");
	default:
		SOCKS_FAIL ("protocol error");
	}
	return true;
}

static bool
socks_5_send_req (int sockfd, struct socks_data *data, struct error **e)
{
	uint8_t req[4 + 256 + 2], *p = req;
	*p++ = 0x05;  // version
	*p++ = 0x01;  // connect
	*p++ = 0x00;  // reserved
	*p++ = data->address.type;

	switch (data->address.type)
	{
	case SOCKS_IPV4:
		memcpy (p, data->address.data.ipv4, sizeof data->address.data.ipv4);
		p += sizeof data->address.data.ipv4;
		break;
	case SOCKS_DOMAIN:
	{
		size_t dlen = strlen (data->address.data.domain);
		if (dlen > 255)
			dlen = 255;

		*p++ = dlen;
		memcpy (p, data->address.data.domain, dlen);
		p += dlen;
		break;
	}
	case SOCKS_IPV6:
		memcpy (p, data->address.data.ipv6, sizeof data->address.data.ipv6);
		p += sizeof data->address.data.ipv6;
		break;
	}
	*p++ = data->port >> 8;
	*p++ = data->port;

	if (send (sockfd, req, p - req, 0) == -1)
		SOCKS_FAIL ("%s: %s", "send", strerror (errno));
	return true;
}

static bool
socks_5_process_resp (int sockfd, struct socks_data *data, struct error **e)
{
	uint8_t resp_header[4];
	ssize_t n;
	SOCKS_RECV (resp_header, sizeof resp_header);
	if (resp_header[0] != 0x05)
		SOCKS_FAIL ("protocol error");

	switch (resp_header[1])
	{
	case 0x00:
		break;
	case 0x01:  SOCKS_FAIL ("general SOCKS server failure");
	case 0x02:  SOCKS_FAIL ("connection not allowed by ruleset");
	case 0x03:  SOCKS_FAIL ("network unreachable");
	case 0x04:  SOCKS_FAIL ("host unreachable");
	case 0x05:  SOCKS_FAIL ("connection refused");
	case 0x06:  SOCKS_FAIL ("TTL expired");
	case 0x07:  SOCKS_FAIL ("command not supported");
	case 0x08:  SOCKS_FAIL ("address type not supported");
	default:    SOCKS_FAIL ("protocol error");
	}

	switch ((data->bound_address.type = resp_header[3]))
	{
	case SOCKS_IPV4:
		SOCKS_RECV (data->bound_address.data.ipv4,
			sizeof data->bound_address.data.ipv4);
		break;
	case SOCKS_IPV6:
		SOCKS_RECV (data->bound_address.data.ipv6,
			sizeof data->bound_address.data.ipv6);
		break;
	case SOCKS_DOMAIN:
	{
		uint8_t len;
		SOCKS_RECV (&len, sizeof len);

		char domain[len + 1];
		SOCKS_RECV (domain, len);
		domain[len] = '\0';

		data->bound_address.data.domain = xstrdup (domain);
		break;
	}
	default:
		SOCKS_FAIL ("protocol error");
	}

	uint16_t port;
	SOCKS_RECV (&port, sizeof port);
	data->bound_port = ntohs (port);
	return true;
}

#undef SOCKS_FAIL
#undef SOCKS_RECV

static bool
socks_5_connect (struct addrinfo *addresses, struct socks_data *data,
	int *fd, struct error **e)
{
	int sockfd;
	if (!socks_get_socket (addresses, &sockfd, e))
		return false;

	if (!socks_5_auth (sockfd, data, e)
	 || !socks_5_send_req (sockfd, data, e)
	 || !socks_5_process_resp (sockfd, data, e))
	{
		xclose (sockfd);
		return false;
	}

	*fd = sockfd;
	return true;
}

static int
socks_connect (const char *socks_host, const char *socks_port,
	const char *host, const char *port,
	const char *username, const char *password, struct error **e)
{
	int result = -1;
	struct addrinfo gai_hints, *gai_result;
	memset (&gai_hints, 0, sizeof gai_hints);
	gai_hints.ai_socktype = SOCK_STREAM;

	unsigned long port_no;
	const struct servent *serv;
	if ((serv = getservbyname (port, "tcp")))
		port_no = (uint16_t) ntohs (serv->s_port);
	else if (!xstrtoul (&port_no, port, 10) || !port_no || port_no > UINT16_MAX)
	{
		error_set (e, "invalid port number");
		goto fail;
	}

	int err = getaddrinfo (socks_host, socks_port, &gai_hints, &gai_result);
	if (err)
	{
		error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err));
		goto fail;
	}

	struct socks_data data =
		{ .username = username, .password = password, .port = port_no };

	if      (inet_pton (AF_INET,  host, &data.address.data.ipv4) == 1)
		data.address.type = SOCKS_IPV4;
	else if (inet_pton (AF_INET6, host, &data.address.data.ipv6) == 1)
		data.address.type = SOCKS_IPV6;
	else
	{
		data.address.type = SOCKS_DOMAIN;
		data.address.data.domain = host;
	}

	if (!socks_5_connect (gai_result, &data, &result, NULL))
		socks_4a_connect (gai_result, &data, &result, e);

	if (data.bound_address.type == SOCKS_DOMAIN)
		free ((char *) data.bound_address.data.domain);
	freeaddrinfo (gai_result);
fail:
	return result;
}

// --- CTCP decoding -----------------------------------------------------------

#define CTCP_M_QUOTE '\020'
#define CTCP_X_DELIM '\001'
#define CTCP_X_QUOTE '\\'

struct ctcp_chunk
{
	LIST_HEADER (struct ctcp_chunk)

	bool is_extended;                   ///< Is this a tagged extended message?
	struct str tag;                     ///< The tag, if any
	struct str text;                    ///< Message contents
};

static struct ctcp_chunk *
ctcp_chunk_new (void)
{
	struct ctcp_chunk *self = xcalloc (1, sizeof *self);
	str_init (&self->tag);
	str_init (&self->text);
	return self;
}

static void
ctcp_chunk_destroy (struct ctcp_chunk *self)
{
	str_free (&self->tag);
	str_free (&self->text);
	free (self);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
ctcp_low_level_decode (const char *message, struct str *output)
{
	bool escape = false;
	for (const char *p = message; *p; p++)
	{
		if (escape)
		{
			switch (*p)
			{
			case '0': str_append_c (output, '\0'); break;
			case 'r': str_append_c (output, '\r'); break;
			case 'n': str_append_c (output, '\n'); break;
			default:  str_append_c (output, *p);
			}
			escape = false;
		}
		else if (*p == CTCP_M_QUOTE)
			escape = true;
		else
			str_append_c (output, *p);
	}
}

static void
ctcp_intra_decode (const char *chunk, size_t len, struct str *output)
{
	bool escape = false;
	for (size_t i = 0; i < len; i++)
	{
		char c = chunk[i];
		if (escape)
		{
			if (c == 'a')
				str_append_c (output, CTCP_X_DELIM);
			else
				str_append_c (output, c);
			escape = false;
		}
		else if (c == CTCP_X_QUOTE)
			escape = true;
		else
			str_append_c (output, c);
	}
}

static void
ctcp_parse_tagged (const char *chunk, size_t len, struct ctcp_chunk *output)
{
	// We may search for the space before doing the higher level decoding,
	// as it doesn't concern space characters at all
	size_t tag_end = len;
	for (size_t i = 0; i < len; i++)
		if (chunk[i] == ' ')
		{
			tag_end = i;
			break;
		}

	output->is_extended = true;
	ctcp_intra_decode (chunk, tag_end, &output->tag);
	if (tag_end++ != len)
		ctcp_intra_decode (chunk + tag_end, len - tag_end, &output->text);
}

static struct ctcp_chunk *
ctcp_parse (const char *message)
{
	struct str m;
	str_init (&m);
	ctcp_low_level_decode (message, &m);

	struct ctcp_chunk *result = NULL, *result_tail = NULL;

	size_t start = 0;
	bool in_ctcp = false;
	for (size_t i = 0; i < m.len; i++)
	{
		char c = m.str[i];
		if (c != CTCP_X_DELIM)
			continue;

		// Remember the current state
		size_t my_start = start;
		bool my_is_ctcp = in_ctcp;

		start = i + 1;
		in_ctcp = !in_ctcp;

		// Skip empty chunks
		if (my_start == i)
			continue;

		struct ctcp_chunk *chunk = ctcp_chunk_new ();
		if (my_is_ctcp)
			ctcp_parse_tagged (m.str + my_start, i - my_start, chunk);
		else
			str_append_data (&chunk->text, m.str + my_start, i - my_start);
		LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
	}

	// Finish the last text part.  We ignore unended tagged chunks.
	// TODO: don't ignore them, e.g. a /me may get cut off
	if (!in_ctcp && start != m.len)
	{
		struct ctcp_chunk *chunk = ctcp_chunk_new ();
		// According to the original CTCP specification we should use
		// ctcp_intra_decode() but no one seems to use that and it breaks
		// normal text with backslashes
		str_append_data (&chunk->text, m.str + start, m.len - start);
		LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
	}

	str_free (&m);
	return result;
}

static void
ctcp_destroy (struct ctcp_chunk *list)
{
	LIST_FOR_EACH (struct ctcp_chunk, iter, list)
		ctcp_chunk_destroy (iter);
}

// --- Advanced configuration --------------------------------------------------

// This is a new configuration format, superseding the one currently present
// in liberty.  It's just a lot more complicated and allows key-value maps.
// We need it in degesch to provide non-sucking user experience.

enum config_item_type
{
	CONFIG_ITEM_NULL,                   ///< No value
	CONFIG_ITEM_OBJECT,                 ///< Key-value map
	CONFIG_ITEM_BOOLEAN,                ///< Truth value
	CONFIG_ITEM_INTEGER,                ///< Integer
	CONFIG_ITEM_STRING,                 ///< Arbitrary string of characters
	CONFIG_ITEM_STRING_ARRAY            ///< Comma-separated list of strings
};

struct config_item_
{
	enum config_item_type type;         ///< Type of the item
	union
	{
		struct str_map object;          ///< Key-value data
		bool boolean;                   ///< Boolean data
		int64_t integer;                ///< Integer data
		struct str string;              ///< String data
	}
	value;                              ///< The value of this item

	struct config_schema *schema;       ///< Schema describing this value
	void *user_data;                    ///< User value attached by schema owner
};

struct config_schema
{
	const char *name;                   ///< Name of the item
	const char *comment;                ///< User-readable description

	enum config_item_type type;         ///< Required type
	const char *default_;               ///< Default as a configuration snippet

	/// Check if the new value can be accepted.
	/// In addition to this, "type" and having a default is considered.
	bool (*validate) (const struct config_item_ *, struct error **e);

	/// The value has changed
	void (*on_change) (struct config_item_ *);
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static const char *
config_item_type_name (enum config_item_type type)
{
	switch (type)
	{
	case CONFIG_ITEM_NULL:          return "null";
	case CONFIG_ITEM_BOOLEAN:       return "boolean";
	case CONFIG_ITEM_INTEGER:       return "integer";
	case CONFIG_ITEM_STRING:        return "string";
	case CONFIG_ITEM_STRING_ARRAY:  return "string array";

	default:
		hard_assert (!"invalid config item type value");
		return NULL;
	}
}

static bool
config_item_type_is_string (enum config_item_type type)
{
	return type == CONFIG_ITEM_STRING
		|| type == CONFIG_ITEM_STRING_ARRAY;
}

static void
config_item_free (struct config_item_ *self)
{
	switch (self->type)
	{
	case CONFIG_ITEM_STRING:
	case CONFIG_ITEM_STRING_ARRAY:
		str_free (&self->value.string);
		break;
	case CONFIG_ITEM_OBJECT:
		str_map_free (&self->value.object);
	default:
		break;
	}
}

static void
config_item_destroy (struct config_item_ *self)
{
	config_item_free (self);
	free (self);
}

/// Doesn't do any validations or handle schemas, just moves source data
/// to the target item and destroys the source item
static void
config_item_move (struct config_item_ *self, struct config_item_ *source)
{
	// Not quite sure how to handle that
	hard_assert (!source->schema);

	config_item_free (self);
	self->type = source->type;
	memcpy (&self->value, &source->value, sizeof source->value);
	free (source);
}

static struct config_item_ *
config_item_new (enum config_item_type type)
{
	struct config_item_ *self = xcalloc (1, sizeof *self);
	self->type = type;
	return self;
}

static struct config_item_ *
config_item_null (void)
{
	return config_item_new (CONFIG_ITEM_NULL);
}

static struct config_item_ *
config_item_boolean (bool b)
{
	struct config_item_ *self = config_item_new (CONFIG_ITEM_BOOLEAN);
	self->value.boolean = b;
	return self;
}

static struct config_item_ *
config_item_integer (int64_t i)
{
	struct config_item_ *self = config_item_new (CONFIG_ITEM_INTEGER);
	self->value.integer = i;
	return self;
}

static struct config_item_ *
config_item_string (const struct str *s)
{
	struct config_item_ *self = config_item_new (CONFIG_ITEM_STRING);
	str_init (&self->value.string);
	hard_assert (utf8_validate
		(self->value.string.str, self->value.string.len));
	if (s) str_append_str (&self->value.string, s);
	return self;
}

static struct config_item_ *
config_item_string_from_cstr (const char *s)
{
	struct str tmp;
	str_init (&tmp);
	str_append (&tmp, s);
	struct config_item_ *self = config_item_string (&tmp);
	str_free (&tmp);
	return self;
}

static struct config_item_ *
config_item_string_array (const struct str *s)
{
	struct config_item_ *self = config_item_string (s);
	self->type = CONFIG_ITEM_STRING_ARRAY;
	return self;
}

static struct config_item_ *
config_item_object (void)
{
	struct config_item_ *self = config_item_new (CONFIG_ITEM_OBJECT);
	str_map_init (&self->value.object);
	self->value.object.free = (void (*)(void *)) config_item_destroy;
	return self;
}

static bool
config_schema_accepts_type
	(struct config_schema *self, enum config_item_type type)
{
	if (self->type == type)
		return true;
	// This is a bit messy but it has its purpose
	if (config_item_type_is_string (self->type)
	 && config_item_type_is_string (type))
		return true;
	return !self->default_ && type == CONFIG_ITEM_NULL;
}

static bool
config_item_validate_by_schema (struct config_item_ *self,
	struct config_schema *schema, struct error **e)
{
	struct error *error = NULL;
	if (!config_schema_accepts_type (schema, self->type))
		error_set (e, "invalid type of value, expected: %s%s",
			config_item_type_name (schema->type),
			!schema->default_ ? " (or null)" : "");
	else if (schema->validate && !schema->validate (self, &error))
	{
		error_set (e, "%s: %s", "invalid value", error->message);
		error_free (error);
	}
	else
		return true;
	return false;
}

static bool
config_item_set_from (struct config_item_ *self, struct config_item_ *source,
	struct error **e)
{
	struct config_schema *schema = self->schema;
	if (!schema)
	{
		// Easy, we don't know what this item is
		config_item_move (self, source);
		return true;
	}

	if (!config_item_validate_by_schema (source, schema, e))
		return false;

	// Make sure the string subtype fits the schema
	if (config_item_type_is_string (source->type)
	 && config_item_type_is_string (schema->type))
		source->type = schema->type;

	config_item_move (self, source);

	// Notify owner about the change so that they can apply it
	if (schema->on_change)
		schema->on_change (self);
	return true;
}

static struct config_item_ *
config_item_get (struct config_item_ *self, const char *path, struct error **e)
{
	hard_assert (self->type == CONFIG_ITEM_OBJECT);

	struct str_vector v;
	str_vector_init (&v);
	split_str (path, '.', &v);

	struct config_item_ *result = NULL;
	size_t i = 0;
	while (true)
	{
		const char *key = v.vector[i];
		if (!*key)
			error_set (e, "empty path element");
		else if (!(self = str_map_find (&self->value.object, key)))
			error_set (e, "`%s' not found in object", key);
		else if (++i == v.len)
			result = self;
		else if (self->type != CONFIG_ITEM_OBJECT)
			error_set (e, "`%s' is not an object", key);
		else
			continue;
		break;
	}
	str_vector_free (&v);
	return result;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct config_writer
{
	struct str *output;
	unsigned indent;
};

static void config_item_write_object_innards
	(struct config_writer *self, struct config_item_ *object);

static void
config_item_write_string (struct str *output, const struct str *s)
{
	str_append_c (output, '"');
	for (size_t i = 0; i < s->len; i++)
	{
		unsigned char c = s->str[i];
		if      (c == '\n')  str_append        (output, "\\n");
		else if (c == '\r')  str_append        (output, "\\r");
		else if (c == '\t')  str_append        (output, "\\t");
		else if (c == '\\')  str_append        (output, "\\\\");
		else if (c == '"')   str_append        (output, "\\\"");
		else if (c < 32)     str_append_printf (output, "\\x%02x", c);
		else                 str_append_c      (output, c);
	}
	str_append_c (output, '"');
}

static void
config_item_write_object
	(struct config_writer *self, struct config_item_ *value)
{
	char indent[self->indent + 1];
	memset (indent, '\t', self->indent);
	indent[self->indent] = 0;

	str_append_c (self->output, '{');
	if (value->value.object.len)
	{
		self->indent++;
		str_append_c (self->output, '\n');
		config_item_write_object_innards (self, value);
		self->indent--;
		str_append (self->output, indent);
	}
	str_append_c (self->output, '}');
}

static void
config_item_write_value (struct config_writer *self, struct config_item_ *value)
{
	switch (value->type)
	{
	case CONFIG_ITEM_NULL:
		str_append (self->output, "null");
		break;
	case CONFIG_ITEM_BOOLEAN:
		str_append (self->output, value->value.boolean ? "on" : "off");
		break;
	case CONFIG_ITEM_INTEGER:
		str_append_printf (self->output, "%" PRIi64, value->value.integer);
		break;
	case CONFIG_ITEM_STRING:
	case CONFIG_ITEM_STRING_ARRAY:
		config_item_write_string (self->output, &value->value.string);
		break;
	case CONFIG_ITEM_OBJECT:
		config_item_write_object (self, value);
		break;
	default:
		hard_assert (!"invalid item type");
	}
}

static void
config_item_write_kv_pair (struct config_writer *self,
	const char *key, struct config_item_ *value)
{
	char indent[self->indent + 1];
	memset (indent, '\t', self->indent);
	indent[self->indent] = 0;

	if (value->schema && value->schema->comment)
		str_append_printf (self->output,
			"%s# %s\n", indent, value->schema->comment);

	str_append_printf (self->output, "%s%s = ", indent, key);
	config_item_write_value (self, value);
	str_append_c (self->output, '\n');
}

static void
config_item_write_object_innards
	(struct config_writer *self, struct config_item_ *object)
{
	hard_assert (object->type == CONFIG_ITEM_OBJECT);

	struct str_map_iter iter;
	str_map_iter_init (&iter, &object->value.object);

	struct config_item_ *value;
	while ((value = str_map_iter_next (&iter)))
		config_item_write_kv_pair (self, iter.link->key, value);
}

static void
config_item_write (struct config_item_ *value,
	bool object_innards, struct str *output)
{
	struct config_writer writer = { .output = output, .indent = 0 };
	if (object_innards)
		config_item_write_object_innards (&writer, value);
	else
		config_item_write_value (&writer, value);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

enum config_token
{
	CONFIG_T_ABORT,                     ///< EOF or error

	CONFIG_T_WORD,                      ///< [a-zA-Z0-9_]+
	CONFIG_T_EQUALS,                    ///< Equal sign
	CONFIG_T_LBRACE,                    ///< Left curly bracket
	CONFIG_T_RBRACE,                    ///< Right curly bracket
	CONFIG_T_NEWLINE,                   ///< New line

	CONFIG_T_NULL,                      ///< CONFIG_ITEM_NULL
	CONFIG_T_BOOLEAN,                   ///< CONFIG_ITEM_BOOLEAN
	CONFIG_T_INTEGER,                   ///< CONFIG_ITEM_INTEGER
	CONFIG_T_STRING                     ///< CONFIG_ITEM_STRING{,_LIST}
};

static const char *
config_token_name (enum config_token token)
{
	switch (token)
	{
	case CONFIG_T_ABORT:    return "end of input";

	case CONFIG_T_WORD:     return "word";
	case CONFIG_T_EQUALS:   return "equal sign";
	case CONFIG_T_LBRACE:   return "left brace";
	case CONFIG_T_RBRACE:   return "right brace";
	case CONFIG_T_NEWLINE:  return "newline";

	case CONFIG_T_NULL:     return "null value";
	case CONFIG_T_BOOLEAN:  return "boolean";
	case CONFIG_T_INTEGER:  return "integer";
	case CONFIG_T_STRING:   return "string";

	default:
		hard_assert (!"invalid token value");
		return NULL;
	}
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct config_tokenizer
{
	const char *p;                      ///< Current position in input
	size_t len;                         ///< How many bytes of input are left

	bool report_line;                   ///< Whether to count lines at all
	unsigned line;                      ///< Current line
	unsigned column;                    ///< Current column

	int64_t integer;                    ///< Parsed boolean or integer value
	struct str string;                  ///< Parsed string value
};

/// Input has to be null-terminated anyway
static void
config_tokenizer_init (struct config_tokenizer *self, const char *p, size_t len)
{
	memset (self, 0, sizeof *self);
	self->p = p;
	self->len = len;
	self->report_line = true;
	str_init (&self->string);
}

static void
config_tokenizer_free (struct config_tokenizer *self)
{
	str_free (&self->string);
}

static bool
config_tokenizer_is_word_char (int c)
{
	return isalnum_ascii (c) || c == '_';
}

static int
config_tokenizer_advance (struct config_tokenizer *self)
{
	int c = *self->p++;
	if (c == '\n' && self->report_line)
	{
		self->column = 0;
		self->line++;
	}
	else
		self->column++;

	self->len--;
	return c;
}

static void config_tokenizer_error (struct config_tokenizer *self,
	struct error **e, const char *format, ...) ATTRIBUTE_PRINTF (3, 4);

static void
config_tokenizer_error (struct config_tokenizer *self,
	struct error **e, const char *format, ...)
{
	struct str description;
	str_init (&description);

	va_list ap;
	va_start (ap, format);
	str_append_vprintf (&description, format, ap);
	va_end (ap);

	if (self->report_line)
		error_set (e, "near line %u, column %u: %s",
			self->line + 1, self->column + 1, description.str);
	else if (self->len)
		error_set (e, "near character %u: %s",
			self->column + 1, description.str);
	else
		error_set (e, "near end: %s", description.str);

	str_free (&description);
}

static bool
config_tokenizer_hexa_escape (struct config_tokenizer *self, struct str *output)
{
	int i;
	unsigned char code = 0;

	for (i = 0; self->len && i < 2; i++)
	{
		unsigned char c = tolower_ascii (*self->p);
		if (c >= '0' && c <= '9')
			code = (code << 4) | (c - '0');
		else if (c >= 'a' && c <= 'f')
			code = (code << 4) | (c - 'a' + 10);
		else
			break;

		config_tokenizer_advance (self);
	}

	if (!i)
		return false;

	str_append_c (output, code);
	return true;
}

static bool
config_tokenizer_octal_escape
	(struct config_tokenizer *self, struct str *output)
{
	int i;
	unsigned char code = 0;

	for (i = 0; self->len && i < 3; i++)
	{
		unsigned char c = *self->p;
		if (c >= '0' && c <= '7')
			code = (code << 3) | (c - '0');
		else
			break;

		config_tokenizer_advance (self);
	}

	if (!i)
		return false;

	str_append_c (output, code);
	return true;
}

static bool
config_tokenizer_escape_sequence
	(struct config_tokenizer *self, struct str *output, struct error **e)
{
	if (!self->len)
	{
		config_tokenizer_error (self, e, "premature end of escape sequence");
		return false;
	}

	unsigned char c;
	switch ((c = *self->p))
	{
	case '"':              break;
	case '\\':             break;
	case 'a':   c = '\a';  break;
	case 'b':   c = '\b';  break;
	case 'f':   c = '\f';  break;
	case 'n':   c = '\n';  break;
	case 'r':   c = '\r';  break;
	case 't':   c = '\t';  break;
	case 'v':   c = '\v';  break;

	case 'x':
	case 'X':
		config_tokenizer_advance (self);
		if (config_tokenizer_hexa_escape (self, output))
			return true;

		config_tokenizer_error (self, e, "invalid hexadecimal escape");
		return false;

	default:
		if (config_tokenizer_octal_escape (self, output))
			return true;

		config_tokenizer_error (self, e, "unknown escape sequence");
		return false;
	}

	str_append_c (output, c);
	config_tokenizer_advance (self);
	return true;
}

static bool
config_tokenizer_string
	(struct config_tokenizer *self, struct str *output, struct error **e)
{
	unsigned char c;
	while (self->len)
	{
		if ((c = config_tokenizer_advance (self)) == '"')
			return true;
		if (c != '\\')
			str_append_c (output, c);
		else if (!config_tokenizer_escape_sequence (self, output, e))
			return false;
	}
	config_tokenizer_error (self, e, "premature end of string");
	return false;
}

static enum config_token
config_tokenizer_next (struct config_tokenizer *self, struct error **e)
{
	// Skip over any whitespace between tokens
	while (self->len && isspace_ascii (*self->p) && *self->p != '\n')
		config_tokenizer_advance (self);
	if (!self->len)
		return CONFIG_T_ABORT;

	switch (*self->p)
	{
	case '\n':  config_tokenizer_advance (self);  return CONFIG_T_NEWLINE;
	case '=':   config_tokenizer_advance (self);  return CONFIG_T_EQUALS;
	case '{':   config_tokenizer_advance (self);  return CONFIG_T_LBRACE;
	case '}':   config_tokenizer_advance (self);  return CONFIG_T_RBRACE;

	case '#':
		// Comments go until newline
		while (self->len)
			if (config_tokenizer_advance (self) == '\n')
				return CONFIG_T_NEWLINE;
		return CONFIG_T_ABORT;

	case '"':
		config_tokenizer_advance (self);
		str_reset (&self->string);
		if (!config_tokenizer_string (self, &self->string, e))
			return CONFIG_T_ABORT;
		if (!utf8_validate (self->string.str, self->string.len))
		{
			config_tokenizer_error (self, e, "not a valid UTF-8 string");
			return CONFIG_T_ABORT;
		}
		return CONFIG_T_STRING;
	}

	char *end;
	errno = 0;
	self->integer = strtoll (self->p, &end, 10);
	if (errno == ERANGE)
	{
		config_tokenizer_error (self, e, "integer out of range");
		return CONFIG_T_ABORT;
	}
	if (end != self->p)
	{
		self->len -= end - self->p;
		self->p = end;
		return CONFIG_T_INTEGER;
	}

	if (!config_tokenizer_is_word_char (*self->p))
	{
		config_tokenizer_error (self, e, "invalid input");
		return CONFIG_T_ABORT;
	}

	str_reset (&self->string);
	do
		str_append_c (&self->string, config_tokenizer_advance (self));
	while (config_tokenizer_is_word_char (*self->p));

	if (!strcmp (self->string.str, "null"))
		return CONFIG_T_NULL;

	bool boolean;
	if (!set_boolean_if_valid (&boolean, self->string.str))
		return CONFIG_T_WORD;

	self->integer = boolean;
	return CONFIG_T_BOOLEAN;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct config_parser
{
	struct config_tokenizer tokenizer;  ///< Tokenizer

	struct error *error;                ///< Tokenizer error
	enum config_token token;            ///< Current token in the tokenizer
	bool replace_token;                 ///< Replace the token
};

static void
config_parser_init (struct config_parser *self, const char *script, size_t len)
{
	memset (self, 0, sizeof *self);
	config_tokenizer_init (&self->tokenizer, script, len);

	// As reading in tokens may cause exceptions, we wait for the first peek()
	// to replace the initial CONFIG_T_ABORT.
	self->replace_token = true;
}

static void
config_parser_free (struct config_parser *self)
{
	config_tokenizer_free (&self->tokenizer);
	if (self->error)
		error_free (self->error);
}

static enum config_token
config_parser_peek (struct config_parser *self, jmp_buf out)
{
	if (self->replace_token)
	{
		self->token = config_tokenizer_next (&self->tokenizer, &self->error);
		if (self->error)
			longjmp (out, 1);
		self->replace_token = false;
	}
	return self->token;
}

static bool
config_parser_accept
	(struct config_parser *self, enum config_token token, jmp_buf out)
{
	return self->replace_token = (config_parser_peek (self, out) == token);
}

static void
config_parser_expect
	(struct config_parser *self, enum config_token token, jmp_buf out)
{
	if (config_parser_accept (self, token, out))
		return;

	config_tokenizer_error (&self->tokenizer, &self->error,
		"unexpected `%s', expected `%s'",
		config_token_name (self->token),
		config_token_name (token));
	longjmp (out, 1);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// We don't need no generator, but a few macros will come in handy.
// From time to time C just doesn't have the right features.

#define PEEK()         config_parser_peek   (self, err)
#define ACCEPT(token)  config_parser_accept (self, token, err)
#define EXPECT(token)  config_parser_expect (self, token, err)
#define SKIP_NL()      do {} while (ACCEPT (CONFIG_T_NEWLINE))

static struct config_item_ *config_parser_parse_object
	(struct config_parser *self, jmp_buf out);

static struct config_item_ *
config_parser_parse_value (struct config_parser *self, jmp_buf out)
{
	struct config_item_ *volatile result = NULL;
	jmp_buf err;

	if (setjmp (err))
	{
		if (result)
			config_item_destroy (result);
		longjmp (out, 1);
	}

	if (ACCEPT (CONFIG_T_LBRACE))
	{
		result = config_parser_parse_object (self, out);
		SKIP_NL ();
		EXPECT (CONFIG_T_RBRACE);
		return result;
	}
	if (ACCEPT (CONFIG_T_NULL))
		return config_item_null ();
	if (ACCEPT (CONFIG_T_BOOLEAN))
		return config_item_boolean (self->tokenizer.integer);
	if (ACCEPT (CONFIG_T_INTEGER))
		return config_item_integer (self->tokenizer.integer);
	if (ACCEPT (CONFIG_T_STRING))
		return config_item_string (&self->tokenizer.string);

	config_tokenizer_error (&self->tokenizer, &self->error,
		"unexpected `%s', expected a value",
		config_token_name (self->token));
	longjmp (out, 1);
}

/// Parse a single "key = value" assignment into @a object
static bool
config_parser_parse_kv_pair (struct config_parser *self,
	struct config_item_ *object, jmp_buf out)
{
	char *volatile key = NULL;
	jmp_buf err;

	if (setjmp (err))
	{
		free (key);
		longjmp (out, 1);
	}

	SKIP_NL ();

	// Either this object's closing right brace if called recursively,
	// or end of file when called on a whole configuration file
	if (PEEK () == CONFIG_T_RBRACE
	 || PEEK () == CONFIG_T_ABORT)
		return false;

	EXPECT (CONFIG_T_WORD);
	key = xstrdup (self->tokenizer.string.str);
	SKIP_NL ();

	EXPECT (CONFIG_T_EQUALS);
	SKIP_NL ();

	str_map_set (&object->value.object, key,
		config_parser_parse_value (self, err));

	free (key);
	key = NULL;

	if (PEEK () == CONFIG_T_RBRACE
	 || PEEK () == CONFIG_T_ABORT)
		return false;

	EXPECT (CONFIG_T_NEWLINE);
	return true;
}

/// Parse the inside of an object definition
static struct config_item_ *
config_parser_parse_object (struct config_parser *self, jmp_buf out)
{
	struct config_item_ *volatile object = config_item_object ();
	jmp_buf err;

	if (setjmp (err))
	{
		config_item_destroy (object);
		longjmp (out, 1);
	}

	while (config_parser_parse_kv_pair (self, object, err))
		;
	return object;
}

#undef PEEK
#undef ACCEPT
#undef EXPECT
#undef SKIP_NL

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

/// Parse a configuration snippet either as an object or a bare value.
/// If it's the latter (@a single_value_only), no newlines may follow.
static struct config_item_ *
config_item_parse (const char *script, size_t len,
	bool single_value_only, struct error **e)
{
	struct config_parser parser;
	config_parser_init (&parser, script, len);

	struct config_item_ *volatile object = NULL;
	jmp_buf err;

	if (setjmp (err))
	{
		if (object)
		{
			config_item_destroy (object);
			object = NULL;
		}

		error_propagate (e, parser.error);
		parser.error = NULL;
		goto end;
	}

	if (single_value_only)
	{
		// This is really only intended for in-program configuration
		// and telling the line number would look awkward
		parser.tokenizer.report_line = false;
		object = config_parser_parse_value (&parser, err);
	}
	else
		object = config_parser_parse_object (&parser, err);
	config_parser_expect (&parser, CONFIG_T_ABORT, err);
end:
	config_parser_free (&parser);
	return object;
}

/// Clone an item.  Schema assignments aren't retained.
struct config_item_ *
config_item_clone (struct config_item_ *self)
{
	// Oh well, it saves code
	struct str tmp;
	str_init (&tmp);
	config_item_write (self, false, &tmp);
	struct config_item_ *result =
		config_item_parse (tmp.str, tmp.len, true, NULL);
	str_free (&tmp);
	return result;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static void
config_schema_initialize_item (struct config_schema *schema,
	struct config_item_ *parent, void *user_data)
{
	struct config_item_ *item =
		str_map_find (&parent->value.object, schema->name);

	bool replace = true;
	if (item)
	{
		// FIXME: either do this silently or tell about it via a callback
		//   or just store it in an output vector; don't print it directly
		struct error *e = NULL;
		replace = !config_item_validate_by_schema (item, schema, &e);
		if (e)
		{
			print_error ("resetting configuration item "
				"`%s' to default: %s", schema->name, e->message);
			error_free (e);
		}
	}

	if (replace)
	{
		struct error *e = NULL;
		if (schema->default_)
			item = config_item_parse
				(schema->default_, strlen (schema->default_), true, &e);
		else
			item = config_item_null ();

		if (e || !config_item_validate_by_schema (item, schema, &e))
			exit_fatal ("invalid default for `%s': %s",
				schema->name, e->message);

		// This will free the old item if there was any
		str_map_set (&parent->value.object, schema->name, item);
	}

	// Make sure the string subtype fits the schema
	if (config_item_type_is_string (item->type)
	 && config_item_type_is_string (schema->type))
		item->type = schema->type;

	item->schema = schema;
	item->user_data = user_data;
}

static void
config_schema_apply_to_object (struct config_schema *schema_array,
	struct config_item_ *object, void *user_data)
{
	hard_assert (object->type == CONFIG_ITEM_OBJECT);
	while (schema_array->name)
		config_schema_initialize_item (schema_array++, object, user_data);
}

static void
config_schema_call_changed (struct config_item_ *item)
{
	if (item->type == CONFIG_ITEM_OBJECT)
	{
		struct str_map_iter iter;
		str_map_iter_init (&iter, &item->value.object);

		struct config_item_ *child;
		while ((child = str_map_iter_next (&iter)))
			config_schema_call_changed (child);
	}
	else if (item->schema && item->schema->on_change)
		item->schema->on_change (item);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

// XXX: this doesn't necessarily have to be well designed at all

typedef void (*config_module_load_fn)
	(struct config_item_ *subtree, void *user_data);

struct config_module
{
	char *name;                         ///< Name of the subtree
	config_module_load_fn loader;       ///< Module config subtree loader
	void *user_data;                    ///< User data
};

static struct config_module *
config_module_new ()
{
	struct config_module *self = xcalloc (1, sizeof *self);
	return self;
}

static void
config_module_destroy (struct config_module *self)
{
	free (self->name);
	free (self);
}

struct config
{
	struct str_map modules;             ///< Toplevel modules
	struct config_item_ *root;          ///< CONFIG_ITEM_OBJECT
};

static void
config_init (struct config *self)
{
	memset (self, 0, sizeof *self);
	str_map_init (&self->modules);
	self->modules.free = (void (*) (void *)) config_module_destroy;
}

static void
config_free (struct config *self)
{
	str_map_free (&self->modules);
	if (self->root)
		config_item_destroy (self->root);
}

static void
config_register_module (struct config *self,
	const char *name, config_module_load_fn loader, void *user_data)
{
	struct config_module *module = config_module_new ();
	module->name = xstrdup (name);
	module->loader = loader;
	module->user_data = user_data;

	str_map_set (&self->modules, name, module);
}

static void
config_load (struct config *self, struct config_item_ *root)
{
	hard_assert (root->type == CONFIG_ITEM_OBJECT);
	self->root = root;

	struct str_map_iter iter;
	str_map_iter_init (&iter, &self->modules);

	struct config_module *module;
	while ((module = str_map_iter_next (&iter)))
	{
		struct config_item_ *subtree = str_map_find
			(&root->value.object, module->name);
		// Silently fix inputs that only a lunatic user could create
		if (!subtree || subtree->type != CONFIG_ITEM_OBJECT)
		{
			subtree = config_item_object ();
			str_map_set (&root->value.object, module->name, subtree);
		}
		if (module->loader)
			module->loader (subtree, module->user_data);
	}
}