/*
 * degesch.c: the experimental IRC client
 *
 * Copyright (c) 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.
 *
 */

/// Some arbitrary limit for the history file
#define HISTORY_LIMIT 10000

// A table of all attributes we use for output
#define ATTR_TABLE(XX)                                                         \
	XX( PROMPT,    "prompt",    "Terminal attributes for the prompt"     )     \
	XX( RESET,     "reset",     "String to reset terminal attributes"    )     \
	XX( WARNING,   "warning",   "Terminal attributes for warnings"       )     \
	XX( ERROR,     "error",     "Terminal attributes for errors"         )     \
	XX( EXTERNAL,  "external",  "Terminal attributes for external lines" )     \
	XX( TIMESTAMP, "timestamp", "Terminal attributes for timestamps"     )     \
	XX( HIGHLIGHT, "highlight", "Terminal attributes for highlights"     )     \
	XX( ACTION,    "action",    "Terminal attributes for user actions"   )     \
	XX( JOIN,      "join",      "Terminal attributes for joins"          )     \
	XX( PART,      "part",      "Terminal attributes for parts"          )

enum
{
#define XX(x, y, z) ATTR_ ## x,
	ATTR_TABLE (XX)
#undef XX
	ATTR_COUNT
};

// User data for logger functions to enable formatted logging
#define print_fatal_data    ((void *) ATTR_ERROR)
#define print_error_data    ((void *) ATTR_ERROR)
#define print_warning_data  ((void *) ATTR_WARNING)

#include "config.h"
#define PROGRAM_NAME "degesch"

#include "common.c"
#include "kike-replies.c"

#include <langinfo.h>
#include <locale.h>
#include <pwd.h>
#include <sys/utsname.h>

#include <curses.h>
#include <term.h>

// Literally cancer
#undef lines
#undef columns

#include <readline/readline.h>
#include <readline/history.h>

// --- Application data --------------------------------------------------------

// All text stored in our data structures is encoded in UTF-8.
// Or at least should be.  The exception is IRC identifiers.

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

// We need a few reference countable objects with support
// for both strong and weak references

/// Callback just before a reference counted object is destroyed
typedef void (*destroy_cb_fn) (void *object, void *user_data);

#define REF_COUNTABLE_HEADER                                                   \
	size_t ref_count;                   /**< Reference count                */ \
	destroy_cb_fn on_destroy;           /**< To remove any weak references  */ \
	void *user_data;                    /**< User data for callbacks        */

#define REF_COUNTABLE_METHODS(name)                                            \
	static struct name *                                                       \
	name ## _ref (struct name *self)                                           \
	{                                                                          \
		self->ref_count++;                                                     \
		return self;                                                           \
	}                                                                          \
																			   \
	static void                                                                \
	name ## _unref (struct name *self)                                         \
	{                                                                          \
		if (--self->ref_count)                                                 \
			return;                                                            \
		if (self->on_destroy)                                                  \
			self->on_destroy (self, self->user_data);                          \
		name ## _destroy (self);                                               \
	}

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

struct user_channel
{
	LIST_HEADER (struct user_channel)

	struct channel *channel;            ///< Reference to channel
};

static struct user_channel *
user_channel_new (void)
{
	struct user_channel *self = xcalloc (1, sizeof *self);
	return self;
}

static void
user_channel_destroy (struct user_channel *self)
{
	// The "channel" reference is weak and this object should get
	// destroyed whenever the user stops being in the channel.
	free (self);
}

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

// We keep references to user information in channels and buffers,
// and weak references in the name lookup table.

struct user
{
	REF_COUNTABLE_HEADER

	// TODO: eventually a reference to the server

	char *nickname;                     ///< Literal nickname
	// TODO: write code to poll for the away status
	bool away;                          ///< User is away

	struct user_channel *channels;      ///< Channels the user is on
};

static struct user *
user_new (void)
{
	struct user *self = xcalloc (1, sizeof *self);
	self->ref_count = 1;
	return self;
}

static void
user_destroy (struct user *self)
{
	free (self->nickname);
	LIST_FOR_EACH (struct user_channel, iter, self->channels)
		user_channel_destroy (iter);
	free (self);
}

REF_COUNTABLE_METHODS (user)

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

struct channel_user
{
	LIST_HEADER (struct channel_user)

	struct user *user;                  ///< Reference to user
	char *modes;                        ///< Op/voice/... characters
};

static struct channel_user *
channel_user_new (void)
{
	struct channel_user *self = xcalloc (1, sizeof *self);
	return self;
}

static void
channel_user_destroy (struct channel_user *self)
{
	user_unref (self->user);
	free (self->modes);
	free (self);
}

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

// We keep references to channels in their buffers,
// and weak references in their users and the name lookup table.

// XXX: this doesn't really have to be reference countable

struct channel
{
	REF_COUNTABLE_HEADER

	// TODO: eventually a reference to the server

	char *name;                         ///< Channel name
	char *mode;                         ///< Channel mode
	char *topic;                        ///< Channel topic

	struct channel_user *users;         ///< Channel users
	struct str_vector names_buf;        ///< Buffer for RPL_NAMREPLY
};

static struct channel *
channel_new (void)
{
	struct channel *self = xcalloc (1, sizeof *self);
	self->ref_count = 1;
	str_vector_init (&self->names_buf);
	return self;
}

static void
channel_destroy (struct channel *self)
{
	free (self->name);
	free (self->mode);
	free (self->topic);
	// Owner has to make sure we have no users by now
	hard_assert (!self->users);
	str_vector_free (&self->names_buf);
	free (self);
}

REF_COUNTABLE_METHODS (channel)

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

enum buffer_line_flags
{
	BUFFER_LINE_HIGHLIGHT   = 1 << 0    ///< The user was highlighted by this
};

enum buffer_line_type
{
	BUFFER_LINE_PRIVMSG,                ///< PRIVMSG
	BUFFER_LINE_ACTION,                 ///< PRIVMSG ACTION
	BUFFER_LINE_NOTICE,                 ///< NOTICE
	BUFFER_LINE_JOIN,                   ///< JOIN
	BUFFER_LINE_PART,                   ///< PART
	BUFFER_LINE_KICK,                   ///< KICK
	BUFFER_LINE_NICK,                   ///< NICK
	BUFFER_LINE_TOPIC,                  ///< TOPIC
	BUFFER_LINE_QUIT,                   ///< QUIT
	BUFFER_LINE_STATUS,                 ///< Whatever status messages
	BUFFER_LINE_ERROR                   ///< Whatever error messages
};

struct buffer_line_args
{
	char *who;                          ///< Name of the origin or NULL (user)
	char *object;                       ///< Object of action
	char *text;                         ///< Text of message
	char *reason;                       ///< Reason for PART, KICK, QUIT
};

struct buffer_line
{
	LIST_HEADER (struct buffer_line)

	// We use the "type" and "flags" mostly just as formatting hints

	enum buffer_line_type type;         ///< Type of the event
	int flags;                          ///< Flags

	time_t when;                        ///< Time of the event
	struct buffer_line_args args;       ///< Arguments
};

struct buffer_line *
buffer_line_new (void)
{
	struct buffer_line *self = xcalloc (1, sizeof *self);
	return self;
}

static void
buffer_line_destroy (struct buffer_line *self)
{
	free (self->args.who);
	free (self->args.object);
	free (self->args.text);
	free (self->args.reason);
	free (self);
}

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

enum buffer_type
{
	BUFFER_GLOBAL,                      ///< Global information
	BUFFER_SERVER,                      ///< Server-related messages
	BUFFER_CHANNEL,                     ///< Channels
	BUFFER_PM                           ///< Private messages (query)
};

struct buffer
{
	LIST_HEADER (struct buffer)

	enum buffer_type type;              ///< Type of the buffer
	char *name;                         ///< The name of the buffer

	// Readline state:

	HISTORY_STATE *history;             ///< Saved history state
	char *saved_line;                   ///< Saved line
	int saved_point;                    ///< Saved position in line
	int saved_mark;                     ///< Saved mark

	// Buffer contents:

	struct buffer_line *lines;          ///< All lines in this buffer
	struct buffer_line *lines_tail;     ///< The tail of buffer lines
	unsigned lines_count;               ///< How many lines we have

	unsigned unseen_messages_count;     ///< # messages since last visited

	// Origin information:

	struct server *server;              ///< Reference to server
	struct channel *channel;            ///< Reference to channel
	struct user *user;                  ///< Reference to user
};

static struct buffer *
buffer_new (void)
{
	struct buffer *self = xcalloc (1, sizeof *self);
	return self;
}

static void
buffer_destroy (struct buffer *self)
{
	free (self->name);
	// Can't really free "history" here
	free (self->saved_line);
	LIST_FOR_EACH (struct buffer_line, iter, self->lines)
		buffer_line_destroy (iter);
	if (self->user)
		user_unref (self->user);
	if (self->channel)
		channel_unref (self->channel);
	free (self);
}

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

struct server
{
	struct app_context *ctx;            ///< Application context

	int irc_fd;                         ///< Socket FD of the server
	struct str read_buffer;             ///< Input yet to be processed
	struct poller_fd irc_event;         ///< IRC FD event
	bool irc_ready;                     ///< Whether we may send messages now

	SSL_CTX *ssl_ctx;                   ///< SSL context
	SSL *ssl;                           ///< SSL connection

	// TODO: an output queue to prevent excess floods (this will be needed
	//   especially for away status polling)

	// XXX: there can be buffers for non-existent users
	// TODO: initialize key_strxfrm according to server properties;
	//   note that collisions may arise on reconnecting
	// TODO: when disconnected, get rid of all users everywhere;
	//   maybe also broadcast all buffers about the disconnection event
	// TODO: when getting connected again, rejoin all current channels

	struct buffer *buffer;              ///< The buffer for this server

	struct str_map irc_users;           ///< IRC user data
	struct str_map irc_channels;        ///< IRC channel data
	struct str_map irc_buffer_map;      ///< Maps IRC identifiers to buffers

	struct user *irc_user;              ///< Our own user
	char *irc_user_mode;                ///< Our current user mode
	char *irc_user_host;                ///< Our current user@host

	// Events:

	struct poller_timer ping_tmr;       ///< We should send a ping
	struct poller_timer timeout_tmr;    ///< Connection seems to be dead
	struct poller_timer reconnect_tmr;  ///< We should reconnect now
};

static void on_irc_ping_timeout (void *user_data);
static void on_irc_timeout (void *user_data);
static void on_irc_reconnect_timeout (void *user_data);

static void
server_init (struct server *self, struct poller *poller)
{
	self->irc_fd = -1;
	str_init (&self->read_buffer);
	self->irc_ready = false;

	str_map_init (&self->irc_users);
	self->irc_users.key_xfrm = irc_strxfrm;
	str_map_init (&self->irc_channels);
	self->irc_channels.key_xfrm = irc_strxfrm;
	str_map_init (&self->irc_buffer_map);
	self->irc_buffer_map.key_xfrm = irc_strxfrm;

	poller_timer_init (&self->timeout_tmr, poller);
	self->timeout_tmr.dispatcher = on_irc_timeout;
	self->timeout_tmr.user_data = self;

	poller_timer_init (&self->ping_tmr, poller);
	self->ping_tmr.dispatcher = on_irc_ping_timeout;
	self->ping_tmr.user_data = self;

	poller_timer_init (&self->reconnect_tmr, poller);
	self->reconnect_tmr.dispatcher = on_irc_reconnect_timeout;
	self->reconnect_tmr.user_data = self;
}

static void
server_free (struct server *self)
{
	if (self->irc_fd != -1)
	{
		xclose (self->irc_fd);
		poller_fd_reset (&self->irc_event);
	}
	str_free (&self->read_buffer);

	if (self->ssl)
		SSL_free (self->ssl);
	if (self->ssl_ctx)
		SSL_CTX_free (self->ssl_ctx);

	if (self->irc_user)
		user_unref (self->irc_user);
	free (self->irc_user_mode);
	free (self->irc_user_host);

	str_map_free (&self->irc_users);
	str_map_free (&self->irc_channels);
	str_map_free (&self->irc_buffer_map);
}

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

struct app_context
{
	// Configuration:

	struct config config;               ///< Program configuration
	char *attrs[ATTR_COUNT];            ///< Terminal attributes
	bool no_colors;                     ///< Colour output mode
	bool reconnect;                     ///< Whether to reconnect on conn. fail.
	unsigned long reconnect_delay;      ///< Reconnect delay in seconds
	bool isolate_buffers;               ///< Isolate global/server buffers

	struct server server;               ///< Our only server so far

	// Events:

	struct poller_fd tty_event;         ///< Terminal input event
	struct poller_fd signal_event;      ///< Signal FD event

	struct poller poller;               ///< Manages polled descriptors
	bool quitting;                      ///< User requested quitting
	bool polling;                       ///< The event loop is running

	// Buffers:

	struct buffer *buffers;             ///< All our buffers in order
	struct buffer *buffers_tail;        ///< The tail of our buffers

	struct buffer *last_buffer;         ///< Last used buffer

	// XXX: when we go multiserver, there will be collisions
	// TODO: make buffer names unique like weechat does
	struct str_map buffers_by_name;     ///< Excludes GLOBAL and SERVER

	struct buffer *global_buffer;       ///< The global buffer
	struct buffer *current_buffer;      ///< The current buffer

	// TODO: So that we always output proper date change messages
	time_t last_displayed_msg_time;     ///< Time of last displayed message

	// Terminal:

	iconv_t term_to_utf8;               ///< Terminal encoding to UTF-8
	iconv_t term_from_utf8;             ///< UTF-8 to terminal encoding
	iconv_t latin1_to_utf8;             ///< ISO Latin 1 to UTF-8

	int lines;                          ///< Current terminal height
	int columns;                        ///< Current ternimal width

	char *readline_prompt;              ///< The prompt we use for readline
	bool readline_prompt_shown;         ///< Whether the prompt is shown now
}
*g_ctx;

static void
app_context_init (struct app_context *self)
{
	memset (self, 0, sizeof *self);

	config_init (&self->config);
	poller_init (&self->poller);

	server_init (&self->server, &self->poller);
	self->server.ctx = self;

	str_map_init (&self->buffers_by_name);
	self->buffers_by_name.key_xfrm = irc_strxfrm;

	self->last_displayed_msg_time = time (NULL);

	char *encoding = nl_langinfo (CODESET);
#ifdef __linux__
	encoding = xstrdup_printf ("%s//TRANSLIT", encoding);
#else // ! __linux__
	encoding = xstrdup (encoding);
#endif // ! __linux__

	if ((self->term_from_utf8 =
		iconv_open (encoding, "UTF-8")) == (iconv_t) -1
	 || (self->latin1_to_utf8 =
		iconv_open ("UTF-8", "ISO-8859-1")) == (iconv_t) -1
	 || (self->term_to_utf8 =
		iconv_open ("UTF-8", nl_langinfo (CODESET))) == (iconv_t) -1)
		exit_fatal ("creating the UTF-8 conversion object failed: %s",
			strerror (errno));

	free (encoding);
}

static void
app_context_free (struct app_context *self)
{
	config_free (&self->config);
	for (size_t i = 0; i < ATTR_COUNT; i++)
		free (self->attrs[i]);

	// FIXME: this doesn't free the history state
	LIST_FOR_EACH (struct buffer, iter, self->buffers)
		buffer_destroy (iter);
	str_map_free (&self->buffers_by_name);

	server_free (&self->server);
	poller_free (&self->poller);

	iconv_close (self->latin1_to_utf8);
	iconv_close (self->term_from_utf8);
	iconv_close (self->term_to_utf8);

	free (self->readline_prompt);
}

static void refresh_prompt (struct app_context *ctx);
static char *irc_cut_nickname (const char *prefix);
static const char *irc_find_userhost (const char *prefix);

// --- Configuration -----------------------------------------------------------

// TODO: eventually add "on_change" callbacks

static bool
config_validate_nonjunk_string
	(const struct config_item_ *item, struct error **e)
{
	if (item->type == CONFIG_ITEM_NULL)
		return true;

	hard_assert (config_item_type_is_string (item->type));
	for (size_t i = 0; i < item->value.string.len; i++)
	{
		// Not even a tabulator
		unsigned char c = item->value.string.str[i];
		if (c < 32)
		{
			error_set (e, "control characters are not allowed");
			return false;
		}
	}
	return true;
}

