/*
 * 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.
 *
 */

// A table of all attributes we use for output
// FIXME: awful naming, collides with ATTRIBUTE_*
#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( USERHOST,  "userhost",  "Terminal attributes for user@host"      )     \
	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 <wchar.h>

#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif  // ! TIOCGWINSZ

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

// Literally cancer
#undef lines
#undef columns

#ifdef HAVE_READLINE
#include <readline/readline.h>
#include <readline/history.h>
#endif // HAVE_READLINE

#ifdef HAVE_EDITLINE
#include <histedit.h>
#endif // HAVE_EDITLINE

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

/// Characters that separate words
#define WORD_BREAKING_CHARS " \f\n\r\t\v"

// --- User interface ----------------------------------------------------------

// I'm not sure which one of these backends is worse: whether it's GNU Readline
// or BSD Editline.  They both have their own annoying problems.

struct input_buffer
{
#ifdef HAVE_READLINE
	HISTORY_STATE *history;             ///< Saved history state
	char *saved_line;                   ///< Saved line content
	int saved_mark;                     ///< Saved mark
#elif defined HAVE_EDITLINE
	HistoryW *history;                  ///< The history object
	wchar_t *saved_line;                ///< Saved line content
	int saved_len;                      ///< Length of the saved line
#endif // HAVE_EDITLINE
	int saved_point;                    ///< Saved cursor position
};

static struct input_buffer *
input_buffer_new (void)
{
	struct input_buffer *self = xcalloc (1, sizeof *self);
#ifdef HAVE_EDITLINE
	self->history = history_winit ();

	HistEventW ev;
	history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
#endif // HAVE_EDITLINE
	return self;
}

static void
input_buffer_destroy (struct input_buffer *self)
{
#ifdef HAVE_READLINE
	// Can't really free "history" contents from here
	free (self->history);
#elif defined HAVE_EDITLINE
	history_wend (self->history);
#endif // HAVE_EDITLINE
	free (self->saved_line);
	free (self);
}

struct input
{
	bool active;                        ///< Are we a thing?

#if defined HAVE_READLINE
	char *saved_line;                   ///< Saved line content
	int saved_point;                    ///< Saved cursor position
	int saved_mark;                     ///< Saved mark
#elif defined HAVE_EDITLINE
	EditLine *editline;                 ///< The EditLine object
	char *(*saved_prompt) (EditLine *); ///< Saved prompt function
	char saved_char;                    ///< Saved char for the prompt
#endif // HAVE_EDITLINE

	char *prompt;                       ///< The prompt we use
	int prompt_shown;                   ///< Whether the prompt is shown now

	struct input_buffer *current;       ///< Current input buffer
};

static void
input_init (struct input *self)
{
	memset (self, 0, sizeof *self);
}

static void
input_free (struct input *self)
{
#ifdef HAVE_READLINE
	free (self->saved_line);
#endif // HAVE_READLINE
	free (self->prompt);
}

// --- GNU Readline ------------------------------------------------------------

#ifdef HAVE_READLINE

#define INPUT_START_IGNORE  RL_PROMPT_START_IGNORE
#define INPUT_END_IGNORE    RL_PROMPT_END_IGNORE

#define input_ding(self) rl_ding ()

static void
input_on_terminal_resized (struct input *self)
{
	(void) self;

	// 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 ();
}

static void
input_on_readable (struct input *self)
{
	(void) self;
	rl_callback_read_char ();
}

static void
input_set_prompt (struct input *self, char *prompt)
{
	free (self->prompt);
	self->prompt = prompt;

	if (!self->active)
		return;

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

	rl_set_prompt (self->prompt);
	if (self->prompt_shown > 0)
		rl_redisplay ();
}

static void
input_erase (struct input *self)
{
	(void) self;

	rl_set_prompt ("");
	rl_replace_line ("", 0);
	rl_point = rl_mark = 0;
	rl_redisplay ();
}

static void
input_bind (struct input *self, const char *seq, const char *function_name)
{
	(void) self;
	rl_bind_keyseq (seq, rl_named_function (function_name));
}

static void
input_bind_meta (struct input *self, char key, const char *function_name)
{
	// This one seems to actually work
	char keyseq[] = { '\\', 'e', key, 0 };
	input_bind (self, keyseq, function_name);
#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), rl_named_function (function_name));
#endif
}

static void
input_bind_control (struct input *self, char key, const char *function_name)
{
	char keyseq[] = { '\\', 'C', '-', key, 0 };
	input_bind (self, keyseq, function_name);
}

static void
input_insert_c (struct input *self, int c)
{
	char s[2] = { c, 0 };
	rl_insert_text (s);

	if (self->prompt_shown > 0)
		rl_redisplay ();
}

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

static int app_readline_init (void);
static void on_readline_input (char *line);
static char **app_readline_completion (const char *text, int start, int end);

static void
input_start (struct input *self, const char *program_name)
{
	using_history ();
	// This can cause memory leaks, or maybe even a segfault.  Funny, eh?
	stifle_history (HISTORY_LIMIT);

	const char *slash = strrchr (program_name, '/');
	rl_readline_name = slash ? ++slash : program_name;
	rl_startup_hook = app_readline_init;
	rl_catch_sigwinch = false;

	rl_basic_word_break_characters = WORD_BREAKING_CHARS;
	rl_completer_word_break_characters = NULL;
	rl_attempted_completion_function = app_readline_completion;

	hard_assert (self->prompt != NULL);
	rl_callback_handler_install (self->prompt, on_readline_input);

	self->prompt_shown = 1;
	self->active = true;
}

static void
input_stop (struct input *self)
{
	if (self->prompt_shown > 0)
		input_erase (self);

	// This is okay as long as we're not called from within readline
	rl_callback_handler_remove ();
	self->active = false;
	self->prompt_shown = false;
}

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

// 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.

static void
input_save_buffer (struct input *self, struct input_buffer *buffer)
{
	(void) self;

	buffer->history = history_get_history_state ();
	buffer->saved_line = rl_copy_text (0, rl_end);
	buffer->saved_point = rl_point;
	buffer->saved_mark = rl_mark;
}

static void
input_restore_buffer (struct input *self, struct input_buffer *buffer)
{
	// 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;
		state->entries = NULL;
		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 = NULL;

		if (self->prompt_shown > 0)
			rl_redisplay ();
	}
}

static void
input_switch_buffer (struct input *self, struct input_buffer *buffer)
{
	// 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 (self->current)
		input_save_buffer (self, self->current);
	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

	input_restore_buffer (self, buffer);
	self->current = buffer;
}

static void
input_destroy_buffer (struct input *self, struct input_buffer *buffer)
{
	(void) self;

	// 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 input_switch_buffer() 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);
		rl_clear_history ();
		// rl_clear_history just removes history entries,
		// we have to reclaim memory for their actual container ourselves
		free (buffer->history->entries);
		free (buffer->history);
		buffer->history = NULL;

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

	input_buffer_destroy (buffer);
}

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

static void
input_save (struct input *self)
{
	hard_assert (!self->saved_line);

	self->saved_point = rl_point;
	self->saved_mark = rl_mark;
	self->saved_line = rl_copy_text (0, rl_end);
}

static void
input_restore (struct input *self)
{
	hard_assert (self->saved_line);

	rl_set_prompt (self->prompt);
	rl_replace_line (self->saved_line, 0);
	rl_point = self->saved_point;
	rl_mark = self->saved_mark;
	free (self->saved_line);
	self->saved_line = NULL;
}

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

static void
input_hide (struct input *self)
{
	if (!self->active || self->prompt_shown-- < 1)
		return;

	input_save (self);
	input_erase (self);
}

static void
input_show (struct input *self)
{
	if (!self->active || ++self->prompt_shown < 1)
		return;

	input_restore (self);
	rl_redisplay ();
}

#endif // HAVE_READLINE

// --- BSD Editline ------------------------------------------------------------

#ifdef HAVE_EDITLINE

#define INPUT_START_IGNORE  '\x01'
#define INPUT_END_IGNORE    '\x01'

static void app_editline_init (struct input *self);

static void
input_ding (struct input *self)
{
	(void) self;

	// XXX: this isn't probably very portable;
	//   we could use "bell" from terminfo but that creates a dependency
	write (STDOUT_FILENO, "\a", 1);
}

static void
input_on_terminal_resized (struct input *self)
{
	el_resize (self->editline);
}

static void
input_bind (struct input *self, const char *seq, const char *function_name)
{
	el_set (self->editline, EL_BIND, seq, function_name, NULL);
}

static void
input_bind_meta (struct input *self, char key, const char *function_name)
{
	char keyseq[] = { 'M', '-', key, 0 };
	input_bind (self, keyseq, function_name);
}

static void
input_bind_control (struct input *self, char key, const char *function_name)
{
	char keyseq[] = { '^', key, 0 };
	input_bind (self, keyseq, function_name);
}

static void
input_redisplay (struct input *self)
{
	// See rl_redisplay()
	// The character is VREPRINT (usually C-r)
	// TODO: read it from terminal info
	// XXX: could we potentially break UTF-8 with this?
	char x[] = { ('R' - 'A' + 1), 0 };
	el_push (self->editline, x);

	// We have to do this or it gets stuck and nothing is done
	(void) el_gets (self->editline, NULL);
}

static void
input_set_prompt (struct input *self, char *prompt)
{
	free (self->prompt);
	self->prompt = prompt;

	if (self->prompt_shown > 0)
		input_redisplay (self);
}

static char *
input_make_prompt (EditLine *editline)
{
	struct input *self;
	el_get (editline, EL_CLIENTDATA, &self);
	if (!self->prompt)
		return "";
	return self->prompt;
}

static char *
input_make_empty_prompt (EditLine *editline)
{
	(void) editline;
	return "";
}

static void
input_erase (struct input *self)
{
	const LineInfoW *info = el_wline (self->editline);
	int len = info->lastchar - info->buffer;
	int point = info->cursor - info->buffer;
	el_cursor (self->editline, len - point);
	el_wdeletestr (self->editline, len);

	// XXX: this doesn't seem to save the escape character
	el_get (self->editline, EL_PROMPT, &self->saved_prompt, &self->saved_char);
	el_set (self->editline, EL_PROMPT, input_make_empty_prompt);
	input_redisplay (self);
}

static void
input_insert_c (struct input *self, int c)
{
	char s[2] = { c, 0 };
	el_insertstr (self->editline, s);

	if (self->prompt_shown > 0)
		input_redisplay (self);
}

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

static void
input_start (struct input *self, const char *program_name)
{
	self->editline = el_init (program_name, stdin, stdout, stderr);
	el_set (self->editline, EL_CLIENTDATA, self);
	el_set (self->editline, EL_PROMPT_ESC,
		input_make_prompt, INPUT_START_IGNORE);
	el_set (self->editline, EL_SIGNAL, false);
	el_set (self->editline, EL_UNBUFFERED, true);
	el_set (self->editline, EL_EDITOR, "emacs");

	app_editline_init (self);

	self->prompt_shown = 1;
	self->active = true;
}

static void
input_stop (struct input *self)
{
	if (self->prompt_shown > 0)
		input_erase (self);

	el_end (self->editline);
	self->editline = NULL;
	self->active = false;
	self->prompt_shown = false;
}

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

static void
input_save_buffer (struct input *self, struct input_buffer *buffer)
{
	const LineInfoW *info = el_wline (self->editline);
	int len = info->lastchar - info->buffer;
	int point = info->cursor - info->buffer;

	wchar_t *line = calloc (sizeof *info->buffer, len + 1);
	memcpy (line, info->buffer, sizeof *info->buffer * len);
	el_cursor (self->editline, len - point);
	el_wdeletestr (self->editline, len);

	buffer->saved_line = line;
	buffer->saved_point = point;
	buffer->saved_len = len;
}

static void
input_restore_buffer (struct input *self, struct input_buffer *buffer)
{
	if (buffer->saved_line)
	{
		el_winsertstr (self->editline, buffer->saved_line);
		el_cursor (self->editline,
			-(buffer->saved_len - buffer->saved_point));
		free (buffer->saved_line);
		buffer->saved_line = NULL;
	}
}

static void
input_switch_buffer (struct input *self, struct input_buffer *buffer)
{
	if (self->current)
		input_save_buffer (self, self->current);

	input_restore_buffer (self, buffer);
	el_wset (self->editline, EL_HIST, history, buffer->history);
	self->current = buffer;
}

static void
input_destroy_buffer (struct input *self, struct input_buffer *buffer)
{
	(void) self;
	input_buffer_destroy (buffer);
}

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

static void
input_save (struct input *self)
{
	if (self->current)
		input_save_buffer (self, self->current);
}

static void
input_restore (struct input *self)
{
	if (self->current)
		input_restore_buffer (self, self->current);
}

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

static void
input_hide (struct input *self)
{
	if (!self->active || self->prompt_shown-- < 1)
		return;

	input_save (self);
	input_erase (self);
}

static void
input_show (struct input *self)
{
	if (!self->active || ++self->prompt_shown < 1)
		return;

	input_restore (self);
	// Would have used "saved_char" but it doesn't seem to work.
	// And it doesn't even when it does anyway (it seems to just strip it).
	el_set (self->editline,
		EL_PROMPT_ESC, input_make_prompt, INPUT_START_IGNORE);
	input_redisplay (self);
}

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

static void
input_on_readable (struct input *self)
{
	// We bind the return key to process it how we need to

	// el_gets() with EL_UNBUFFERED doesn't work with UTF-8,
	// we must use the wide-character interface
	int count = 0;
	const wchar_t *buf = el_wgets (self->editline, &count);
	if (!buf || count-- <= 0)
		return;

	// The character is VEOF (usually C-d)
	// TODO: read it from terminal info
	if (count == 0 && buf[0] == ('D' - 'A' + 1))
	{
		el_deletestr (self->editline, 1);
		input_redisplay (self);
		input_ding (self);
	}
}

#endif // HAVE_EDITLINE

// --- 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

	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
	struct str prefixes;                ///< Ordered @+... characters
};

static struct channel_user *
channel_user_new (void)
{
	struct channel_user *self = xcalloc (1, sizeof *self);
	str_init (&self->prefixes);
	return self;
}

static void
channel_user_destroy (struct channel_user *self)
{
	user_unref (self->user);
	str_free (&self->prefixes);
	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

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

	// XXX: write something like an ordered set of characters object?
	struct str no_param_modes;          ///< No parameter channel modes
	struct str_map param_modes;         ///< Parametrized channel modes

	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_init (&self->no_param_modes);
	str_map_init (&self->param_modes);
	self->param_modes.free = free;
	str_vector_init (&self->names_buf);
	return self;
}

static void
channel_destroy (struct channel *self)
{
	free (self->name);
	free (self->topic);
	str_free (&self->no_param_modes);
	str_map_free (&self->param_modes);
	// 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 formatter_item_type
{
	FORMATTER_ITEM_TEXT,                ///< Text
	FORMATTER_ITEM_ATTR,                ///< Formatting attributes
	FORMATTER_ITEM_FG_COLOR,            ///< Foreground color
	FORMATTER_ITEM_BG_COLOR,            ///< Background color
	FORMATTER_ITEM_SIMPLE,              ///< For mIRC formatting only so far
	FORMATTER_ITEM_IGNORE_ATTR          ///< Un/set attribute ignoration
};

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
	struct server *s;                   ///< Server

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

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

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

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

enum buffer_line_flags
{
	BUFFER_LINE_STATUS      = 1 << 0,   ///< Status message
	BUFFER_LINE_ERROR       = 1 << 1,   ///< Error message
	BUFFER_LINE_HIGHLIGHT   = 1 << 2,   ///< The user was highlighted by this
	BUFFER_LINE_SKIP_FILE   = 1 << 3,   ///< Don't log this to file
	BUFFER_LINE_INDENT      = 1 << 4    ///< Just indent the line
};

struct buffer_line
{
	LIST_HEADER (struct buffer_line)

	int flags;                          ///< Flags
	time_t when;                        ///< Time of the event
	struct formatter *formatter;        ///< Line data
};

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)
{
	if (self->formatter)
		formatter_free (self->formatter);
	free (self->formatter);
	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

	struct input_buffer *input_data;    ///< User interface data

	// 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
	bool highlighted;                   ///< We've been highlighted

	FILE *log_file;                     ///< Log file

	// 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);
	self->input_data = input_buffer_new ();
	return self;
}

static void
buffer_destroy (struct buffer *self)
{
	free (self->name);
	if (self->input_data)
		input_buffer_destroy (self->input_data);
	LIST_FOR_EACH (struct buffer_line, iter, self->lines)
		buffer_line_destroy (iter);
	if (self->log_file)
		(void) fclose (self->log_file);
	if (self->user)
		user_unref (self->user);
	if (self->channel)
		channel_unref (self->channel);
	free (self);
}

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

enum transport_io_result
{
	TRANSPORT_IO_OK = 0,                ///< Completed successfully
	TRANSPORT_IO_EOF,                   ///< Connection shut down by peer
	TRANSPORT_IO_ERROR                  ///< Connection error
};

// The only real purpose of this is to abstract away TLS/SSL
struct transport
{
	/// Initialize the transport
	bool (*init) (struct server *s, struct error **e);
	/// Destroy the user data pointer
	void (*cleanup) (struct server *s);

	/// The underlying socket may have become readable, update `read_buffer'
	enum transport_io_result (*try_read) (struct server *s);
	/// The underlying socket may have become writeable, flush `write_buffer'
	enum transport_io_result (*try_write) (struct server *s);
	/// Return event mask to use in the poller
	int (*get_poll_events) (struct server *s);

	/// Called just before closing the connection from our side
	void (*in_before_shutdown) (struct server *s);
};

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

enum server_state
{
	IRC_DISCONNECTED,                   ///< Not connected
	IRC_CONNECTING,                     ///< Connecting to the server
	IRC_CONNECTED,                      ///< Trying to register
	IRC_REGISTERED,                     ///< We can chat now
	IRC_CLOSING,                        ///< Flushing output before shutdown
	IRC_HALF_CLOSED                     ///< Connection shutdown from our side
};

/// Convert an IRC identifier character to lower-case
typedef int (*irc_tolower_fn) (int);

/// Key conversion function for hashmap lookups
typedef size_t (*irc_strxfrm_fn) (char *, const char *, size_t);

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

	char *name;                         ///< Server identifier
	struct buffer *buffer;              ///< The buffer for this server
	struct config_item_ *config;        ///< Configuration root

	// Connection:

	enum server_state state;            ///< Connection state
	struct connector *connector;        ///< Connection establisher
	bool manual_disconnect;             ///< Don't reconnect after disconnect

	int socket;                         ///< Socket FD of the server
	struct str read_buffer;             ///< Input yet to be processed
	struct str write_buffer;            ///< Outut yet to be be sent out
	struct poller_fd socket_event;      ///< We can read from the socket

	struct transport *transport;        ///< Transport method
	void *transport_data;               ///< Transport data

	// 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

	// IRC:

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

	bool rehashing;                     ///< Rehashing IRC identifiers

	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
	int nick_counter;                   ///< Iterates "nicks" when registering
	struct str irc_user_mode;           ///< Our current user modes
	char *irc_user_host;                ///< Our current user@host

	bool cap_echo_message;              ///< Whether the server echos messages

	// Server-specific information (from RPL_ISUPPORT):

	irc_tolower_fn irc_tolower;         ///< Server tolower()
	irc_strxfrm_fn irc_strxfrm;         ///< Server strxfrm()

	char *irc_chantypes;                ///< Channel types (name prefixes)
	char *irc_idchan_prefixes;          ///< Prefixes for "safe channels"
	char *irc_statusmsg;                ///< Prefixes for channel targets

	char *irc_chanmodes_list;           ///< Channel modes for lists
	char *irc_chanmodes_param_always;   ///< Channel modes with mandatory param
	char *irc_chanmodes_param_when_set; ///< Channel modes with param when set
	char *irc_chanmodes_param_never;    ///< Channel modes without param

	char *irc_chanuser_prefixes;        ///< Channel user prefixes
	char *irc_chanuser_modes;           ///< Channel user modes

	unsigned irc_max_modes;             ///< Max parametrized modes per command
};

static void on_irc_timeout (void *user_data);
static void on_irc_ping_timeout (void *user_data);
static void irc_initiate_connect (struct server *s);

static void
server_init_specifics (struct server *self)
{
	// Defaults as per the RPL_ISUPPORT drafts, or RFC 1459

	self->irc_tolower                   = irc_tolower;
	self->irc_strxfrm                   = irc_strxfrm;

	self->irc_chantypes                 = xstrdup ("#&");
	self->irc_idchan_prefixes           = xstrdup ("");
	self->irc_statusmsg                 = xstrdup ("");

	self->irc_chanmodes_list            = xstrdup ("b");
	self->irc_chanmodes_param_always    = xstrdup ("k");
	self->irc_chanmodes_param_when_set  = xstrdup ("l");
	self->irc_chanmodes_param_never     = xstrdup ("imnpst");

	self->irc_chanuser_prefixes         = xstrdup ("@+");
	self->irc_chanuser_modes            = xstrdup ("ov");

	self->irc_max_modes                 = 3;
}

static void
server_free_specifics (struct server *self)
{
	free (self->irc_chantypes);
	free (self->irc_idchan_prefixes);
	free (self->irc_statusmsg);

	free (self->irc_chanmodes_list);
	free (self->irc_chanmodes_param_always);
	free (self->irc_chanmodes_param_when_set);
	free (self->irc_chanmodes_param_never);

	free (self->irc_chanuser_prefixes);
	free (self->irc_chanuser_modes);
}

static void
server_init (struct server *self, struct poller *poller)
{
	memset (self, 0, sizeof *self);

	self->socket = -1;
	str_init (&self->read_buffer);
	str_init (&self->write_buffer);
	self->state = IRC_DISCONNECTED;

	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 = (poller_timer_fn) irc_initiate_connect;
	self->reconnect_tmr.user_data = self;

	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;

	str_init (&self->irc_user_mode);

	server_free_specifics (self);
	server_init_specifics (self);
}

static void
server_free (struct server *self)
{
	free (self->name);

	if (self->connector)
	{
		connector_free (self->connector);
		free (self->connector);
	}

	if (self->transport
	 && self->transport->cleanup)
		self->transport->cleanup (self);

	if (self->socket != -1)
	{
		xclose (self->socket);
		self->socket_event.closed = true;
		poller_fd_reset (&self->socket_event);
	}
	str_free (&self->read_buffer);
	str_free (&self->write_buffer);

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

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

	server_free_specifics (self);
}

static void
server_destroy (void *self)
{
	server_free (self);
	free (self);
}

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

struct app_context
{
	bool no_colors;                     ///< Disable attribute printing
	char *attrs_defaults[ATTR_COUNT];   ///< Default terminal attributes

	// Configuration:

	struct config config;               ///< Program configuration
	char *attrs[ATTR_COUNT];            ///< Terminal attributes
	bool isolate_buffers;               ///< Isolate global/server buffers
	bool beep_on_highlight;             ///< Beep on highlight
	bool logging;                       ///< Logging to file enabled

	struct str_map servers;             ///< Our servers

	// Events:

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

	struct poller_timer flush_timer;    ///< Flush all open files (e.g. logs)

	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 *global_buffer;       ///< The global buffer
	struct buffer *current_buffer;      ///< The current buffer
	struct buffer *last_buffer;         ///< Last used buffer

	// TODO: make buffer names fully unique like weechat does
	struct str_map buffers_by_name;     ///< Buffers by name

	// 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

	struct input input;                 ///< User interface

	bool awaiting_mirc_escape;          ///< Awaiting a mIRC attribute escape
	char char_buf[MB_LEN_MAX + 1];      ///< Buffered multibyte char
	size_t char_buf_len;                ///< How much of an MB char is buffered
}
*g_ctx;

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

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

	str_map_init (&self->servers);
	self->servers.free = server_destroy;
	self->servers.key_xfrm = tolower_ascii_strxfrm;

	str_map_init (&self->buffers_by_name);
	self->buffers_by_name.key_xfrm = tolower_ascii_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);

	input_init (&self->input);
}

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_defaults[i]);
		free (self->attrs[i]);
	}

	LIST_FOR_EACH (struct buffer, iter, self->buffers)
	{
#ifdef HAVE_READLINE
		input_destroy_buffer (&self->input, iter->input_data);
		iter->input_data = NULL;
#endif // HAVE_READLINE
		buffer_destroy (iter);
	}
	str_map_free (&self->buffers_by_name);

	str_map_free (&self->servers);
	poller_free (&self->poller);

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

	input_free (&self->input);
}

static void refresh_prompt (struct app_context *ctx);

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

static void on_config_attribute_change (struct config_item_ *item);
static void on_config_logging_change (struct config_item_ *item);

#define TRIVIAL_BOOLEAN_ON_CHANGE(name)                                        \
	static void                                                                \
	on_config_ ## name ## _change (struct config_item_ *item)                  \
	{                                                                          \
		struct app_context *ctx = item->user_data;                             \
		ctx->name = item->value.boolean;                                       \
	}

TRIVIAL_BOOLEAN_ON_CHANGE (isolate_buffers)
TRIVIAL_BOOLEAN_ON_CHANGE (beep_on_highlight)

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_addresses
	(const struct config_item_ *item, struct error **e)
{
	if (item->type == CONFIG_ITEM_NULL)
		return true;
	if (!config_validate_nonjunk_string (item, e))
		return false;

	// Comma-separated list of "host[:port]" pairs
	regex_t re;
	int err = regcomp (&re, "^([^/:,]+(:[^/:,]+)?)?"
		"(,([^/:,]+(:[^/:,]+)?)?)*$", REG_EXTENDED | REG_NOSUB);
	hard_assert (!err);

	bool result = !regexec (&re, item->value.string.str, 0, NULL, 0);
	if (!result)
		error_set (e, "invalid address list string");

	regfree (&re);
	return result;
}

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;
}

static struct config_schema g_config_server[] =
{
	{ .name      = "nicks",
	  .comment   = "IRC nickname",
	  .type      = CONFIG_ITEM_STRING_ARRAY,
	  .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      = "addresses",
	  .comment   = "Addresses of the IRC network (e.g. \"irc.net:6667\")",
	  .type      = CONFIG_ITEM_STRING_ARRAY,
	  .validate  = config_validate_addresses },
	{ .name      = "password",
	  .comment   = "Password to connect to the server, if any",
	  .type      = CONFIG_ITEM_STRING,
	  .validate  = config_validate_nonjunk_string },

	{ .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,
	  .validate  = config_validate_nonnegative,
	  .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 },
	{}
};

static 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",
	  .on_change = on_config_isolate_buffers_change },
	{ .name      = "beep_on_highlight",
	  .comment   = "Beep when highlighted or on a new invisible PM",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on",
	  .on_change = on_config_beep_on_highlight_change },
	{ .name      = "logging",
	  .comment   = "Log buffer contents to file",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off",
	  .on_change = on_config_logging_change },
	{}
};

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

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

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

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

static void
register_config_modules (struct app_context *ctx)
{
	struct config *config = &ctx->config;
	// The servers are loaded later when we can create buffers for them
	config_register_module (config, "servers",    NULL, NULL);
	config_register_module (config, "aliases",    NULL, NULL);
	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 config_item_ *root, const char *key)
{
	struct config_item_ *item = config_item_get (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 config_item_ *root, const char *key, const char *value)
{
	struct config_item_ *item = config_item_get (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 config_item_ *root, const char *key)
{
	struct config_item_ *item = config_item_get (root, key, NULL);
	hard_assert (item && item->type == CONFIG_ITEM_INTEGER);
	return item->value.integer;
}

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

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

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);
}

// --- Terminal output ---------------------------------------------------------

/// Default color pair
#define COLOR_DEFAULT -1

/// Bright versions of the basic color set
#define COLOR_BRIGHT(x) (COLOR_ ## x + 8)

/// Builds a color pair for 256-color terminals with a 16-color backup value
#define COLOR_256(name, c256) \
	(((COLOR_ ## name) & 0xFFFF) | ((c256 & 0xFFFF) << 16))

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[256];            ///< Codes to set the foreground colour
	char *color_set_bg[256];            ///< Codes to set the background colour

	int lines;                          ///< Number of lines
	int columns;                        ///< Number of columns
}
g_terminal;

static void
update_screen_size (void)
{
#ifdef TIOCGWINSZ
	if (!g_terminal.stdout_is_tty)
		return;

	struct winsize size;
	if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
	{
		char *row = getenv ("LINES");
		char *col = getenv ("COLUMNS");
		unsigned long tmp;
		g_terminal.lines =
			(row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row;
		g_terminal.columns =
			(col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col;
	}
#endif // TIOCGWINSZ
}

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;
	}

	g_terminal.lines   = tigetnum ("lines");
	g_terminal.columns = tigetnum ("cols");
	update_screen_size ();

	int max = MIN (256, max_colors);
	for (int i = 0; i < max; 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 (int i = 0; i < 256; i++)
	{
		free (g_terminal.color_set_fg[i]);
		free (g_terminal.color_set_bg[i]);
	}
	del_curterm (cur_term);
}

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

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_context *ctx = g_ctx;

	input_hide (&ctx->input);

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

	input_show (&ctx->input);
}

static void
apply_attribute_change (struct config_item_ *item, int id)
{
	struct app_context *ctx = item->user_data;
	free (ctx->attrs[id]);
	ctx->attrs[id] = xstrdup (item->type == CONFIG_ITEM_NULL
		? ctx->attrs_defaults[id]
		: item->value.string.str);
}

static void
on_config_attribute_change (struct config_item_ *item)
{
	static const char *table[ATTR_COUNT] =
	{
#define XX(x, y, z) [ATTR_ ## x] = y,
		ATTR_TABLE (XX)
#undef XX
	};

	for (size_t i = 0; i < N_ELEMENTS (table); i++)
		if (!strcmp (item->schema->name, table[i]))
		{
			apply_attribute_change (item, i);
			return;
		}
}

static void
init_colors (struct app_context *ctx)
{
	bool have_ti = init_terminal ();
	char **defaults = ctx->attrs_defaults;

#define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup (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 (USERHOST,  g_terminal.color_set_fg[COLOR_CYAN]);
	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;

	// Apply the default values so that we start with any formatting at all
	config_schema_call_changed
		(config_item_get (ctx->config.root, "attributes", NULL));
}

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

// A little tool that tries to make the most of the terminal's capabilities
// to set up text attributes.  It mostly targets just terminal emulators as that
// is what people are using these days.  At least no stupid ncurses limits us
// with color pairs.

enum
{
	ATTRIBUTE_BOLD      = 1 << 0,
	ATTRIBUTE_ITALIC    = 1 << 1,
	ATTRIBUTE_UNDERLINE = 1 << 2,
	ATTRIBUTE_INVERSE   = 1 << 3,
	ATTRIBUTE_BLINK     = 1 << 4
};

struct attribute_printer
{
	struct app_context *ctx;            ///< Application context
	terminal_printer_fn printer;        ///< Terminal printer
	bool dirty;                         ///< Attributes are set

	int want;                           ///< Desired attributes
	int want_foreground;                ///< Desired foreground color
	int want_background;                ///< Desired background color
};

static void
attribute_printer_reset (struct attribute_printer *self)
{
	if (self->dirty)
		tputs (self->ctx->attrs[ATTR_RESET], 1, self->printer);

	self->dirty = false;
}

static void
attribute_printer_init (struct attribute_printer *self,
	struct app_context *ctx, terminal_printer_fn printer)
{
	self->ctx = ctx;
	self->printer = printer;
	self->dirty = true;

	self->want = 0;
	self->want_foreground = -1;
	self->want_background = -1;
}

static void
attribute_printer_apply (struct attribute_printer *self, int attribute)
{
	attribute_printer_reset (self);
	if (attribute != ATTR_RESET)
	{
		tputs (self->ctx->attrs[attribute], 1, self->printer);
		self->dirty = true;
	}
}

// NOTE: commonly terminals have:
//   8 colors (worst, bright fg with BOLD, bg sometimes with BLINK)
//   16 colors (okayish, we have the full basic range guaranteed)
//   88 colors (the same plus a 4^3 RGB cube and a few shades of gray)
//   256 colors (best, like above but with a larger cube and more gray)

/// Interpolate from the 256-color palette to the 88-color one
static int
attribute_printer_256_to_88 (int color)
{
	// These colours are the same everywhere
	if (color < 16)
		return color;

	// 24 -> 8 extra shades of gray
	if (color >= 232)
		return 80 + (color - 232) / 3;

	// 6 * 6 * 6 cube -> 4 * 4 * 4 cube
	int x[6] = { 0, 1, 1, 2, 2, 3 };
	int index = color - 16;
	return 16 +
		( x[ index / 36      ] << 8
		| x[(index /  6) % 6 ] << 4
		| x[(index %  6)     ] );
}

static int
attribute_printer_decode_color (int color, bool *is_bright)
{
	int16_t c16  = color;        hard_assert (c16  < 16);
	int16_t c256 = color >> 16;  hard_assert (c256 < 256);

	*is_bright = false;
	switch (max_colors)
	{
	case 8:
		if (c16 >= 8)
		{
			c16 -= 8;
			*is_bright = true;
		}
	case 16:
		return c16;

	case 88:
		return c256 <= 0 ? c16 : attribute_printer_256_to_88 (c256);
	case 256:
		return c256 <= 0 ? c16 : c256;

	default:
		// Unsupported palette
		return -1;
	}
}

static void
attribute_printer_update (struct attribute_printer *self)
{
	bool fg_is_bright;
	int fg = attribute_printer_decode_color
		(self->want_foreground, &fg_is_bright);
	bool bg_is_bright;
	int bg = attribute_printer_decode_color
		(self->want_background, &bg_is_bright);

	// TODO: (INVERSE | BOLD) should be used for bright backgrounds
	//   when possible, i.e. when the foreground shouldn't be bright as well
	//   and when the BOLD attribute hasn't already been set
	int attributes = self->want;
	if (attributes & ATTRIBUTE_INVERSE)
	{
		bool tmp = fg_is_bright;
		fg_is_bright = bg_is_bright;
		bg_is_bright = tmp;
	}

	if (fg_is_bright) attributes |= ATTRIBUTE_BOLD;
	if (bg_is_bright) attributes |= ATTRIBUTE_BLINK;

	attribute_printer_reset (self);

	if (attributes)
		tputs (tparm (set_attributes,
			0, // standout
			attributes & ATTRIBUTE_UNDERLINE,
			attributes & ATTRIBUTE_INVERSE,
			attributes & ATTRIBUTE_BLINK,
			0, // dim
			attributes & ATTRIBUTE_BOLD,
			0, // blank
			0, // protect
			0) // acs
			, 1, self->printer);
	if (enter_italics_mode && (attributes & ATTRIBUTE_ITALIC))
		tputs (enter_italics_mode, 1, self->printer);

	if (fg >= 0)
		tputs (g_terminal.color_set_fg[fg], 1, self->printer);
	if (bg >= 0)
		tputs (g_terminal.color_set_bg[bg], 1, self->printer);

	self->dirty = true;
}

// --- Helpers -----------------------------------------------------------------

static int
irc_server_strcmp (struct server *s, const char *a, const char *b)
{
	int x;
	while (*a || *b)
		if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
			return x;
	return 0;
}

static int
irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n)
{
	int x;
	while (n-- && (*a || *b))
		if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++)))
			return x;
	return 0;
}

static char *
irc_cut_nickname (const char *prefix)
{
	return str_cut_until (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)
{
	// This shouldn't be called before successfully registering.
	// Better safe than sorry, though.
	if (!s->irc_user)
		return false;

	char *nick = irc_cut_nickname (prefix);
	bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname);
	free (nick);
	return result;
}

static bool
irc_is_channel (struct server *s, const char *ident)
{
	return *ident
		&& (!!strchr (s->irc_chantypes,       *ident) ||
			!!strchr (s->irc_idchan_prefixes, *ident));
}

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

// 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)
{
	if (!text)
		return NULL;
	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;
}

// --- 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 (expected to be in UTF-8)
//   #d inserts a signed integer
//
//   #S inserts a string from the server with unknown encoding
//   #m inserts a mIRC-formatted string (auto-resets at boundaries)
//   #n cuts the nickname from a string and automatically colours it
//   #N is like #n but also appends userhost, if present
//
//   #a inserts named attributes (auto-resets)
//   #r resets terminal attributes
//   #c sets foreground color
//   #C sets background color
//
// Modifiers:
//    & free() the string argument after using it

static void
formatter_add_item (struct formatter *self, struct formatter_item template_)
{
	if (template_.text)
		template_.text = xstrdup (template_.text);

	struct formatter_item *item = formatter_item_new ();
	*item = template_;
	LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item);
}

#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \
	(struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ })

#define FORMATTER_ADD_RESET(self) \
	FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET)
#define FORMATTER_ADD_TEXT(self, text_) \
	FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_))
#define FORMATTER_ADD_SIMPLE(self, attribute_) \
	FORMATTER_ADD_ITEM ((self), SIMPLE, .attribute = ATTRIBUTE_ ## attribute_)

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

enum
{
	MIRC_WHITE,  MIRC_BLACK,    MIRC_BLUE,   MIRC_GREEN,
	MIRC_L_RED,  MIRC_RED,      MIRC_PURPLE, MIRC_ORANGE,
	MIRC_YELLOW, MIRC_L_GREEN,  MIRC_CYAN,   MIRC_L_CYAN,
	MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY,   MIRC_L_GRAY,
};

// We use estimates from the 16 color terminal palette, or the 256 color cube,
// which is not always available.  The mIRC orange colour is only in the cube.

static const int g_mirc_to_terminal[] =
{
	[MIRC_WHITE]    = COLOR_256 (BRIGHT (WHITE),  231),
	[MIRC_BLACK]    = COLOR_256         (BLACK,    16),
	[MIRC_BLUE]     = COLOR_256         (BLUE,     19),
	[MIRC_GREEN]    = COLOR_256         (GREEN,    34),
	[MIRC_L_RED]    = COLOR_256 (BRIGHT (RED),    196),
	[MIRC_RED]      = COLOR_256         (RED,     124),
	[MIRC_PURPLE]   = COLOR_256         (MAGENTA, 127),
	[MIRC_ORANGE]   = COLOR_256 (BRIGHT (YELLOW), 214),
	[MIRC_YELLOW]   = COLOR_256 (BRIGHT (YELLOW), 226),
	[MIRC_L_GREEN]  = COLOR_256 (BRIGHT (GREEN),   46),
	[MIRC_CYAN]     = COLOR_256         (CYAN,     37),
	[MIRC_L_CYAN]   = COLOR_256 (BRIGHT (CYAN),    51),
	[MIRC_L_BLUE]   = COLOR_256 (BRIGHT (BLUE),    21),
	[MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201),
	[MIRC_GRAY]     = COLOR_256 (BRIGHT (BLACK),  244),
	[MIRC_L_GRAY]   = COLOR_256         (WHITE,   252),
};

static const char *
formatter_parse_mirc_color (struct formatter *self, const char *s)
{
	if (!isdigit_ascii (*s))
		return s;

	int fg = *s++ - '0';
	if (isdigit_ascii (*s))
		fg = fg * 10 + (*s++ - '0');
	if (fg >= 0 && fg < 16)
		FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);

	if (*s != ',' || !isdigit_ascii (s[1]))
		return s;
	s++;

	int bg = *s++ - '0';
	if (isdigit_ascii (*s))
		bg = bg * 10 + (*s++ - '0');
	if (bg >= 0 && bg < 16)
		FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);

	return s;
}

static void
formatter_parse_mirc (struct formatter *self, const char *s)
{
	struct str buf;
	str_init (&buf);

	FORMATTER_ADD_RESET (self);

	unsigned char c;
	while ((c = *s++))
	{
		if (buf.len && c < 0x20)
		{
			FORMATTER_ADD_TEXT (self, buf.str);
			str_reset (&buf);
		}

		switch (c)
		{
		case '\x02': FORMATTER_ADD_SIMPLE (self, BOLD);      break;
		case '\x1d': FORMATTER_ADD_SIMPLE (self, ITALIC);    break;
		case '\x1f': FORMATTER_ADD_SIMPLE (self, UNDERLINE); break;
		case '\x16': FORMATTER_ADD_SIMPLE (self, INVERSE);   break;

		case '\x03':
			s = formatter_parse_mirc_color (self, s);
			break;
		case '\x0f':
			FORMATTER_ADD_RESET (self);
			break;
		default:
			str_append_c (&buf, c);
		}
	}

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

	str_free (&buf);
	FORMATTER_ADD_RESET (self);
}

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

static void
formatter_parse_nick (struct formatter *self, char *s)
{
	char *nick = irc_cut_nickname (s);
	int color = str_map_hash (nick, strlen (nick)) % 8;

	// We always use the default color for ourselves
	if (self->s && irc_is_this_us (self->s, nick))
		color = -1;

	// Never use the black colour, could become transparent on black terminals
	if (color == COLOR_BLACK)
		color = -1;

	FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color);

	char *x = irc_to_utf8 (self->ctx, nick);
	free (nick);
	FORMATTER_ADD_TEXT (self, x);
	free (x);

	// Need to reset the color afterwards
	FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
}

static void
formatter_parse_nick_full (struct formatter *self, char *s)
{
	formatter_parse_nick (self, s);

	const char *userhost;
	if (!(userhost = irc_find_userhost (s)))
		return;

	FORMATTER_ADD_TEXT (self, " (");
	FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST);

	char *x = irc_to_utf8 (self->ctx, userhost);
	FORMATTER_ADD_TEXT (self, x);
	free (x);

	FORMATTER_ADD_RESET (self);
	FORMATTER_ADD_TEXT (self, ")");
}

static const char *
formatter_parse_field (struct formatter *self,
	const char *field, struct str *buf, va_list *ap)
{
	bool free_string = false;
	char *s = NULL;
	char *tmp = NULL;
	int c;

restart:
	switch ((c = *field++))
	{
		// We can push boring text content to the caller's buffer
		// and let it flush the buffer only when it's actually needed
	case 'd':
		tmp = xstrdup_printf ("%d", va_arg (*ap, int));
		str_append (buf, tmp);
		free (tmp);
		break;
	case 's':
		str_append (buf, (s = va_arg (*ap, char *)));
		break;

	case 'S':
		tmp = irc_to_utf8 (self->ctx, (s = va_arg (*ap, char *)));
		str_append (buf, tmp);
		free (tmp);
		break;
	case 'm':
		tmp = irc_to_utf8 (self->ctx, (s = va_arg (*ap, char *)));
		formatter_parse_mirc (self, tmp);
		free (tmp);
		break;
	case 'n':
		formatter_parse_nick (self, (s = va_arg (*ap, char *)));
		break;
	case 'N':
		formatter_parse_nick_full (self, (s = va_arg (*ap, char *)));
		break;

	case 'a':
		FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int));
		break;
	case 'c':
		FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int));
		break;
	case 'C':
		FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int));
		break;
	case 'r':
		FORMATTER_ADD_RESET (self);
		break;

	default:
		if (c == '&' && !free_string)
			free_string = true;
		else if (c)
			hard_assert (!"unexpected format specifier");
		else
			hard_assert (!"unexpected end of format string");
		goto restart;
	}

	if (free_string)
		free (s);
	return field;
}

// I was unable to take a pointer of a bare "va_list" when it was passed in
// as a function argument, so it has to be a pointer from the beginning
static void
formatter_addv (struct formatter *self, const char *format, va_list *ap)
{
	struct str buf;
	str_init (&buf);

	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);
}

static void
formatter_add (struct formatter *self, const char *format, ...)
{
	va_list ap;
	va_start (ap, format);
	formatter_addv (self, format, &ap);
	va_end (ap);
}

static void
formatter_add_from (struct formatter *self, struct formatter *other)
{
	for (struct formatter_item *iter = other->items; iter; iter = iter->next)
		formatter_add_item (self, *iter);
}

static bool
formatter_flush_attr
	(struct attribute_printer *state, struct formatter_item *item)
{
	switch (item->type)
	{
	case FORMATTER_ITEM_ATTR:
		attribute_printer_apply (state, item->attribute);
		state->want = 0;
		state->want_foreground = -1;
		state->want_background = -1;
		return true;
	case FORMATTER_ITEM_SIMPLE:
		state->want |= item->attribute;
		attribute_printer_update (state);
		return true;
	case FORMATTER_ITEM_FG_COLOR:
		state->want_foreground = item->color;
		attribute_printer_update (state);
		return true;
	case FORMATTER_ITEM_BG_COLOR:
		state->want_background = item->color;
		attribute_printer_update (state);
		return true;
	default:
		return false;
	}
}

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;
	}

	struct attribute_printer state;
	attribute_printer_init (&state, self->ctx, printer);
	attribute_printer_reset (&state);

	int attribute_ignore = 0;
	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_IGNORE_ATTR:
			attribute_ignore += iter->attribute;
			break;
		default:
			if (attribute_ignore <= 0
			 && !formatter_flush_attr (&state, iter))
				hard_assert (!"unhandled formatter item type");
		}
	}
	attribute_printer_reset (&state);
}

// --- 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_flush (struct buffer_line *line, struct formatter *f, FILE *output)
{
	int flags = line->flags;
	if (flags & BUFFER_LINE_INDENT)  formatter_add (f, "    ");
	if (flags & BUFFER_LINE_STATUS)  formatter_add (f, " -  ");
	if (flags & BUFFER_LINE_ERROR)   formatter_add (f, "#a=!=#r ", ATTR_ERROR);

	formatter_add_from (f, line->formatter);
	formatter_add (f, "\n");
	formatter_flush (f, output);
	formatter_free (f);
}

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 formatter f;
	formatter_init (&f, ctx, NULL);

	struct tm current;
	char buf[9];
	if (!localtime_r (&line->when, &current))
		print_error ("%s: %s", "localtime_r", strerror (errno));
	else if (!strftime (buf, sizeof buf, "%T", &current))
		print_error ("%s: %s", "strftime", "buffer too small");
	else
		formatter_add (&f, "#a#s#r ", ATTR_TIMESTAMP, buf);

	// 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);
		FORMATTER_ADD_ITEM (&f, IGNORE_ATTR, .attribute = 1);
	}

	input_hide (&ctx->input);
	buffer_line_flush (line, &f, stdout);
	input_show (&ctx->input);
}

static void
buffer_line_write_to_log (struct app_context *ctx,
	struct buffer_line *line, FILE *log_file)
{
	if (line->flags & BUFFER_LINE_SKIP_FILE)
		return;

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

	struct tm current;
	char buf[20];
	if (!gmtime_r (&line->when, &current))
		print_error ("%s: %s", "gmtime_r", strerror (errno));
	else if (!strftime (buf, sizeof buf, "%F %T", &current))
		print_error ("%s: %s", "strftime", "buffer too small");
	else
		formatter_add (&f, "#s ", buf);