static bool
config_validate_nonnegative
	(const struct config_item_ *item, struct error **e)
{
	if (item->type == CONFIG_ITEM_NULL)
		return true;

	hard_assert (item->type == CONFIG_ITEM_INTEGER);
	if (item->value.integer >= 0)
		return true;

	error_set (e, "must be non-negative");
	return false;
}

struct config_schema g_config_server[] =
{
	{ .name      = "nickname",
	  .comment   = "IRC nickname",
	  .type      = CONFIG_ITEM_STRING,
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "username",
	  .comment   = "IRC user name",
	  .type      = CONFIG_ITEM_STRING,
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "realname",
	  .comment   = "IRC real name/e-mail",
	  .type      = CONFIG_ITEM_STRING,
	  .validate  = config_validate_nonjunk_string },

	{ .name      = "irc_host",
	  .comment   = "Address of the IRC server",
	  .type      = CONFIG_ITEM_STRING,
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "irc_port",
	  .comment   = "Port of the IRC server",
	  .type      = CONFIG_ITEM_INTEGER,
	  .validate  = config_validate_nonnegative,
	  .default_  = "6667" },

	{ .name      = "ssl",
	  .comment   = "Whether to use SSL/TLS",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off" },
	{ .name      = "ssl_cert",
	  .comment   = "Client SSL certificate (PEM)",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "ssl_verify",
	  .comment   = "Whether to verify certificates",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on" },
	{ .name      = "ssl_ca_file",
	  .comment   = "OpenSSL CA bundle file",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "ssl_ca_path",
	  .comment   = "OpenSSL CA bundle path",
	  .type      = CONFIG_ITEM_STRING },

	{ .name      = "autojoin",
	  .comment   = "Channels to join on start",
	  .type      = CONFIG_ITEM_STRING_ARRAY,
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "reconnect",
	  .comment   = "Whether to reconnect on error",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on" },
	{ .name      = "reconnect_delay",
	  .comment   = "Time between reconnecting",
	  .type      = CONFIG_ITEM_INTEGER,
	  .default_  = "5" },

	{ .name      = "socks_host",
	  .comment   = "Address of a SOCKS 4a/5 proxy",
	  .type      = CONFIG_ITEM_STRING,
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "socks_port",
	  .comment   = "SOCKS port number",
	  .type      = CONFIG_ITEM_INTEGER,
	  .validate  = config_validate_nonnegative,
	  .default_  = "1080" },
	{ .name      = "socks_username",
	  .comment   = "SOCKS auth. username",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "socks_password",
	  .comment   = "SOCKS auth. password",
	  .type      = CONFIG_ITEM_STRING },
	{}
};

struct config_schema g_config_behaviour[] =
{
	{ .name      = "isolate_buffers",
	  .comment   = "Don't leak messages from the server and global buffers",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off" },
	{}
};

struct config_schema g_config_attributes[] =
{
#define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING },
	ATTR_TABLE (XX)
#undef XX
	{}
};

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

static void
load_config_server (struct config_item_ *subtree, void *user_data)
{
	(void) user_data;
	// This will eventually iterate over the object and create servers
	config_schema_apply_to_object (g_config_server, subtree);
}

static void
load_config_behaviour (struct config_item_ *subtree, void *user_data)
{
	(void) user_data;
	config_schema_apply_to_object (g_config_behaviour, subtree);
}

static void
load_config_attributes (struct config_item_ *subtree, void *user_data)
{
	(void) user_data;
	config_schema_apply_to_object (g_config_attributes, subtree);
}

static void
register_config_modules (struct app_context *ctx)
{
	struct config *config = &ctx->config;
	config_register_module (config,
		"server", load_config_server, ctx);
	config_register_module (config,
		"behaviour", load_config_behaviour, ctx);
	config_register_module (config,
		"attributes", load_config_attributes, ctx);
}

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

static const char *
get_config_string (struct app_context *ctx, const char *key)
{
	struct config_item_ *item = config_item_get (ctx->config.root, key, NULL);
	hard_assert (item);
	if (item->type == CONFIG_ITEM_NULL)
		return NULL;
	hard_assert (config_item_type_is_string (item->type));
	return item->value.string.str;
}

static bool
set_config_string (struct app_context *ctx, const char *key, const char *value)
{
	struct config_item_ *item = config_item_get (ctx->config.root, key, NULL);
	hard_assert (item);

	struct str s;
	str_init (&s);
	str_append (&s, value);
	struct config_item_ *new_ = config_item_string (&s);
	str_free (&s);

	struct error *e = NULL;
	if (config_item_set_from (item, new_, &e))
		return true;

	config_item_destroy (new_);
	print_error ("couldn't set `%s' in configuration: %s", key, e->message);
	error_free (e);
	return false;
}

static int64_t
get_config_integer (struct app_context *ctx, const char *key)
{
	struct config_item_ *item = config_item_get (ctx->config.root, key, NULL);
	hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
	return item->value.integer;
}

static bool
get_config_boolean (struct app_context *ctx, const char *key)
{
	struct config_item_ *item = config_item_get (ctx->config.root, key, NULL);
	hard_assert (item && item->type == CONFIG_ITEM_BOOLEAN);
	return item->value.boolean;
}

// --- Attributed output -------------------------------------------------------

static struct
{
	bool initialized;                   ///< Terminal is available
	bool stdout_is_tty;                 ///< `stdout' is a terminal
	bool stderr_is_tty;                 ///< `stderr' is a terminal

	char *color_set_fg[8];              ///< Codes to set the foreground colour
	char *color_set_bg[8];              ///< Codes to set the background colour
}
g_terminal;

static bool
init_terminal (void)
{
	int tty_fd = -1;
	if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO)))
		tty_fd = STDERR_FILENO;
	if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO)))
		tty_fd = STDOUT_FILENO;

	int err;
	if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
		return false;

	// Make sure all terminal features used by us are supported
	if (!set_a_foreground || !set_a_background
	 || !enter_bold_mode || !exit_attribute_mode)
	{
		del_curterm (cur_term);
		return false;
	}

	for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++)
	{
		g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground,
			i, 0, 0, 0, 0, 0, 0, 0, 0));
		g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background,
			i, 0, 0, 0, 0, 0, 0, 0, 0));
	}

	return g_terminal.initialized = true;
}

static void
free_terminal (void)
{
	if (!g_terminal.initialized)
		return;

	for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++)
	{
		free (g_terminal.color_set_fg[i]);
		free (g_terminal.color_set_bg[i]);
	}
	del_curterm (cur_term);
}

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

struct app_readline_state
{
	char *saved_line;
	int saved_point;
	int saved_mark;
};

static void
app_readline_hide (struct app_readline_state *state)
{
	state->saved_point = rl_point;
	state->saved_mark = rl_mark;
	state->saved_line = rl_copy_text (0, rl_end);
	rl_set_prompt ("");
	rl_replace_line ("", 0);
	rl_redisplay ();
}

static void
app_readline_restore (struct app_readline_state *state, const char *prompt)
{
	rl_set_prompt (prompt);
	rl_replace_line (state->saved_line, 0);
	rl_point = state->saved_point;
	rl_mark = state->saved_mark;
	rl_redisplay ();
	free (state->saved_line);
}

static void
app_readline_erase_to_bol (const char *prompt)
{
	rl_set_prompt ("");
	rl_replace_line ("", 0);
	rl_point = rl_mark = 0;
	rl_redisplay ();
	rl_set_prompt (prompt);
}

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

typedef int (*terminal_printer_fn) (int);

static int
putchar_stderr (int c)
{
	return fputc (c, stderr);
}

static terminal_printer_fn
get_attribute_printer (FILE *stream)
{
	if (stream == stdout && g_terminal.stdout_is_tty)
		return putchar;
	if (stream == stderr && g_terminal.stderr_is_tty)
		return putchar_stderr;
	return NULL;
}

static void
vprint_attributed (struct app_context *ctx,
	FILE *stream, intptr_t attribute, const char *fmt, va_list ap)
{
	terminal_printer_fn printer = get_attribute_printer (stream);
	if (!attribute)
		printer = NULL;

	if (printer)
		tputs (ctx->attrs[attribute], 1, printer);

	vfprintf (stream, fmt, ap);

	if (printer)
		tputs (ctx->attrs[ATTR_RESET], 1, printer);
}

static void
print_attributed (struct app_context *ctx,
	FILE *stream, intptr_t attribute, const char *fmt, ...)
{
	va_list ap;
	va_start (ap, fmt);
	vprint_attributed (ctx, stream, attribute, fmt, ap);
	va_end (ap);
}

static void
log_message_attributed (void *user_data, const char *quote, const char *fmt,
	va_list ap)
{
	FILE *stream = stderr;

	struct app_readline_state state;
	if (g_ctx->readline_prompt_shown)
		app_readline_hide (&state);

	print_attributed (g_ctx, stream, (intptr_t) user_data, "%s", quote);
	vprint_attributed (g_ctx, stream, (intptr_t) user_data, fmt, ap);
	fputs ("\n", stream);

	if (g_ctx->readline_prompt_shown)
		app_readline_restore (&state, g_ctx->readline_prompt);
}

static void
init_attribute (struct app_context *ctx, int id, const char *default_)
{
	static const char *table[ATTR_COUNT] =
	{
#define XX(x, y, z) [ATTR_ ## x] = "attributes." y,
		ATTR_TABLE (XX)
#undef XX
	};

	const char *user = get_config_string (ctx, table[id]);
	if (user)
		ctx->attrs[id] = xstrdup (user);
	else
		ctx->attrs[id] = xstrdup (default_);
}

static void
init_colors (struct app_context *ctx)
{
	bool have_ti = init_terminal ();

	// Use escape sequences from terminfo if possible, and SGR as a fallback
#define INIT_ATTR(id, ti) init_attribute (ctx, ATTR_ ## id, have_ti ? (ti) : "")

	INIT_ATTR (PROMPT,    enter_bold_mode);
	INIT_ATTR (RESET,     exit_attribute_mode);
	INIT_ATTR (WARNING,   g_terminal.color_set_fg[COLOR_YELLOW]);
	INIT_ATTR (ERROR,     g_terminal.color_set_fg[COLOR_RED]);

	INIT_ATTR (EXTERNAL,  g_terminal.color_set_fg[COLOR_WHITE]);
	INIT_ATTR (TIMESTAMP, g_terminal.color_set_fg[COLOR_WHITE]);
	INIT_ATTR (ACTION,    g_terminal.color_set_fg[COLOR_RED]);
	INIT_ATTR (JOIN,      g_terminal.color_set_fg[COLOR_GREEN]);
	INIT_ATTR (PART,      g_terminal.color_set_fg[COLOR_RED]);

	char *highlight = xstrdup_printf ("%s%s%s",
		g_terminal.color_set_fg[COLOR_YELLOW],
		g_terminal.color_set_bg[COLOR_MAGENTA],
		enter_bold_mode);
	INIT_ATTR (HIGHLIGHT, highlight);
	free (highlight);

#undef INIT_ATTR

	if (ctx->no_colors)
	{
		g_terminal.stdout_is_tty = false;
		g_terminal.stderr_is_tty = false;
	}

	g_log_message_real = log_message_attributed;
}

// --- Signals -----------------------------------------------------------------

static int g_signal_pipe[2];            ///< A pipe used to signal... signals

/// Program termination has been requested by a signal
static volatile sig_atomic_t g_termination_requested;
/// The window has changed in size
static volatile sig_atomic_t g_winch_received;

static void
sigterm_handler (int signum)
{
	(void) signum;

	g_termination_requested = true;

	int original_errno = errno;
	if (write (g_signal_pipe[1], "t", 1) == -1)
		soft_assert (errno == EAGAIN);
	errno = original_errno;
}

static void
sigwinch_handler (int signum)
{
	(void) signum;

	g_winch_received = true;

	int original_errno = errno;
	if (write (g_signal_pipe[1], "w", 1) == -1)
		soft_assert (errno == EAGAIN);
	errno = original_errno;
}

static void
setup_signal_handlers (void)
{
	if (pipe (g_signal_pipe) == -1)
		exit_fatal ("%s: %s", "pipe", strerror (errno));

	set_cloexec (g_signal_pipe[0]);
	set_cloexec (g_signal_pipe[1]);

	// So that the pipe cannot overflow; it would make write() block within
	// the signal handler, which is something we really don't want to happen.
	// The same holds true for read().
	set_blocking (g_signal_pipe[0], false);
	set_blocking (g_signal_pipe[1], false);

	signal (SIGPIPE, SIG_IGN);

	struct sigaction sa;
	sa.sa_flags = SA_RESTART;
	sa.sa_handler = sigwinch_handler;
	sigemptyset (&sa.sa_mask);

	if (sigaction (SIGWINCH, &sa, NULL) == -1)
		exit_fatal ("sigaction: %s", strerror (errno));

	sa.sa_handler = sigterm_handler;
	if (sigaction (SIGINT, &sa, NULL) == -1
	 || sigaction (SIGTERM, &sa, NULL) == -1)
		exit_fatal ("sigaction: %s", strerror (errno));
}

// --- Output formatter --------------------------------------------------------

// This complicated piece of code makes attributed text formatting simple.
// We use a printf-inspired syntax to push attributes and text to the object,
// then flush it either to a terminal, or a log file with formatting stripped.
//
// Format strings use a #-quoted notation, to differentiate from printf:
//   #s inserts a string
//   #d inserts a signed integer; also supports the #<N> and #0<N> notation
//
//   #a inserts named attributes (auto-resets)
//   #r resets terminal attributes
//   #c sets foreground color
//   #C sets background color

enum formatter_item_type
{
	FORMATTER_ITEM_TEXT,                ///< Text
	FORMATTER_ITEM_ATTR,                ///< Formatting attributes
	FORMATTER_ITEM_FG_COLOR,            ///< Foreground color
	FORMATTER_ITEM_BG_COLOR             ///< Background color
};

struct formatter_item
{
	LIST_HEADER (struct formatter_item)

	enum formatter_item_type type;      ///< Type of this item
	int color;                          ///< Color
	int attribute;                      ///< Attribute ID
	char *text;                         ///< Either text or an attribute string
};

static struct formatter_item *
formatter_item_new (void)
{
	struct formatter_item *self = xcalloc (1, sizeof *self);
	return self;
}

static void
formatter_item_destroy (struct formatter_item *self)
{
	free (self->text);
	free (self);
}

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

struct formatter
{
	struct app_context *ctx;            ///< Application context
	bool ignore_new_attributes;         ///< Whether to ignore new attributes

	struct formatter_item *items;       ///< Items
	struct formatter_item *items_tail;  ///< Tail of items
};

static void
formatter_init (struct formatter *self, struct app_context *ctx)
{
	memset (self, 0, sizeof *self);
	self->ctx = ctx;
}

static void
formatter_free (struct formatter *self)
{
	LIST_FOR_EACH (struct formatter_item, iter, self->items)
		formatter_item_destroy (iter);
}

static struct formatter_item *
formatter_add_blank (struct formatter *self)
{
	struct formatter_item *item = formatter_item_new ();
	LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item);
	return item;
}

static void
formatter_add_text (struct formatter *self, const char *text)
{
	struct formatter_item *item = formatter_add_blank (self);
	item->type = FORMATTER_ITEM_TEXT;
	item->text = xstrdup (text);
}

static void
formatter_add_reset (struct formatter *self)
{
	if (self->ignore_new_attributes)
		return;

	struct formatter_item *item = formatter_add_blank (self);
	item->type = FORMATTER_ITEM_ATTR;
	item->attribute = ATTR_RESET;
}

static void
formatter_add_attr (struct formatter *self, int attr_id)
{
	if (self->ignore_new_attributes)
		return;

	struct formatter_item *item = formatter_add_blank (self);
	item->type = FORMATTER_ITEM_ATTR;
	item->attribute = attr_id;
}

static void
formatter_add_fg_color (struct formatter *self, int color)
{
	if (self->ignore_new_attributes)
		return;

	struct formatter_item *item = formatter_add_blank (self);
	item->type = FORMATTER_ITEM_FG_COLOR;
	item->color = color;
}

static void
formatter_add_bg_color (struct formatter *self, int color)
{
	if (self->ignore_new_attributes)
		return;

	struct formatter_item *item = formatter_add_blank (self);
	item->type = FORMATTER_ITEM_BG_COLOR;
	item->color = color;
}

static const char *
formatter_parse_field (struct formatter *self,
	const char *field, struct str *buf, va_list *ap)
{
	size_t width = 0;
	bool zero_padded = false;
	int c;

restart:
	switch ((c = *field++))
	{
		char *s;

		// We can push boring text content to the caller's buffer
		// and let it flush the buffer only when it's actually needed
	case 's':
		s = va_arg (*ap, char *);
		for (size_t len = strlen (s); len < width; len++)
			str_append_c (buf, ' ');
		str_append (buf, s);
		break;
	case 'd':
		s = xstrdup_printf ("%d", va_arg (*ap, int));
		for (size_t len = strlen (s); len < width; len++)
			str_append_c (buf, " 0"[zero_padded]);
		str_append (buf, s);
		free (s);
		break;

	case 'a':
		formatter_add_attr     (self, va_arg (*ap, int));
		break;
	case 'c':
		formatter_add_fg_color (self, va_arg (*ap, int));
		break;
	case 'C':
		formatter_add_bg_color (self, va_arg (*ap, int));
		break;
	case 'r':
		formatter_add_reset    (self);
		break;

	default:
		if (c == '0' && !zero_padded)
			zero_padded = true;
		else if (isdigit_ascii (c))
			width = width * 10 + (c - '0');
		else if (c)
			hard_assert (!"unexpected format specifier");
		else
			hard_assert (!"unexpected end of format string");
		goto restart;
	}
	return field;
}

static void
formatter_add (struct formatter *self, const char *format, ...)
{
	struct str buf;
	str_init (&buf);

	va_list ap;
	va_start (ap, format);

	while (*format)
	{
		if (*format != '#' || *++format == '#')
		{
			str_append_c (&buf, *format++);
			continue;
		}
		if (buf.len)
		{
			formatter_add_text (self, buf.str);
			str_reset (&buf);
		}

		format = formatter_parse_field (self, format, &buf, &ap);
	}

	if (buf.len)
		formatter_add_text (self, buf.str);

	str_free (&buf);
	va_end (ap);
}

static void
formatter_flush (struct formatter *self, FILE *stream)
{
	terminal_printer_fn printer = get_attribute_printer (stream);
	if (!printer)
	{
		LIST_FOR_EACH (struct formatter_item, iter, self->items)
			if (iter->type == FORMATTER_ITEM_TEXT)
				fputs (iter->text, stream);
		return;
	}

	const char *attr_reset = self->ctx->attrs[ATTR_RESET];
	tputs (attr_reset, 1, printer);

	bool is_attributed = false;
	LIST_FOR_EACH (struct formatter_item, iter, self->items)
	{
		switch (iter->type)
		{
			char *term;
		case FORMATTER_ITEM_TEXT:
			term = iconv_xstrdup
				(self->ctx->term_from_utf8, iter->text, -1, NULL);
			fputs (term, stream);
			free (term);
			break;
		case FORMATTER_ITEM_ATTR:
			if (is_attributed)
			{
				tputs (attr_reset, 1, printer);
				is_attributed = false;
			}
			if (iter->attribute != ATTR_RESET)
			{
				tputs (self->ctx->attrs[iter->attribute], 1, printer);
				is_attributed = true;
			}
			break;
		case FORMATTER_ITEM_FG_COLOR:
			tputs (g_terminal.color_set_fg[iter->color], 1, printer);
			is_attributed = true;
			break;
		case FORMATTER_ITEM_BG_COLOR:
			tputs (g_terminal.color_set_bg[iter->color], 1, printer);
			is_attributed = true;
			break;
		}
	}

	if (is_attributed)
		tputs (attr_reset, 1, printer);
}

// --- Buffers -----------------------------------------------------------------

static void
buffer_update_time (struct app_context *ctx, time_t now)
{
	struct tm last, current;
	if (!localtime_r (&ctx->last_displayed_msg_time, &last)
	 || !localtime_r (&now, &current))
	{
		// Strange but nonfatal
		print_error ("%s: %s", "localtime_r", strerror (errno));
		return;
	}

	ctx->last_displayed_msg_time = now;
	if (last.tm_year == current.tm_year
	 && last.tm_mon  == current.tm_mon
	 && last.tm_mday == current.tm_mday)
		return;

	char buf[32] = "";
	if (soft_assert (strftime (buf, sizeof buf, "%F", &current)))
		print_status ("%s", buf);
	// Else the buffer was too small, which is pretty weird
}

static void
buffer_line_display (struct app_context *ctx,
	struct buffer_line *line, bool is_external)
{
	// Normal timestamps don't include the date, this way the user won't be
	// confused as to when an event has happened
	buffer_update_time (ctx, line->when);

	struct buffer_line_args *a = &line->args;

	char *nick = NULL;
	const char *userhost = NULL;
	int nick_color = -1;
	int object_color = -1;

	if (a->who)
	{
		nick = irc_cut_nickname (a->who);
		userhost = irc_find_userhost (a->who);
		nick_color = str_map_hash (nick, strlen (nick)) % 8;
	}
	if (a->object)
		object_color = str_map_hash (a->object, strlen (a->object)) % 8;

	struct formatter f;
	formatter_init (&f, ctx);

	struct tm current;
	if (!localtime_r (&line->when, &current))
		print_error ("%s: %s", "localtime_r", strerror (errno));
	else
		formatter_add (&f, "#a#02d:#02d:#02d#r ",
			ATTR_TIMESTAMP, current.tm_hour, current.tm_min, current.tm_sec);

	// Ignore all formatting for messages coming from other buffers, that is
	// either from the global or server buffer.  Instead print them in grey.
	if (is_external)
	{
		formatter_add (&f, "#a", ATTR_EXTERNAL);
		f.ignore_new_attributes = true;
	}

	// TODO: try to decode as much as possible using mIRC formatting;
	//   could either add a #m format specifier, or write a separate function
	//   to translate the formatting into formatter API calls

	switch (line->type)
	{
	case BUFFER_LINE_PRIVMSG:
		if (line->flags & BUFFER_LINE_HIGHLIGHT)
			formatter_add (&f, "#a<#s>#r #s", ATTR_HIGHLIGHT, nick, a->text);
		else
			formatter_add (&f, "<#c#s#r> #s", nick_color, nick, a->text);
		break;
	case BUFFER_LINE_ACTION:
		if (line->flags & BUFFER_LINE_HIGHLIGHT)
			formatter_add (&f, " #a*#r  ", ATTR_HIGHLIGHT);
		else
			formatter_add (&f, " #a*#r  ", ATTR_ACTION);
		formatter_add (&f, "#c#s#r #s", nick_color, nick, a->text);
		break;
	case BUFFER_LINE_NOTICE:
		formatter_add (&f, " -  ");
		if (line->flags & BUFFER_LINE_HIGHLIGHT)
			formatter_add (&f, "#a#s(#s)#r: #s",
				ATTR_HIGHLIGHT, "Notice", nick, a->text);
		else
			formatter_add (&f, "#s(#c#s#r): #s",
				"Notice", nick_color, nick, a->text);
		break;
	case BUFFER_LINE_JOIN:
		formatter_add (&f, "#a-->#r ", ATTR_JOIN);
		formatter_add (&f, "#c#s#r (#s) #a#s#r #s",
			nick_color, nick, userhost,
			ATTR_JOIN, "has joined", a->object);
		break;
	case BUFFER_LINE_PART:
		formatter_add (&f, "#a<--#r ", ATTR_PART);
		formatter_add (&f, "#c#s#r (#s) #a#s#r #s",
			nick_color, nick, userhost,
			ATTR_PART, "has left", a->object);
		if (a->reason)
			formatter_add (&f, " (#s)", a->reason);
		break;
	case BUFFER_LINE_KICK:
		formatter_add (&f, "#a<--#r ", ATTR_PART);
		formatter_add (&f, "#c#s#r (#s) #a#s#r #c#s#r",
			nick_color, nick, userhost,
			ATTR_PART, "has kicked", object_color, a->object);
		if (a->reason)
			formatter_add (&f, " (#s)", a->reason);
		break;
	case BUFFER_LINE_NICK:
		formatter_add (&f, " -  ");
		if (a->who)
			formatter_add (&f, "#c#s#r #s #c#s#r",
				nick_color, nick,
				"is now known as", object_color, a->object);
		else
			formatter_add (&f, "#s #s",
				"You are now known as", a->object);
		break;
	case BUFFER_LINE_TOPIC:
		formatter_add (&f, " -  ");
		formatter_add (&f, "#c#s#r #s \"#s\"",
			nick_color, nick,
			"has changed the topic to", a->text);
		break;
	case BUFFER_LINE_QUIT:
		formatter_add (&f, "#a<--#r ", ATTR_PART);
		formatter_add (&f, "#c#s#r (%s) #a#s#r",
			nick_color, nick, userhost,
			ATTR_PART, "has quit");
		if (a->reason)
			formatter_add (&f, " (#s)", a->reason);
		break;
	case BUFFER_LINE_STATUS:
		formatter_add (&f, " -  ");
		formatter_add (&f, "#s", a->text);
		break;
	case BUFFER_LINE_ERROR:
		formatter_add (&f, "#a=!=#r ", ATTR_ERROR);
		formatter_add (&f, "#s", a->text);
	}

	free (nick);

	struct app_readline_state state;
	if (ctx->readline_prompt_shown)
		app_readline_hide (&state);

	// TODO: write the line to a log file; note that the global and server
	//   buffers musn't collide with filenames

	formatter_add (&f, "\n");
	formatter_flush (&f, stdout);
	formatter_free (&f);

	if (ctx->readline_prompt_shown)
		app_readline_restore (&state, ctx->readline_prompt);
}

static void
buffer_send_internal (struct app_context *ctx, struct buffer *buffer,
	enum buffer_line_type type, int flags,
	struct buffer_line_args a)
{
	struct buffer_line *line = buffer_line_new ();
	line->type = type;
	line->flags = flags;
	line->when = time (NULL);
	line->args = a;

	LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
	buffer->lines_count++;

	if (buffer == ctx->current_buffer)
		buffer_line_display (ctx, line, false);
	else if (!ctx->isolate_buffers &&
		(buffer == ctx->global_buffer ||
			buffer == ctx->current_buffer->server->buffer))
		buffer_line_display (ctx, line, true);
	else
	{
		buffer->unseen_messages_count++;
		refresh_prompt (ctx);
	}
}

#define buffer_send(ctx, buffer, type, flags, ...)                             \
	buffer_send_internal ((ctx), (buffer), (type), (flags),                    \
	(struct buffer_line_args) { __VA_ARGS__ })

#define buffer_send_status(ctx, buffer, ...)                                   \
	buffer_send (ctx, buffer, BUFFER_LINE_STATUS, 0,                           \
	.text = xstrdup_printf (__VA_ARGS__))
#define buffer_send_error(ctx, buffer, ...)                                    \
	buffer_send (ctx, buffer, BUFFER_LINE_ERROR, 0,                            \
	.text = xstrdup_printf (__VA_ARGS__))

static struct buffer *
buffer_by_name (struct app_context *ctx, const char *name)
{
	return str_map_find (&ctx->buffers_by_name, name);
}

static void
buffer_add (struct app_context *ctx, struct buffer *buffer)
{
	hard_assert (!buffer_by_name (ctx, buffer->name));

	str_map_set (&ctx->buffers_by_name, buffer->name, buffer);
	LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);

	// In theory this can't cause changes in the prompt
	refresh_prompt (ctx);
}

static void
buffer_remove (struct app_context *ctx, struct buffer *buffer)
{
	hard_assert (buffer != ctx->current_buffer);

	// TODO: part from the channel if needed

	// rl_clear_history, being the only way I know of to get rid of the complete
	// history including attached data, is a pretty recent addition.  *sigh*
#if RL_READLINE_VERSION >= 0x0603
	if (buffer->history)
	{
		// See buffer_activate() for why we need to do this BS
		rl_free_undo_list ();

		// This is probably the only way we can free the history fully
		HISTORY_STATE *state = history_get_history_state ();

		history_set_history_state (buffer->history);
		free (buffer->history);
		rl_clear_history ();

		history_set_history_state (state);
		free (state);
	}
#endif // RL_READLINE_VERSION

	// And make sure to unlink the buffer from "irc_buffer_map"
	struct server *s = buffer->server;
	if (buffer->channel)
		str_map_set (&s->irc_buffer_map, buffer->channel->name, NULL);
	if (buffer->user)
		str_map_set (&s->irc_buffer_map, buffer->user->nickname, NULL);

	str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
	LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
	buffer_destroy (buffer);

	if (buffer == ctx->last_buffer)
		ctx->last_buffer = NULL;

	// It's not a good idea to remove these buffers, but it's even a worse
	// one to leave the pointers point to invalid memory
	if (buffer == ctx->global_buffer)
		ctx->global_buffer = NULL;
	if (buffer == ctx->server.buffer)
		ctx->server.buffer = NULL;

	refresh_prompt (ctx);
}

static void
buffer_activate (struct app_context *ctx, struct buffer *buffer)
{
	if (ctx->current_buffer == buffer)
		return;

	print_status ("%s", buffer->name);

	// That is, minus the buffer switch line and the readline prompt
	int to_display = MAX (10, ctx->lines - 2);
	struct buffer_line *line = buffer->lines_tail;
	while (line && line->prev && --to_display > 0)
		line = line->prev;

	// Once we've found where we want to start with the backlog, print it
	for (; line; line = line->next)
		buffer_line_display (ctx, line, false);
	buffer->unseen_messages_count = 0;

	// The following part shows you why it's not a good idea to use
	// GNU Readline for this kind of software.  Or for anything else, really.

	// There could possibly be occurences of the current undo list in some
	// history entry.  We either need to free the undo list, or move it
	// somewhere else to load back later, as the buffer we're switching to
	// has its own history state.
	rl_free_undo_list ();

	// Save this buffer's history so that it's independent for each buffer
	if (ctx->current_buffer)
	{
		ctx->current_buffer->history = history_get_history_state ();
		ctx->current_buffer->saved_line = rl_copy_text (0, rl_end);
		ctx->current_buffer->saved_point = rl_point;
		ctx->current_buffer->saved_mark = rl_mark;
	}
	else
		// Just throw it away; there should always be an active buffer however
#if RL_READLINE_VERSION >= 0x0603
		rl_clear_history ();
#else // RL_READLINE_VERSION < 0x0603
		// At least something... this may leak undo entries
		clear_history ();
#endif // RL_READLINE_VERSION < 0x0603

	// Restore the target buffer's history
	if (buffer->history)
	{
		// history_get_history_state() just allocates a new HISTORY_STATE
		// and fills it with its current internal data.  We don't need that
		// shell anymore after reviving it.
		history_set_history_state (buffer->history);
		free (buffer->history);
		buffer->history = NULL;
	}
	else
	{
		// This should get us a clean history while keeping the flags.
		// Note that we've either saved the previous history entries, or we've
		// cleared them altogether, so there should be nothing to leak.
		HISTORY_STATE *state = history_get_history_state ();
		state->offset = state->length = state->size = 0;
		history_set_history_state (state);
		free (state);
	}

	// Try to restore the target buffer's readline state
	if (buffer->saved_line)
	{
		rl_replace_line (buffer->saved_line, 0);
		rl_point = buffer->saved_point;
		rl_mark = buffer->saved_mark;
		free (buffer->saved_line);
		buffer->saved_line = 0;

		if (ctx->readline_prompt_shown)
			rl_redisplay ();
	}

	// Now at last we can switch the pointers
	ctx->last_buffer = ctx->current_buffer;
	ctx->current_buffer = buffer;

	refresh_prompt (ctx);
}

static void
buffer_merge (struct app_context *ctx,
	struct buffer *buffer, struct buffer *merged)
{
	// TODO: try to merge the buffers as best as we can
}

static void
buffer_rename (struct app_context *ctx,
	struct buffer *buffer, const char *new_name)
{
	hard_assert (buffer->type == BUFFER_PM);

	struct buffer *collision =
		str_map_find (&buffer->server->irc_buffer_map, new_name);
	if (collision)
	{
		// TODO: use full weechat-style buffer names
		//   to prevent name collisions with the global buffer
		hard_assert (collision->type == BUFFER_PM);

		// When there's a collision, there's not much else we can do
		// other than somehow trying to merge them
		buffer_merge (ctx, collision, buffer);
		// TODO: log a status message about the merge
		if (ctx->current_buffer == buffer)
			buffer_activate (ctx, collision);
		buffer_remove (ctx, buffer);
	}
	else
	{
		// Otherwise we just rename the buffer and that's it
		str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
		str_map_set (&ctx->buffers_by_name, new_name, buffer);

		free (buffer->name);
		buffer->name = xstrdup (new_name);

		// We might have renamed the current buffer
		refresh_prompt (ctx);
	}
}