	buffer_line_flush (line, &f, log_file);
}

static void
log_formatter (struct app_context *ctx,
	struct buffer *buffer, int flags, struct formatter *f)
{
	if (!buffer)
		buffer = ctx->global_buffer;

	struct buffer_line *line = buffer_line_new ();
	line->flags = flags;
	line->when = time (NULL);

	// Move the formatter inside
	line->formatter = xmalloc (sizeof *line->formatter);
	*line->formatter = *f;

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

	if (buffer->log_file)
		buffer_line_write_to_log (ctx, line, buffer->log_file);

	if (ctx->beep_on_highlight)
		if ((flags & BUFFER_LINE_HIGHLIGHT)
		 || (buffer->type == BUFFER_PM && buffer != ctx->current_buffer))
			input_ding (&ctx->input);

	bool can_leak = false;
	if ((buffer == ctx->global_buffer)
	 || (ctx->current_buffer->type == BUFFER_GLOBAL
		&& buffer->type == BUFFER_SERVER)
	 || (ctx->current_buffer->type != BUFFER_GLOBAL
		&& buffer == ctx->current_buffer->server->buffer))
		can_leak = true;

	if (buffer == ctx->current_buffer)
		buffer_line_display (ctx, line, false);
	else if (!ctx->isolate_buffers && can_leak)
		buffer_line_display (ctx, line, true);
	else
	{
		buffer->unseen_messages_count++;
		if (flags & BUFFER_LINE_HIGHLIGHT)
			buffer->highlighted = true;

		refresh_prompt (ctx);
	}
}

static void
log_full (struct app_context *ctx, struct server *s, struct buffer *buffer,
	int flags, const char *format, ...)
{
	va_list ap;
	va_start (ap, format);

	struct formatter f;
	formatter_init (&f, ctx, s);
	formatter_addv (&f, format, &ap);
	log_formatter (ctx, buffer, flags, &f);

	va_end (ap);
}

#define log_global(ctx, flags, ...)                                            \
	log_full ((ctx), NULL, (ctx)->global_buffer, flags, __VA_ARGS__)
#define log_server(s, buffer, flags, ...)                                      \
	log_full ((s)->ctx, s, (buffer), flags, __VA_ARGS__)

#define log_global_status(ctx, ...)                                            \
	log_global ((ctx), BUFFER_LINE_STATUS, __VA_ARGS__)
#define log_global_error(ctx, ...)                                             \
	log_global ((ctx), BUFFER_LINE_ERROR,  __VA_ARGS__)
#define log_global_indent(ctx, ...)                                            \
	log_global ((ctx), BUFFER_LINE_INDENT, __VA_ARGS__)

#define log_server_status(s, buffer, ...)                                      \
	log_server ((s), (buffer), BUFFER_LINE_STATUS, __VA_ARGS__)
#define log_server_error(s, buffer, ...)                                       \
	log_server ((s), (buffer), BUFFER_LINE_ERROR,  __VA_ARGS__)

#define log_global_debug(ctx, ...)                                             \
	BLOCK_START                                                                \
		if (g_debug_mode)                                                      \
			log_global ((ctx), 0, "(*) " __VA_ARGS__);                         \
	BLOCK_END

#define log_server_debug(s, ...)                                               \
	BLOCK_START                                                                \
		if (g_debug_mode)                                                      \
			log_server ((s), (s)->buffer, 0, "(*) " __VA_ARGS__);              \
	BLOCK_END

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

// Lines that are used in more than one place

#define log_nick_self(s, buffer, new_)                                         \
	log_server_status ((s), (buffer), "You are now known as #n", (new_))
#define log_nick(s, buffer, old, new_)                                         \
	log_server_status ((s), (buffer), "#n is now known as #n", (old), (new_))

#define log_outcoming_notice(s, buffer, who, text)                             \
	log_server_status ((s), (buffer), "#s(#n): #m", "Notice", (who), (text))
#define log_outcoming_privmsg(s, buffer, prefixes, who, text)                  \
	log_server ((s), (buffer), 0, "<#s#n> #m", (prefixes), (who), (text))
#define log_outcoming_action(s, buffer, who, text)                             \
	log_server ((s), (buffer), 0, " #a*#r  #n #m", ATTR_ACTION, (who), (text))

#define log_outcoming_orphan_notice(s, target, text)                           \
	log_server_status ((s), (s)->buffer, "Notice -> #n: #m", (target), (text))
#define log_outcoming_orphan_privmsg(s, target, text)                          \
	log_server_status ((s), (s)->buffer, "MSG(#n): #m", (target), (text))

#define log_ctcp_query(s, target, tag)                                         \
	log_server_status ((s), (s)->buffer, "CTCP query to #S: #S", target, tag)
#define log_ctcp_reply(s, target, reply /* freed! */)                          \
	log_server_status ((s), (s)->buffer, "CTCP reply to #S: #&S", target, reply)

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

static void
make_log_filename (const char *filename, struct str *output)
{
	for (const char *p = filename; *p; p++)
		// XXX: anything more to replace?
		if (strchr ("/\\ ", *p))
			str_append_c (output, '_');
		else
			str_append_c (output, tolower_ascii (*p));
}

static void
buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
{
	if (!ctx->logging || buffer->log_file)
		return;

	struct str path;
	str_init (&path);
	get_xdg_home_dir (&path, "XDG_DATA_HOME", ".local/share");
	str_append_printf (&path, "/%s/%s", PROGRAM_NAME, "logs");

	(void) mkdir_with_parents (path.str, NULL);

	// TODO: make sure global and server buffers don't collide with filenames
	str_append_c (&path, '/');
	make_log_filename (buffer->name, &path);
	str_append (&path, ".log");

	if (!(buffer->log_file = fopen (path.str, "ab")))
		log_global_error (ctx, "Couldn't open log file `#s': #s",
			path.str, strerror (errno));
	str_free (&path);
}

static void
buffer_close_log_file (struct buffer *buffer)
{
	if (buffer->log_file)
		(void) fclose (buffer->log_file);
	buffer->log_file = NULL;
}

static void
on_config_logging_change (struct config_item_ *item)
{
	struct app_context *ctx = item->user_data;
	ctx->logging = item->value.boolean;

	for (struct buffer *buffer = ctx->buffers; buffer; buffer = buffer->next)
		if (ctx->logging)
			buffer_open_log_file (ctx, buffer);
		else
			buffer_close_log_file (buffer);
}

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

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);

	buffer_open_log_file (ctx, 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);
	hard_assert (buffer != ctx->global_buffer);
	hard_assert (buffer->type != BUFFER_SERVER);

	input_destroy_buffer (&ctx->input, buffer->input_data);
	buffer->input_data = NULL;

	// 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);

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

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

	refresh_prompt (ctx);
}

static void
buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
{
	// The prompt can take considerable time to redraw
	input_hide (&ctx->input);
	print_status ("%s", buffer->name);

	// That is, minus the buffer switch line and the readline prompt
	int to_display = MAX (10, g_terminal.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;
	buffer->highlighted = false;

	refresh_prompt (ctx);
	input_show (&ctx->input);
}

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

	buffer_print_backlog (ctx, buffer);
	input_switch_buffer (&ctx->input, buffer->input_data);

	// 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)
{
	// XXX: anything better to do?  This situation is arguably rare and I'm
	//   not entirely sure what action to take.
	log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS,
		"Buffer #s was merged into this buffer", merged->name);

	// Find all lines from "merged" newer than the newest line in "buffer"
	struct buffer_line *start = merged->lines;
	if (buffer->lines_tail)
		while (start && start->when < buffer->lines_tail->when)
			start = start->next;
	if (!start)
		return;

	// Count how many of them we have
	size_t n = 0;
	for (struct buffer_line *iter = start; iter; iter = iter->next)
		n++;
	struct buffer_line *tail = merged->lines_tail;

	// Cut them from the original buffer
	if (start == merged->lines)
		merged->lines = NULL;
	else if (start->prev)
		start->prev->next = NULL;
	if (start == merged->lines_tail)
		merged->lines_tail = start->prev;
	merged->lines_count -= n;

	// And append them to current lines in the buffer
	buffer->lines_tail->next = start;
	start->prev = buffer->lines_tail;
	buffer->lines_tail = tail;
	buffer->lines_count += n;

	log_full (ctx, NULL, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_SKIP_FILE,
		"End of merged content");
}

static void
buffer_rename (struct app_context *ctx,
	struct buffer *buffer, const char *new_name)
{
	struct buffer *collision = str_map_find (&ctx->buffers_by_name, new_name);
	if (collision == buffer)
		return;

	hard_assert (!collision);

	str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
	str_map_set (&ctx->buffers_by_name, new_name, buffer);

	buffer_close_log_file (buffer);
	buffer_open_log_file (ctx, buffer);

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

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

static void
buffer_clear (struct buffer *buffer)
{
	LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
		buffer_line_destroy (iter);

	buffer->lines = buffer->lines_tail = NULL;
	buffer->lines_count = 0;
}

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_global_buffer (struct app_context *ctx)
{
	struct buffer *global = ctx->global_buffer = buffer_new ();
	global->type = BUFFER_GLOBAL;
	global->name = xstrdup (PROGRAM_NAME);

	buffer_add (ctx, global);
	buffer_activate (ctx, global);
}

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

static void
irc_user_on_destroy (void *object, void *user_data)
{
	struct user *user = object;
	struct server *s = user_data;
	if (!s->rehashing)
		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;
}

struct user *
irc_get_or_make_user (struct server *s, const char *nickname)
{
	struct user *user = str_map_find (&s->irc_users, nickname);
	if (user)
		return user_ref (user);
	return irc_make_user (s, xstrdup (nickname));
}

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 = irc_get_or_make_user (s, nickname);

	// Open a new buffer for the user
	buffer = buffer_new ();
	buffer->type = BUFFER_PM;
	buffer->name = xstrdup_printf ("%s.%s", s->name, nickname);
	buffer->server = s;
	buffer->user = user;
	str_map_set (&s->irc_buffer_map, user->nickname, buffer);

	buffer_add (s->ctx, buffer);
	return buffer;
}

// Note that this eats the user reference
static void
irc_channel_link_user (struct channel *channel, struct user *user,
	const char *prefixes)
{
	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;
	str_append (&channel_user->prefixes, prefixes);
	LIST_PREPEND (channel->users, channel_user);
}

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);
	if (!s->rehashing)
		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->topic = NULL;
	str_map_set (&s->irc_channels, channel->name, channel);
	return channel;
}

static struct channel_user *
irc_channel_get_user (struct channel *channel, struct user *user)
{
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
		if (iter->user == user)
			return iter;
	return NULL;
}

static void
irc_remove_user_from_channel (struct user *user, struct channel *channel)
{
	struct channel_user *channel_user = irc_channel_get_user (channel, user);
	if (channel_user)
		irc_channel_unlink_user (channel, channel_user);
}

static void
irc_left_channel (struct channel *channel)
{
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
		irc_channel_unlink_user (channel, iter);
}

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

static void
remove_conflicting_buffer (struct server *s, struct buffer *buffer)
{
	log_server_status (s, s->buffer,
		"Removed buffer #s because of casemapping conflict", buffer->name);
	if (s->ctx->current_buffer == buffer)
		buffer_activate (s->ctx, s->buffer);
	buffer_remove (s->ctx, buffer);
}

static void
irc_try_readd_user (struct server *s,
	struct user *user, struct buffer *buffer)
{
	if (str_map_find (&s->irc_users, user->nickname))
	{
		// Remove user from all channels and destroy any PM buffer
		user_ref (user);
		LIST_FOR_EACH (struct user_channel, iter, user->channels)
			irc_remove_user_from_channel (user, iter->channel);
		if (buffer)
			remove_conflicting_buffer (s, buffer);
		user_unref (user);
	}
	else
	{
		str_map_set (&s->irc_users, user->nickname, user);
		str_map_set (&s->irc_buffer_map, user->nickname, buffer);
	}
}

static void
irc_try_readd_channel (struct server *s,
	struct channel *channel, struct buffer *buffer)
{
	if (str_map_find (&s->irc_channels, channel->name))
	{
		// Remove all users from channel and destroy any channel buffer
		channel_ref (channel);
		LIST_FOR_EACH (struct channel_user, iter, channel->users)
			irc_channel_unlink_user (channel, iter);
		if (buffer)
			remove_conflicting_buffer (s, buffer);
		channel_unref (channel);
	}
	else
	{
		str_map_set (&s->irc_channels, channel->name, channel);
		str_map_set (&s->irc_buffer_map, channel->name, buffer);
	}
}

static void
irc_rehash_and_fix_conflicts (struct server *s)
{
	// Save the old maps and initialize new ones
	struct str_map old_users      = s->irc_users;
	struct str_map old_channels   = s->irc_channels;
	struct str_map old_buffer_map = s->irc_buffer_map;

	str_map_init (&s->irc_users);
	str_map_init (&s->irc_channels);
	str_map_init (&s->irc_buffer_map);

	s->irc_users     .key_xfrm = s->irc_strxfrm;
	s->irc_channels  .key_xfrm = s->irc_strxfrm;
	s->irc_buffer_map.key_xfrm = s->irc_strxfrm;

	// Prevent channels and users from unsetting themselves
	// from server maps upon removing the last reference to them
	s->rehashing = true;

	// XXX: to be perfectly sure, we should also check
	//   whether any users collide with channels and vice versa

	// Our own user always takes priority, add him first
	if (s->irc_user)
		irc_try_readd_user (s, s->irc_user,
			str_map_find (&old_buffer_map, s->irc_user->nickname));

	struct str_map_iter iter;
	struct user *user;
	struct channel *channel;

	str_map_iter_init (&iter, &old_users);
	while ((user = str_map_iter_next (&iter)))
		irc_try_readd_user (s, user,
			str_map_find (&old_buffer_map, user->nickname));

	str_map_iter_init (&iter, &old_channels);
	while ((channel = str_map_iter_next (&iter)))
		irc_try_readd_channel (s, channel,
			str_map_find (&old_buffer_map, channel->name));

	// Hopefully we've either moved or destroyed all the old content
	s->rehashing = false;

	str_map_free (&old_users);
	str_map_free (&old_channels);
	str_map_free (&old_buffer_map);
}

static void
irc_set_casemapping (struct server *s,
	irc_tolower_fn tolower, irc_strxfrm_fn strxfrm)
{
	if (tolower == s->irc_tolower
	 && strxfrm == s->irc_strxfrm)
		return;

	s->irc_tolower = tolower;
	s->irc_strxfrm = strxfrm;

	// Ideally we would never have to do this but I can't think of a workaround
	irc_rehash_and_fix_conflicts (s);
}

// --- Core functionality ------------------------------------------------------

static bool
irc_is_connected (struct server *s)
{
	return s->state != IRC_DISCONNECTED && s->state != IRC_CONNECTING;
}

static void
irc_update_poller (struct server *s, const struct pollfd *pfd)
{
	int new_events = s->transport->get_poll_events (s);
	hard_assert (new_events != 0);

	if (!pfd || pfd->events != new_events)
		poller_fd_set (&s->socket_event, new_events);
}

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
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
irc_queue_reconnect (struct server *s)
{
	// As long as the user wants us to, that is
	if (!get_config_boolean (s->config, "reconnect"))
		return;
	int64_t delay = get_config_integer (s->config, "reconnect_delay");

	// TODO: exponentional backoff
	// XXX: maybe add a state for when a connect is queued?
	hard_assert (s->state == IRC_DISCONNECTED);
	log_server_status (s, s->buffer,
		"Trying to reconnect in #&s seconds...",
		xstrdup_printf ("%" PRId64, delay));
	poller_timer_set (&s->reconnect_tmr, delay * 1000);
}

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

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

static void
irc_send (struct server *s, const char *format, ...)
{
	if (!soft_assert (irc_is_connected (s)))
	{
		log_server_debug (s, "sending a message to a dead server connection");
		return;
	}

	if (s->state == IRC_CLOSING
	 || s->state == IRC_HALF_CLOSED)
		return;

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

	log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str);

	str_append_str (&s->write_buffer, &str);
	str_free (&str);
	str_append (&s->write_buffer, "\r\n");
	irc_update_poller (s, NULL);
}

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

static void
irc_real_shutdown (struct server *s)
{
	hard_assert (irc_is_connected (s) && s->state != IRC_HALF_CLOSED);

	if (s->transport
	 && s->transport->in_before_shutdown)
		s->transport->in_before_shutdown (s);

	while (shutdown (s->socket, SHUT_WR) == -1)
		if (!soft_assert (errno == EINTR))
			break;

	s->state = IRC_HALF_CLOSED;
}

static void
irc_shutdown (struct server *s)
{
	if (s->state == IRC_CLOSING
	 || s->state == IRC_HALF_CLOSED)
		return;

	// TODO: set a timer to cut the connection if we don't receive an EOF
	s->state = IRC_CLOSING;

	// Either there's still some data in the write buffer and we wait
	// until they're sent, or we send an EOF to the server right away
	if (!s->write_buffer.len)
		irc_real_shutdown (s);
}

static void
irc_destroy_connector (struct server *s)
{
	connector_free (s->connector);
	free (s->connector);
	s->connector = NULL;

	// Not connecting anymore
	s->state = IRC_DISCONNECTED;
}

static void
try_finish_quit (struct app_context *ctx)
{
	if (!ctx->quitting)
		return;

	struct str_map_iter iter;
	str_map_iter_init (&iter, &ctx->servers);

	bool disconnected_all = true;
	struct server *s;
	while ((s = str_map_iter_next (&iter)))
		if (irc_is_connected (s))
			disconnected_all = false;

	if (disconnected_all)
		ctx->polling = false;
}