static struct buffer *
buffer_at_index (struct app_context *ctx, int n)
{
	int i = 0;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
		if (++i == n)
			return iter;
	return NULL;
}

static struct buffer *
buffer_next (struct app_context *ctx, int count)
{
	struct buffer *new_buffer = ctx->current_buffer;
	while (count-- > 0)
		if (!(new_buffer = new_buffer->next))
			new_buffer = ctx->buffers;
	return new_buffer;
}

static struct buffer *
buffer_previous (struct app_context *ctx, int count)
{
	struct buffer *new_buffer = ctx->current_buffer;
	while (count-- > 0)
		if (!(new_buffer = new_buffer->prev))
			new_buffer = ctx->buffers_tail;
	return new_buffer;
}

static bool
buffer_goto (struct app_context *ctx, int n)
{
	struct buffer *buffer = buffer_at_index (ctx, n);
	if (!buffer)
		return false;

	buffer_activate (ctx, buffer);
	return true;
}

static int
buffer_get_index (struct app_context *ctx, struct buffer *buffer)
{
	int index = 1;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
	{
		if (iter == buffer)
			return index;
		index++;
	}
	return -1;
}

static void
init_buffers (struct app_context *ctx)
{
	// At the moment  we have only two global everpresent buffers
	struct buffer *global = ctx->global_buffer = buffer_new ();
	struct buffer *server = ctx->server.buffer = buffer_new ();

	global->type = BUFFER_GLOBAL;
	global->name = xstrdup (PROGRAM_NAME);

	server->type = BUFFER_SERVER;
	server->name = xstrdup ("server");
	server->server = &ctx->server;

	LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, global);
	LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, server);
}

// --- Users, channels ---------------------------------------------------------

static void
irc_user_on_destroy (void *object, void *user_data)
{
	struct user *user = object;
	struct server *s = user_data;
	str_map_set (&s->irc_users, user->nickname, NULL);
}

static struct user *
irc_make_user (struct server *s, char *nickname)
{
	hard_assert (!str_map_find (&s->irc_users, nickname));

	struct user *user = user_new ();
	user->on_destroy = irc_user_on_destroy;
	user->user_data = s;
	user->nickname = nickname;
	str_map_set (&s->irc_users, user->nickname, user);
	return user;
}

static struct buffer *
irc_get_or_make_user_buffer (struct server *s, const char *nickname)
{
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, nickname);
	if (buffer)
		return buffer;

	struct user *user = str_map_find (&s->irc_users, nickname);
	if (!user)
		user = irc_make_user (s, xstrdup (nickname));
	else
		user = user_ref (user);

	// Open a new buffer for the user
	buffer = buffer_new ();
	buffer->type = BUFFER_PM;
	buffer->name = xstrdup (nickname);
	buffer->server = s;
	buffer->user = user;
	LIST_APPEND_WITH_TAIL (s->ctx->buffers, s->ctx->buffers_tail, buffer);
	str_map_set (&s->irc_buffer_map, user->nickname, buffer);
	return buffer;
}

static void
irc_channel_unlink_user
	(struct channel *channel, struct channel_user *channel_user)
{
	// First destroy the user's weak references to the channel
	struct user *user = channel_user->user;
	LIST_FOR_EACH (struct user_channel, iter, user->channels)
		if (iter->channel == channel)
		{
			LIST_UNLINK (user->channels, iter);
			user_channel_destroy (iter);
		}

	// Then just unlink the user from the channel
	LIST_UNLINK (channel->users, channel_user);
	channel_user_destroy (channel_user);
}

static void
irc_channel_on_destroy (void *object, void *user_data)
{
	struct channel *channel = object;
	struct server *s = user_data;
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
		irc_channel_unlink_user (channel, iter);
	str_map_set (&s->irc_channels, channel->name, NULL);
}

static struct channel *
irc_make_channel (struct server *s, char *name)
{
	hard_assert (!str_map_find (&s->irc_channels, name));

	struct channel *channel = channel_new ();
	channel->on_destroy = irc_channel_on_destroy;
	channel->user_data = s;
	channel->name = name;
	channel->mode = xstrdup ("");
	channel->topic = NULL;
	str_map_set (&s->irc_channels, channel->name, channel);
	return channel;
}

static void
irc_remove_user_from_channel (struct user *user, struct channel *channel)
{
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
		if (iter->user == user)
			irc_channel_unlink_user (channel, iter);
}

// --- Supporting code ---------------------------------------------------------

static char *
irc_cut_nickname (const char *prefix)
{
	return xstrndup (prefix, strcspn (prefix, "!@"));
}

static const char *
irc_find_userhost (const char *prefix)
{
	const char *p = strchr (prefix, '!');
	return p ? p + 1 : NULL;
}

static bool
irc_is_this_us (struct server *s, const char *prefix)
{
	char *nick = irc_cut_nickname (prefix);
	bool result = !irc_strcmp (nick, s->irc_user->nickname);
	free (nick);
	return result;
}

static bool
irc_is_channel (struct server *s, const char *ident)
{
	(void) s;  // TODO: parse prefixes from server features

	return *ident && !!strchr ("#&+!", *ident);
}

static void
irc_shutdown (struct server *s)
{
	// TODO: set a timer after which we cut the connection?
	// Generally non-critical
	if (s->ssl)
		soft_assert (SSL_shutdown (s->ssl) != -1);
	else
		soft_assert (shutdown (s->irc_fd, SHUT_WR) == 0);
}

static void
try_finish_quit (struct app_context *ctx)
{
	// TODO: multiserver
	if (ctx->quitting && ctx->server.irc_fd == -1)
		ctx->polling = false;
}

static void
initiate_quit (struct app_context *ctx)
{
	// First get rid of readline
	if (ctx->readline_prompt_shown)
	{
		app_readline_erase_to_bol (ctx->readline_prompt);
		ctx->readline_prompt_shown = false;
	}

	// This is okay as long as we're not called from within readline
	rl_callback_handler_remove ();

	buffer_send_status (ctx, ctx->global_buffer, "Shutting down");

	// Initiate a connection close
	// TODO: multiserver
	struct server *s = &ctx->server;
	if (s->irc_fd != -1)
		// XXX: when we go async, we'll have to flush output buffers first
		irc_shutdown (s);

	ctx->quitting = true;
	try_finish_quit (ctx);
}

// As of 2015, everything should be in UTF-8.  And if it's not, we'll decode it
// as ISO Latin 1.  This function should not be called on the whole message.
static char *
irc_to_utf8 (struct app_context *ctx, const char *text)
{
	size_t len = strlen (text) + 1;
	if (utf8_validate (text, len))
		return xstrdup (text);
	return iconv_xstrdup (ctx->latin1_to_utf8, (char *) text, len, NULL);
}

// This function is used to output debugging IRC traffic to the terminal.
// It's far from ideal, as any non-UTF-8 text degrades the entire line to
// ISO Latin 1.  But it should work good enough most of the time.
static char *
irc_to_term (struct app_context *ctx, const char *text)
{
	char *utf8 = irc_to_utf8 (ctx, text);
	char *term = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
	free (utf8);
	return term;
}

static bool irc_send (struct server *s,
	const char *format, ...) ATTRIBUTE_PRINTF (2, 3);

static bool
irc_send (struct server *s, const char *format, ...)
{
	if (!soft_assert (s->irc_fd != -1))
	{
		print_debug ("tried sending a message to a dead server connection");
		return false;
	}

	va_list ap;
	va_start (ap, format);
	struct str str;
	str_init (&str);
	str_append_vprintf (&str, format, ap);
	va_end (ap);

	if (g_debug_mode)
	{
		struct app_readline_state state;
		if (s->ctx->readline_prompt_shown)
			app_readline_hide (&state);

		char *term = irc_to_term (s->ctx, str.str);
		fprintf (stderr, "[IRC] <== \"%s\"\n", term);
		free (term);

		if (s->ctx->readline_prompt_shown)
			app_readline_restore (&state, s->ctx->readline_prompt);
	}
	str_append (&str, "\r\n");

	bool result = true;
	if (s->ssl)
	{
		// TODO: call SSL_get_error() to detect if a clean shutdown has occured
		if (SSL_write (s->ssl, str.str, str.len) != (int) str.len)
		{
			LOG_FUNC_FAILURE ("SSL_write",
				ERR_error_string (ERR_get_error (), NULL));
			result = false;
		}
	}
	else if (write (s->irc_fd, str.str, str.len) != (ssize_t) str.len)
	{
		LOG_LIBC_FAILURE ("write");
		result = false;
	}

	str_free (&str);
	return result;
}

static bool
irc_initialize_ssl_ctx (struct server *s, struct error **e)
{
	// XXX: maybe we should call SSL_CTX_set_options() for some workarounds

	bool verify = get_config_boolean (s->ctx, "server.ssl_verify");
	if (!verify)
		SSL_CTX_set_verify (s->ssl_ctx, SSL_VERIFY_NONE, NULL);

	const char *ca_file = get_config_string (s->ctx, "server.ca_file");
	const char *ca_path = get_config_string (s->ctx, "server.ca_path");

	struct error *error = NULL;
	if (ca_file || ca_path)
	{
		if (SSL_CTX_load_verify_locations (s->ssl_ctx, ca_file, ca_path))
			return true;

		error_set (&error, "%s: %s",
			"Failed to set locations for the CA certificate bundle",
			ERR_reason_error_string (ERR_get_error ()));
		goto ca_error;
	}

	if (!SSL_CTX_set_default_verify_paths (s->ssl_ctx))
	{
		error_set (&error, "%s: %s",
			"Couldn't load the default CA certificate bundle",
			ERR_reason_error_string (ERR_get_error ()));
		goto ca_error;
	}
	return true;

ca_error:
	if (verify)
	{
		error_propagate (e, error);
		return false;
	}

	// Only inform the user if we're not actually verifying
	buffer_send_error (s->ctx, s->buffer, "%s", error->message);
	error_free (error);
	return true;
}

static bool
irc_initialize_ssl (struct server *s, struct error **e)
{
	const char *error_info = NULL;
	s->ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
	if (!s->ssl_ctx)
		goto error_ssl_1;
	if (!irc_initialize_ssl_ctx (s, e))
		goto error_ssl_2;

	s->ssl = SSL_new (s->ssl_ctx);
	if (!s->ssl)
		goto error_ssl_2;

	const char *ssl_cert = get_config_string (s->ctx, "server.ssl_cert");
	if (ssl_cert)
	{
		char *path = resolve_config_filename (ssl_cert);
		if (!path)
			buffer_send_error (s->ctx, s->ctx->global_buffer,
				"%s: %s", "Cannot open file", ssl_cert);
		// XXX: perhaps we should read the file ourselves for better messages
		else if (!SSL_use_certificate_file (s->ssl, path, SSL_FILETYPE_PEM)
			|| !SSL_use_PrivateKey_file (s->ssl, path, SSL_FILETYPE_PEM))
			buffer_send_error (s->ctx, s->ctx->global_buffer,
				"%s: %s", "Setting the SSL client certificate failed",
				ERR_error_string (ERR_get_error (), NULL));
		free (path);
	}

	SSL_set_connect_state (s->ssl);
	if (!SSL_set_fd (s->ssl, s->irc_fd))
		goto error_ssl_3;
	// Avoid SSL_write() returning SSL_ERROR_WANT_READ
	SSL_set_mode (s->ssl, SSL_MODE_AUTO_RETRY);

	switch (xssl_get_error (s->ssl, SSL_connect (s->ssl), &error_info))
	{
	case SSL_ERROR_NONE:
		return true;
	case SSL_ERROR_ZERO_RETURN:
		error_info = "server closed the connection";
	default:
		break;
	}

error_ssl_3:
	SSL_free (s->ssl);
	s->ssl = NULL;
error_ssl_2:
	SSL_CTX_free (s->ssl_ctx);
	s->ssl_ctx = NULL;
error_ssl_1:
	// XXX: these error strings are really nasty; also there could be
	//   multiple errors on the OpenSSL stack.
	if (!error_info)
		error_info = ERR_error_string (ERR_get_error (), NULL);
	error_set (e, "%s: %s", "could not initialize SSL", error_info);
	return false;
}

static bool
irc_establish_connection (struct server *s,
	const char *host, const char *port, struct error **e)
{
	struct addrinfo gai_hints, *gai_result, *gai_iter;
	memset (&gai_hints, 0, sizeof gai_hints);
	gai_hints.ai_socktype = SOCK_STREAM;

	int err = getaddrinfo (host, port, &gai_hints, &gai_result);
	if (err)
	{
		error_set (e, "%s: %s: %s",
			"connection failed", "getaddrinfo", gai_strerror (err));
		return false;
	}

	int sockfd;
	for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
	{
		sockfd = socket (gai_iter->ai_family,
			gai_iter->ai_socktype, gai_iter->ai_protocol);
		if (sockfd == -1)
			continue;
		set_cloexec (sockfd);

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

		const char *real_host = host;

		// Let's try to resolve the address back into a real hostname;
		// we don't really need this, so we can let it quietly fail
		char buf[NI_MAXHOST];
		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, port);
		buffer_send_status (s->ctx, s->buffer, "Connecting to %s...", address);
		free (address);

		if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen))
			break;

		xclose (sockfd);
	}

	freeaddrinfo (gai_result);

	if (!gai_iter)
	{
		error_set (e, "connection failed");
		return false;
	}

	s->irc_fd = sockfd;
	return true;
}

// --- More readline funky stuff -----------------------------------------------

static char *
make_unseen_prefix (struct app_context *ctx)
{
	struct str active_buffers;
	str_init (&active_buffers);

	size_t i = 0;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
	{
		i++;
		if (!iter->unseen_messages_count)
			continue;

		if (active_buffers.len)
			str_append_c (&active_buffers, ',');
		str_append_printf (&active_buffers, "%zu", i);
	}

	if (active_buffers.len)
		return str_steal (&active_buffers);

	str_free (&active_buffers);
	return NULL;
}

static void
make_prompt (struct app_context *ctx, struct str *output)
{
	struct buffer *buffer = ctx->current_buffer;
	if (!soft_assert (buffer))
		return;

	str_append_c (output, '[');

	char *unseen_prefix = make_unseen_prefix (ctx);
	if (unseen_prefix)
		str_append_printf (output, "(%s) ", unseen_prefix);
	free (unseen_prefix);

	str_append_printf (output, "%d:%s",
		buffer_get_index (ctx, buffer), buffer->name);
	if (buffer->type == BUFFER_CHANNEL && *buffer->channel->mode)
		str_append_printf (output, "(%s)", buffer->channel->mode);

	if (buffer != ctx->global_buffer)
	{
		struct server *s = buffer->server;
		str_append_c (output, ' ');
		if (s->irc_fd == -1)
			str_append (output, "(disconnected)");
		else
		{
			str_append (output, s->irc_user->nickname);
			if (*s->irc_user_mode)
				str_append_printf (output, "(%s)", s->irc_user_mode);
		}
	}

	str_append_c (output, ']');
}

static void
refresh_prompt (struct app_context *ctx)
{
	bool have_attributes = !!get_attribute_printer (stdout);

	struct str prompt;
	str_init (&prompt);
	make_prompt (ctx, &prompt);
	str_append_c (&prompt, ' ');

	// After building the new prompt, replace the old one
	free (ctx->readline_prompt);

	if (!have_attributes)
		ctx->readline_prompt = xstrdup (prompt.str);
	else
	{
		// XXX: to be completely correct, we should use tputs, but we cannot
		ctx->readline_prompt = xstrdup_printf ("%c%s%c%s%c%s%c",
			RL_PROMPT_START_IGNORE, ctx->attrs[ATTR_PROMPT],
			RL_PROMPT_END_IGNORE,
			prompt.str,
			RL_PROMPT_START_IGNORE, ctx->attrs[ATTR_RESET],
			RL_PROMPT_END_IGNORE);
	}
	str_free (&prompt);

	// First reset the prompt to work around a bug in readline
	rl_set_prompt ("");
	if (ctx->readline_prompt_shown)
		rl_redisplay ();

	rl_set_prompt (ctx->readline_prompt);
	if (ctx->readline_prompt_shown)
		rl_redisplay ();
}