static void
initiate_quit (struct app_context *ctx)
{
	log_global_status (ctx, "Shutting down");

	// Destroy the user interface
	input_stop (&ctx->input);

	struct str_map_iter iter;
	str_map_iter_init (&iter, &ctx->servers);

	// Initiate a connection close
	struct server *s;
	while ((s = str_map_iter_next (&iter)))
	{
		// There may be a timer set to reconnect to the server
		poller_timer_reset (&s->reconnect_tmr);

		if (irc_is_connected (s))
		{
			irc_shutdown (s);
			s->manual_disconnect = true;
		}
		else if (s->state == IRC_CONNECTING)
			irc_destroy_connector (s);
	}

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

static void
irc_disconnect (struct server *s)
{
	hard_assert (irc_is_connected (s));

	// Get rid of the dead socket
	if (s->transport
	 && s->transport->cleanup)
		s->transport->cleanup (s);
	s->transport = NULL;

	xclose (s->socket);
	s->socket = -1;
	s->state = IRC_DISCONNECTED;

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

	str_reset (&s->read_buffer);
	str_reset (&s->write_buffer);

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

	// Reset state bound to the connection
	struct str_map_iter iter;
	str_map_iter_init (&iter, &s->irc_channels);
	struct channel *channel;
	while ((channel = str_map_iter_next (&iter)))
		irc_left_channel (channel);

	if (s->irc_user)
	{
		user_unref (s->irc_user);
		s->irc_user = NULL;
	}

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

	s->cap_echo_message = false;

	// Need to call this before server_init_specifics()
	irc_set_casemapping (s, irc_tolower, irc_strxfrm);

	server_free_specifics (s);
	server_init_specifics (s);

	// Take any relevant actions
	if (s->ctx->quitting)
		try_finish_quit (s->ctx);
	else if (s->manual_disconnect)
		s->manual_disconnect = false;
	else
		irc_queue_reconnect (s);

	refresh_prompt (s->ctx);
}

static void
irc_initiate_disconnect (struct server *s, const char *reason)
{
	hard_assert (irc_is_connected (s));

	s->manual_disconnect = true;
	if (reason)
		irc_send (s, "QUIT :%s", reason);
	else
		irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
}

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

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

static void
on_irc_timeout (void *user_data)
{
	// Provoke a response from the server
	struct server *s = user_data;
	irc_send (s, "PING :%" PRIi64, (int64_t) time (NULL));
}

// --- Server I/O --------------------------------------------------------------

static void irc_process_message
	(const struct irc_message *msg, const char *raw, void *user_data);

static enum transport_io_result
irc_try_read (struct server *s)
{
	enum transport_io_result result = s->transport->try_read (s);
	if (result == TRANSPORT_IO_OK)
	{
		if (s->read_buffer.len >= (1 << 20))
		{
			// XXX: this is stupid; if anything, count it in dependence of time
			log_server_error (s, s->buffer,
				"The IRC server seems to spew out data frantically");
			return TRANSPORT_IO_ERROR;
		}
		if (s->read_buffer.len)
			irc_process_buffer (&s->read_buffer, irc_process_message, s);
	}
	return result;
}

static enum transport_io_result
irc_try_write (struct server *s)
{
	enum transport_io_result result = s->transport->try_write (s);
	if (result == TRANSPORT_IO_OK)
	{
		// If we're flushing the write buffer and our job is complete, we send
		// an EOF to the server, changing the state to IRC_HALF_CLOSED
		if (s->state == IRC_CLOSING && !s->write_buffer.len)
			irc_real_shutdown (s);
	}
	return result;
}

static bool
irc_try_read_write (struct server *s)
{
	enum transport_io_result read_result;
	enum transport_io_result write_result;
	if ((read_result  = irc_try_read  (s)) == TRANSPORT_IO_ERROR
	 || (write_result = irc_try_write (s)) == TRANSPORT_IO_ERROR)
	{
		log_server_error (s, s->buffer, "Server connection failed");
		return false;
	}

	// FIXME: this may probably fire multiple times when we're flushing,
	//   we should probably store a flag next to the state
	if (read_result  == TRANSPORT_IO_EOF
	 || write_result == TRANSPORT_IO_EOF)
		log_server_error (s, s->buffer, "Server closed the connection");

	// If the write needs to read and we receive an EOF, we can't flush
	if (write_result == TRANSPORT_IO_EOF)
		return false;

	if (read_result  == TRANSPORT_IO_EOF)
	{
		// Eventually initiate shutdown to flush the write buffer
		irc_shutdown (s);

		// If there's nothing to write, we can disconnect now
		if (s->state == IRC_HALF_CLOSED)
			return false;
	}
	return true;
}

static void
on_irc_ready (const struct pollfd *pfd, struct server *s)
{
	if (irc_try_read_write (s))
	{
		// XXX: shouldn't we rather wait for PONG messages?
		irc_reset_connection_timeouts (s);
		irc_update_poller (s, pfd);
	}
	else
		// We don't want to keep the socket anymore
		irc_disconnect (s);
}

// --- Plain transport ---------------------------------------------------------

static enum transport_io_result
transport_plain_try_read (struct server *s)
{
	struct str *buf = &s->read_buffer;
	ssize_t n_read;

	while (true)
	{
		str_ensure_space (buf, 512);
		n_read = recv (s->socket, buf->str + buf->len,
			buf->alloc - buf->len - 1 /* null byte */, 0);

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

		if (errno == EAGAIN)
			return TRANSPORT_IO_OK;
		if (errno == EINTR)
			continue;

		LOG_LIBC_FAILURE ("recv");
		return TRANSPORT_IO_ERROR;
	}
}

static enum transport_io_result
transport_plain_try_write (struct server *s)
{
	struct str *buf = &s->write_buffer;
	ssize_t n_written;

	while (buf->len)
	{
		n_written = send (s->socket, buf->str, buf->len, 0);
		if (n_written >= 0)
		{
			str_remove_slice (buf, 0, n_written);
			continue;
		}

		if (errno == EAGAIN)
			return TRANSPORT_IO_OK;
		if (errno == EINTR)
			continue;

		LOG_LIBC_FAILURE ("send");
		return TRANSPORT_IO_ERROR;
	}
	return TRANSPORT_IO_OK;
}

static int
transport_plain_get_poll_events (struct server *s)
{
	int events = POLLIN;
	if (s->write_buffer.len)
		events |= POLLOUT;
	return events;
}

static struct transport g_transport_plain =
{
	.try_read         = transport_plain_try_read,
	.try_write        = transport_plain_try_write,
	.get_poll_events  = transport_plain_get_poll_events,
};

// --- SSL/TLS transport -------------------------------------------------------

struct transport_tls_data
{
	SSL_CTX *ssl_ctx;                   ///< SSL context
	SSL *ssl;                           ///< SSL/TLS connection
	bool ssl_rx_want_tx;                ///< SSL_read() wants to write
	bool ssl_tx_want_rx;                ///< SSL_write() wants to read
};

static bool
transport_tls_init_ctx (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
{
	bool verify = get_config_boolean (s->config, "ssl_verify");
	if (!verify)
		SSL_CTX_set_verify (ssl_ctx, SSL_VERIFY_NONE, NULL);

	const char *ca_file = get_config_string (s->config, "ssl_ca_file");
	const char *ca_path = get_config_string (s->config, "ssl_ca_path");

	struct error *error = NULL;
	if (ca_file || ca_path)
	{
		if (SSL_CTX_load_verify_locations (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 (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;
	}

	// TODO: allow specifying SSL_CTX_set_cipher_list()
	SSL_CTX_set_mode (ssl_ctx,
		SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
	return true;

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

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

static bool
transport_tls_init_cert (struct server *s, SSL *ssl, struct error **e)
{
	const char *ssl_cert = get_config_string (s->config, "ssl_cert");
	if (!ssl_cert)
		return true;

	bool result = false;
	char *path = resolve_config_filename (ssl_cert);
	if (!path)
		error_set (e, "%s: %s", "Cannot open file", ssl_cert);
	// XXX: perhaps we should read the file ourselves for better messages
	else if (!SSL_use_certificate_file (ssl, path, SSL_FILETYPE_PEM)
		|| !SSL_use_PrivateKey_file (ssl, path, SSL_FILETYPE_PEM))
		error_set (e, "%s: %s", "Setting the SSL client certificate failed",
			ERR_error_string (ERR_get_error (), NULL));
	else
		result = true;
	free (path);
	return result;
}

static bool
transport_tls_init (struct server *s, struct error **e)
{
	const char *error_info = NULL;
	SSL_CTX *ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
	if (!ssl_ctx)
		goto error_ssl_1;
	if (!transport_tls_init_ctx (s, ssl_ctx, e))
		goto error_ssl_2;

	SSL *ssl = SSL_new (ssl_ctx);
	if (!ssl)
		goto error_ssl_2;

	struct error *error = NULL;
	if (!transport_tls_init_cert (s, ssl, &error))
	{
		// XXX: is this a reason to abort the connection?
		log_server_error (s, s->buffer, "#s", error->message);
		error_free (error);
	}

	SSL_set_connect_state (ssl);
	if (!SSL_set_fd (ssl, s->socket))
		goto error_ssl_3;

	// XXX: maybe set `ssl_rx_want_tx' to force a handshake?
	struct transport_tls_data *data = xcalloc (1, sizeof *data);
	data->ssl_ctx = ssl_ctx;
	data->ssl = ssl;

	s->transport_data = data;
	return true;

error_ssl_3:
	SSL_free (ssl);
error_ssl_2:
	SSL_CTX_free (ssl_ctx);
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/TLS", error_info);
	return false;
}

static void
transport_tls_cleanup (struct server *s)
{
	struct transport_tls_data *data = s->transport_data;
	if (data->ssl)
		SSL_free (data->ssl);
	if (data->ssl_ctx)
		SSL_CTX_free (data->ssl_ctx);
	free (data);
}

static enum transport_io_result
transport_tls_try_read (struct server *s)
{
	struct transport_tls_data *data = s->transport_data;
	if (data->ssl_tx_want_rx)
		return TRANSPORT_IO_OK;

	struct str *buf = &s->read_buffer;
	data->ssl_rx_want_tx = false;
	while (true)
	{
		str_ensure_space (buf, 512);
		int n_read = SSL_read (data->ssl, buf->str + buf->len,
			buf->alloc - buf->len - 1 /* null byte */);

		const char *error_info = NULL;
		switch (xssl_get_error (data->ssl, n_read, &error_info))
		{
		case SSL_ERROR_NONE:
			buf->str[buf->len += n_read] = '\0';
			continue;
		case SSL_ERROR_ZERO_RETURN:
			return TRANSPORT_IO_EOF;
		case SSL_ERROR_WANT_READ:
			return TRANSPORT_IO_OK;
		case SSL_ERROR_WANT_WRITE:
			data->ssl_rx_want_tx = true;
			return TRANSPORT_IO_OK;
		case XSSL_ERROR_TRY_AGAIN:
			continue;
		default:
			LOG_FUNC_FAILURE ("SSL_read", error_info);
			return TRANSPORT_IO_ERROR;
		}
	}
}

static enum transport_io_result
transport_tls_try_write (struct server *s)
{
	struct transport_tls_data *data = s->transport_data;
	if (data->ssl_rx_want_tx)
		return TRANSPORT_IO_OK;

	struct str *buf = &s->write_buffer;
	data->ssl_tx_want_rx = false;
	while (buf->len)
	{
		int n_written = SSL_write (data->ssl, buf->str, buf->len);

		const char *error_info = NULL;
		switch (xssl_get_error (data->ssl, n_written, &error_info))
		{
		case SSL_ERROR_NONE:
			str_remove_slice (buf, 0, n_written);
			continue;
		case SSL_ERROR_ZERO_RETURN:
			return TRANSPORT_IO_EOF;
		case SSL_ERROR_WANT_WRITE:
			return TRANSPORT_IO_OK;
		case SSL_ERROR_WANT_READ:
			data->ssl_tx_want_rx = true;
			return TRANSPORT_IO_OK;
		case XSSL_ERROR_TRY_AGAIN:
			continue;
		default:
			LOG_FUNC_FAILURE ("SSL_write", error_info);
			return TRANSPORT_IO_ERROR;
		}
	}
	return TRANSPORT_IO_OK;
}

static int
transport_tls_get_poll_events (struct server *s)
{
	struct transport_tls_data *data = s->transport_data;

	int events = POLLIN;
	if (s->write_buffer.len || data->ssl_rx_want_tx)
		events |= POLLOUT;

	// While we're waiting for an opposite event, we ignore the original
	if (data->ssl_rx_want_tx)  events &= ~POLLIN;
	if (data->ssl_tx_want_rx)  events &= ~POLLOUT;
	return events;
}

static void
transport_tls_in_before_shutdown (struct server *s)
{
	struct transport_tls_data *data = s->transport_data;
	(void) SSL_shutdown (data->ssl);
}

static struct transport g_transport_tls =
{
	.init               = transport_tls_init,
	.cleanup            = transport_tls_cleanup,
	.try_read           = transport_tls_try_read,
	.try_write          = transport_tls_try_write,
	.get_poll_events    = transport_tls_get_poll_events,
	.in_before_shutdown = transport_tls_in_before_shutdown,
};

// --- Connection establishment ------------------------------------------------

static bool
irc_autofill_user_info (struct server *s, struct error **e)
{
	const char *nicks    = get_config_string (s->config, "nicks");
	const char *username = get_config_string (s->config, "username");
	const char *realname = get_config_string (s->config, "realname");

	if (nicks && *nicks && username && *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 (!nicks    || !*nicks)
		set_config_string (s->config, "nicks",    pwd->pw_name);
	if (!username || !*username)
		set_config_string (s->config, "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 (s->config, "realname", gecos);
	}

	return true;
}

static char *
irc_fetch_next_nickname (struct server *s)
{
	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (get_config_string (s->config, "nicks"), ',', &v);

	char *result = NULL;
	if (s->nick_counter >= 0 && s->nick_counter < v.len)
		result = str_vector_steal (&v, s->nick_counter++);
	if (s->nick_counter >= v.len)
		// Exhausted all nicknames
		s->nick_counter = -1;

	str_vector_free (&v);
	return result;
}

static void
irc_register (struct server *s)
{
	// Fill in user information automatically if needed
	irc_autofill_user_info (s, NULL);

	const char *username = get_config_string (s->config, "username");
	const char *realname = get_config_string (s->config, "realname");
	hard_assert (username && realname);

	// Start IRCv3.1 capability negotiation;
	// at worst the server will ignore this or send a harmless error message
	irc_send (s, "CAP LS");

	const char *password = get_config_string (s->config, "password");
	if (password)
		irc_send (s, "PASS :%s", password);

	s->nick_counter = 0;

	char *nickname = irc_fetch_next_nickname (s);
	if (nickname)
		irc_send (s, "NICK :%s", nickname);
	else
		log_server_error (s, s->buffer, "No nicks present in configuration");
	free (nickname);

	// IRC servers may ignore the last argument if it's empty
	irc_send (s, "USER %s 8 * :%s", username, *realname ? realname : " ");
}

static void
irc_finish_connection (struct server *s, int socket)
{
	struct app_context *ctx = s->ctx;

	set_blocking (socket, false);
	s->socket = socket;
	s->transport = get_config_boolean (s->config, "ssl")
		? &g_transport_tls
		: &g_transport_plain;

	struct error *e = NULL;
	if (s->transport->init && !s->transport->init (s, &e))
	{
		log_server_error (s, s->buffer, "Connection failed: #s", e->message);
		error_free (e);

		xclose (s->socket);
		s->socket = -1;

		s->transport = NULL;
		return;
	}

	log_server_status (s, s->buffer, "Connection established");
	s->state = IRC_CONNECTED;

	poller_fd_init (&s->socket_event, &ctx->poller, s->socket);
	s->socket_event.dispatcher = (poller_fd_fn) on_irc_ready;
	s->socket_event.user_data = s;

	irc_update_poller (s, NULL);
	irc_reset_connection_timeouts (s);
	irc_register (s);

	refresh_prompt (s->ctx);
}

static void
irc_on_connector_connecting (void *user_data, const char *address)
{
	struct server *s = user_data;
	log_server_status (s, s->buffer, "Connecting to #s...", address);
}

static void
irc_on_connector_error (void *user_data, const char *error)
{
	struct server *s = user_data;
	log_server_error (s, s->buffer, "Connection failed: #s", error);
}

static void
irc_on_connector_failure (void *user_data)
{
	struct server *s = user_data;
	irc_destroy_connector (s);
	irc_queue_reconnect (s);
}

static void
irc_on_connector_connected (void *user_data, int socket)
{
	struct server *s = user_data;
	irc_destroy_connector (s);
	irc_finish_connection (s, socket);
}

static void
irc_split_host_port (char *s, char **host, char **port)
{
	char *colon = strchr (s, ':');
	if (colon)
	{
		*colon = '\0';
		*port = ++colon;
	}
	else
		*port = "6667";

	*host = s;
}

static bool
irc_setup_connector (struct server *s,
	const struct str_vector *addresses, struct error **e)
{
	struct connector *connector = xmalloc (sizeof *connector);
	connector_init (connector, &s->ctx->poller);

	connector->user_data     = s;
	connector->on_connecting = irc_on_connector_connecting;
	connector->on_error      = irc_on_connector_error;
	connector->on_connected  = irc_on_connector_connected;
	connector->on_failure    = irc_on_connector_failure;

	s->state = IRC_CONNECTING;
	s->connector = connector;

	for (size_t i = 0; i < addresses->len; i++)
	{
		char *host, *port;
		irc_split_host_port (addresses->vector[i], &host, &port);

		if (!connector_add_target (connector, host, port, e))
		{
			irc_destroy_connector (s);
			return false;
		}
	}

	connector_step (connector);
	return true;
}

static bool
irc_initiate_connect_socks (struct server *s,
	const struct str_vector *addresses, struct error **e)
{
	const char *socks_host = get_config_string (s->config, "socks_host");
	int64_t socks_port_int = get_config_integer (s->config, "socks_port");

	const char *socks_username =
		get_config_string (s->config, "socks_username");
	const char *socks_password =
		get_config_string (s->config, "socks_password");

	if (!socks_host)
		return false;

	// FIXME: we only try the first address (still better than nothing)
	char *irc_host, *irc_port;
	irc_split_host_port (addresses->vector[0], &irc_host, &irc_port);

	char *socks_port = xstrdup_printf ("%" PRIi64, socks_port_int);

	log_server_status (s, s->buffer, "Connecting to #&s via #&s...",
		format_host_port_pair (irc_host, irc_port),
		format_host_port_pair (socks_host, socks_port));

	// TODO: the SOCKS code needs a rewrite so that we don't block on it either;
	//   perhaps it could act as a special kind of connector
	struct error *error = NULL;
	bool result = true;
	int fd = socks_connect (socks_host, socks_port, irc_host, irc_port,
		socks_username, socks_password, &error);
	if (fd != -1)
		irc_finish_connection (s, fd);
	else
	{
		error_set (e, "%s: %s", "SOCKS connection failed", error->message);
		error_free (error);
		result = false;
	}

	free (socks_port);
	return result;
}

static void
irc_initiate_connect (struct server *s)
{
	hard_assert (s->state == IRC_DISCONNECTED);

	const char *addresses = get_config_string (s->config, "addresses");
	if (!addresses || !addresses[strspn (addresses, ",")])
	{
		// No sense in trying to reconnect
		log_server_error (s, s->buffer,
			"No addresses specified in configuration");
		return;
	}

	struct str_vector servers;
	str_vector_init (&servers);
	split_str_ignore_empty (addresses, ',', &servers);

	struct error *e = NULL;
	if (!irc_initiate_connect_socks (s, &servers, &e) && !e)
		irc_setup_connector (s, &servers, &e);

	str_vector_free (&servers);

	if (e)
	{
		log_server_error (s, s->buffer, "#s", e->message);
		error_free (e);
		irc_queue_reconnect (s);
	}
}

// --- Input prompt ------------------------------------------------------------

static void
make_unseen_prefix (struct app_context *ctx, struct str *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, ',');
		if (iter->highlighted)
			str_append_c (active_buffers, '!');
		str_append_printf (active_buffers, "%zu", i);
	}
}

static void
make_chanmode_postfix (struct channel *channel, struct str *modes)
{
	if (channel->no_param_modes.len)
		str_append (modes, channel->no_param_modes.str);

	struct str_map_iter iter;
	str_map_iter_init (&iter, &channel->param_modes);

	char *param;
	while ((param = str_map_iter_next (&iter)))
		str_append_c (modes, iter.link->key[0]);
}

static void
make_server_postfix (struct buffer *buffer, struct str *output)
{
	struct server *s = buffer->server;
	str_append_c (output, ' ');
	if (!irc_is_connected (s))
		str_append (output, "(disconnected)");
	else if (s->state != IRC_REGISTERED)
		str_append (output, "(unregistered)");
	else
	{
		if (buffer->type == BUFFER_CHANNEL)
		{
			struct channel_user *channel_user =
				irc_channel_get_user (buffer->channel, s->irc_user);
			if (channel_user)
				str_append (output, channel_user->prefixes.str);
		}
		str_append (output, s->irc_user->nickname);
		if (s->irc_user_mode.len)
			str_append_printf (output, "(%s)", s->irc_user_mode.str);
	}
}
static void
make_prompt (struct app_context *ctx, struct str *output)
{
	struct buffer *buffer = ctx->current_buffer;
	if (!buffer)
		return;

	str_append_c (output, '[');

	struct str active_buffers;
	str_init (&active_buffers);
	make_unseen_prefix (ctx, &active_buffers);
	if (active_buffers.len)
		str_append_printf (output, "(%s) ", active_buffers.str);
	str_free (&active_buffers);

	str_append_printf (output, "%d:%s",
		buffer_get_index (ctx, buffer), buffer->name);
	if (buffer->type == BUFFER_CHANNEL)
	{
		struct str modes;
		str_init (&modes);
		make_chanmode_postfix (buffer->channel, &modes);
		if (modes.len)
			str_append_printf (output, "(+%s)", modes.str);
		str_free (&modes);
	}

	if (buffer != ctx->global_buffer)
		make_server_postfix (buffer, output);

	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, ' ');

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

// --- Helpers -----------------------------------------------------------------

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)
	{
		// 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);
		if (irc_is_this_us (s, target))
			buffer = irc_get_or_make_user_buffer (s, nickname);
		free (nickname);

		// With the IRCv3.2 echo-message capability, we can receive messages
		// as they are delivered to the target; in that case we return NULL
		// and the caller should check the origin
	}
	return buffer;
}

static bool
irc_is_highlight (struct server *s, const char *message)
{
	// This may be called by notices before even successfully registering
	if (!s->irc_user)
		return false;

	// 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);
	transform_str (copy, s->irc_tolower);

	char *nick = xstrdup (s->irc_user->nickname);
	transform_str (nick, s->irc_tolower);

	// 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 const char *
irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target)
{
	if (user && irc_is_channel (s, target))
	{
		struct channel *channel;
		struct channel_user *channel_user;
		if ((channel = str_map_find (&s->irc_channels, target))
		 && (channel_user = irc_channel_get_user (channel, user)))
			return channel_user->prefixes.str;
	}
	return "";
}

// --- Mode processor ----------------------------------------------------------

struct mode_processor
{
	char **params;                      ///< Mode string parameters
	bool adding;                        ///< Currently adding modes
	char mode_char;                     ///< Currently processed mode char

	// User data:

	struct server *s;                   ///< Server
	struct channel *channel;            ///< The channel being modified
};

/// Process a single mode character
typedef bool (*mode_processor_apply_fn) (struct mode_processor *);

static const char *
mode_processor_next_param (struct mode_processor *self)
{
	if (!*self->params)
		return NULL;
	return *self->params++;
}

static void
mode_processor_run (struct mode_processor *self,
	char **params, mode_processor_apply_fn apply_cb)
{
	self->params = params;

	const char *mode_string;
	while ((mode_string = mode_processor_next_param (self)))
	{
		self->adding = true;
		while ((self->mode_char = *mode_string++))
		{
			if      (self->mode_char == '+') self->adding = true;
			else if (self->mode_char == '-') self->adding = false;
			else if (!apply_cb (self))
				break;
		}
	}
}

static int
mode_char_cmp (const void *a, const void *b)
{
	return *(const char *) a - *(const char *) b;
}

/// Add/remove the current mode character to/from the given ordered list
static void
mode_processor_toggle (struct mode_processor *self, struct str *modes)
{
	const char *pos = strchr (modes->str, self->mode_char);
	if (self->adding == !!pos)
		return;

	if (self->adding)
	{
		str_append_c (modes, self->mode_char);
		qsort (modes->str, modes->len, 1, mode_char_cmp);
	}
	else
		str_remove_slice (modes, pos - modes->str, 1);
}

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

static void
mode_processor_do_user (struct mode_processor *self)
{
	const char *nickname;
	struct user *user;
	struct channel_user *channel_user;
	if (!(nickname = mode_processor_next_param (self))
	 || !(user = str_map_find (&self->s->irc_users, nickname))
	 || !(channel_user = irc_channel_get_user (self->channel, user)))
		return;

	const char *all_prefixes = self->s->irc_chanuser_prefixes;
	const char *all_modes    = self->s->irc_chanuser_modes;
	char prefix = all_prefixes[strchr (all_modes, self->mode_char) - all_modes];

	struct str *prefixes = &channel_user->prefixes;
	const char *pos = strchr (prefixes->str, prefix);
	if (self->adding == !!pos)
		return;

	if (self->adding)
	{
		// Add the new mode prefix while retaining the right order
		char *old_prefixes = str_steal (prefixes);
		str_init (prefixes);
		for (const char *p = all_prefixes; *p; p++)
			if (*p == prefix || strchr (old_prefixes, *p))
				str_append_c (prefixes, *p);
		free (old_prefixes);
	}
	else
		str_remove_slice (prefixes, pos - prefixes->str, 1);
}

static void
mode_processor_do_param_always (struct mode_processor *self)
{
	const char *param = NULL;
	if (!(param = mode_processor_next_param (self)))
		return;

	char key[2] = { self->mode_char, 0 };
	str_map_set (&self->channel->param_modes, key,
		self->adding ? xstrdup (param) : NULL);
}

static void
mode_processor_do_param_when_set (struct mode_processor *self)
{
	const char *param = NULL;
	if (self->adding && !(param = mode_processor_next_param (self)))
		return;

	char key[2] = { self->mode_char, 0 };
	str_map_set (&self->channel->param_modes, key,
		self->adding ? xstrdup (param) : NULL);
}

static bool
mode_processor_apply_channel (struct mode_processor *self)
{
	if      (strchr (self->s->irc_chanuser_modes,           self->mode_char))
		mode_processor_do_user (self);
	else if (strchr (self->s->irc_chanmodes_list,           self->mode_char))
		// Nothing to do here, just skip the next argument if there's any
		(void) mode_processor_next_param (self);
	else if (strchr (self->s->irc_chanmodes_param_always,   self->mode_char))
		mode_processor_do_param_always (self);
	else if (strchr (self->s->irc_chanmodes_param_when_set, self->mode_char))
		mode_processor_do_param_when_set (self);
	else if (strchr (self->s->irc_chanmodes_param_never,    self->mode_char))
		mode_processor_toggle (self, &self->channel->no_param_modes);
	else
		// It's not safe to continue, results could be undesired
		return false;
	return true;
}

static void
irc_handle_mode_channel
	(struct server *s, struct channel *channel, char **params)
{
	struct mode_processor p = { .s = s, .channel = channel };
	mode_processor_run (&p, params, mode_processor_apply_channel);
}

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

static bool
mode_processor_apply_user (struct mode_processor *self)
{
	mode_processor_toggle (self, &self->s->irc_user_mode);
	return true;
}

static void
irc_handle_mode_user (struct server *s, char **params)
{
	struct mode_processor p = { .s = s };
	mode_processor_run (&p, params, mode_processor_apply_user);
}

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

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

	struct str_vector v;
	str_vector_init (&v);

	if (msg->params.len > 2)
		split_str_ignore_empty (msg->params.vector[2], ' ', &v);

	const char *subcommand = msg->params.vector[1];
	if (!strcasecmp_ascii (subcommand, "ACK"))
	{
		for (size_t i = 0; i < v.len; i++)
		{
			const char *cap = v.vector[i];
			bool active = true;
			if (*cap == '-')
			{
				active = false;
				cap++;
			}
			if (!strcasecmp_ascii (cap, "echo-message"))
				s->cap_echo_message = active;
		}
		irc_send (s, "CAP END");
	}
	else if (!strcasecmp_ascii (subcommand, "NAK"))
		irc_send (s, "CAP END");
	else if (!strcasecmp_ascii (subcommand, "LS"))
	{
		struct str_vector chosen;
		str_vector_init (&chosen);

		// Filter server capabilities for ones we can make use of
		for (size_t i = 0; i < v.len; i++)
		{
			const char *cap = v.vector[i];
			if (!strcasecmp_ascii (cap, "multi-prefix")
			 || !strcasecmp_ascii (cap, "invite-notify")
			 || !strcasecmp_ascii (cap, "echo-message"))
				str_vector_add (&chosen, cap);
		}

		char *chosen_str = join_str_vector (&chosen, ' ');
		str_vector_free (&chosen);
		irc_send (s, "CAP REQ :%s", chosen_str);
		free (chosen_str);
	}

	str_vector_free (&v);
}

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

	const char *target       = msg->params.vector[0];
	const char *channel_name = msg->params.vector[1];

	struct buffer *buffer;
	if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
		buffer = s->buffer;

	// IRCv3.2 invite-notify extension allows the target to be someone else
	if (irc_is_this_us (s, target))
		log_server_status (s, buffer,
			"#n has invited you to #S", msg->prefix, channel_name);
	else
		log_server_status (s, buffer,
			"#n has invited #n to #S", msg->prefix, target, channel_name);
}

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_printf ("%s.%s", s->name, channel_name);
		buffer->server = s;
		buffer->channel = channel =
			irc_make_channel (s, xstrdup (channel_name));
		str_map_set (&s->irc_buffer_map, channel->name, buffer);

		buffer_add (s->ctx, buffer);
		buffer_activate (s->ctx, buffer);

		// Request the channel mode as we don't get it automatically
		irc_send (s, "MODE %s", channel_name);
	}

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

	// Add the user to the channel
	char *nickname = irc_cut_nickname (msg->prefix);
	irc_channel_link_user (channel, irc_get_or_make_user (s, nickname), "");
	free (nickname);

	// Finally log the message
	if (buffer)
	{
		log_server (s, buffer, 0, "#a-->#r #N #a#s#r #S",
			ATTR_JOIN, msg->prefix, ATTR_JOIN, "has joined", 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 = NULL;
	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)
	{
		if (irc_is_this_us (s, target))
			irc_left_channel (channel);
		else
			irc_remove_user_from_channel (user, channel);
	}

	if (buffer)
	{
		struct formatter f;
		formatter_init (&f, s->ctx, s);
		formatter_add (&f, "#a<--#r #N #a#s#r #n",
			ATTR_PART, msg->prefix, ATTR_PART, "has kicked", target);
		if (message)
			formatter_add (&f, " (#m)", message);
		log_formatter (s->ctx, buffer, 0, &f);
	}
}

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

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

	// Join the modes back to a single string
	struct str_vector copy;
	str_vector_init (&copy);
	str_vector_add_vector (&copy, msg->params.vector + 1);
	char *modes = join_str_vector (&copy, ' ');
	str_vector_free (&copy);

	if (irc_is_channel (s, context))
	{
		struct channel *channel = str_map_find (&s->irc_channels, context);
		struct buffer *buffer = str_map_find (&s->irc_buffer_map, context);
		hard_assert ((channel && buffer) ||
			(channel && !buffer) || (!channel && !buffer));

		if (channel)
			irc_handle_mode_channel (s, channel, msg->params.vector + 1);

		if (buffer)
		{
			log_server_status (s, buffer,
				"Mode #S [#S] by #n", context, modes, msg->prefix);
		}
	}
	else if (irc_is_this_us (s, context))
	{
		irc_handle_mode_user (s, msg->params.vector + 1);
		log_server_status (s, s->buffer,
			"User mode [#S] by #n", modes, msg->prefix);
	}

	free (modes);

	// Our own modes might have changed
	refresh_prompt (s->ctx);
}

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;

	bool lexicographically_different =
		!!irc_server_strcmp (s, user->nickname, new_nickname);

	// What the fuck, someone renamed themselves to ourselves
	// TODO: probably log a message and force a reconnect
	if (lexicographically_different
	 && !irc_server_strcmp (s, new_nickname, s->irc_user->nickname))
		return;

	// Log a message in any PM buffer (we may even have one for ourselves)
	struct buffer *pm_buffer =
		str_map_find (&s->irc_buffer_map, user->nickname);
	if (pm_buffer)
	{
		if (irc_is_this_us (s, msg->prefix))
			log_nick_self (s, pm_buffer, new_nickname);
		else
			log_nick (s, pm_buffer, msg->prefix, new_nickname);
	}

	// The new nickname may collide with a user referenced by a PM buffer,
	// or in case of data inconsistency with the server, channels.
	// In the latter case we need the colliding user to leave all of them.
	struct user *user_collision = NULL;
	if (lexicographically_different
	 && (user_collision = str_map_find (&s->irc_users, new_nickname)))
		LIST_FOR_EACH (struct user_channel, iter, user_collision->channels)
			irc_remove_user_from_channel (user_collision, iter->channel);

	struct buffer *buffer_collision = NULL;
	if (lexicographically_different
	 && (buffer_collision = str_map_find (&s->irc_buffer_map, new_nickname)))
	{
		hard_assert (buffer_collision->type == BUFFER_PM);
		hard_assert (buffer_collision->user == user_collision);

		user_unref (buffer_collision->user);
		buffer_collision->user = user_ref (user);

		// There's not much else we can do other than somehow try to merge
		// one buffer into the other.  In our case, the original buffer wins.
		buffer_merge (s->ctx, buffer_collision, pm_buffer);
		if (s->ctx->current_buffer == pm_buffer)
			buffer_activate (s->ctx, buffer_collision);
		buffer_remove (s->ctx, pm_buffer);
		pm_buffer = buffer_collision;
	}

	// The colliding user should be completely gone by now
	if (lexicographically_different)
		hard_assert (!str_map_find (&s->irc_users, new_nickname));

	// Now we can rename the PM buffer to reflect the new nickname
	if (pm_buffer)
	{
		str_map_set (&s->irc_buffer_map, user->nickname, NULL);
		str_map_set (&s->irc_buffer_map, new_nickname, pm_buffer);

		char *x = xstrdup_printf ("%s.%s", s->name, new_nickname);
		buffer_rename (s->ctx, pm_buffer, x);
		free (x);
	}

	if (irc_is_this_us (s, msg->prefix))
	{
		log_nick_self (s, s->buffer, new_nickname);

		// 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)
				log_nick_self (s, buffer, 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);
			log_nick (s, buffer, msg->prefix, new_nickname);
		}
	}

	// Finally rename the user as it should be safe now
	str_map_set (&s->irc_users, user->nickname, NULL);
	str_map_set (&s->irc_users, new_nickname, user);

	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)
{
	const char *target = msg->params.vector[0];
	if (irc_is_this_us (s, msg->prefix))
		log_ctcp_reply (s, target,
			xstrdup_printf ("%s %s", chunk->tag.str, chunk->text.str));
	else
		log_server_status (s, s->buffer, "CTCP reply from #n: #S #S",
			msg->prefix, chunk->tag.str, chunk->text.str);
}

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)
	{
		if (irc_is_this_us (s, msg->prefix))
			log_outcoming_orphan_notice (s, target, text->str);
		return;
	}

	char *nick = irc_cut_nickname (msg->prefix);
	// IRCv3.2 echo-message could otherwise cause us to highlight ourselves
	if (!irc_is_this_us (s, msg->prefix) && irc_is_highlight (s, text->str))
		log_server (s, buffer, BUFFER_LINE_STATUS | BUFFER_LINE_HIGHLIGHT,
			"#a#s(#S)#r: #m", ATTR_HIGHLIGHT, "Notice", nick, text->str);
	else
		log_outcoming_notice (s, buffer, msg->prefix, text->str);
	free (nick);
}

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 = NULL;
	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)
	{
		if (irc_is_this_us (s, msg->prefix))
			irc_left_channel (channel);
		else
			irc_remove_user_from_channel (user, channel);
	}

	if (buffer)
	{
		struct formatter f;
		formatter_init (&f, s->ctx, s);
		formatter_add (&f, "#a<--#r #N #a#s#r #S",
			ATTR_PART, msg->prefix, ATTR_PART, "has left", channel_name);
		if (message)
			formatter_add (&f, " (#m)", message);
		log_formatter (s->ctx, buffer, 0, &f);
	}
}

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);
	if (!s->cap_echo_message)
		log_ctcp_reply (s, recipient, str_steal (&m));
	else
		str_free (&m);
}