static int
on_readline_goto_buffer (int count, int key)
{
	(void) count;

	int n = UNMETA (key) - '0';
	if (n < 0 || n > 9)
		return 0;

	// There's no buffer zero
	if (n == 0)
		n = 10;

	struct app_context *ctx = g_ctx;
	if (ctx->last_buffer && buffer_get_index (ctx, ctx->current_buffer) == n)
		// Fast switching between two buffers
		buffer_activate (ctx, ctx->last_buffer);
	else if (!buffer_goto (ctx, n == 0 ? 10 : n))
		rl_ding ();
	return 0;
}

static int
on_readline_previous_buffer (int count, int key)
{
	(void) key;

	struct app_context *ctx = g_ctx;
	if (ctx->current_buffer)
		buffer_activate (ctx, buffer_previous (ctx, count));
	return 0;
}

static int
on_readline_next_buffer (int count, int key)
{
	(void) key;

	struct app_context *ctx = g_ctx;
	if (ctx->current_buffer)
		buffer_activate (ctx, buffer_next (ctx, count));
	return 0;
}

static int
on_readline_return (int count, int key)
{
	(void) count;
	(void) key;

	struct app_context *ctx = g_ctx;

	// Let readline pass the line to our input handler
	rl_done = 1;

	// Save readline state
	int saved_point = rl_point;
	int saved_mark = rl_mark;
	char *saved_line = rl_copy_text (0, rl_end);

	// Erase the entire line from screen
	rl_set_prompt ("");
	rl_replace_line ("", 0);
	rl_redisplay ();
	ctx->readline_prompt_shown = false;

	// Restore readline state
	rl_set_prompt (ctx->readline_prompt);
	rl_replace_line (saved_line, 0);
	rl_point = saved_point;
	rl_mark = saved_mark;
	free (saved_line);
	return 0;
}

static void
app_readline_bind_meta (char key, rl_command_func_t cb)
{
	// This one seems to actually work
	char keyseq[] = { '\\', 'e', key, 0 };
	rl_bind_keyseq (keyseq, cb);
#if 0
	// While this one only fucks up UTF-8
	// Tested with urxvt and xterm, on Debian Jessie/Arch, default settings
	// \M-<key> behaves exactly the same
	rl_bind_key (META (key), cb);
#endif
}

static int
init_readline (void)
{
	// XXX: maybe use rl_make_bare_keymap() and start from there;
	//   our dear user could potentionally rig things up in a way that might
	//   result in some funny unspecified behaviour

	rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1);
	rl_add_defun ("next-buffer", on_readline_next_buffer, -1);

	// Redefine M-0 through M-9 to switch buffers
	for (int i = 0; i <= 9; i++)
		app_readline_bind_meta ('0' + i, on_readline_goto_buffer);

	rl_bind_keyseq ("\\C-p", rl_named_function ("previous-buffer"));
	rl_bind_keyseq ("\\C-n", rl_named_function ("next-buffer"));
	app_readline_bind_meta ('p', rl_named_function ("previous-history"));
	app_readline_bind_meta ('n', rl_named_function ("next-history"));

	// We need to hide the prompt first
	rl_bind_key (RETURN, on_readline_return);

	return 0;
}

// --- 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
			ctcp_intra_decode (m.str + my_start, i - my_start, &chunk->text);
		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 ();
		ctcp_intra_decode (m.str + start, m.len - start, &chunk->text);
		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);
}

// --- Input handling ----------------------------------------------------------

// TODO: we will need a proper mode parser; to be shared with kike
// TODO: we alse definitely need to parse server capability messages

static struct buffer *
irc_get_buffer_for_message (struct server *s,
	const struct irc_message *msg, const char *target)
{
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
	if (irc_is_channel (s, target))
	{
		struct channel *channel = str_map_find (&s->irc_channels, target);
		hard_assert ((channel && buffer) ||
			(channel && !buffer) || (!channel && !buffer));

		// This is weird
		if (!channel)
			return NULL;
	}
	else if (!buffer)
	{
		// Implying that the target is us

		// Don't make user buffers for servers (they can send NOTICEs)
		if (!irc_find_userhost (msg->prefix))
			return s->buffer;

		char *nickname = irc_cut_nickname (msg->prefix);
		buffer = irc_get_or_make_user_buffer (s, nickname);
		free (nickname);
	}
	return buffer;
}

static bool
irc_is_highlight (struct server *s, const char *message)
{
	// Well, this is rather crude but it should make most users happy.
	// Ideally we could do this at least in proper Unicode.
	char *copy = xstrdup (message);
	for (char *p = copy; *p; p++)
		*p = irc_tolower (*p);

	char *nick = xstrdup (s->irc_user->nickname);
	for (char *p = nick; *p; p++)
		*p = irc_tolower (*p);

	// Special characters allowed in nicknames by RFC 2812: []\`_^{|} and -
	// Also excluded from the ASCII: common user channel prefixes: +%@&~
	const char *delimiters = ",.;:!?()<>/=#$* \t\r\n\v\f\"'";

	bool result = false;
	char *save = NULL;
	for (char *token = strtok_r (copy, delimiters, &save);
		token; token = strtok_r (NULL, delimiters, &save))
		if (!strcmp (token, nick))
		{
			result = true;
			break;
		}

	free (copy);
	free (nick);
	return result;
}

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

static void
irc_handle_join (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 1)
		return;

	const char *channel_name = msg->params.vector[0];
	if (!irc_is_channel (s, channel_name))
		return;

	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
	hard_assert ((channel && buffer) ||
		(channel && !buffer) || (!channel && !buffer));

	// We've joined a new channel
	if (!channel && irc_is_this_us (s, msg->prefix))
	{
		buffer = buffer_new ();
		buffer->type = BUFFER_CHANNEL;
		buffer->name = xstrdup (channel_name);
		buffer->server = s;
		buffer->channel = channel =
			irc_make_channel (s, xstrdup (channel_name));
		LIST_APPEND_WITH_TAIL (s->ctx->buffers, s->ctx->buffers_tail, buffer);
		str_map_set (&s->irc_buffer_map, channel->name, buffer);

		buffer_activate (s->ctx, buffer);
	}

	// This is weird, ignoring
	if (!channel)
		return;

	// Get or make a user object
	char *nickname = irc_cut_nickname (msg->prefix);
	struct user *user = str_map_find (&s->irc_users, nickname);
	if (!user)
		user = irc_make_user (s, nickname);
	else
	{
		user = user_ref (user);
		free (nickname);
	}

	// Link the user with the channel
	struct user_channel *user_channel = user_channel_new ();
	user_channel->channel = channel;
	LIST_PREPEND (user->channels, user_channel);

	struct channel_user *channel_user = channel_user_new ();
	channel_user->user = user;
	channel_user->modes = xstrdup ("");
	LIST_PREPEND (channel->users, channel_user);

	// Finally log the message
	if (buffer)
	{
		buffer_send (s->ctx, buffer, BUFFER_LINE_JOIN, 0,
			.who    = irc_to_utf8 (s->ctx, msg->prefix),
			.object = irc_to_utf8 (s->ctx, channel_name));
	}
}

static void
irc_handle_kick (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 2)
		return;

	const char *channel_name = msg->params.vector[0];
	const char *target = msg->params.vector[1];
	if (!irc_is_channel (s, channel_name)
	 || irc_is_channel (s, target))
		return;

	const char *message = "";
	if (msg->params.len > 2)
		message = msg->params.vector[2];

	struct user *user = str_map_find (&s->irc_users, target);
	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
	hard_assert ((channel && buffer) ||
		(channel && !buffer) || (!channel && !buffer));

	// It would be is weird for this to be false
	if (user && channel)
		irc_remove_user_from_channel (user, channel);

	if (buffer)
	{
		buffer_send (s->ctx, buffer, BUFFER_LINE_KICK, 0,
			.who    = irc_to_utf8 (s->ctx, msg->prefix),
			.object = irc_to_utf8 (s->ctx, target),
			.reason = irc_to_utf8 (s->ctx, message));
	}
}

static void
irc_handle_mode (struct server *s, const struct irc_message *msg)
{
	// TODO: parse the mode change and apply it
	// TODO: log a message
}

static void
irc_handle_nick (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 1)
		return;

	const char *new_nickname = msg->params.vector[0];

	char *nickname = irc_cut_nickname (msg->prefix);
	struct user *user = str_map_find (&s->irc_users, nickname);
	free (nickname);
	if (!user)
		return;

	// What the fuck
	// TODO: probably log a message and force a reconnect
	if (str_map_find (&s->irc_users, new_nickname))
		return;

	// Log a message in any PM buffer and rename it;
	// we may even have one for ourselves
	struct buffer *pm_buffer =
		str_map_find (&s->irc_buffer_map, user->nickname);
	if (pm_buffer)
	{
		str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer);
		str_map_set (&s->irc_buffer_map, user->nickname, NULL);

		char *who = irc_is_this_us (s, msg->prefix)
			? irc_to_utf8 (s->ctx, msg->prefix)
			: NULL;
		buffer_send (s->ctx, pm_buffer, BUFFER_LINE_NICK, 0,
			.who    = who,
			.object = irc_to_utf8 (s->ctx, new_nickname));
		// TODO: use a full weechat-style buffer name here
		buffer_rename (s->ctx, pm_buffer, new_nickname);
	}

	if (irc_is_this_us (s, msg->prefix))
	{
		// Log a message in all open buffers on this server
		struct str_map_iter iter;
		str_map_iter_init (&iter, &s->irc_buffer_map);
		struct buffer *buffer;
		while ((buffer = str_map_iter_next (&iter)))
		{
			// We've already done that
			if (buffer == pm_buffer)
				continue;

			buffer_send (s->ctx, buffer, BUFFER_LINE_NICK, 0,
				.object = irc_to_utf8 (s->ctx, new_nickname));
		}
	}
	else
	{
		// Log a message in all channels the user is in
		LIST_FOR_EACH (struct user_channel, iter, user->channels)
		{
			struct buffer *buffer =
				str_map_find (&s->irc_buffer_map, iter->channel->name);
			hard_assert (buffer != NULL);
			buffer_send (s->ctx, buffer, BUFFER_LINE_NICK, 0,
				.who    = irc_to_utf8 (s->ctx, msg->prefix),
				.object = irc_to_utf8 (s->ctx, new_nickname));
		}
	}

	// Finally rename the user
	str_map_set (&s->irc_users, new_nickname, user_ref (user));
	str_map_set (&s->irc_users, user->nickname, NULL);

	free (user->nickname);
	user->nickname = xstrdup (new_nickname);

	// We might have renamed ourselves
	refresh_prompt (s->ctx);
}

static void
irc_handle_ctcp_reply (struct server *s,
	const struct irc_message *msg, struct ctcp_chunk *chunk)
{
	char *nickname      = irc_cut_nickname (msg->prefix);
	char *nickname_utf8 = irc_to_utf8 (s->ctx, nickname);
	char *tag_utf8      = irc_to_utf8 (s->ctx, chunk->tag.str);
	char *text_utf8     = irc_to_utf8 (s->ctx, chunk->text.str);

	buffer_send_status (s->ctx, s->buffer,
		"CTCP reply from %s: %s %s", nickname_utf8, tag_utf8, text_utf8);

	free (nickname);
	free (nickname_utf8);
	free (tag_utf8);
	free (text_utf8);
}

static void
irc_handle_notice_text (struct server *s,
	const struct irc_message *msg, struct str *text)
{
	const char *target = msg->params.vector[0];
	struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);

	if (buffer)
	{
		// TODO: some more obvious indication of highlights
		int flags = irc_is_highlight (s, text->str)
			? BUFFER_LINE_HIGHLIGHT
			: 0;
		buffer_send (s->ctx, buffer, BUFFER_LINE_NOTICE, flags,
			.who  = irc_to_utf8 (s->ctx, msg->prefix),
			.text = irc_to_utf8 (s->ctx, text->str));
	}
}

static void
irc_handle_notice (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 2)
		return;

	// This ignores empty messages which we should never receive anyway
	struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
	LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
		if (!iter->is_extended)
			irc_handle_notice_text (s, msg, &iter->text);
		else
			irc_handle_ctcp_reply (s, msg, iter);
	ctcp_destroy (chunks);
}

static void
irc_handle_part (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 1)
		return;

	const char *channel_name = msg->params.vector[0];
	if (!irc_is_channel (s, channel_name))
		return;

	const char *message = "";
	if (msg->params.len > 1)
		message = msg->params.vector[1];

	char *nickname = irc_cut_nickname (msg->prefix);
	struct user *user = str_map_find (&s->irc_users, nickname);
	free (nickname);

	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
	hard_assert ((channel && buffer) ||
		(channel && !buffer) || (!channel && !buffer));

	// It would be is weird for this to be false
	if (user && channel)
		irc_remove_user_from_channel (user, channel);

	if (buffer)
	{
		buffer_send (s->ctx, buffer, BUFFER_LINE_PART, 0,
			.who    = irc_to_utf8 (s->ctx, msg->prefix),
			.object = irc_to_utf8 (s->ctx, channel_name),
			.reason = irc_to_utf8 (s->ctx, message));
	}
}

static void
irc_handle_ping (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len)
		irc_send (s, "PONG :%s", msg->params.vector[0]);
	else
		irc_send (s, "PONG");
}

static char *
ctime_now (char buf[26])
{
	struct tm tm_;
	time_t now = time (NULL);
	if (!asctime_r (localtime_r (&now, &tm_), buf))
		return NULL;

	// Annoying thing
	*strchr (buf, '\n') = '\0';
	return buf;
}

static void irc_send_ctcp_reply (struct server *s, const char *recipient,
	const char *format, ...) ATTRIBUTE_PRINTF (3, 4);

static void
irc_send_ctcp_reply (struct server *s,
	const char *recipient, const char *format, ...)
{
	struct str m;
	str_init (&m);

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

	irc_send (s, "NOTICE %s :\x01%s\x01", recipient, m.str);

	char *text_utf8      = irc_to_utf8 (s->ctx, m.str);
	char *recipient_utf8 = irc_to_utf8 (s->ctx, recipient);
	str_free (&m);

	buffer_send_status (s->ctx, s->buffer,
		"CTCP reply to %s: %s", recipient_utf8, text_utf8);
	free (text_utf8);
	free (recipient_utf8);
}

static void
irc_handle_ctcp_request (struct server *s,
	const struct irc_message *msg, struct ctcp_chunk *chunk)
{
	char *nickname      = irc_cut_nickname (msg->prefix);
	char *nickname_utf8 = irc_to_utf8 (s->ctx, nickname);
	char *tag_utf8      = irc_to_utf8 (s->ctx, chunk->tag.str);

	buffer_send_status (s->ctx, s->buffer,
		"CTCP requested by %s: %s", nickname_utf8, tag_utf8);

	const char *target = msg->params.vector[0];
	const char *recipient = nickname;
	if (irc_is_channel (s, target))
		recipient = target;

	if (!strcmp (chunk->tag.str, "CLIENTINFO"))
		irc_send_ctcp_reply (s, recipient, "CLIENTINFO %s %s %s %s",
			"PING", "VERSION", "TIME", "CLIENTINFO");
	else if (!strcmp (chunk->tag.str, "PING"))
		irc_send_ctcp_reply (s, recipient, "PING %s", chunk->text.str);
	else if (!strcmp (chunk->tag.str, "VERSION"))
	{
		struct utsname info;
		if (uname (&info))
			LOG_LIBC_FAILURE ("uname");
		else
			irc_send_ctcp_reply (s, recipient, "VERSION %s %s on %s %s",
				PROGRAM_NAME, PROGRAM_VERSION, info.sysname, info.machine);
	}
	else if (!strcmp (chunk->tag.str, "TIME"))
	{
		char buf[26];
		if (!ctime_now (buf))
			LOG_LIBC_FAILURE ("asctime_r");
		else
			irc_send_ctcp_reply (s, recipient, "TIME %s", buf);
	}

	free (nickname);
	free (nickname_utf8);
	free (tag_utf8);
}

static void
irc_handle_privmsg_text (struct server *s,
	const struct irc_message *msg, struct str *text, bool is_action)
{
	const char *target = msg->params.vector[0];
	struct buffer *buffer = irc_get_buffer_for_message (s, msg, target);

	if (buffer)
	{
		// TODO: some more obvious indication of highlights
		int flags = irc_is_highlight (s, text->str)
			? BUFFER_LINE_HIGHLIGHT
			: 0;
		enum buffer_line_type type = is_action
			? BUFFER_LINE_ACTION
			: BUFFER_LINE_PRIVMSG;
		buffer_send (s->ctx, buffer, type, flags,
			.who  = irc_to_utf8 (s->ctx, msg->prefix),
			.text = irc_to_utf8 (s->ctx, text->str));
	}
}