static void
irc_handle_ctcp_request (struct server *s,
	const struct irc_message *msg, struct ctcp_chunk *chunk)
{
	const char *target = msg->params.vector[0];
	if (irc_is_this_us (s, msg->prefix))
		log_ctcp_query (s, target, chunk->tag.str);

	log_server_status (s, s->buffer,
		"CTCP requested by #n: #S", msg->prefix, chunk->tag.str);

	char *recipient = irc_is_channel (s, target)
		? irc_cut_nickname (msg->prefix)
		: xstrdup (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 (recipient);
}

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)
	{
		if (irc_is_this_us (s, msg->prefix))
			log_outcoming_orphan_privmsg (s, target, text->str);
		return;
	}

	char *nickname = irc_cut_nickname (msg->prefix);
	const char *prefixes = irc_get_privmsg_prefix
		(s, str_map_find (&s->irc_users, nickname), target);

	// IRCv3.2 echo-message could otherwise cause us to highlight ourselves
	if (irc_is_this_us (s, msg->prefix) || !irc_is_highlight (s, text->str))
	{
		if (is_action)
			log_outcoming_action (s, buffer, nickname, text->str);
		else
			log_outcoming_privmsg (s, buffer, prefixes, nickname, text->str);
	}
	else if (is_action)
		log_server (s, buffer, BUFFER_LINE_HIGHLIGHT,
			" #a*#r  #n #m", ATTR_HIGHLIGHT, msg->prefix, text->str);
	else
		log_server (s, buffer, BUFFER_LINE_HIGHLIGHT,
			"#a<#S#S>#r #m", ATTR_HIGHLIGHT, prefixes, nickname, text->str);

	free (nickname);
}

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
log_quit (struct server *s,
	struct buffer *buffer, const char *prefix, const char *reason)
{
	struct formatter f;
	formatter_init (&f, s->ctx, s);
	formatter_add (&f, "#a<--#r #N #a#s#r",
		ATTR_PART, prefix, ATTR_PART, "has quit");
	if (reason)
		formatter_add (&f, " (#m)", reason);
	log_formatter (s->ctx, buffer, 0, &f);
}

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

	// What the fuck, the server never sends this back
	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 = NULL;
	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)
	{
		log_quit (s, buffer, msg->prefix, message);

		// TODO: set some kind of a flag in the buffer and when the user
		//   reappears 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)
	{
		if ((buffer = str_map_find (&s->irc_buffer_map, iter->channel->name)))
			log_quit (s, buffer, msg->prefix, 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)
	{
		log_server (s, buffer, BUFFER_LINE_STATUS, "#n #s \"#m\"",
			msg->prefix, "has changed the topic to", topic);
	}
}

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

static struct irc_handler
{
	const char *name;
	void (*handler) (struct server *s, const struct irc_message *msg);
}
g_irc_handlers[] =
{
	// This list needs to stay sorted
	{ "CAP",     irc_handle_cap     },
	{ "INVITE",  irc_handle_invite  },
	{ "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_on_registered (struct server *s, const char *nickname)
{
	s->irc_user = irc_get_or_make_user (s, nickname);
	str_reset (&s->irc_user_mode);
	s->irc_user_host = NULL;

	s->state = IRC_REGISTERED;
	refresh_prompt (s->ctx);

	// XXX: 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->config, "autojoin");
	if (autojoin)
		irc_send (s, "JOIN :%s", autojoin);

	// TODO: rejoin all current channels (mark those we've left manually?)
}

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

	const char *response = msg->params.vector[1];
	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (response, ' ', &v);

	for (size_t i = 0; i < v.len; i++)
	{
		char *nick = v.vector[i];
		char *equals = strchr (nick, '=');

		if (!equals || equals == nick)
			continue;

		// User is an IRC operator
		if (equals[-1] == '*')
			equals[-1] = '\0';
		else
			equals[ 0] = '\0';

		// TODO: make use of this (away status polling?)
		char away_status = equals[1];
		if (!strchr ("+-", away_status))
			continue;

		char *userhost = equals + 2;
		if (irc_is_this_us (s, nick))
		{
			free (s->irc_user_host);
			s->irc_user_host = xstrdup (userhost);
		}
	}

	str_vector_free (&v);
}

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

	str_reset (&s->irc_user_mode);
	irc_handle_mode_user (s, msg->params.vector + 1);

	// XXX: do we want to log a message?
	refresh_prompt (s->ctx);
}

static void
irc_handle_rpl_namreply (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 4)
		return;

	const char *channel_name = msg->params.vector[2];
	const char *nicks        = msg->params.vector[3];

	// Just push the nicknames to a string vector to process later
	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
	if (channel)
		split_str_ignore_empty (nicks, ' ', &channel->names_buf);
}

static void
irc_sync_channel_user (struct server *s, struct channel *channel,
	const char *nickname, const char *prefixes)
{
	struct user *user = irc_get_or_make_user (s, nickname);
	struct channel_user *channel_user =
		irc_channel_get_user (channel, user);
	if (!channel_user)
	{
		irc_channel_link_user (channel, user, prefixes);
		return;
	}

	user_unref (user);

	// If our idea of the user's modes disagrees with what the server's
	// sent us (the most powerful modes differ), use the latter one
	if (channel_user->prefixes.str[0] != prefixes[0])
	{
		str_reset (&channel_user->prefixes);
		str_append (&channel_user->prefixes, prefixes);
	}
}

static void
irc_process_names (struct server *s, struct channel *channel)
{
	struct str_map present;
	str_map_init (&present);
	present.key_xfrm = s->irc_strxfrm;

	struct str_vector *updates = &channel->names_buf;
	for (size_t i = 0; i < updates->len; i++)
	{
		const char *item = updates->vector[i];
		size_t n_prefixes = strspn (item, s->irc_chanuser_prefixes);
		const char *nickname = item + n_prefixes;

		// Store the nickname in a hashset
		str_map_set (&present, nickname, (void *) 1);

		char *prefixes = xstrndup (item, n_prefixes);
		irc_sync_channel_user (s, channel, nickname, prefixes);
		free (prefixes);
	}

	// Get rid of channel users missing from "updates"
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
		if (!str_map_find (&present, iter->user->nickname))
			irc_channel_unlink_user (channel, iter);

	str_map_free (&present);
	str_vector_reset (&channel->names_buf);

	struct str_vector v;
	str_vector_init (&v);
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
		str_vector_add_owned (&v,
			xstrdup_printf ("%s%s", iter->prefixes.str, iter->user->nickname));
	char *all_users = join_str_vector (&v, ' ');
	str_vector_free (&v);

	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
	if (buffer)
	{
		log_server_status (s, buffer, "Users on #S: #S",
			channel->name, all_users);
	}

	free (all_users);
}

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

	const char *channel_name = msg->params.vector[1];
	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
	if (!strcmp (channel_name, "*"))
	{
		struct str_map_iter iter;
		str_map_iter_init (&iter, &s->irc_channels);
		struct channel *channel;
		while ((channel = str_map_iter_next (&iter)))
			irc_process_names (s, channel);
	}
	else if (channel)
		irc_process_names (s, channel);
}

static void
irc_handle_rpl_topic (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 3)
		return;

	const char *channel_name = msg->params.vector[1];
	const char *topic        = msg->params.vector[2];

	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));

	if (channel)
	{
		free (channel->topic);
		channel->topic = xstrdup (topic);
	}

	if (buffer)
		log_server_status (s, buffer, "The topic is: #m", topic);
}

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

	const char *channel_name = msg->params.vector[1];

	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));

	if (channel)
	{
		str_reset (&channel->no_param_modes);
		str_map_clear (&channel->param_modes);

		irc_handle_mode_channel (s, channel, msg->params.vector + 1);
	}

	// XXX: do we want to log a message?
	refresh_prompt (s->ctx);
}

static char *
make_time_string (time_t time)
{
	char buf[32];
	struct tm tm;
	strftime (buf, sizeof buf, "%a %b %d %Y %T", localtime_r (&time, &tm));
	return xstrdup (buf);
}

static void
irc_handle_rpl_creationtime (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 3)
		return;

	const char *channel_name  = msg->params.vector[1];
	const char *creation_time = msg->params.vector[2];

	unsigned long created;
	if (!xstrtoul (&created, creation_time, 10))
		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));

	if (buffer)
	{
		log_server_status (s, buffer, "Channel created on #&s",
			make_time_string (created));
	}
}

static void
irc_handle_rpl_topicwhotime (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 4)
		return;

	const char *channel_name = msg->params.vector[1];
	const char *who          = msg->params.vector[2];
	const char *change_time  = msg->params.vector[3];

	unsigned long changed;
	if (!xstrtoul (&changed, change_time, 10))
		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));

	if (buffer)
	{
		log_server_status (s, buffer, "Topic set by #N on #&s",
			who, make_time_string (changed));
	}
}

static void
irc_handle_rpl_inviting (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 3)
		return;

	const char *channel_name = msg->params.vector[1];
	const char *nickname     = msg->params.vector[2];

	struct buffer *buffer;
	if (!(buffer = str_map_find (&s->irc_buffer_map, channel_name)))
		buffer = s->buffer;

	log_server_status (s, buffer,
		"You have invited #n to #S", nickname, channel_name);
}

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

	log_server_error (s, s->buffer,
		"Nickname is already in use: #S", msg->params.vector[1]);

	// Only do this while we haven't successfully registered yet
	if (s->state != IRC_CONNECTED)
		return;

	char *nickname = irc_fetch_next_nickname (s);
	if (nickname)
	{
		log_server_status (s, s->buffer, "Retrying with #s...", nickname);
		irc_send (s, "NICK :%s", nickname);
		free (nickname);
	}
}

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

static void
irc_handle_isupport_prefix (struct server *s, char *value)
{
	char *modes = value;
	char *prefixes = strchr (value, ')');
	size_t n_prefixes = prefixes - modes;
	if (*modes++ != '(' || !prefixes++ || strlen (value) != 2 * n_prefixes--)
		return;

	free (s->irc_chanuser_modes);
	free (s->irc_chanuser_prefixes);

	s->irc_chanuser_modes    = xstrndup (modes,    n_prefixes);
	s->irc_chanuser_prefixes = xstrndup (prefixes, n_prefixes);
}

static void
irc_handle_isupport_casemapping (struct server *s, char *value)
{
	if      (!strcmp (value, "ascii"))
		irc_set_casemapping (s, tolower_ascii,      tolower_ascii_strxfrm);
	else if (!strcmp (value, "rfc1459"))
		irc_set_casemapping (s, irc_tolower,        irc_strxfrm);
	else if (!strcmp (value, "rfc1459-strict"))
		irc_set_casemapping (s, irc_tolower_strict, irc_strxfrm_strict);
}

static void
irc_handle_isupport_chantypes (struct server *s, char *value)
{
	free (s->irc_chantypes);
	s->irc_chantypes = xstrdup (value);
}

static void
irc_handle_isupport_idchan (struct server *s, char *value)
{
	struct str prefixes;
	str_init (&prefixes);

	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (value, ',', &v);
	for (size_t i = 0; i < v.len; i++)
	{
		// Not using or validating the numeric part
		const char *pair = v.vector[i];
		const char *colon = strchr (pair, ':');
		if (colon)
			str_append_data (&prefixes, pair, colon - pair);
	}
	str_vector_free (&v);

	free (s->irc_idchan_prefixes);
	s->irc_idchan_prefixes = str_steal (&prefixes);
}

static void
irc_handle_isupport_statusmsg (struct server *s, char *value)
{
	free (s->irc_statusmsg);
	s->irc_statusmsg = xstrdup (value);
}

static void
irc_handle_isupport_chanmodes (struct server *s, char *value)
{
	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (value, ',', &v);
	if (v.len >= 4)
	{
		free (s->irc_chanmodes_list);
		s->irc_chanmodes_list           = xstrdup (v.vector[0]);
		free (s->irc_chanmodes_param_always);
		s->irc_chanmodes_param_always   = xstrdup (v.vector[1]);
		free (s->irc_chanmodes_param_when_set);
		s->irc_chanmodes_param_when_set = xstrdup (v.vector[2]);
		free (s->irc_chanmodes_param_never);
		s->irc_chanmodes_param_never    = xstrdup (v.vector[3]);
	}
	str_vector_free (&v);
}

static void
irc_handle_isupport_modes (struct server *s, char *value)
{
	unsigned long modes;
	if (!*value)
		s->irc_max_modes = UINT_MAX;
	else if (xstrtoul (&modes, value, 10) && modes && modes <= UINT_MAX)
		s->irc_max_modes = modes;
}

static void
unescape_isupport_value (const char *value, struct str *output)
{
	const char *alphabet = "0123456789abcdef", *a, *b;
	for (const char *p = value; *p; p++)
	{
		if (p[0] == '\\'
		 && p[1] == 'x'
		 && p[2] && (a = strchr (alphabet, tolower_ascii (p[2])))
		 && p[3] && (b = strchr (alphabet, tolower_ascii (p[3]))))
		{
			str_append_c (output, (a - alphabet) << 4 | (b - alphabet));
			p += 3;
		}
		else
			str_append_c (output, *p);
	}
}

static void
dispatch_isupport (struct server *s, const char *name, char *value)
{
#define MATCH(from, to) if (!strcmp (name, (from))) { (to) (s, value); return; }

	// TODO: also make use of TARGMAX to split client commands as necessary

	MATCH ("PREFIX",      irc_handle_isupport_prefix);
	MATCH ("CASEMAPPING", irc_handle_isupport_casemapping);
	MATCH ("CHANTYPES",   irc_handle_isupport_chantypes);
	MATCH ("IDCHAN",      irc_handle_isupport_idchan);
	MATCH ("STATUSMSG",   irc_handle_isupport_statusmsg);
	MATCH ("CHANMODES",   irc_handle_isupport_chanmodes);
	MATCH ("MODES",       irc_handle_isupport_modes);

#undef MATCH
}

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

	for (size_t i = 1; i < msg->params.len - 1; i++)
	{
		// TODO: if the parameter starts with "-", it resets to default
		char *param = msg->params.vector[i];
		char *value = param + strcspn (param, "=");
		if (*value) *value++ = '\0';

		struct str value_unescaped;
		str_init (&value_unescaped);
		unescape_isupport_value (value, &value_unescaped);
		dispatch_isupport (s, param, value_unescaped.str);
		str_free (&value_unescaped);
	}
}

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