static void
irc_handle_privmsg (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 2)
		return;

	// This ignores empty messages which we should never receive anyway
	struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
	LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
		if (!iter->is_extended)
			irc_handle_privmsg_text (s, msg, &iter->text, false);
		else if (!strcmp (iter->tag.str, "ACTION"))
			irc_handle_privmsg_text (s, msg, &iter->text, true);
		else
			irc_handle_ctcp_request (s, msg, iter);
	ctcp_destroy (chunks);
}

static void
irc_handle_quit (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix)
		return;

	// What the fuck
	if (irc_is_this_us (s, msg->prefix))
		return;

	char *nickname = irc_cut_nickname (msg->prefix);
	struct user *user = str_map_find (&s->irc_users, nickname);
	free (nickname);
	if (!user)
		return;

	const char *message = "";
	if (msg->params.len > 0)
		message = msg->params.vector[0];

	// Log a message in any PM buffer
	struct buffer *buffer =
		str_map_find (&s->irc_buffer_map, user->nickname);
	if (buffer)
	{
		buffer_send (s->ctx, buffer, BUFFER_LINE_QUIT, 0,
			.who    = irc_to_utf8 (s->ctx, msg->prefix),
			.reason = irc_to_utf8 (s->ctx, message));

		// TODO: set some kind of a flag in the buffer and when the user
		//   reappers on a channel (JOIN), log a "is back online" message.
		//   Also set this flag when we receive a "no such nick" numeric
		//   and reset it when we send something to the buffer.
	}

	// Log a message in all channels the user is in
	LIST_FOR_EACH (struct user_channel, iter, user->channels)
	{
		buffer = str_map_find (&s->irc_buffer_map, iter->channel->name);
		if (buffer)
			buffer_send (s->ctx, buffer, BUFFER_LINE_QUIT, 0,
				.who    = irc_to_utf8 (s->ctx, msg->prefix),
				.reason = irc_to_utf8 (s->ctx, message));

		// This destroys "iter" which doesn't matter to us
		irc_remove_user_from_channel (user, iter->channel);
	}
}

static void
irc_handle_topic (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 2)
		return;

	const char *channel_name = msg->params.vector[0];
	const char *topic = msg->params.vector[1];
	if (!irc_is_channel (s, channel_name))
		return;

	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel_name);
	hard_assert ((channel && buffer) ||
		(channel && !buffer) || (!channel && !buffer));

	// It would be is weird for this to be false
	if (channel)
	{
		free (channel->topic);
		channel->topic = xstrdup (topic);
	}

	if (buffer)
	{
		buffer_send (s->ctx, buffer, BUFFER_LINE_TOPIC, 0,
			.who  = irc_to_utf8 (s->ctx, msg->prefix),
			.text = irc_to_utf8 (s->ctx, topic));
	}
}

static struct irc_handler
{
	char *name;
	void (*handler) (struct server *s, const struct irc_message *msg);
}
g_irc_handlers[] =
{
	// This list needs to stay sorted
	{ "JOIN",    irc_handle_join    },
	{ "KICK",    irc_handle_kick    },
	{ "MODE",    irc_handle_mode    },
	{ "NICK",    irc_handle_nick    },
	{ "NOTICE",  irc_handle_notice  },
	{ "PART",    irc_handle_part    },
	{ "PING",    irc_handle_ping    },
	{ "PRIVMSG", irc_handle_privmsg },
	{ "QUIT",    irc_handle_quit    },
	{ "TOPIC",   irc_handle_topic   },
};

static int
irc_handler_cmp_by_name (const void *a, const void *b)
{
	const struct irc_handler *first  = a;
	const struct irc_handler *second = b;
	return strcasecmp_ascii (first->name, second->name);
}

static bool
irc_try_parse_word_for_userhost (struct server *s, const char *word)
{
	regex_t re;
	int err = regcomp (&re, "^[^!@]+!([^!@]+@[^!@]+)$", REG_EXTENDED);
	if (!soft_assert (!err))
		return false;

	regmatch_t matches[2];
	bool result = false;
	if (!regexec (&re, word, 2, matches, 0))
	{
		free (s->irc_user_host);
		s->irc_user_host = xstrndup (word + matches[1].rm_so,
			matches[1].rm_eo - matches[1].rm_so);
		result = true;
	}
	regfree (&re);
	return result;
}

static void
irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
{
	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (m, ' ', &v);
	for (size_t i = 0; i < v.len; i++)
		if (irc_try_parse_word_for_userhost (s, v.vector[i]))
			break;
	str_vector_free (&v);
}

static void
irc_process_numeric (struct server *s,
	const struct irc_message *msg, unsigned long numeric)
{
	// Numerics typically have human-readable information
	// TODO: try to output certain replies in more specific buffers

	// Get rid of the first parameter, if there's any at all,
	// as it contains our nickname and is of no practical use to the user
	struct str_vector copy;
	str_vector_init (&copy);
	str_vector_add_vector (&copy, msg->params.vector + !!msg->params.len);

	// Join the parameter vector back, recode it to our internal encoding
	// and send it to the server buffer
	char *reconstructed = join_str_vector (&copy, ' ');
	str_vector_free (&copy);
	buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0,
		.text = irc_to_utf8 (s->ctx, reconstructed));
	free (reconstructed);

	switch (numeric)
	{
	case IRC_RPL_WELCOME:
		// We still issue a USERHOST anyway as this is in general unreliable
		if (msg->params.len == 2)
			irc_try_parse_welcome_for_userhost (s, msg->params.vector[1]);
		break;
	case IRC_RPL_ISUPPORT:
		// TODO: parse this, mainly PREFIX; see
		//   http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt
		break;
	case IRC_RPL_NAMREPLY:
		// TODO: find the channel and if found, push nicks to names_buf
		break;
	case IRC_RPL_ENDOFNAMES:
		// TODO: find the channel and if found, overwrite users;
		//   however take care to combine channel user modes
		break;
	case IRC_ERR_NICKNAMEINUSE:
		// TODO: if not connected yet (irc_ready), use a different nick;
		//   either use a number suffix, or accept commas in "nickname" config
		break;
	}
}

static void
irc_process_message (const struct irc_message *msg,
	const char *raw, void *user_data)
{
	struct server *s = user_data;

	if (g_debug_mode)
	{
		struct app_readline_state state;
		if (s->ctx->readline_prompt_shown)
			app_readline_hide (&state);

		char *term = irc_to_term (s->ctx, raw);
		fprintf (stderr, "[IRC] ==> \"%s\"\n", term);
		free (term);

		if (s->ctx->readline_prompt_shown)
			app_readline_restore (&state, s->ctx->readline_prompt);
	}

	// XXX: or is the 001 numeric enough?  For what?
	if (!s->irc_ready && (!strcasecmp (msg->command, "MODE")
		|| !strcasecmp (msg->command, "376")    // RPL_ENDOFMOTD
		|| !strcasecmp (msg->command, "422")))  // ERR_NOMOTD
	{
		// XXX: should we really print this?
		buffer_send_status (s->ctx, s->buffer, "Successfully connected");
		s->irc_ready = true;
		refresh_prompt (s->ctx);

		// TODO: parse any response and store the result for us in app_context;
		//   this enables proper message splitting on output;
		//   we can also use WHOIS if it's not supported (optional by RFC 2812)
		irc_send (s, "USERHOST %s", s->irc_user->nickname);

		const char *autojoin = get_config_string (s->ctx, "server.autojoin");
		if (autojoin)
			irc_send (s, "JOIN :%s", autojoin);
	}

	struct irc_handler key = { .name = msg->command };
	struct irc_handler *handler = bsearch (&key, g_irc_handlers,
		N_ELEMENTS (g_irc_handlers), sizeof key, irc_handler_cmp_by_name);
	if (handler)
		handler->handler (s, msg);

	unsigned long numeric;
	if (xstrtoul (&numeric, msg->command, 10))
		irc_process_numeric (s, msg, numeric);
}

// --- Message autosplitting magic ---------------------------------------------

// This is the most basic acceptable algorithm; something like ICU with proper
// locale specification would be needed to make it work better.

static size_t
wrap_text_for_single_line (const char *text, size_t text_len,
	size_t line_len, struct str *output)
{
	int eaten = 0;

	// First try going word by word
	const char *word_start;
	const char *word_end = text + strcspn (text, " ");
	size_t word_len = word_end - text;
	while (line_len && word_len <= line_len)
	{
		if (word_len)
		{
			str_append_data (output, text, word_len);

			text += word_len;
			eaten += word_len;
			line_len -= word_len;
		}

		// Find the next word's end
		word_start = text + strspn (text, " ");
		word_end = word_start + strcspn (word_start, " ");
		word_len = word_end - text;
	}

	if (eaten)
		// Discard whitespace between words if split
		return eaten + (word_start - text);

	// And if that doesn't help, cut the longest valid block of characters
	while (true)
	{
		const char *next = utf8_next (text, text_len - eaten);
		hard_assert (next);

		size_t char_len = next - text;
		if (char_len > line_len)
			break;

		str_append_data (output, text, char_len);

		text += char_len;
		eaten += char_len;
		line_len -= char_len;
	}
	return eaten;
}

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

static bool
wrap_message (const char *message,
	int line_max, struct str_vector *output, struct error **e)
{
	if (line_max <= 0)
		goto error;

	for (size_t message_left = strlen (message); message_left; )
	{
		struct str m;
		str_init (&m);

		size_t eaten = wrap_text_for_single_line (message,
			MIN ((size_t) line_max, message_left), message_left, &m);
		if (!eaten)
		{
			str_free (&m);
			goto error;
		}

		str_vector_add_owned (output, str_steal (&m));
		message += eaten;
		message_left -= eaten;
	}
	return true;

error:
	// Well, that's just weird
	error_set (e,
		"Message splitting was unsuccessful as there was "
		"too little room for UTF-8 characters");
	return false;
}

/// Automatically splits messages that arrive at other clients with our prefix
/// so that they don't arrive cut off by the server
static bool
irc_autosplit_message (struct server *s, const char *message,
	int fixed_part, struct str_vector *output, struct error **e)
{
	// :<nick>!<user>@<host> <fixed-part><message>
	int space_in_one_message = 0;
	if (s->irc_user_host)
		space_in_one_message = 510
			- 1 - (int) strlen (s->irc_user->nickname)
			- 1 - (int) strlen (s->irc_user_host)
			- 1 - fixed_part;

	// However we don't always have the full info for message splitting
	if (!space_in_one_message)
		str_vector_add (output, message);
	else if (!wrap_message (message, space_in_one_message, output, e))
		return false;
	return true;
}

struct send_autosplit_args;

typedef void (*send_autosplit_logger_fn) (struct server *s,
	struct send_autosplit_args *args, struct buffer *buffer, const char *line);

struct send_autosplit_args
{
	const char *command;                ///< E.g. PRIVMSG or NOTICE
	const char *target;                 ///< User or channel
	const char *message;                ///< A message to be autosplit
	send_autosplit_logger_fn logger;    ///< Logger for all resulting lines
	const char *prefix;                 ///< E.g. "\x01ACTION"
	const char *suffix;                 ///< E.g. "\x01"
};

static void
send_autosplit_message (struct server *s, struct send_autosplit_args a)
{
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, a.target);
	int fixed_part = strlen (a.command) + 1 + strlen (a.target) + 1 + 1
		+ strlen (a.prefix) + strlen (a.suffix);

	struct str_vector lines;
	str_vector_init (&lines);
	struct error *e = NULL;
	if (!irc_autosplit_message (s, a.message, fixed_part, &lines, &e))
	{
		buffer_send_error (s->ctx,
			buffer ? buffer : s->buffer, "%s", e->message);
		error_free (e);
		goto end;
	}

	for (size_t i = 0; i < lines.len; i++)
	{
		irc_send (s, "%s %s :%s%s%s", a.command, a.target,
			a.prefix, lines.vector[i], a.suffix);
		a.logger (s, &a, buffer, lines.vector[i]);
	}
end:
	str_vector_free (&lines);
}

static void
log_outcoming_action (struct server *s,
	struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
	(void) a;

	if (buffer)
		buffer_send (s->ctx, buffer, BUFFER_LINE_ACTION, 0,
			.who  = irc_to_utf8 (s->ctx, s->irc_user->nickname),
			.text = irc_to_utf8 (s->ctx, line));

	// This can only be sent from a user or channel buffer
}

#define SEND_AUTOSPLIT_ACTION(s, target, message)                              \
	send_autosplit_message ((s), (struct send_autosplit_args)                  \
		{ "PRIVMSG", (target), (message), log_outcoming_action,                \
		  "\x01" "ACTION ", "\x01" })

static void
log_outcoming_privmsg (struct server *s,
	struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
	if (buffer)
		buffer_send (s->ctx, buffer, BUFFER_LINE_PRIVMSG, 0,
			.who  = irc_to_utf8 (s->ctx, s->irc_user->nickname),
			.text = irc_to_utf8 (s->ctx, line));
	else
		// TODO: fix logging and encoding
		buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0,
			.text = xstrdup_printf ("MSG(%s): %s", a->target, line));
}

#define SEND_AUTOSPLIT_PRIVMSG(s, target, message)                             \
	send_autosplit_message ((s), (struct send_autosplit_args)                  \
		{ "PRIVMSG", (target), (message), log_outcoming_privmsg, "", "" })

static void
log_outcoming_notice (struct server *s,
	struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
	if (buffer)
		buffer_send (s->ctx, buffer, BUFFER_LINE_NOTICE, 0,
			.who  = irc_to_utf8 (s->ctx, s->irc_user->nickname),
			.text = irc_to_utf8 (s->ctx, line));
	else
		// TODO: fix logging and encoding
		buffer_send (s->ctx, s->buffer, BUFFER_LINE_STATUS, 0,
			.text = xstrdup_printf ("Notice -> %s: %s", a->target, line));
}

#define SEND_AUTOSPLIT_NOTICE(s, target, message)                              \
	send_autosplit_message ((s), (struct send_autosplit_args)                  \
		{ "NOTICE", (target), (message), log_outcoming_notice, "", "" })

// --- User input handling -----------------------------------------------------

static bool handle_command_help (struct app_context *, char *);

/// Cuts the longest non-whitespace portion of text and advances the pointer
static char *
cut_word (char **s)
{
	char *start = *s;
	size_t word_len = strcspn (*s, " \t");
	char *end = start + word_len;
	*s = end + strspn (end, " \t");
	*end = '\0';
	return start;
}

static bool
try_handle_buffer_goto (struct app_context *ctx, const char *word)
{
	unsigned long n;
	if (!xstrtoul (&n, word, 10))
		return false;

	if (n > INT_MAX || !buffer_goto (ctx, n))
		buffer_send_error (ctx, ctx->global_buffer,
			"%s: %s", "no such buffer", word);
	return true;
}

static struct buffer *
try_decode_buffer (struct app_context *ctx, const char *word)
{
	unsigned long n;
	struct buffer *buffer = NULL;
	if (xstrtoul (&n, word, 10) && n <= INT_MAX)
		buffer = buffer_at_index (ctx, n);
	if (!buffer)
		buffer = buffer_by_name (ctx, word);
	// TODO: decode the global and server buffers, partial matches
	return buffer;
}

static bool
server_command_check (struct app_context *ctx, const char *action)
{
	if (ctx->current_buffer->type == BUFFER_GLOBAL)
		buffer_send_error (ctx, ctx->current_buffer,
			"Can't do this from a global buffer (%s)", action);
	else
	{
		struct server *s = ctx->current_buffer->server;
		if (s->irc_fd == -1)
			buffer_send_error (ctx, s->buffer, "Not connected");
		else
			return true;
	}
	return false;
}

static void
show_buffers_list (struct app_context *ctx)
{
	buffer_send_status (ctx, ctx->global_buffer, "%s", "");
	buffer_send_status (ctx, ctx->global_buffer, "Buffers list:");

	int i = 1;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
		buffer_send_status (ctx, ctx->global_buffer,
			"  [%d] %s", i++, iter->name);
}