static void
irc_process_numeric (struct server *s,
	const struct irc_message *msg, unsigned long numeric)
{
	// Numerics typically have human-readable information

	// 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);

	struct buffer *buffer = s->buffer;
	switch (numeric)
	{
	case IRC_RPL_WELCOME:
		irc_on_registered (s, msg->params.vector[0]);

		// 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:
		irc_handle_rpl_isupport      (s, msg);                break;
	case IRC_RPL_USERHOST:
		irc_handle_rpl_userhost      (s, msg);                break;
	case IRC_RPL_UMODEIS:
		irc_handle_rpl_umodeis       (s, msg); buffer = NULL; break;
	case IRC_RPL_NAMREPLY:
		irc_handle_rpl_namreply      (s, msg); buffer = NULL; break;
	case IRC_RPL_ENDOFNAMES:
		irc_handle_rpl_endofnames    (s, msg); buffer = NULL; break;
	case IRC_RPL_TOPIC:
		irc_handle_rpl_topic         (s, msg); buffer = NULL; break;
	case IRC_RPL_CHANNELMODEIS:
		irc_handle_rpl_channelmodeis (s, msg); buffer = NULL; break;
	case IRC_RPL_CREATIONTIME:
		irc_handle_rpl_creationtime  (s, msg); buffer = NULL; break;
	case IRC_RPL_TOPICWHOTIME:
		irc_handle_rpl_topicwhotime  (s, msg); buffer = NULL; break;
	case IRC_RPL_INVITING:
		irc_handle_rpl_inviting      (s, msg); buffer = NULL; break;

	case IRC_ERR_NICKNAMEINUSE:
		irc_handle_err_nicknameinuse (s, msg); buffer = NULL; break;

	case IRC_RPL_LIST:
	case IRC_RPL_WHOREPLY:
	case IRC_RPL_ENDOFWHO:

	case IRC_ERR_UNKNOWNCOMMAND:
	case IRC_ERR_NEEDMOREPARAMS:
		// Just preventing these commands from getting printed in a more
		// specific buffer as that would be unwanted
		break;

	default:
		// If the second parameter is something we have a buffer for
		// (a channel, a PM buffer), log it in that buffer.  This is very basic.
		// TODO: whitelist/blacklist a lot more replies in here.
		// TODO: we should either strip the first parameter from the resulting
		//   buffer line, or at least put it in brackets
		if (msg->params.len > 1)
		{
			struct buffer *x;
			if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1])))
				buffer = x;
		}
	}

	if (buffer)
	{
		// Join the parameter vector back and send it to the server buffer
		log_server (s, buffer, BUFFER_LINE_STATUS,
			"#&m", join_str_vector (&copy, ' '));
	}

	str_vector_free (&copy);
}

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

	log_server_debug (s, "#a>> \"#S\"#r", ATTR_JOIN, raw);

	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, NULL);
		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 && 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);

	// We might also want to preserve attributes across splits but
	// that would make this code a lot more complicated

	struct str_vector lines;
	str_vector_init (&lines);
	struct error *e = NULL;
	if (!irc_autosplit_message (s, a.message, fixed_part, &lines, &e))
	{
		log_server_error (s, 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);
		if (!s->cap_echo_message)
			a.logger (s, &a, buffer, lines.vector[i]);
	}
end:
	str_vector_free (&lines);
}

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

	if (buffer && soft_assert (s->irc_user))
		log_outcoming_action (s, buffer, s->irc_user->nickname, 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_autosplit_action,                \
		  "\x01" "ACTION ", "\x01" })

static void
log_autosplit_privmsg (struct server *s,
	struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
	const char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, a->target);
	if (buffer && soft_assert (s->irc_user))
		log_outcoming_privmsg (s, buffer,
			prefixes, s->irc_user->nickname, line);
	else
		log_outcoming_orphan_privmsg (s, a->target, line);
}

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

static void
log_autosplit_notice (struct server *s,
	struct send_autosplit_args *a, struct buffer *buffer, const char *line)
{
	if (buffer && soft_assert (s->irc_user))
		log_outcoming_notice (s, buffer, s->irc_user->nickname, line);
	else
		log_outcoming_orphan_notice (s, a->target, line);
}

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

// --- Configuration dumper ----------------------------------------------------

struct config_dump_level
{
	struct config_dump_level *next;     ///< Next print level
	const char *name;                   ///< Name of the object
};

struct config_dump_data
{
	struct config_dump_level *head;     ///< The first level
	struct config_dump_level **tail;    ///< Where to place further levels
	struct str_vector *output;          ///< Where to place new entries
};

static void config_dump_item
	(struct config_item_ *item, struct config_dump_data *data);

static void
config_dump_children
	(struct config_item_ *object, struct config_dump_data *data)
{
	hard_assert (object->type = CONFIG_ITEM_OBJECT);

	struct config_dump_level level;
	level.next = NULL;

	struct config_dump_level **prev_tail = data->tail;
	*data->tail = &level;
	data->tail = &level.next;

	struct str_map_iter iter;
	str_map_iter_init (&iter, &object->value.object);
	struct config_item_ *child;
	while ((child = str_map_iter_next (&iter)))
	{
		level.name = iter.link->key;
		config_dump_item (child, data);
	}

	data->tail = prev_tail;
	*data->tail = NULL;
}

static void
config_dump_item (struct config_item_ *item, struct config_dump_data *data)
{
	// Empty objects will show as such
	if (item->type == CONFIG_ITEM_OBJECT
	 && item->value.object.len)
	{
		config_dump_children (item, data);
		return;
	}

	struct str line;
	str_init (&line);

	struct config_dump_level *iter = data->head;
	if (iter)
	{
		str_append (&line, iter->name);
		iter = iter->next;
	}
	for (; iter; iter = iter->next)
		str_append_printf (&line, ".%s", iter->name);

	struct str value;
	str_init (&value);
	config_item_write (item, false, &value);

	// Don't bother writing out null values everywhere
	struct config_schema *schema = item->schema;
	bool has_default = schema && schema->default_;
	if (item->type != CONFIG_ITEM_NULL || has_default)
	{
		str_append (&line, " = ");
		str_append_str (&line, &value);
	}

	if (!schema)
		str_append (&line, " (unrecognized)");
	else if (has_default && strcmp (schema->default_, value.str))
		str_append_printf (&line, " (default: %s)", schema->default_);
	else if (!has_default && item->type != CONFIG_ITEM_NULL)
		str_append_printf (&line, " (default: %s)", "null");

	str_free (&value);
	str_vector_add_owned (data->output, str_steal (&line));
}

static void
config_dump (struct config_item_ *root, struct str_vector *output)
{
	struct config_dump_data data;
	data.head = NULL;
	data.tail = &data.head;
	data.output = output;

	config_dump_item (root, &data);
}

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

static int
str_vector_sort_cb (const void *a, const void *b)
{
	return strcmp (*(const char **) a, *(const char **) b);
}

static void
str_vector_sort (struct str_vector *self)
{
	qsort (self->vector, self->len, sizeof *self->vector, str_vector_sort_cb);
}

static void
dump_matching_options
	(struct app_context *ctx, const char *mask, struct str_vector *output)
{
	config_dump (ctx->config.root, output);
	str_vector_sort (output);

	// Filter out results by wildcard matching
	for (size_t i = 0; i < output->len; i++)
	{
		// Yeah, I know
		char *key = str_cut_until (output->vector[i], " ");
		if (fnmatch (mask, key, 0))
			str_vector_remove (output, i--);
		free (key);
	}
}

// --- Server management -------------------------------------------------------

static struct str_map *
get_servers_config (struct app_context *ctx)
{
	return &config_item_get (ctx->config.root, "servers", NULL)->value.object;
}

static bool
validate_server_name (const char *name)
{
	for (const unsigned char *p = (const unsigned char *) name; *p; p++)
		if (*p < 32 || *p == '.')
			return false;
	return true;
}

static bool
check_server_name_for_addition (struct app_context *ctx, const char *name)
{
	if (!strcasecmp_ascii (name, ctx->global_buffer->name))
		log_global_error (ctx, "Cannot create server `#s': #s",
			name, "name collides with the global buffer");
	else if (str_map_find (&ctx->servers, name))
		log_global_error (ctx, "Cannot create server `#s': #s",
			name, "server already exists");
	else if (!validate_server_name (name))
		log_global_error (ctx, "Cannot create server `#s': #s",
			name, "invalid server name");
	else
		return true;
	return false;
}

static struct server *
server_add (struct app_context *ctx,
	const char *name, struct config_item_ *subtree)
{
	hard_assert (!str_map_find (&ctx->servers, name));

	struct server *s = xmalloc (sizeof *s);
	server_init (s, &ctx->poller);

	s->ctx = ctx;
	s->name = xstrdup (name);
	str_map_set (&ctx->servers, s->name, s);
	s->config = subtree;

	// Add a buffer and activate it
	struct buffer *buffer = s->buffer = buffer_new ();
	buffer->type = BUFFER_SERVER;
	buffer->name = xstrdup (s->name);
	buffer->server = s;

	buffer_add (ctx, buffer);
	buffer_activate (ctx, buffer);

	config_schema_apply_to_object (g_config_server, subtree, s);
	config_schema_call_changed (subtree);

	// Connect to the server ASAP
	// TODO: make this configurable ("autoconnect")
	poller_timer_set (&s->reconnect_tmr, 0);
	return s;
}

static void
server_add_new (struct app_context *ctx, const char *name)
{
	// Note that there may already be something in the configuration under
	// that key that we've ignored earlier, and there may also be
	// a case-insensitive conflict.  Those things may only happen as a result
	// of manual edits to the configuration, though, and they're not really
	// going to break anything.  They only cause surprises when loading.
	struct str_map *servers = get_servers_config (ctx);
	struct config_item_ *subtree = config_item_object ();
	str_map_set (servers, name, subtree);

	struct server *s = server_add (ctx, name, subtree);
	struct error *e = NULL;
	if (!irc_autofill_user_info (s, &e))
	{
		log_server_error (s, s->buffer,
			"#s: #s", "Failed to fill in user details", e->message);
		error_free (e);
	}
}

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

// HANDLER_NEEDS_REG is primarily for message sending commands,
// as they may want to log buffer lines and use our current nickname

enum handler_flags
{
	HANDLER_SERVER        = (1 << 0),   ///< Server context required
	HANDLER_NEEDS_REG     = (1 << 1),   ///< Server registration required
	HANDLER_CHANNEL_FIRST = (1 << 2),   ///< Channel required, first argument
	HANDLER_CHANNEL_LAST  = (1 << 3)    ///< Channel required, last argument
};

struct handler_args
{
	struct app_context *ctx;            ///< Application context
	struct buffer *buffer;              ///< Current buffer
	struct server *s;                   ///< Related server
	const char *channel_name;           ///< Related channel name
	char *arguments;                    ///< Command arguments
};

/// 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, WORD_BREAKING_CHARS);
	char *end = start + word_len;
	*s = end + strspn (end, WORD_BREAKING_CHARS);
	*end = '\0';
	return start;
}

/// Validates a word to be cut from a string
typedef bool (*word_validator_fn) (void *, char *);

static char *
maybe_cut_word (char **s, word_validator_fn validator, void *user_data)
{
	char *start = *s;
	size_t word_len = strcspn (*s, WORD_BREAKING_CHARS);

	char *word = xstrndup (start, word_len);
	bool ok = validator (user_data, word);
	free (word);

	if (!ok)
		return NULL;

	char *end = start + word_len;
	*s = end + strspn (end, WORD_BREAKING_CHARS);
	*end = '\0';
	return start;
}

static char *
maybe_cut_word_from_end (char **s, word_validator_fn validator, void *user_data)
{
	// Find the start and end of the last word
	char *start = *s, *end = start + strlen (start);
	while (end  > start &&  strchr (WORD_BREAKING_CHARS, end [-1]))
		end--;
	char *word = end;
	while (word > start && !strchr (WORD_BREAKING_CHARS, word[-1]))
		word--;

	// There's just one word at maximum, starting at the beginning
	if (word == start)
		return maybe_cut_word (s, validator, user_data);

	char *tmp = xstrndup (word, word - start);
	bool ok = validator (user_data, tmp);
	free (tmp);

	if (!ok)
		return NULL;

	// It doesn't start at the beginning, cut it off and return it
	word[-1] = *end = '\0';
	return word;
}

static bool
validate_channel_name (void *user_data, char *word)
{
	return irc_is_channel (user_data, word);
}

static char *
try_get_channel (struct handler_args *a,
	char *(*cutter) (char **, word_validator_fn, void *))
{
	char *channel_name = cutter (&a->arguments, validate_channel_name, a->s);
	if (channel_name)
		return channel_name;
	if (a->buffer->type == BUFFER_CHANNEL)
		return a->buffer->channel->name;
	return NULL;
}

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))
		log_global_error (ctx, "#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: partial matches
	return buffer;
}

static void
show_buffers_list (struct app_context *ctx)
{
	log_global_indent (ctx, "");
	log_global_indent (ctx, "Buffers list:");

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

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

	if (!buffer)
		log_global_error (ctx, "#s: #s", "No such buffer", which);
	else if (buffer == ctx->global_buffer)
		log_global_error (ctx, "Can't close the global buffer");
	else if (buffer->type == BUFFER_SERVER)
		log_global_error (ctx, "Can't close a server buffer");
	else
	{
		// The user would be unable to recreate the buffer otherwise
		if (buffer->type == BUFFER_CHANNEL)
			irc_send (buffer->server, "PART %s", buffer->channel->name);

		if (buffer == ctx->current_buffer)
			buffer_activate (ctx, ctx->last_buffer
				? ctx->last_buffer
				: buffer_next (ctx, 1));
		buffer_remove (ctx, buffer);
	}
}

static bool
handle_buffer_move (struct app_context *ctx, struct handler_args *a)
{
	unsigned long request;
	if (!xstrtoul (&request, a->arguments, 10))
		return false;

	unsigned long total = 0;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
		total++;

	if (request == 0 || request > total)
	{
		log_global_error (ctx, "#s: #s",
			"Can't move buffer", "requested position is out of range");
		return true;
	}

	struct buffer *buffer = a->buffer;
	LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);

	struct buffer *following = ctx->buffers;
	while (--request && following)
		following = following->next;

	LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following);

	refresh_prompt (ctx);
	return true;
}

static bool
handle_command_buffer (struct handler_args *a)
{
	struct app_context *ctx = a->ctx;
	char *action = cut_word (&a->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
	bool result = true;
	if (!strcasecmp_ascii (action, "list"))
		show_buffers_list (ctx);
	else if (!strcasecmp_ascii (action, "clear"))
	{
		buffer_clear (a->buffer);
		// XXX: clear screen?
		buffer_print_backlog (ctx, a->buffer);
	}
	else if (!strcasecmp_ascii (action, "move"))
		result = handle_buffer_move (ctx, a);
	else if (!strcasecmp_ascii (action, "close"))
		handle_buffer_close (ctx, a);
	else
		result = false;
	return result;
}

static bool
replace_string_array
	(struct config_item_ *item, struct str_vector *array, struct error **e)
{
	char *changed = join_str_vector (array, ',');
	struct str tmp = { .str = changed, .len = strlen (changed) };
	bool result = config_item_set_from (item,
		config_item_string_array (&tmp), e);
	free (changed);
	return result;
}

static bool
handle_command_set_add
	(struct config_item_ *item, const char *value, struct error **e)
{
	bool result = false;
	struct str_vector items;
	str_vector_init (&items);
	split_str (item->value.string.str, ',', &items);
	if (items.len == 1 && !*items.vector[0])
		str_vector_reset (&items);

	if (str_vector_find (&items, value) != -1)
		error_set (e, "already present in the array: %s", value);
	else
	{
		str_vector_add (&items, value);
		result = replace_string_array (item, &items, e);
	}

	str_vector_free (&items);
	return result;
}

static bool
handle_command_set_remove
	(struct config_item_ *item, const char *value, struct error **e)
{
	bool result = false;
	struct str_vector items;
	str_vector_init (&items);
	split_str (item->value.string.str, ',', &items);
	if (items.len == 1 && !*items.vector[0])
		str_vector_reset (&items);

	ssize_t i = str_vector_find (&items, value);
	if (i == -1)
		error_set (e, "not present in the array: %s", value);
	else
	{
		str_vector_remove (&items, i);
		result = replace_string_array (item, &items, e);
	}

	str_vector_free (&items);
	return result;
}

static void
handle_command_set_assign_item (struct app_context *ctx,
	char *key, struct config_item_ *new_, bool add, bool remove)
{
	struct config_item_ *item =
		config_item_get (ctx->config.root, key, NULL);
	hard_assert (item);

	struct error *e = NULL;
	if (!item->schema)
		error_set (&e, "option not recognized");
	else if ((add | remove) && item->type != CONFIG_ITEM_STRING_ARRAY)
		// FIXME: it can also be null, which makes this message confusing
		error_set (&e, "not a string array");
	else if (add)
		handle_command_set_add (item, new_->value.string.str, &e);
	else if (remove)
		handle_command_set_remove (item, new_->value.string.str, &e);
	else
		config_item_set_from (item, config_item_clone (new_), &e);

	if (e)
	{
		log_global_error (ctx,
			"Failed to set option \"#s\": #s", key, e->message);
		error_free (e);
	}
	else
	{
		struct str_vector tmp;
		str_vector_init (&tmp);
		dump_matching_options (ctx, key, &tmp);
		log_global_status (ctx, "Option changed: #s", tmp.vector[0]);
		str_vector_free (&tmp);
	}
}

static bool
handle_command_set_assign
	(struct app_context *ctx, struct str_vector *all, char *arguments)
{
	char *op = cut_word (&arguments);
	bool add = false;
	bool remove = false;

	if      (!strcmp (op, "+="))  add = true;
	else if (!strcmp (op, "-="))  remove = true;
	else if  (strcmp (op, "="))   return false;

	if (!arguments)
		return false;

	struct error *e = NULL;
	struct config_item_ *new_ =
		config_item_parse (arguments, strlen (arguments), true, &e);
	if (e)
	{
		log_global_error (ctx, "Invalid value: #s", e->message);
		error_free (e);
		return true;
	}

	if ((add | remove) && !config_item_type_is_string (new_->type))
	{
		log_global_error (ctx, "+= / -= operators need a string argument");
		config_item_destroy (new_);
		return true;
	}
	for (size_t i = 0; i < all->len; i++)
	{
		char *key = str_cut_until (all->vector[i], " ");
		handle_command_set_assign_item (ctx, key, new_, add, remove);
		free (key);
	}
	config_item_destroy (new_);
	return true;
}

static bool
handle_command_set (struct handler_args *a)
{
	struct app_context *ctx = a->ctx;
	char *option = "*";
	if (*a->arguments)
		option = cut_word (&a->arguments);

	struct str_vector all;
	str_vector_init (&all);
	dump_matching_options (ctx, option, &all);

	bool result = true;
	if (!all.len)
		log_global_error (ctx, "No matches: #s", option);
	else if (!*a->arguments)
	{
		log_global_indent (ctx, "");
		for (size_t i = 0; i < all.len; i++)
			log_global_indent (ctx, "#s", all.vector[i]);
	}
	else
		result = handle_command_set_assign (ctx, &all, a->arguments);

	str_vector_free (&all);
	return result;
}

static bool
handle_command_save (struct handler_args *a)
{
	struct app_context *ctx = a->ctx;
	if (*a->arguments)
		return false;

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

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

	if (!filename)
	{
		log_global_error (ctx,
			"#s: #s", "Saving configuration failed", e->message);
		error_free (e);
	}
	else
		log_global_status (ctx, "Configuration written to `#s'", filename);
	free (filename);
	return true;
}

static bool
handle_command_msg (struct handler_args *a)
{
	if (!*a->arguments)
		return false;

	char *target = cut_word (&a->arguments);
	if (!*a->arguments)
		log_server_error (a->s, a->s->buffer, "No text to send");
	else
		SEND_AUTOSPLIT_PRIVMSG (a->s, target, a->arguments);
	return true;
}

static bool
handle_command_query (struct handler_args *a)
{
	if (!*a->arguments)
		return false;

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

static bool
handle_command_notice (struct handler_args *a)
{
	if (!*a->arguments)
		return false;

	char *target = cut_word (&a->arguments);
	if (!*a->arguments)
		log_server_error (a->s, a->s->buffer, "No text to send");
	else
		SEND_AUTOSPLIT_NOTICE (a->s, target, a->arguments);
	return true;
}

static bool
handle_command_ctcp (struct handler_args *a)
{
	if (!*a->arguments)
		return false;

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

	char *tag = cut_word (&a->arguments);
	transform_str (tag, toupper_ascii);

	if (*a->arguments)
		irc_send (a->s, "PRIVMSG %s :\x01%s %s\x01", target, tag, a->arguments);
	else
		irc_send (a->s, "PRIVMSG %s :\x01%s\x01", target, tag);

	if (!a->s->cap_echo_message)
		log_ctcp_query (a->s, target, tag);
	return true;
}

static bool
handle_command_me (struct handler_args *a)
{
	if (a->buffer->type == BUFFER_CHANNEL)
		SEND_AUTOSPLIT_ACTION (a->s,
			a->buffer->channel->name, a->arguments);
	else if (a->buffer->type == BUFFER_PM)
		SEND_AUTOSPLIT_ACTION (a->s,
			a->buffer->user->nickname, a->arguments);
	else
		log_server_error (a->s, a->s->buffer,
			"Can't do this from a server buffer (#s)",
			"send CTCP actions");
	return true;
}

static bool
handle_command_quit (struct handler_args *a)
{
	struct str_map_iter iter;
	str_map_iter_init (&iter, &a->ctx->servers);

	struct server *s;
	while ((s = str_map_iter_next (&iter)))
	{
		if (irc_is_connected (s))
			irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL);
	}

	initiate_quit (a->ctx);
	return true;
}

static bool
handle_command_join (struct handler_args *a)
{
	// XXX: send the last known channel key?
	if (irc_is_channel (a->s, a->arguments))
		// XXX: we may want to split the list of channels
		irc_send (a->s, "JOIN %s", a->arguments);
	else if (a->buffer->type != BUFFER_CHANNEL)
		log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
			"no channel name given and this buffer is not a channel");
	// TODO: have a better way of checking if we're on the channel
	else if (a->buffer->channel->users)
		log_server_error (a->s, a->buffer, "#s: #s", "Can't join",
			"you already are on the channel");
	else if (*a->arguments)
		irc_send (a->s, "JOIN %s :%s", a->buffer->channel->name, a->arguments);
	else
		irc_send (a->s, "JOIN %s", a->buffer->channel->name);
	return true;
}

static void
part_channel (struct server *s, const char *channel_name, const char *reason)
{
	if (*reason)
		irc_send (s, "PART %s :%s", channel_name, reason);
	else
		irc_send (s, "PART %s", channel_name);
}

static bool
handle_command_part (struct handler_args *a)
{
	if (irc_is_channel (a->s, a->arguments))
	{
		struct str_vector v;
		str_vector_init (&v);
		split_str_ignore_empty (cut_word (&a->arguments), ' ', &v);
		for (size_t i = 0; i < v.len; i++)
			part_channel (a->s, v.vector[i], a->arguments);
		str_vector_free (&v);
	}
	else if (a->buffer->type != BUFFER_CHANNEL)
		log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
			"no channel name given and this buffer is not a channel");
	// TODO: have a better way of checking if we're on the channel
	else if (!a->buffer->channel->users)
		log_server_error (a->s, a->buffer, "#s: #s", "Can't part",
			"you're not on the channel");
	else
		part_channel (a->s, a->buffer->channel->name, a->arguments);
	return true;
}

static void
cycle_channel (struct server *s, const char *channel_name, const char *reason)
{
	// If a channel key is set, we must specify it when rejoining
	const char *key = NULL;
	struct channel *channel;
	if ((channel = str_map_find (&s->irc_channels, channel_name)))
		key = str_map_find (&channel->param_modes, "k");

	if (*reason)
		irc_send (s, "PART %s :%s", channel_name, reason);
	else
		irc_send (s, "PART %s", channel_name);

	if (key)
		irc_send (s, "JOIN %s :%s", channel_name, key);
	else
		irc_send (s, "JOIN %s", channel_name);
}

static bool
handle_command_cycle (struct handler_args *a)
{
	if (irc_is_channel (a->s, a->arguments))
	{
		struct str_vector v;
		str_vector_init (&v);
		split_str_ignore_empty (cut_word (&a->arguments), ' ', &v);
		for (size_t i = 0; i < v.len; i++)
			cycle_channel (a->s, v.vector[i], a->arguments);
		str_vector_free (&v);
	}
	else if (a->buffer->type != BUFFER_CHANNEL)
		log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
			"no channel name given and this buffer is not a channel");
	// TODO: have a better way of checking if we're on the channel
	else if (!a->buffer->channel->users)
		log_server_error (a->s, a->buffer, "#s: #s", "Can't cycle",
			"you're not on the channel");
	else
		cycle_channel (a->s, a->buffer->channel->name, a->arguments);
	return true;
}