static void
handle_buffer_close (struct app_context *ctx, char *arguments)
{
	struct buffer *buffer = NULL;
	const char *which = NULL;
	if (!*arguments)
		buffer = ctx->current_buffer;
	else
		buffer = try_decode_buffer (ctx, (which = cut_word (&arguments)));

	if (!buffer)
		buffer_send_error (ctx, ctx->global_buffer,
			"%s: %s", "No such buffer", which);
	else if (buffer == ctx->global_buffer)
		buffer_send_error (ctx, ctx->global_buffer,
			"Can't close the global buffer");
	else if (buffer->type == BUFFER_SERVER)
		buffer_send_error (ctx, ctx->global_buffer,
			"Can't close a server buffer");
	else
	{
		if (buffer == ctx->current_buffer)
			buffer_activate (ctx, buffer_next (ctx, 1));
		buffer_remove (ctx, buffer);
	}
}

static bool
handle_command_buffer (struct app_context *ctx, char *arguments)
{
	char *action = cut_word (&arguments);
	if (try_handle_buffer_goto (ctx, action))
		return true;

	// XXX: also build a prefix map?
	// TODO: some subcommand to print N last lines from the buffer
	if (!strcasecmp_ascii (action, "list"))
		show_buffers_list (ctx);
	else if (!strcasecmp_ascii (action, "clear"))
	{
		// TODO
	}
	else if (!strcasecmp_ascii (action, "move"))
	{
		// TODO: unlink the buffer and link it back at index;
		//   we will probably need to extend liberty for this
	}
	else if (!strcasecmp_ascii (action, "close"))
		handle_buffer_close (ctx, arguments);
	else
		return false;

	return true;
}

static bool
handle_command_msg (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "send messages"))
		return true;
	if (!*arguments)
		return false;

	struct server *s = ctx->current_buffer->server;
	char *target = cut_word (&arguments);
	if (!*arguments)
		buffer_send_error (ctx, s->buffer, "No text to send");
	else
		SEND_AUTOSPLIT_PRIVMSG (s, target, arguments);
	return true;
}

static bool
handle_command_query (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "send messages"))
		return true;
	if (!*arguments)
		return false;

	struct server *s = ctx->current_buffer->server;
	char *target = cut_word (&arguments);
	if (irc_is_channel (s, target))
		buffer_send_error (ctx, s->buffer, "Cannot query a channel");
	else if (!*arguments)
		buffer_send_error (ctx, s->buffer, "No text to send");
	else
	{
		buffer_activate (ctx, irc_get_or_make_user_buffer (s, target));
		SEND_AUTOSPLIT_PRIVMSG (s, target, arguments);
	}
	return true;
}

static bool
handle_command_notice (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "send messages"))
		return true;
	if (!*arguments)
		return false;

	struct server *s = ctx->current_buffer->server;
	char *target = cut_word (&arguments);
	if (!*arguments)
		buffer_send_error (ctx, s->buffer, "No text to send");
	else
		SEND_AUTOSPLIT_NOTICE (s, target, arguments);
	return true;
}

static bool
handle_command_ctcp (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "send messages"))
		return true;
	if (!*arguments)
		return false;

	char *target = cut_word (&arguments);
	if (!*arguments)
		return false;

	char *tag = cut_word (&arguments);
	for (char *p = tag; *p; p++)
		*p = toupper_ascii (*p);

	struct server *s = ctx->current_buffer->server;
	if (*arguments)
		irc_send (s, "PRIVMSG %s :\x01%s %s\x01", target, tag, arguments);
	else
		irc_send (s, "PRIVMSG %s :\x01%s\x01", target, tag);

	buffer_send_status (ctx, s->buffer,
		"CTCP query to %s: %s", target, tag);
	return true;
}

static bool
handle_command_me (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "send messages"))
		return true;

	struct server *s = ctx->current_buffer->server;
	if (ctx->current_buffer->type == BUFFER_CHANNEL)
		SEND_AUTOSPLIT_ACTION (s,
			ctx->current_buffer->channel->name, arguments);
	else if (ctx->current_buffer->type == BUFFER_PM)
		SEND_AUTOSPLIT_ACTION (s,
			ctx->current_buffer->user->nickname, arguments);
	else
		buffer_send_error (ctx, s->buffer,
			"Can't do this from a server buffer (%s)",
			"send CTCP actions");
	return true;
}

static bool
handle_command_quit (struct app_context *ctx, char *arguments)
{
	// TODO: multiserver
	struct server *s = &ctx->server;
	if (s->irc_fd != -1)
	{
		if (*arguments)
			irc_send (s, "QUIT :%s", arguments);
		else
			irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
	}
	initiate_quit (ctx);
	return true;
}

static bool
handle_command_join (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "join"))
		return true;

	struct server *s = ctx->current_buffer->server;
	if (*arguments)
		// TODO: check if the arguments are in the form of
		//   "channel(,channel)* key(,key)*"
		irc_send (s, "JOIN %s", arguments);
	else
	{
		if (ctx->current_buffer->type != BUFFER_CHANNEL)
			buffer_send_error (ctx, ctx->current_buffer,
				"%s: %s", "Can't join",
				"no argument given and this buffer is not a channel");
		// TODO: have a better way of checking if we're on the channel
		else if (ctx->current_buffer->channel->users)
			buffer_send_error (ctx, ctx->current_buffer,
				"%s: %s", "Can't join",
				"you already are on the channel");
		else
			// TODO: send the key if known
			irc_send (s, "JOIN %s", ctx->current_buffer->channel->name);
	}
	return true;
}

static bool
handle_command_part (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "part"))
		return true;

	struct server *s = ctx->current_buffer->server;
	if (*arguments)
		// TODO: check if the arguments are in the form of "channel(,channel)*"
		// TODO: make sure to send the reason as one argument
		irc_send (s, "PART %s", arguments);
	else
	{
		if (ctx->current_buffer->type != BUFFER_CHANNEL)
			buffer_send_error (ctx, ctx->current_buffer,
				"%s: %s", "Can't part",
				"no argument given and this buffer is not a channel");
		// TODO: have a better way of checking if we're on the channel
		else if (!ctx->current_buffer->channel->users)
			buffer_send_error (ctx, ctx->current_buffer,
				"%s: %s", "Can't join", "you're not on the channel");
		else
			irc_send (s, "PART %s", ctx->current_buffer->channel->name);
	}
	return true;
}

static bool
handle_command_list (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "list channels"))
		return true;

	struct server *s = ctx->current_buffer->server;
	if (*arguments)
		irc_send (s, "LIST %s", arguments);
	else
		irc_send (s, "LIST");
	return true;
}

static bool
handle_command_nick (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "change nickname"))
		return true;
	if (!*arguments)
		return false;

	struct server *s = ctx->current_buffer->server;
	irc_send (s, "NICK %s", cut_word (&arguments));
	return true;
}

static bool
handle_command_quote (struct app_context *ctx, char *arguments)
{
	if (!server_command_check (ctx, "quote"))
		return true;

	struct server *s = ctx->current_buffer->server;
	irc_send (s, "%s", arguments);
	return true;
}

static struct command_handler
{
	const char *name;
	bool (*handler) (struct app_context *ctx, char *arguments);
	const char *description;
	const char *usage;
}
g_command_handlers[] =
{
	{ "help",    handle_command_help,   "Show help",
	  "[<command> | <option>]" },
	{ "quit",    handle_command_quit,   "Quit the program",
	  "[<message>]" },
	{ "buffer",  handle_command_buffer, "Manage buffers",
	  "list | clear | move | { close [<number> | <name>] } | <number>" },

	{ "msg",     handle_command_msg,    "Send message to a nick or channel",
	  "<target> <message>" },
	{ "query",   handle_command_query,  "Send a private message to a nick",
	  "<nick> <message>" },
	{ "notice",  handle_command_notice, "Send notice to a nick or channel",
	  "<target> <message>" },
	{ "ctcp",    handle_command_ctcp,   "Send a CTCP query",
	  "<target> <tag>" },
	{ "me",      handle_command_me,     "Send a CTCP action",
	  "<message>" },

	{ "join",    handle_command_join,   "Join channels",
	  "[<channel>[,<channel>...]]" },
	{ "part",    handle_command_part,   "Leave channels",
	  "[<channel>[,<channel>...]]" },
#if 0
	{ "cycle",   NULL, "", "" },

	{ "mode",    NULL, "", "" },
	{ "topic",   NULL, "", "" },
	{ "kick",    NULL, "", "" },
	{ "kickban", NULL, "", "" },
	{ "ban",     NULL, "", "" },
	{ "invite",  NULL, "", "" },
#endif

	{ "list",    handle_command_list,   "List channels and their topic",
	  "[<channel>[,<channel>...]] [server]" },
#if 0
	{ "names",   NULL, "", "" },
	{ "who",     NULL, "", "" },
	{ "whois",   NULL, "", "" },

	{ "motd",    NULL, "", "" },
	{ "away",    NULL, "", "" },
#endif
	{ "nick",    handle_command_nick,   "Change current nick",
	  "<nickname>" },
	{ "quote",   handle_command_quote,  "Send a raw command to the server",
	  "<command>" },
};

static bool
handle_command_help (struct app_context *ctx, char *arguments)
{
	if (!*arguments)
	{
		buffer_send_status (ctx, ctx->global_buffer, "%s", "");
		buffer_send_status (ctx, ctx->global_buffer, "Commands:");
		for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
		{
			struct command_handler *handler = &g_command_handlers[i];
			buffer_send_status (ctx, ctx->global_buffer, "  %s: %s",
				handler->name, handler->description);
		}
		return true;
	}

	char *command = cut_word (&arguments);
	for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
	{
		struct command_handler *handler = &g_command_handlers[i];
		if (!strcasecmp_ascii (command, handler->name))
		{
			buffer_send_status (ctx, ctx->global_buffer, "%s", "");
			buffer_send_status (ctx, ctx->global_buffer, "%s: %s",
				handler->name, handler->description);
			buffer_send_status (ctx, ctx->global_buffer, "  Arguments: %s",
				handler->usage);
			return true;
		}
	}

	struct config_item_ *item =
		config_item_get (ctx->config.root, command, NULL);
	if (item)
	{
		struct config_schema *schema = item->schema;
		buffer_send_status (ctx, ctx->global_buffer, "%s", "");
		buffer_send_status (ctx, ctx->global_buffer,
			"Option \"%s\":", command);
		buffer_send_status (ctx, ctx->global_buffer,
			"  Description: %s", schema->comment);
		buffer_send_status (ctx, ctx->global_buffer,
			"  Type: %s", config_item_type_name (schema->type));
		buffer_send_status (ctx, ctx->global_buffer,
			"  Default: %s", schema->default_ ? schema->default_ : "null");

		struct str tmp;
		str_init (&tmp);
		config_item_write (item, false, &tmp);
		buffer_send_status (ctx, ctx->global_buffer,
			"  Current value: %s", tmp.str);
		str_free (&tmp);
		return true;
	}

	buffer_send_error (ctx, ctx->global_buffer,
		"%s: %s", "No such command or option", command);
	return true;
}

static int
command_handler_cmp_by_length (const void *a, const void *b)
{
	const struct command_handler *first  = a;
	const struct command_handler *second = b;
	return strlen (first->name) - strlen (second->name);
}

static void
init_partial_matching_user_command_map (struct str_map *partial)
{
	// Trivially create a partial matching map
	str_map_init (partial);
	partial->key_xfrm = tolower_ascii_strxfrm;

	// We process them from the longest to the shortest one,
	// so that common prefixes favor shorter entries
	struct command_handler *by_length[N_ELEMENTS (g_command_handlers)];
	for (size_t i = 0; i < N_ELEMENTS (by_length); i++)
		by_length[i] = &g_command_handlers[i];
	qsort (by_length, N_ELEMENTS (by_length), sizeof *by_length,
		command_handler_cmp_by_length);

	for (size_t i = N_ELEMENTS (by_length); i--; )
	{
		char *copy = xstrdup (by_length[i]->name);
		for (size_t part = strlen (copy); part; part--)
		{
			copy[part] = '\0';
			str_map_set (partial, copy, by_length[i]);
		}
		free (copy);
	}
}

static void
process_user_command (struct app_context *ctx, char *command)
{
	static bool initialized = false;
	static struct str_map partial;
	if (!initialized)
	{
		init_partial_matching_user_command_map (&partial);
		initialized = true;
	}

	char *name = cut_word (&command);
	if (try_handle_buffer_goto (ctx, name))
		return;

	struct command_handler *handler = str_map_find (&partial, name);
	if (!handler)
		buffer_send_error (ctx, ctx->global_buffer,
			"%s: %s", "No such command", name);
	else if (!handler->handler (ctx, command))
		buffer_send_error (ctx, ctx->global_buffer,
			"%s: /%s %s", "Usage", handler->name, handler->usage);
}

static void
send_message_to_target (struct server *s,
	const char *target, char *message, struct buffer *buffer)
{
	if (s->irc_fd == -1)
	{
		buffer_send_error (s->ctx, buffer, "Not connected");
		return;
	}

	SEND_AUTOSPLIT_PRIVMSG (s, target, message);
}

static void
send_message_to_current_buffer (struct app_context *ctx, char *message)
{
	struct buffer *buffer = ctx->current_buffer;
	hard_assert (buffer != NULL);

	switch (buffer->type)
	{
	case BUFFER_GLOBAL:
	case BUFFER_SERVER:
		buffer_send_error (ctx, buffer, "This buffer is not a channel");
		break;
	case BUFFER_CHANNEL:
		send_message_to_target (buffer->server,
			buffer->channel->name, message, buffer);
		break;
	case BUFFER_PM:
		send_message_to_target (buffer->server,
			buffer->user->nickname, message, buffer);
		break;
	}
}

static void
process_input (struct app_context *ctx, char *user_input)
{
	char *input;
	size_t len;

	if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len)))
		print_error ("character conversion failed for `%s'", "user input");
	else if (input[0] != '/')
		send_message_to_current_buffer (ctx, input);
	else if (input[1] == '/')
		send_message_to_current_buffer (ctx, input + 1);
	else
		process_user_command (ctx, input + 1);

	free (input);
}

// --- Supporting code (continued) ---------------------------------------------

enum irc_read_result
{
	IRC_READ_OK,                        ///< Some data were read successfully
	IRC_READ_EOF,                       ///< The server has closed connection
	IRC_READ_AGAIN,                     ///< No more data at the moment
	IRC_READ_ERROR                      ///< General connection failure
};

static enum irc_read_result
irc_fill_read_buffer_ssl (struct server *s, struct str *buf)
{
	int n_read;
start:
	n_read = SSL_read (s->ssl, buf->str + buf->len,
		buf->alloc - buf->len - 1 /* null byte */);

	const char *error_info = NULL;
	switch (xssl_get_error (s->ssl, n_read, &error_info))
	{
	case SSL_ERROR_NONE:
		buf->str[buf->len += n_read] = '\0';
		return IRC_READ_OK;
	case SSL_ERROR_ZERO_RETURN:
		return IRC_READ_EOF;
	case SSL_ERROR_WANT_READ:
		return IRC_READ_AGAIN;
	case SSL_ERROR_WANT_WRITE:
	{
		// Let it finish the handshake as we don't poll for writability;
		// any errors are to be collected by SSL_read() in the next iteration
		struct pollfd pfd = { .fd = s->irc_fd, .events = POLLOUT };
		soft_assert (poll (&pfd, 1, 0) > 0);
		goto start;
	}
	case XSSL_ERROR_TRY_AGAIN:
		goto start;
	default:
		LOG_FUNC_FAILURE ("SSL_read", error_info);
		return IRC_READ_ERROR;
	}
}

static enum irc_read_result
irc_fill_read_buffer (struct server *s, struct str *buf)
{
	ssize_t n_read;
start:
	n_read = recv (s->irc_fd, buf->str + buf->len,
		buf->alloc - buf->len - 1 /* null byte */, 0);

	if (n_read > 0)
	{
		buf->str[buf->len += n_read] = '\0';
		return IRC_READ_OK;
	}
	if (n_read == 0)
		return IRC_READ_EOF;

	if (errno == EAGAIN)
		return IRC_READ_AGAIN;
	if (errno == EINTR)
		goto start;

	LOG_LIBC_FAILURE ("recv");
	return IRC_READ_ERROR;
}

static bool irc_connect (struct server *s, bool *should_retry, struct error **);
static void irc_queue_reconnect (struct server *s);

static void
irc_cancel_timers (struct server *s)
{
	poller_timer_reset (&s->timeout_tmr);
	poller_timer_reset (&s->ping_tmr);
	poller_timer_reset (&s->reconnect_tmr);
}

static void
on_irc_reconnect_timeout (void *user_data)
{
	struct server *s = user_data;

	struct error *e = NULL;
	bool should_retry = false;
	if (irc_connect (s, &should_retry, &e))
		return;

	buffer_send_error (s->ctx, s->buffer, "%s", e->message);
	error_free (e);

	if (should_retry)
		irc_queue_reconnect (s);
}