static bool
handle_command_mode (struct handler_args *a)
{
	// Channel names prefixed by "+" collide with mode strings,
	// so we just disallow specifying these channels
	char *target = NULL;
	if (strchr ("+-\0", *a->arguments))
	{
		if (a->buffer->type == BUFFER_CHANNEL)
			target = a->buffer->channel->name;
		if (a->buffer->type == BUFFER_PM)
			target = a->buffer->user->nickname;
		if (a->buffer->type == BUFFER_SERVER)
			target = a->s->irc_user->nickname;
	}
	else
		// If there a->arguments and they don't begin with a mode string,
		// they're either a user name or a channel name
		target = cut_word (&a->arguments);

	if (!target)
		log_server_error (a->s, a->buffer, "#s: #s", "Can't change mode",
			"no target given and this buffer is neither a PM nor a channel");
	else if (*a->arguments)
		// XXX: split channel mode params as necessary using irc_max_modes?
		irc_send (a->s, "MODE %s %s", target, a->arguments);
	else
		irc_send (a->s, "MODE %s", target);
	return true;
}

static bool
handle_command_topic (struct handler_args *a)
{
	if (*a->arguments)
		// FIXME: there's no way to unset the topic
		irc_send (a->s, "TOPIC %s :%s", a->channel_name, a->arguments);
	else
		irc_send (a->s, "TOPIC %s", a->channel_name);
	return true;
}

static bool
handle_command_kick (struct handler_args *a)
{
	if (!*a->arguments)
		return false;

	char *target = cut_word (&a->arguments);
	if (*a->arguments)
		irc_send (a->s, "KICK %s %s :%s",
			a->channel_name, target, a->arguments);
	else
		irc_send (a->s, "KICK %s %s", a->channel_name, target);
	return true;
}

static bool
handle_command_kickban (struct handler_args *a)
{
	if (!*a->arguments)
		return false;

	char *target = cut_word (&a->arguments);
	if (strpbrk (target, "!@*?"))
		return false;

	// XXX: how about other masks?
	irc_send (a->s, "MODE %s +b %s!*@*", a->channel_name, target);
	if (*a->arguments)
		irc_send (a->s, "KICK %s %s :%s",
			a->channel_name, target, a->arguments);
	else
		irc_send (a->s, "KICK %s %s", a->channel_name, target);
	return true;
}

static void
mass_channel_mode (struct server *s, const char *channel_name,
	bool adding, char mode_char, struct str_vector *v)
{
	size_t n;
	for (size_t i = 0; i < v->len; i += n)
	{
		struct str modes;   str_init (&modes);
		struct str params;  str_init (&params);

		n = MIN (v->len - i, s->irc_max_modes);
		str_append_printf (&modes, "MODE %s %c", channel_name, "-+"[adding]);
		for (size_t k = 0; k < n; k++)
		{
			str_append_c (&modes, mode_char);
			str_append_printf (&params, " %s", v->vector[i + k]);
		}

		irc_send (s, "%s%s", modes.str, params.str);

		str_free (&modes);
		str_free (&params);
	}
}

static void
mass_channel_mode_mask_list
	(struct handler_args *a, bool adding, char mode_char)
{
	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (a->arguments, ' ', &v);

	// XXX: this may be a bit too trivial; we could map also nicknames
	//   to information from WHO polling or userhost-in-names
	for (size_t i = 0; i < v.len; i++)
	{
		char *target = v.vector[i];
		if (strpbrk (target, "!@*?"))
			continue;

		v.vector[i] = xstrdup_printf ("%s!*@*", target);
		free (target);
	}

	mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
	str_vector_free (&v);
}

static bool
handle_command_ban (struct handler_args *a)
{
	if (*a->arguments)
		mass_channel_mode_mask_list (a, true, 'b');
	else
		irc_send (a->s, "MODE %s +b", a->channel_name);
	return true;
}

static bool
handle_command_unban (struct handler_args *a)
{
	if (*a->arguments)
		mass_channel_mode_mask_list (a, false, 'b');
	else
		return false;
	return true;
}

static bool
handle_command_invite (struct handler_args *a)
{
	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (a->arguments, ' ', &v);

	bool result = !!v.len;
	for (size_t i = 0; i < v.len; i++)
		irc_send (a->s, "INVITE %s %s", v.vector[i], a->channel_name);

	str_vector_free (&v);
	return result;
}

static struct server *
resolve_server (struct app_context *ctx, struct handler_args *a,
	const char *command_name)
{
	struct server *s = NULL;
	if (*a->arguments)
	{
		char *server_name = cut_word (&a->arguments);
		if (!(s = str_map_find (&ctx->servers, server_name)))
			log_global_error (ctx, "/#s: #s: #s",
				command_name, "no such server", server_name);
	}
	else if (a->buffer->type == BUFFER_GLOBAL)
		log_global_error (ctx, "/#s: #s",
			command_name, "no server name given and this buffer is global");
	else
		s = a->buffer->server;
	return s;
}

static bool
handle_command_connect (struct handler_args *a)
{
	struct server *s = NULL;
	if (!(s = resolve_server (a->ctx, a, "connect")))
		return true;

	if (irc_is_connected (s))
	{
		log_server_error (s, s->buffer, "Already connected");
		return true;
	}
	if (s->state == IRC_CONNECTING)
		irc_destroy_connector (s);

	irc_cancel_timers (s);
	irc_initiate_connect (s);
	return true;
}

static bool
handle_command_disconnect (struct handler_args *a)
{
	struct server *s = NULL;
	if (!(s = resolve_server (a->ctx, a, "disconnect")))
		return true;

	if (s->state == IRC_CONNECTING)
	{
		log_server_status (s, s->buffer, "Connecting aborted");
		irc_destroy_connector (s);
	}
	else if (!irc_is_connected (s))
		log_server_error (s, s->buffer, "Not connected");
	else
		irc_initiate_disconnect (s, *a->arguments ? a->arguments : NULL);
	return true;
}

static void
show_servers_list (struct app_context *ctx)
{
	log_global_indent (ctx, "");
	log_global_indent (ctx, "Servers list:");

	struct str_map_iter iter;
	str_map_iter_init (&iter, &ctx->servers);
	struct server *s;
	while ((s = str_map_iter_next (&iter)))
		log_global_indent (ctx, "  #s", s->name);
}

static bool
handle_server_add (struct handler_args *a)
{
	char *name = cut_word (&a->arguments);
	if (*a->arguments)
		return false;

	struct app_context *ctx = a->ctx;
	if (check_server_name_for_addition (ctx, name))
		server_add_new (ctx, name);
	return true;
}

static bool
handle_command_server (struct handler_args *a)
{
	struct app_context *ctx = a->ctx;
	char *action = cut_word (&a->arguments);
	bool result = true;
	if (!strcasecmp_ascii (action, "list"))
		show_servers_list (ctx);
	else if (!strcasecmp_ascii (action, "add"))
		result = handle_server_add (a);
	else if (!strcasecmp_ascii (action, "remove"))
		; // TODO: <name>
	else if (!strcasecmp_ascii (action, "rename"))
		; // TODO: <old> <new>
	else
		result = false;
	return result;
}

static bool
handle_command_names (struct handler_args *a)
{
	char *channel_name = try_get_channel (a, maybe_cut_word);
	if (channel_name)
		irc_send (a->s, "NAMES %s", channel_name);
	else
		irc_send (a->s, "NAMES");
	return true;
}

static bool
handle_command_whois (struct handler_args *a)
{
	if (*a->arguments)
		irc_send (a->s, "WHOIS %s", a->arguments);
	else if (a->buffer->type == BUFFER_PM)
		irc_send (a->s, "WHOIS %s", a->buffer->user->nickname);
	else if (a->buffer->type == BUFFER_SERVER)
		irc_send (a->s, "WHOIS %s", a->s->irc_user->nickname);
	else
		log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
			"no target given and this buffer is not a PM nor a server");
	return true;
}

static bool
handle_command_whowas (struct handler_args *a)
{
	if (*a->arguments)
		irc_send (a->s, "WHOWAS %s", a->arguments);
	else if (a->buffer->type == BUFFER_PM)
		irc_send (a->s, "WHOWAS %s", a->buffer->user->nickname);
	else
		log_server_error (a->s, a->buffer, "#s: #s", "Can't request info",
			"no target given and this buffer is not a PM");
	return true;
}

static bool
handle_command_nick (struct handler_args *a)
{
	if (!*a->arguments)
		return false;

	irc_send (a->s, "NICK %s", cut_word (&a->arguments));
	return true;
}

static bool
handle_command_quote (struct handler_args *a)
{
	irc_send (a->s, "%s", a->arguments);
	return true;
}

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

static bool
handle_command_channel_mode
	(struct handler_args *a, bool adding, char mode_char)
{
	if (!*a->arguments)
		return false;

	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (a->arguments, ' ', &v);
	mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
	str_vector_free (&v);
	return true;
}

#define CHANMODE_HANDLER(name, adding, mode_char)                              \
	static bool                                                                \
	handle_command_ ## name (struct handler_args *a)                           \
	{                                                                          \
		return handle_command_channel_mode (a, (adding), (mode_char));         \
	}

CHANMODE_HANDLER (op,      true,  'o')  CHANMODE_HANDLER (deop,    false, 'o')
CHANMODE_HANDLER (voice,   true,  'v')  CHANMODE_HANDLER (devoice, false, 'v')

#define TRIVIAL_HANDLER(name, command)                                         \
	static bool                                                                \
	handle_command_ ## name (struct handler_args *a)                           \
	{                                                                          \
		if (*a->arguments)                                                     \
			irc_send (a->s, command " %s", a->arguments);                      \
		else                                                                   \
			irc_send (a->s, command);                                          \
		return true;                                                           \
	}

TRIVIAL_HANDLER (list,  "LIST")
TRIVIAL_HANDLER (who,   "WHO")
TRIVIAL_HANDLER (motd,  "MOTD")
TRIVIAL_HANDLER (stats, "STATS")
TRIVIAL_HANDLER (away,  "AWAY")

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

static bool handle_command_help (struct handler_args *);

static struct command_handler
{
	const char *name;
	const char *description;
	const char *usage;
	bool (*handler) (struct handler_args *a);
	enum handler_flags flags;
}
g_command_handlers[] =
{
	{ "help",       "Show help",
	  "[<command> | <option>]",
	  handle_command_help,       0 },
	{ "quit",       "Quit the program",
	  "[<message>]",
	  handle_command_quit,       0 },
	{ "buffer",     "Manage buffers",
	  "<N> | list | clear | move <N> | close [<N> | <name>]",
	  handle_command_buffer,     0 },
	{ "set",        "Manage configuration",
	  "[<option>]",
	  handle_command_set,        0 },
	{ "save",       "Save configuration",
	  NULL,
	  handle_command_save,       0 },

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

	{ "join",       "Join channels",
	  "[<channel>[,<channel>...]] [<key>[,<key>...]]",
	  handle_command_join,       HANDLER_SERVER },
	{ "part",       "Leave channels",
	  "[<channel>[,<channel>...]] [<reason>]",
	  handle_command_part,       HANDLER_SERVER },
	{ "cycle",      "Rejoin channels",
	  "[<channel>[,<channel>...]] [<reason>]",
	  handle_command_cycle,      HANDLER_SERVER },

	{ "op",         "Give channel operator status",
	  "<nick>...",
	  handle_command_op,         HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "deop",       "Remove channel operator status",
	  "<nick>...",
	  handle_command_deop,       HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "voice",      "Give voice",
	  "<nick>...",
	  handle_command_voice,      HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "devoice",    "Remove voice",
	  "<nick>...",
	  handle_command_devoice,    HANDLER_SERVER | HANDLER_CHANNEL_FIRST },

	{ "mode",       "Change mode",
	  "[<channel>] [<mode>...]",
	  handle_command_mode,       HANDLER_SERVER },
	{ "topic",      "Change topic",
	  "[<channel>] [<topic>]",
	  handle_command_topic,      HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "kick",       "Kick user from channel",
	  "[<channel>] <user> [<reason>]",
	  handle_command_kick,       HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "kickban",    "Kick and ban user from channel",
	  "[<channel>] <user> [<reason>]",
	  handle_command_kickban,    HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "ban",        "Ban user from channel",
	  "[<channel>] [<mask>...]",
	  handle_command_ban,        HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "unban",      "Unban user from channel",
	  "[<channel>] <mask>...",
	  handle_command_unban,      HANDLER_SERVER | HANDLER_CHANNEL_FIRST },
	{ "invite",     "Invite user to channel",
	  "<user>... [<channel>]",
	  handle_command_invite,     HANDLER_SERVER | HANDLER_CHANNEL_LAST },

	{ "server",     "Manage servers",
	  "list | add <name> | delete <name> | rename <old> <new>",
	  handle_command_server,     0 },
	{ "connect",    "Connect to the server",
	  "[<server>]",
	  handle_command_connect,    0 },
	{ "disconnect", "Disconnect from the server",
	  "[<server> [<reason>]]",
	  handle_command_disconnect, 0 },

	{ "list",       "List channels and their topic",
	  "[<channel>[,<channel>...]] [<target>]",
	  handle_command_list,       HANDLER_SERVER },
	{ "names",      "List users on channel",
	  "[<channel>[,<channel>...]]",
	  handle_command_names,      HANDLER_SERVER },
	{ "who",        "List users",
	  "[<mask> [o]]",
	  handle_command_who,        HANDLER_SERVER },
	{ "whois",      "Get user information",
	  "[<target>] <mask>",
	  handle_command_whois,      HANDLER_SERVER },
	{ "whowas",     "Get user information",
	  "<user> [<count> [<target>]]",
	  handle_command_whowas,     HANDLER_SERVER },

	{ "motd",       "Get the Message of The Day",
	  "[<target>]",
	  handle_command_motd,       HANDLER_SERVER },
	{ "stats",      "Query server statistics",
	  "[<query> [<target>]]",
	  handle_command_stats,      HANDLER_SERVER },
	{ "away",       "Set away status",
	  "[<text>]",
	  handle_command_away,       HANDLER_SERVER },
	{ "nick",       "Change current nick",
	  "<nickname>",
	  handle_command_nick,       HANDLER_SERVER },
	{ "quote",      "Send a raw command to the server",
	  "<command>",
	  handle_command_quote,      HANDLER_SERVER },
};

static bool
try_handle_command_help_option (struct app_context *ctx, const char *name)
{
	struct config_item_ *item =
		config_item_get (ctx->config.root, name, NULL);
	if (!item)
		return false;

	struct config_schema *schema = item->schema;
	if (!schema)
	{
		log_global_error (ctx, "#s: #s", "Option not recognized", name);
		return true;
	}

	log_global_indent (ctx, "");
	log_global_indent (ctx, "Option \"#s\":", name);
	log_global_indent (ctx, "  Description: #s", schema->comment);
	log_global_indent (ctx, "  Type: #s", config_item_type_name (schema->type));
	log_global_indent (ctx, "  Default: #s",
		schema->default_ ? schema->default_ : "null");

	struct str tmp;
	str_init (&tmp);
	config_item_write (item, false, &tmp);
	log_global_indent (ctx, "  Current value: #s", tmp.str);
	str_free (&tmp);
	return true;
}

static bool
show_command_list (struct app_context *ctx)
{
	log_global_indent (ctx, "");
	log_global_indent (ctx, "Commands:");

	int longest = 0;
	for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
	{
		int len = strlen (g_command_handlers[i].name);
		longest = MAX (longest, len);
	}
	for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
	{
		struct command_handler *handler = &g_command_handlers[i];
		log_global_indent (ctx, "  #&s", xstrdup_printf
			("%-*s  %s", longest, handler->name, handler->description));
	}
	return true;
}

static bool
show_command_help (struct app_context *ctx, struct command_handler *handler)
{
	log_global_indent (ctx, "");
	log_global_indent (ctx, "#s: #s", handler->name, handler->description);
	log_global_indent (ctx, "  Arguments: #s",
		handler->usage ? handler->usage : "(none)");
	return true;
}

static bool
handle_command_help (struct handler_args *a)
{
	struct app_context *ctx = a->ctx;
	if (!*a->arguments)
		return show_command_list (ctx);

	char *command = cut_word (&a->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))
			return show_command_help (ctx, handler);
	}

	if (!try_handle_command_help_option (ctx, command))
		log_global_error (ctx, "#s: #s", "No such command or option", command);
	return true;
}

static void
init_user_command_map (struct str_map *map)
{
	str_map_init (map);
	map->key_xfrm = tolower_ascii_strxfrm;

	for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
	{
		struct command_handler *handler = &g_command_handlers[i];
		str_map_set (map, handler->name, handler);
	}
}

static bool
process_user_command
	(struct app_context *ctx, const char *command_name, char *input)
{
	static bool initialized = false;
	static struct str_map map;
	if (!initialized)
	{
		init_user_command_map (&map);
		initialized = true;
	}

	if (try_handle_buffer_goto (ctx, command_name))
		return true;

	struct handler_args args =
	{
		.ctx = ctx,
		.buffer = ctx->current_buffer,
		.arguments = input,
	};

	struct command_handler *handler;
	if (!(handler = str_map_find (&map, command_name)))
		return false;

	if ((handler->flags & HANDLER_SERVER)
		&& args.buffer->type == BUFFER_GLOBAL)
		log_global_error (ctx, "/#s: #s",
			command_name, "can't do this from a global buffer");
	else if ((handler->flags & HANDLER_SERVER)
		&& !irc_is_connected ((args.s = args.buffer->server)))
		log_server_error (args.s, args.s->buffer, "Not connected");
	else if ((handler->flags & HANDLER_NEEDS_REG)
		&& args.s->state != IRC_REGISTERED)
		log_server_error (args.s, args.s->buffer, "Not registered");
	else if (((handler->flags & HANDLER_CHANNEL_FIRST)
			&& !(args.channel_name =
				try_get_channel (&args, maybe_cut_word)))
		|| ((handler->flags & HANDLER_CHANNEL_LAST)
			&& !(args.channel_name =
				try_get_channel (&args, maybe_cut_word_from_end))))
		log_server_error (args.s, args.buffer, "/#s: #s", command_name,
			"no channel name given and this buffer is not a channel");
	else if (!handler->handler (&args))
		log_global_error (ctx,
			"#s: /#s #s", "Usage", handler->name, handler->usage);
	return true;
}

static char *
expand_alias_definition (const struct str *definition, const char *arguments)
{
	struct str_vector v;
	str_vector_init (&v);
	split_str_ignore_empty (arguments, ' ', &v);

	struct str expanded;
	str_init (&expanded);

	// TODO: eventually also support argument ranges
	bool escape = false;
	for (const char *p = definition->str; *p; p++)
	{
		if (!escape)
		{
			if (*p == '$' && p[1])
				escape = true;
			else
				str_append_c (&expanded, *p);
			continue;
		}

		int as_number = *p - '0';
		if (as_number > 0 && as_number <= 9
		 && (size_t) as_number <= v.len)
			str_append (&expanded, v.vector[as_number - 1]);
		else if (*p == '*')
			str_append (&expanded, arguments);
		else if (*p == '$')
			str_append_c (&expanded, '$');
		else
			str_append_printf (&expanded, "$%c", *p);
		escape = false;
	}
	str_vector_free (&v);
	return str_steal (&expanded);
}

static char *
expand_alias (struct app_context *ctx, const char *alias_name, char *input)
{
	struct str_map *aliases =
		&config_item_get (ctx->config.root, "aliases", NULL)->value.object;

	struct config_item_ *entry = str_map_find (aliases, alias_name);
	if (!entry)
		return NULL;

	if (config_item_type_is_string (entry->type))
		return expand_alias_definition (&entry->value.string, input);

	log_global_error (ctx, "Error executing `/%s': "
		"alias definition is not a string", alias_name);
	return NULL;
}

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

static void
send_message_to_target (struct server *s,
	const char *target, char *message, struct buffer *buffer)
{
	if (!irc_is_connected (s))
		log_server_error (s, buffer, "Not connected");
	else
		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_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;
	default:
		log_full (ctx, NULL, buffer, BUFFER_LINE_ERROR,
			"This buffer is not a channel");
	}
}

static void
process_input_utf8 (struct app_context *ctx, char *input, int alias_level)
{
	if (*input != '/' || *++input == '/')
	{
		send_message_to_current_buffer (ctx, input);
		return;
	}

	char *name = cut_word (&input);
	if (process_user_command (ctx, name, input))
		return;

	char *expanded = expand_alias (ctx, name, input);
	if (expanded)
		log_global_debug (ctx, "Alias expanded to \"#s\"", expanded);

	if (!expanded)
		log_global_error (ctx, "#s: /#s", "No such command or alias", name);
	else if (alias_level != 0)
		log_global_error (ctx, "#s: /#s", "Aliases can't nest", name);
	else
		process_input_utf8 (ctx, expanded, alias_level++);
	free (expanded);
}

static void
process_input (struct app_context *ctx, char *user_input)
{
	char *input;
	if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL)))
		print_error ("character conversion failed for `%s'", "user input");
	else
		process_input_utf8 (ctx, input, 0);
	free (input);
}

// --- Word completion ---------------------------------------------------------

// The amount of crap that goes into this is truly insane.
// It's mostly because of Editline's total ignorance of this task.

struct completion_word
{
	size_t start;                       ///< Offset to start of word
	size_t end;                         ///< Offset to end of word
};

struct completion
{
	char *line;                         ///< The line which is being completed

	struct completion_word *words;      ///< Word locations
	size_t words_len;                   ///< Number of words
	size_t words_alloc;                 ///< Number of words allocated

	size_t location;                    ///< Which word is being completed
};

static void
completion_init (struct completion *self)
{
	memset (self, 0, sizeof *self);
}

static void
completion_free (struct completion *self)
{
	free (self->line);
	free (self->words);
}

static void
completion_add_word (struct completion *self, size_t start, size_t end)
{
	if (!self->words)
		self->words = xcalloc ((self->words_alloc = 4), sizeof *self->words);
	if (self->words_len == self->words_alloc)
		self->words = xreallocarray (self->words,
			(self->words_alloc <<= 1), sizeof *self->words);
	self->words[self->words_len++] = (struct completion_word) { start, end };
}

static void
completion_parse (struct completion *self, const char *line, size_t len)
{
	self->line = xstrndup (line, len);

	// The first and the last word may be empty
	const char *s = self->line;
	while (true)
	{
		const char *start = s;
		size_t word_len = strcspn (s, WORD_BREAKING_CHARS);
		const char *end = start + word_len;
		s = end + strspn (end, WORD_BREAKING_CHARS);

		completion_add_word (self, start - self->line, end - self->line);
		if (s == end)
			break;
	}
}

static void
completion_locate (struct completion *self, size_t offset)
{
	size_t i = 0;
	for (; i < self->words_len; i++)
		if (self->words[i].start > offset)
			break;
	self->location = i - 1;
}

static bool
completion_matches (struct completion *self, int word, const char *pattern)
{
	hard_assert (word >= 0 && word < (int) self->words_len);
	char *text = xstrndup (self->line + self->words[word].start,
		self->words[word].end - self->words[word].start);
	bool result = !fnmatch (pattern, text, 0);
	free (text);
	return result;
}

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

// XXX: this isn't completely right because Unicode, but let's keep it simple.
//   At worst it will stop before a combining mark, or fail to compare
//   non-ASCII identifiers case-insensitively.