static void
irc_queue_reconnect (struct server *s)
{
	// TODO: exponentional backoff
	hard_assert (s->irc_fd == -1);
	buffer_send_status (s->ctx, s->buffer,
		"Trying to reconnect in %ld seconds...", s->ctx->reconnect_delay);
	poller_timer_set (&s->reconnect_tmr, s->ctx->reconnect_delay * 1000);
}

static void
on_irc_disconnected (struct server *s)
{
	// Get rid of the dead socket and related things
	if (s->ssl)
	{
		SSL_free (s->ssl);
		s->ssl = NULL;
		SSL_CTX_free (s->ssl_ctx);
		s->ssl_ctx = NULL;
	}

	xclose (s->irc_fd);
	s->irc_fd = -1;
	s->irc_ready = false;

	user_unref (s->irc_user);
	s->irc_user = NULL;

	free (s->irc_user_mode);
	s->irc_user_mode = NULL;
	free (s->irc_user_host);
	s->irc_user_host = NULL;

	s->irc_event.closed = true;
	poller_fd_reset (&s->irc_event);

	// All of our timers have lost their meaning now
	irc_cancel_timers (s);

	if (s->ctx->quitting)
		try_finish_quit (s->ctx);
	else if (!s->ctx->reconnect)
		// XXX: not sure if we want this in a client
		// FIXME: no, we don't, would need to be changed for multiserver anyway
		initiate_quit (s->ctx);
	else
		irc_queue_reconnect (s);
}

static void
on_irc_ping_timeout (void *user_data)
{
	struct server *s = user_data;
	buffer_send_error (s->ctx, s->buffer, "Connection timeout");
	on_irc_disconnected (s);
}

static void
on_irc_timeout (void *user_data)
{
	// Provoke a response from the server
	struct server *s = user_data;
	irc_send (s, "PING :%s", get_config_string (s->ctx, "server.nickname"));
}

static void
irc_reset_connection_timeouts (struct server *s)
{
	irc_cancel_timers (s);
	poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000);
	poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000);
}

static void
on_irc_readable (const struct pollfd *fd, struct server *s)
{
	if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
		print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);

	(void) set_blocking (s->irc_fd, false);

	struct str *buf = &s->read_buffer;
	enum irc_read_result (*fill_buffer)(struct server *, struct str *)
		= s->ssl
		? irc_fill_read_buffer_ssl
		: irc_fill_read_buffer;
	bool disconnected = false;
	while (true)
	{
		str_ensure_space (buf, 512);
		switch (fill_buffer (s, buf))
		{
		case IRC_READ_AGAIN:
			goto end;
		case IRC_READ_ERROR:
			buffer_send_error (s->ctx, s->buffer,
				"Reading from the IRC server failed");
			disconnected = true;
			goto end;
		case IRC_READ_EOF:
			buffer_send_error (s->ctx, s->buffer,
				"The IRC server closed the connection");
			disconnected = true;
			goto end;
		case IRC_READ_OK:
			break;
		}

		if (buf->len >= (1 << 20))
		{
			buffer_send_error (s->ctx, s->buffer,
				"The IRC server seems to spew out data frantically");
			irc_shutdown (s);
			goto end;
		}
	}
end:
	(void) set_blocking (s->irc_fd, true);
	irc_process_buffer (buf, irc_process_message, s);

	if (disconnected)
		on_irc_disconnected (s);
	else
		irc_reset_connection_timeouts (s);
}

static bool
irc_connect (struct server *s, bool *should_retry, struct error **e)
{
	// TODO: connect asynchronously so that we don't freeze
	struct app_context *ctx = s->ctx;
	*should_retry = true;

	const char *irc_host = get_config_string (ctx, "server.irc_host");
	int64_t irc_port_int = get_config_integer (ctx, "server.irc_port");

	if (!get_config_string (ctx, "server.irc_host"))
	{
		error_set (e, "No hostname specified in configuration");
		*should_retry = false;
		return false;
	}

	const char *socks_host = get_config_string (ctx, "server.socks_host");
	int64_t socks_port_int = get_config_integer (ctx, "server.socks_port");
	const char *socks_username =
		get_config_string (ctx, "server.socks_username");
	const char *socks_password =
		get_config_string (ctx, "server.socks_password");

	// FIXME: use it as a number everywhere, there's no need for named services
	// FIXME: memory leak
	char *irc_port   = xstrdup_printf ("%" PRIi64, irc_port_int);
	char *socks_port = xstrdup_printf ("%" PRIi64, socks_port_int);

	const char *nickname = get_config_string (ctx, "server.nickname");
	const char *username = get_config_string (ctx, "server.username");
	const char *realname = get_config_string (ctx, "server.realname");

	// These are filled automatically if needed
	hard_assert (nickname && username && realname);

	bool use_ssl = get_config_boolean (ctx, "server.ssl");
	if (socks_host)
	{
		char *address = format_host_port_pair (irc_host, irc_port);
		char *socks_address = format_host_port_pair (socks_host, socks_port);
		buffer_send_status (ctx, s->buffer,
			"Connecting to %s via %s...", address, socks_address);
		free (socks_address);
		free (address);

		struct error *error = NULL;
		int fd = socks_connect (socks_host, socks_port, irc_host, irc_port,
			socks_username, socks_password, &error);
		if (fd == -1)
		{
			error_set (e, "%s: %s", "SOCKS connection failed", error->message);
			error_free (error);
			return false;
		}
		s->irc_fd = fd;
	}
	else if (!irc_establish_connection (s, irc_host, irc_port, e))
		return false;

	if (use_ssl && !irc_initialize_ssl (s, e))
	{
		xclose (s->irc_fd);
		s->irc_fd = -1;
		return false;
	}
	buffer_send_status (ctx, s->buffer, "Connection established");

	poller_fd_init (&s->irc_event, &ctx->poller, s->irc_fd);
	s->irc_event.dispatcher = (poller_fd_fn) on_irc_readable;
	s->irc_event.user_data = s;

	poller_fd_set (&s->irc_event, POLLIN);
	irc_reset_connection_timeouts (s);

	irc_send (s, "NICK %s", nickname);
	irc_send (s, "USER %s 8 * :%s", username, realname);

	// XXX: maybe we should wait for the first message from the server
	// FIXME: the user may exist already after we've reconnected. Either
	//   make sure that there's no reference of this nick upon disconnection,
	//   or search in "irc_users" first... or something.
	s->irc_user = irc_make_user (s, xstrdup (nickname));
	s->irc_user_mode = xstrdup ("");
	s->irc_user_host = NULL;
	return true;
}

// --- I/O event handlers ------------------------------------------------------

static void
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
{
	char dummy;
	(void) read (fd->fd, &dummy, 1);

	if (g_termination_requested && !ctx->quitting)
	{
		// There may be a timer set to reconnect to the server
		// TODO: multiserver
		struct server *s = &ctx->server;
		// TODO: a faster timer for quitting
		irc_reset_connection_timeouts (s);

		// FIXME: use a normal quit message
		if (s->irc_fd != -1)
			irc_send (s, "QUIT :Terminated by signal");
		initiate_quit (ctx);
	}

	if (g_winch_received)
	{
		// This fucks up big time on terminals with automatic wrapping such as
		// rxvt-unicode or newer VTE when the current line overflows, however we
		// can't do much about that
		rl_resize_terminal ();
		rl_get_screen_size (&ctx->lines, &ctx->columns);
	}
}

static void
on_tty_readable (const struct pollfd *fd, struct app_context *ctx)
{
	(void) ctx;

	if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
		print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);

	rl_callback_read_char ();
}

static void
on_readline_input (char *line)
{
	if (line)
	{
		if (*line)
			add_history (line);

		process_input (g_ctx, line);
		free (line);
	}
	else
	{
		app_readline_erase_to_bol (g_ctx->readline_prompt);
		rl_ding ();
	}

	// initiate_quit() disables readline; we just wait then
	if (!g_ctx->quitting)
		g_ctx->readline_prompt_shown = true;
}

// --- Configuration loading ---------------------------------------------------

static bool
autofill_user_info (struct app_context *ctx, struct error **e)
{
	const char *nickname = get_config_string (ctx, "server.nickname");
	const char *username = get_config_string (ctx, "server.username");
	const char *realname = get_config_string (ctx, "server.realname");

	if (nickname && username && realname)
		return true;

	// Read POSIX user info and fill the configuration if needed
	struct passwd *pwd = getpwuid (geteuid ());
	if (!pwd)
		FAIL ("cannot retrieve user information: %s", strerror (errno));

	// FIXME: set_config_strings() writes errors on its own
	if (!nickname)
		set_config_string (ctx, "server.nickname", pwd->pw_name);
	if (!username)
		set_config_string (ctx, "server.username", pwd->pw_name);

	// Not all systems have the GECOS field but the vast majority does
	if (!realname)
	{
		char *gecos = pwd->pw_gecos;

		// The first comma, if any, ends the user's real name
		char *comma = strchr (gecos, ',');
		if (comma)
			*comma = '\0';

		set_config_string (ctx, "server.realname", gecos);
	}

	return true;
}

static bool
read_file (const char *filename, struct str *output, struct error **e)
{
	FILE *fp = fopen (filename, "rb");
	if (!fp)
	{
		error_set (e, "could not open `%s' for reading: %s",
			filename, strerror (errno));
		return false;
	}

	char buf[BUFSIZ];
	size_t len;

	while ((len = fread (buf, 1, sizeof buf, fp)) == sizeof buf)
		str_append_data (output, buf, len);
	str_append_data (output, buf, len);

	bool success = !ferror (fp);
	fclose (fp);

	if (success)
		return true;

	error_set (e, "error while reading `%s': %s",
		filename, strerror (errno));
	return false;
}

static struct config_item_ *
load_configuration_file (const char *filename, struct error **e)
{
	struct config_item_ *root = NULL;

	struct str data;
	str_init (&data);
	if (!read_file (filename, &data, e))
		goto end;

	struct error *error = NULL;
	if (!(root = config_item_parse (data.str, data.len, false, &error)))
	{
		error_set (e, "configuration parse error: %s", error->message);
		error_free (error);
	}
end:
	str_free (&data);
	return root;
}

static void
load_configuration (struct app_context *ctx)
{
	struct config_item_ *root = NULL;
	struct error *e = NULL;

	char *filename = resolve_config_filename (PROGRAM_NAME ".conf");
	if (filename)
		root = load_configuration_file (filename, &e);
	else
		print_status ("configuration file not found");
	free (filename);

	if (e)
	{
		print_error ("%s", e->message);
		error_free (e);
		e = NULL;
	}

	config_load (&ctx->config, root ? root : config_item_object ());
	if (!autofill_user_info (ctx, &e))
	{
		print_error ("%s: %s", "failed to fill in user details", e->message);
		error_free (e);
	}

	ctx->reconnect =
		get_config_boolean (ctx, "server.reconnect");
	ctx->isolate_buffers =
		get_config_boolean (ctx, "behaviour.isolate_buffers");
	ctx->reconnect_delay =
		get_config_integer (ctx, "server.reconnect_delay");
}

static char *
write_configuration_file (const struct str *data, struct error **e)
{
	struct str path;
	str_init (&path);
	get_xdg_home_dir (&path, "XDG_CONFIG_HOME", ".config");
	str_append (&path, "/" PROGRAM_NAME);

	if (!mkdir_with_parents (path.str, e))
		goto error;

	str_append (&path, "/" PROGRAM_NAME ".conf");
	FILE *fp = fopen (path.str, "w");
	if (!fp)
	{
		error_set (e, "could not open `%s' for writing: %s",
			path.str, strerror (errno));
		goto error;
	}

	errno = 0;
	fwrite (data->str, data->len, 1, fp);
	fclose (fp);

	if (errno)
	{
		error_set (e, "writing to `%s' failed: %s", path.str, strerror (errno));
		goto error;
	}
	return str_steal (&path);

error:
	str_free (&path);
	return NULL;
}

static void
serialize_configuration (struct app_context *ctx, struct str *output)
{
	str_append (output,
		"# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n"
		"#\n"
		"# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}\n"
		"# /" PROGRAM_NAME " as well as in $XDG_CONFIG_DIRS/" PROGRAM_NAME "\n"
		"#\n"
		"# Everything is in UTF-8.  Any custom comments will be overwritten.\n"
		"\n");

	config_item_write (ctx->config.root, true, output);
}

static void
write_default_configuration ()
{
	// XXX: this is a hack before we remove this awkward functionality
	//   altogether; the user will want to do this from the user interface

	struct app_context ctx = {};
	config_init (&ctx.config);
	register_config_modules (&ctx);
	config_load (&ctx.config, config_item_object ());

	struct str data;
	str_init (&data);
	serialize_configuration (&ctx, &data);

	struct error *e = NULL;
	char *filename = write_configuration_file (&data, &e);
	str_free (&data);
	config_free (&ctx.config);

	if (!filename)
	{
		print_error ("%s", e->message);
		error_free (e);
		exit (EXIT_FAILURE);
	}
	print_status ("configuration written to `%s'", filename);
	free (filename);
}


// --- Main program ------------------------------------------------------------

static void
init_poller_events (struct app_context *ctx)
{
	poller_fd_init (&ctx->signal_event, &ctx->poller, g_signal_pipe[0]);
	ctx->signal_event.dispatcher = (poller_fd_fn) on_signal_pipe_readable;
	ctx->signal_event.user_data = ctx;
	poller_fd_set (&ctx->signal_event, POLLIN);

	poller_fd_init (&ctx->tty_event, &ctx->poller, STDIN_FILENO);
	ctx->tty_event.dispatcher = (poller_fd_fn) on_tty_readable;
	ctx->tty_event.user_data = &ctx;
	poller_fd_set (&ctx->tty_event, POLLIN);
}

int
main (int argc, char *argv[])
{
	// We include a generated file from kike including this array we don't use;
	// let's just keep it there and silence the compiler warning instead
	(void) g_default_replies;

	static const struct opt opts[] =
	{
		{ 'd', "debug", NULL, 0, "run in debug mode" },
		{ 'h', "help", NULL, 0, "display this help and exit" },
		{ 'V', "version", NULL, 0, "output version information and exit" },
		{ 'w', "write-default-cfg", NULL, OPT_LONG_ONLY,
		  "write a default configuration file and exit" },
		{ 0, NULL, NULL, 0, NULL }
	};

	struct opt_handler oh;
	opt_handler_init (&oh, argc, argv, opts, NULL, "Experimental IRC client.");

	int c;
	while ((c = opt_handler_get (&oh)) != -1)
	switch (c)
	{
	case 'd':
		g_debug_mode = true;
		break;
	case 'h':
		opt_handler_usage (&oh, stdout);
		exit (EXIT_SUCCESS);
	case 'V':
		printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
		exit (EXIT_SUCCESS);
	case 'w':
		write_default_configuration ();
		exit (EXIT_SUCCESS);
	default:
		print_error ("wrong options");
		opt_handler_usage (&oh, stderr);
		exit (EXIT_FAILURE);
	}

	opt_handler_free (&oh);

	print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting");

	// We only need to convert to and from the terminal encoding
	setlocale (LC_CTYPE, "");

	struct app_context ctx;
	app_context_init (&ctx);
	g_ctx = &ctx;

	SSL_library_init ();
	atexit (EVP_cleanup);
	SSL_load_error_strings ();
	atexit (ERR_free_strings);

	using_history ();
	// This can cause memory leaks, or maybe even a segfault.  Funny, eh?
	stifle_history (HISTORY_LIMIT);

	setup_signal_handlers ();
	register_config_modules (&ctx);
	load_configuration (&ctx);

	init_colors (&ctx);
	init_poller_events (&ctx);
	init_buffers (&ctx);
	ctx.current_buffer = ctx.server.buffer;
	refresh_prompt (&ctx);

	// Connect to the server ASAP
	poller_timer_set (&ctx.server.reconnect_tmr, 0);

	rl_startup_hook = init_readline;
	rl_catch_sigwinch = false;
	rl_callback_handler_install (ctx.readline_prompt, on_readline_input);
	rl_get_screen_size (&ctx.lines, &ctx.columns);
	ctx.readline_prompt_shown = true;

	ctx.polling = true;
	while (ctx.polling)
		poller_run (&ctx.poller);

	app_context_free (&ctx);
	free_terminal ();
	return EXIT_SUCCESS;
}