static size_t
utf8_common_prefix (const char **vector, size_t len)
{
	size_t prefix = 0;
	if (!vector || !vector[0])
		return 0;

	struct utf8_iter a[len];
	for (size_t i = 0; i < len; i++)
		utf8_iter_init (&a[i], vector[i]);

	size_t ch_len;
	int32_t ch;
	while ((ch = utf8_iter_next (&a[0], &ch_len)) != -1)
	{
		for (size_t i = 1; i < len; i++)
		{
			int32_t other = utf8_iter_next (&a[i], NULL);
			if (ch == other)
				continue;
			// Not bothering with lowercasing non-ASCII
			if (ch >= 0x80 || other >= 0x80
			 || tolower_ascii (ch) != tolower_ascii (other))
				return prefix;
		}
		prefix += ch_len;
	}
	return prefix;
}

static void
complete_command (struct app_context *ctx, struct completion *data,
	const char *word, struct str_vector *output)
{
	(void) ctx;
	(void) data;

	const char *prefix = "";
	if (*word == '/')
	{
		word++;
		prefix = "/";
	}

	size_t word_len = strlen (word);
	for (size_t i = 0; i < N_ELEMENTS (g_command_handlers); i++)
	{
		struct command_handler *handler = &g_command_handlers[i];
		if (!strncasecmp_ascii (word, handler->name, word_len))
			str_vector_add_owned (output,
				xstrdup_printf ("%s%s", prefix, handler->name));
	}
}

static void
complete_option (struct app_context *ctx, struct completion *data,
	const char *word, struct str_vector *output)
{
	(void) data;

	struct str_vector options;
	str_vector_init (&options);

	config_dump (ctx->config.root, &options);
	str_vector_sort (&options);

	// Wildcard expansion is an interesting side-effect
	char *mask = xstrdup_printf ("%s*", word);
	for (size_t i = 0; i < options.len; i++)
	{
		char *key = str_cut_until (options.vector[i], " ");
		if (!fnmatch (mask, key, 0))
			str_vector_add_owned (output, key);
		else
			free (key);
	}
	free (mask);
	str_vector_free (&options);
}

static void
complete_nicknames (struct app_context *ctx, struct completion *data,
	const char *word, struct str_vector *output)
{
	struct buffer *buffer = ctx->current_buffer;
	if (buffer->type != BUFFER_CHANNEL)
		return;

	size_t word_len = strlen (word);
	LIST_FOR_EACH (struct channel_user, iter, buffer->channel->users)
	{
		const char *nickname = iter->user->nickname;
		if (irc_server_strncmp (buffer->server, word, nickname, word_len))
			continue;
		str_vector_add_owned (output, data->location == 0
			? xstrdup_printf ("%s:", nickname)
			: xstrdup (nickname));
	}
}

static char **
complete_word (struct app_context *ctx, struct completion *data,
	const char *word)
{
	// First figure out what exactly do we need to complete
	bool try_commands = false;
	bool try_options = false;
	bool try_nicknames = false;

	if (data->location == 0 && completion_matches (data, 0, "/*"))
		try_commands = true;
	else if (data->location == 1 && completion_matches (data, 0, "/set"))
		try_options = true;
	else if (data->location == 1 && completion_matches (data, 0, "/help"))
		try_commands = try_options = true;
	else
		try_nicknames = true;

	struct str_vector words;
	str_vector_init (&words);

	// Add placeholder
	str_vector_add_owned (&words, NULL);

	if (try_commands)   complete_command (ctx, data, word, &words);
	if (try_options)    complete_option (ctx, data, word, &words);
	if (try_nicknames)  complete_nicknames (ctx, data, word, &words);

	if (words.len == 1)
	{
		// Nothing matched
		str_vector_free (&words);
		return NULL;
	}

	if (words.len == 2)
	{
		words.vector[0] = words.vector[1];
		words.vector[1] = NULL;
	}
	else
	{
		size_t prefix = utf8_common_prefix
			((const char **) words.vector + 1, words.len - 1);
		if (!prefix)
			words.vector[0] = xstrdup (word);
		else
			words.vector[0] = xstrndup (words.vector[1], prefix);
	}
	return words.vector;
}

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

/// A special wrapper for iconv_xstrdup() that also fixes indexes into the
/// original string to point to the right location in the output.
/// Thanks, Readline!  Without you I would have never needed to deal with this.
static char *
locale_to_utf8 (struct app_context *ctx, const char *locale,
	int *indexes[], size_t n_indexes)
{
	struct str utf8;  str_init (&utf8);
	mbstate_t state;  memset (&state, 0, sizeof state);

	size_t remaining = strlen (locale) + 1;
	const char *p = locale;

	// Reset the shift state, FWIW
	(void) iconv (ctx->term_to_utf8, NULL, NULL, NULL, NULL);

	bool fixed[n_indexes];
	memset (fixed, 0, sizeof fixed);

	while (true)
	{
		size_t len = mbrlen (p, remaining, &state);

		// Incomplete multibyte character or illegal sequence (probably)
		if (len == (size_t) -2
		 || len == (size_t) -1)
		{
			str_free (&utf8);
			return NULL;
		}

		// Convert indexes into the multibyte string to UTF-8
		for (size_t i = 0; i < n_indexes; i++)
			if (!fixed[i] && *indexes[i] <= p - locale)
			{
				*indexes[i] = utf8.len;
				fixed[i] = true;
			}

		// End of string
		if (!len)
			break;

		// EINVAL (incomplete sequence) should never happen and
		// EILSEQ neither because we've already checked for that with mbrlen().
		// E2BIG is what iconv_xstrdup solves.  This must succeed.
		size_t ch_len;
		char *ch = iconv_xstrdup (ctx->term_to_utf8, (char *) p, len, &ch_len);
		hard_assert (ch != NULL);
		str_append_data (&utf8, ch, ch_len);
		free (ch);

		p += len;
		remaining -= len;
	}
	return str_steal (&utf8);
}

static void
utf8_vector_to_locale (struct app_context *ctx, char **vector)
{
	for (; *vector; vector++)
	{
		char *converted = iconv_xstrdup
			(ctx->term_from_utf8, *vector, -1, NULL);
		if (!soft_assert (converted))
			converted = xstrdup ("");

		free (*vector);
		*vector = converted;
	}
}

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

/// Takes a line in locale-specific encoding and position of a word to complete,
/// returns a vector of matches in locale-specific encoding.
static char **
make_completions (struct app_context *ctx, char *line, int start, int end)
{
	int *fixes[] = { &start, &end };
	char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes));
	if (!line_utf8)
		return NULL;

	hard_assert (start >= 0 && end >= 0 && start <= end);

	struct completion c;
	completion_init (&c);
	completion_parse (&c, line, strlen (line));
	completion_locate (&c, start);
	char *word = xstrndup (line + start, end - start);
	char **completions = complete_word (ctx, &c, word);
	free (word);
	completion_free (&c);

	if (completions)
		utf8_vector_to_locale (ctx, completions);

	free (line_utf8);
	return completions;
}

// --- Common code for user actions --------------------------------------------

static bool
redraw_screen (struct app_context *ctx)
{
	if (!soft_assert (clear_screen != NULL))
		return false;

	input_hide (&ctx->input);

	terminal_printer_fn printer = get_attribute_printer (stdout);
	tputs (clear_screen, 1, printer);
	fflush (stdout);
	buffer_print_backlog (ctx, ctx->current_buffer);

	input_show (&ctx->input);
	return true;
}

static bool
jump_to_buffer (struct app_context *ctx, int n)
{
	if (n < 0 || n > 9)
		return false;

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

	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))
		return false;
	return true;
}

static void
await_mirc_escape (struct app_context *ctx)
{
	ctx->awaiting_mirc_escape = true;
	ctx->char_buf_len = 0;
}

static void
bind_common_keys (struct app_context *ctx)
{
	struct input *self = &ctx->input;
	input_bind_control (self, 'p', "previous-buffer");
	input_bind_control (self, 'n', "next-buffer");

	// Redefine M-0 through M-9 to switch buffers
	for (int i = 0; i <= 9; i++)
		input_bind_meta (self, '0' + i, "goto-buffer");

	input_bind_meta (self, 'm', "insert-attribute");

	if (key_f5)
		input_bind (self, key_f5, "previous-buffer");
	if (key_f6)
		input_bind (self, key_f6, "next-buffer");

	if (clear_screen)
		input_bind_control (self, 'l', "redraw-screen");
}

// --- GNU Readline user actions -----------------------------------------------

#ifdef HAVE_READLINE

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

	struct app_context *ctx = g_ctx;
	if (!jump_to_buffer (ctx, UNMETA (key) - '0'))
		input_ding (&ctx->input);
	return 0;
}

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

	struct app_context *ctx = g_ctx;
	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;
	buffer_activate (ctx, buffer_next (ctx, count));
	return 0;
}

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

	struct app_context *ctx = g_ctx;
	if (!redraw_screen (ctx))
		input_ding (&ctx->input);
	return 0;
}

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

	struct app_context *ctx = g_ctx;
	await_mirc_escape (ctx);
	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;

	// Hide the line, don't redisplay it
	input_hide (&ctx->input);
	input_restore (&ctx->input);
	return 0;
}

static void
on_readline_input (char *line)
{
	struct app_context *ctx = g_ctx;

	if (line)
	{
		if (*line)
			add_history (line);

		// Normally, the text is deleted _afterwards_
		rl_replace_line ("", false);
		process_input (ctx, line);
		free (line);
	}
	else
	{
		input_hide (&ctx->input);
		input_restore (&ctx->input);
		input_ding (&ctx->input);
	}

	if (ctx->input.active)
		// Readline automatically redisplays it
		ctx->input.prompt_shown = 1;
}

static char **
app_readline_completion (const char *text, int start, int end)
{
	// We will reconstruct that ourselves
	(void) text;

	// Don't iterate over filenames and stuff
	rl_attempted_completion_over = true;

	return make_completions (g_ctx, rl_line_buffer, start, end);
}

static int
app_readline_init (void)
{
	struct app_context *ctx = g_ctx;
	struct input *self = &ctx->input;

	// 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);
	rl_add_defun ("goto-buffer",      on_readline_goto_buffer,      -1);
	rl_add_defun ("redraw-screen",    on_readline_redraw_screen,    -1);
	rl_add_defun ("insert-attribute", on_readline_insert_attribute, -1);
	rl_add_defun ("send-line",        on_readline_return,           -1);

	bind_common_keys (ctx);

	// Move native history commands
	input_bind_meta (self, 'p', "previous-history");
	input_bind_meta (self, 'n', "next-history");

	// We need to hide the prompt and input first
	rl_bind_key (RETURN, rl_named_function ("send-line"));

	rl_variable_bind ("completion-ignore-case", "on");
	rl_bind_key (TAB, rl_named_function ("menu-complete"));
	if (key_btab)
		input_bind (self, key_btab, "menu-complete-backward");
	return 0;
}

#endif // HAVE_READLINE

// --- BSD Editline user actions -----------------------------------------------

#ifdef HAVE_EDITLINE

static unsigned char
on_editline_goto_buffer (EditLine *editline, int key)
{
	(void) editline;

	struct app_context *ctx = g_ctx;
	if (!jump_to_buffer (ctx, key - '0'))
		return CC_ERROR;
	return CC_NORM;
}

static unsigned char
on_editline_previous_buffer (EditLine *editline, int key)
{
	(void) editline;
	(void) key;

	struct app_context *ctx = g_ctx;
	buffer_activate (ctx, buffer_previous (ctx, 1));
	return CC_NORM;
}

static unsigned char
on_editline_next_buffer (EditLine *editline, int key)
{
	(void) editline;
	(void) key;

	struct app_context *ctx = g_ctx;
	buffer_activate (ctx, buffer_next (ctx, 1));
	return CC_NORM;
}

static unsigned char
on_editline_redraw_screen (EditLine *editline, int key)
{
	(void) editline;
	(void) key;

	if (!redraw_screen (g_ctx))
		return CC_ERROR;
	return CC_NORM;
}

static unsigned char
on_editline_insert_attribute (EditLine *editline, int key)
{
	(void) editline;
	(void) key;

	await_mirc_escape (g_ctx);
	return CC_NORM;
}

static unsigned char
on_editline_complete (EditLine *editline, int key)
{
	(void) key;
	(void) editline;

	struct app_context *ctx = g_ctx;

	// First prepare what Readline would have normally done for us...
	const LineInfo *info_mb = el_line (editline);
	int len = info_mb->lastchar - info_mb->buffer;
	int point = info_mb->cursor - info_mb->buffer;
	char *copy = xstrndup (info_mb->buffer, len);

	// XXX: possibly incorrect wrt. shift state encodings
	int el_start = point, el_end = point;
	while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1]))
		el_start--;

	char **completions = make_completions (ctx, copy, el_start, el_end);

	// XXX: possibly incorrect wrt. shift state encodings
	copy[el_end] = '\0';
	int el_len = mbstowcs (NULL, copy + el_start, 0);
	free (copy);

	if (!completions)
		return CC_REFRESH_BEEP;

	// Remove the original word
	el_wdeletestr (editline, el_len);

	// Insert the best match instead
	el_insertstr (editline, completions[0]);
	bool only_match = !completions[1];
	for (char **p = completions; *p; p++)
		free (*p);
	free (completions);

	// I'm not sure if Readline's menu-complete can at all be implemented
	// with Editline.  Spamming the terminal with possible completions
	// probably isn't what the user wants and we have no way of detecting
	// what the last executed handler was.
	if (!only_match)
		return CC_REFRESH_BEEP;

	// But if there actually is just one match, finish the word
	el_insertstr (editline, " ");
	return CC_REFRESH;
}

static unsigned char
on_editline_return (EditLine *editline, int key)
{
	(void) key;
	struct app_context *ctx = g_ctx;

	const LineInfoW *info = el_wline (editline);
	int len = info->lastchar - info->buffer;
	int point = info->cursor - info->buffer;

	wchar_t *line = calloc (sizeof *info->buffer, len + 1);
	memcpy (line, info->buffer, sizeof *info->buffer * len);

	// XXX: Editline seems to remember its position in history,
	//   so it's not going to work as you'd expect it to
	if (*line)
	{
		HistEventW ev;
		history_w (ctx->input.current->history, &ev, H_ENTER, line);
		print_debug ("history: %d %ls", ev.num, ev.str);
	}
	free (line);

	// process_input() expects a multibyte string
	const LineInfo *info_mb = el_line (editline);
	char copy[info_mb->lastchar - info_mb->buffer + 1];
	memcpy (copy, info_mb->buffer, sizeof copy - 1);
	copy[sizeof copy - 1] = '\0';
	process_input (ctx, copy);

	el_cursor (editline, len - point);
	el_wdeletestr (editline, len);
	return CC_REFRESH;
}

static void
app_editline_init (struct input *self)
{
	static const struct { const char *name; const char *help;
		unsigned char (*func) (EditLine *, int); } x[] =
	{
		{ "goto-buffer",      "Go to buffer",    on_editline_goto_buffer      },
		{ "previous-buffer",  "Previous buffer", on_editline_previous_buffer  },
		{ "next-buffer",      "Next buffer",     on_editline_next_buffer      },
		{ "redraw-screen",    "Redraw screen",   on_editline_redraw_screen    },
		{ "insert-attribute", "mIRC formatting", on_editline_insert_attribute },
		{ "send-line",        "Send line",       on_editline_return           },
		{ "complete",         "Complete word",   on_editline_complete         },
	};
	for (size_t i = 0; i < N_ELEMENTS (x); i++)
		el_set (self->editline, EL_ADDFN, x[i].name, x[i].help, x[i].func);

	bind_common_keys (g_ctx);

	// Move native history commands
	input_bind_meta (self, 'p', "ed-prev-history");
	input_bind_meta (self, 'n', "ed-next-history");

	// No, editline, it's not supposed to kill the entire line
	input_bind_control (self, 'w', "ed-delete-prev-word");
	// Just what are you doing?
	input_bind_control (self, 'u', "vi-kill-line-prev");

	// We need to hide the prompt and input first
	input_bind (self, "\n", "send-line");

	input_bind_control (self, 'i', "complete");

	// Source the user's defaults file
	el_source (self->editline, NULL);
}

#endif // HAVE_EDITLINE

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

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 const char *g_first_time_help[] =
{
	"",
	"\x02Welcome to degesch!",
	"",
	"To get a list of all commands, type \x02/help\x0f.  To obtain",
	"more information on a command or option, simply add it as",
	"a parameter, e.g. \x02/help set\x0f or \x02/help behaviour.logging\x0f.",
	"",
	"To switch between buffers, press \x02"
		"F5/Ctrl-P\x0f or \x02" "F6/Ctrl-N\x0f.",
	"",
	"Finally, adding a network is as simple as:",
	" - \x02/server add freenode\x0f",
	" - \x02/set servers.freenode.addresses = \"chat.freenode.net\"\x0f",
	" - \x02/connect freenode\x0f",
	"",
	"That should be enough to get you started.  Have fun!",
	""
};

static void
show_first_time_help (struct app_context *ctx)
{
	for (size_t i = 0; i < N_ELEMENTS (g_first_time_help); i++)
		log_global_indent (ctx, "#m", g_first_time_help[i]);
}

const char *g_default_aliases[][2] =
{
	{ "c",     "/buffer clear" }, { "close", "/buffer close" },
	{ "j",     "/join $*"      }, { "p",     "/part $*"      },
	{ "k",     "/kick $*"      }, { "kb",    "/kickban $*"   },
	{ "m",     "/msg $*"       }, { "q",     "/query $*"     },
	{ "n",     "/names $*"     }, { "t",     "/topic $*"     },
	{ "w",     "/who $*"       }, { "wi",    "/whois $*"     },
	{ "ww",    "/whowas $*"    },
};

static void
load_default_aliases (struct app_context *ctx)
{
	struct str_map *aliases =
		&config_item_get (ctx->config.root, "aliases", NULL)->value.object;
	for (size_t i = 0; i < N_ELEMENTS (g_default_aliases); i++)
	{
		const char **pair = g_default_aliases[i];

		struct str tmp;
		str_init (&tmp);
		str_append (&tmp, pair[1]);
		str_map_set (aliases, pair[0], config_item_string (&tmp));
		str_free (&tmp);
	}
}

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
		log_global_error (ctx, "Configuration file not found");
	free (filename);

	if (e)
	{
		log_global_error (ctx, "#s", e->message);
		error_free (e);
	}

	if (root)
	{
		config_item_destroy (ctx->config.root);
		ctx->config.root = NULL;
		config_load (&ctx->config, root);
		log_global_status (ctx, "Configuration loaded");
	}
	else
	{
		show_first_time_help (ctx);
		load_default_aliases (ctx);
	}
}

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

static void
load_servers (struct app_context *ctx)
{
	struct str_map_iter iter;
	str_map_iter_init (&iter, get_servers_config (ctx));

	struct config_item_ *subtree;
	while ((subtree = str_map_iter_next (&iter)))
	{
		const char *name = iter.link->key;
		if (subtree->type != CONFIG_ITEM_OBJECT)
			log_global_error (ctx, "Error in configuration: "
				"ignoring server `#s' as it's not an object", name);
		else if (check_server_name_for_addition (ctx, name))
			server_add (ctx, name, subtree);
	}
}

// --- 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));
}

// --- 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)
		// TODO: this way we don't send a QUIT message but just close the
		//   connection from our side and wait for a full close.
		//   Once we allow for custom quit messages, we will probably want to
		//   call irc_initiate_disconnect() for all servers.
		initiate_quit (ctx);

	if (g_winch_received)
	{
		if (ctx->input.active)
			input_on_terminal_resized (&ctx->input);

		update_screen_size ();
		g_winch_received = false;
	}
}

static void
process_mirc_escape (const struct pollfd *fd, struct app_context *ctx)
{
	// There's no other way with libedit, as both el_getc() in a function
	// handler and CC_ARGHACK would block execution
	if (read (fd->fd, ctx->char_buf + ctx->char_buf_len, 1) != 1)
		goto error;

	mbstate_t state;
	memset (&state, 0, sizeof state);

	size_t len = mbrlen (ctx->char_buf, ++ctx->char_buf_len, &state);

	// Illegal sequence
	if (len == (size_t) -1)
		goto error;

	// Incomplete multibyte character
	if (len == (size_t) -2)
		return;

	if (ctx->char_buf_len != 1)
		goto error;
	switch (ctx->char_buf[0])
	{
	case 'b': input_insert_c (&ctx->input, '\x02'); break;
	case 'c': input_insert_c (&ctx->input, '\x03'); break;
	case 'i':
	case ']': input_insert_c (&ctx->input, '\x1d'); break;
	case 'u':
	case '_': input_insert_c (&ctx->input, '\x1f'); break;
	case 'v': input_insert_c (&ctx->input, '\x16'); break;
	case 'o': input_insert_c (&ctx->input, '\x0f'); break;

	default:
		goto error;
	}
	goto done;

error:
	input_ding (&ctx->input);
done:
	ctx->awaiting_mirc_escape = false;
}

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);

	if (ctx->awaiting_mirc_escape)
	{
		process_mirc_escape (fd, ctx);
		return;
	}

	// XXX: this may loop for a bit: stop the event or eat the input?
	//   (This prevents a segfault when the input has been stopped.)
	if (ctx->input.active)
		input_on_readable (&ctx->input);
}

static void
rearm_flush_timer (struct app_context *ctx)
{
	poller_timer_set (&ctx->flush_timer, 60 * 1000);
}

static void
on_flush_timer (struct app_context *ctx)
{
	// I guess we don't need to do anything more complicated
	fflush (NULL);
	rearm_flush_timer (ctx);
}

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);

	poller_timer_init (&ctx->flush_timer, &ctx->poller);
	ctx->flush_timer.dispatcher = (poller_timer_fn) on_flush_timer;
	ctx->flush_timer.user_data = ctx;
	rearm_flush_timer (ctx);
}

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

static const char *g_logo[] =
{
	"      __                                __    ",
	"   __/ / ____  ____  ____  ____  ____  / /_   ",
	"  /   / / , / /   / / , / / __/ / __/ / __ \\ ",
	" / / / / __/ / / / / __/ /_  / / /_  / / / /  ",
	"/___/ /___/ /_  / /___/ /___/ /___/ /_/ /_/   " PROGRAM_VERSION,
	"           /___/ ",
	""
};

static void
show_logo (struct app_context *ctx)
{
	for (size_t i = 0; i < N_ELEMENTS (g_logo); i++)
		log_global_indent (ctx, "#m", g_logo[i]);
}

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" },
		{ 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);
	default:
		print_error ("wrong options");
		opt_handler_usage (&oh, stderr);
		exit (EXIT_FAILURE);
	}
	opt_handler_free (&oh);

	// 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);

	// Bootstrap configuration, so that we can access schema items at all
	register_config_modules (&ctx);
	config_load (&ctx.config, config_item_object ());

	// The following part is a bit brittle because of interdependencies
	init_colors (&ctx);
	init_global_buffer (&ctx);
	show_logo (&ctx);
	setup_signal_handlers ();
	init_poller_events (&ctx);
	load_configuration (&ctx);

	// At this moment we can safely call any "on_change" callbacks
	config_schema_call_changed (ctx.config.root);

	// Finally, we juice the configuration for some servers to create
	load_servers (&ctx);

	refresh_prompt (&ctx);
	input_start (&ctx.input, argv[0]);

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

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