/*
 * xC.c: a terminal-based IRC client
 *
 * Copyright (c) 2015 - 2022, Přemysl Eric Janouch 
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted.
 *
 * 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
#define ATTR_TABLE(XX)                                                  \
	XX( PROMPT,      prompt,      Terminal attrs for the prompt       ) \
	XX( DATE_CHANGE, date_change, Terminal attrs for date change      ) \
	XX( READ_MARKER, read_marker, Terminal attrs for the read marker  ) \
	XX( WARNING,     warning,     Terminal attrs for warnings         ) \
	XX( ERROR,       error,       Terminal attrs for errors           ) \
	XX( EXTERNAL,    external,    Terminal attrs for external lines   ) \
	XX( TIMESTAMP,   timestamp,   Terminal attrs for timestamps       ) \
	XX( HIGHLIGHT,   highlight,   Terminal attrs for highlights       ) \
	XX( ACTION,      action,      Terminal attrs for user actions     ) \
	XX( USERHOST,    userhost,    Terminal attrs for user@host        ) \
	XX( JOIN,        join,        Terminal attrs for joins            ) \
	XX( PART,        part,        Terminal attrs for parts            )
enum
{
	ATTR_RESET,
#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 "xC"
// fmemopen
#define _POSIX_C_SOURCE 200809L
#include "common.c"
#include "xD-replies.c"
#include "xC-proto.c"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// Literally cancer
#undef lines
#undef columns
#include 
#ifdef HAVE_LUA
#include 
#include 
#include 
#endif // HAVE_LUA
// --- Terminal information ----------------------------------------------------
static struct
{
	bool initialized;                   ///< Terminal is available
	bool stdout_is_tty;                 ///< `stdout' is a terminal
	bool stderr_is_tty;                 ///< `stderr' is a terminal
	struct termios termios;             ///< Terminal attributes
	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
	 || tcgetattr (tty_fd, &g_terminal.termios))
	{
		del_curterm (cur_term);
		return false;
	}
	// Make sure newlines are output correctly
	g_terminal.termios.c_oflag |= ONLCR;
	(void) tcsetattr (tty_fd, TCSADRAIN, &g_terminal.termios);
	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);
}
// --- 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.  We use lots
// of hacks to get the results we want and need.
//
// The abstraction is a necessary evil.  It's still not 100%, though.
/// Some arbitrary limit for the history
#define HISTORY_LIMIT 10000
/// Characters that separate words
#define WORD_BREAKING_CHARS " \f\n\r\t\v"
struct input
{
	struct input_vtable *vtable;        ///< Virtual methods
	void (*add_functions) (void *);     ///< Define functions for binding
	void *user_data;                    ///< User data for callbacks
};
typedef void *input_buffer_t;           ///< Pointer alias for input buffers
/// Named function that can be bound to a sequence of characters
typedef bool (*input_fn) (int count, int key, void *user_data);
// A little bit better than tons of forwarder functions in our case
#define CALL(self, name)        ((self)->vtable->name ((self)))
#define CALL_(self, name, ...)  ((self)->vtable->name ((self), __VA_ARGS__))
struct input_vtable
{
	/// Start the interface under the given program name
	void (*start) (void *input, const char *program_name);
	/// Stop the interface
	void (*stop) (void *input);
	/// Prepare or unprepare terminal for our needs
	void (*prepare) (void *input, bool enabled);
	/// Destroy the object
	void (*destroy) (void *input);
	/// Hide the prompt if shown
	void (*hide) (void *input);
	/// Show the prompt if hidden
	void (*show) (void *input);
	/// Retrieve current prompt string
	const char *(*get_prompt) (void *input);
	/// Change the prompt string; takes ownership
	void (*set_prompt) (void *input, char *prompt);
	/// Ring the terminal bell
	void (*ding) (void *input);
	/// Create a new input buffer
	input_buffer_t (*buffer_new) (void *input);
	/// Destroy an input buffer
	void (*buffer_destroy) (void *input, input_buffer_t buffer);
	/// Switch to a different input buffer
	void (*buffer_switch) (void *input, input_buffer_t buffer);
	/// Return all history lines in the locale encoding
	struct strv (*buffer_history) (void *input, input_buffer_t buffer);
	/// Register a function that can be bound to character sequences
	void (*register_fn) (void *input,
		const char *name, const char *help, input_fn fn, void *user_data);
	/// Bind an arbitrary sequence of characters to the given named function
	void (*bind) (void *input, const char *seq, const char *fn);
	/// Bind Ctrl+key to the given named function
	void (*bind_control) (void *input, char key, const char *fn);
	/// Bind Alt+key to the given named function
	void (*bind_meta) (void *input, char key, const char *fn);
	/// Get the current line input and position within
	char *(*get_line) (void *input, int *position);
	/// Clear the current line input
	void (*clear_line) (void *input);
	/// Insert text at current position
	bool (*insert) (void *input, const char *text);
	/// Handle terminal resize
	void (*on_tty_resized) (void *input);
	/// Handle terminal input
	void (*on_tty_readable) (void *input);
};
#define INPUT_VTABLE(XX)                                                       \
	XX (start) XX (stop) XX (prepare) XX (destroy)                             \
	XX (hide) XX (show) XX (get_prompt) XX (set_prompt) XX (ding)              \
	XX (buffer_new) XX (buffer_destroy) XX (buffer_switch) XX (buffer_history) \
	XX (register_fn) XX (bind) XX (bind_control) XX (bind_meta)                \
	XX (get_line) XX (clear_line) XX (insert)                                  \
	XX (on_tty_resized) XX (on_tty_readable)
// ~~~ GNU Readline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#ifdef HAVE_READLINE
#include 
#include 
#define INPUT_START_IGNORE  RL_PROMPT_START_IGNORE
#define INPUT_END_IGNORE    RL_PROMPT_END_IGNORE
struct input_rl_fn
{
	ffi_closure closure;                ///< Closure
	LIST_HEADER (struct input_rl_fn)
	input_fn callback;                  ///< Real callback
	void *user_data;                    ///< Real callback user data
};
struct input_rl_buffer
{
	HISTORY_STATE *history;             ///< Saved history state
	char *saved_line;                   ///< Saved line content
	int saved_point;                    ///< Saved cursor position
	int saved_mark;                     ///< Saved mark
};
struct input_rl
{
	struct input super;                 ///< Parent class
	bool active;                        ///< Interface has been started
	char *prompt;                       ///< The prompt we use
	int prompt_shown;                   ///< Whether the prompt is shown now
	char *saved_line;                   ///< Saved line content
	int saved_point;                    ///< Saved cursor position
	int saved_mark;                     ///< Saved mark
	struct input_rl_fn *fns;            ///< Named functions
	struct input_rl_buffer *current;    ///< Current input buffer
};
static void
input_rl_ding (void *input)
{
	(void) input;
	rl_ding ();
}
static const char *
input_rl_get_prompt (void *input)
{
	struct input_rl *self = input;
	return self->prompt;
}
static void
input_rl_set_prompt (void *input, char *prompt)
{
	struct input_rl *self = input;
	cstr_set (&self->prompt, prompt);
	if (!self->active || self->prompt_shown <= 0)
		return;
	// First reset the prompt to work around a bug in readline
	rl_set_prompt ("");
	rl_redisplay ();
	rl_set_prompt (self->prompt);
	rl_redisplay ();
}
static void
input_rl_clear_line (void *input)
{
	(void) input;
	rl_replace_line ("", false);
	rl_redisplay ();
}
static void
input_rl__erase (struct input_rl *self)
{
	rl_set_prompt ("");
	input_rl_clear_line (self);
}
static bool
input_rl_insert (void *input, const char *s)
{
	struct input_rl *self = input;
	rl_insert_text (s);
	if (self->prompt_shown > 0)
		rl_redisplay ();
	// GNU Readline, contrary to Editline, doesn't care about validity
	return true;
}
static char *
input_rl_get_line (void *input, int *position)
{
	(void) input;
	if (position) *position = rl_point;
	return rl_copy_text (0, rl_end);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_bind (void *input, const char *seq, const char *function_name)
{
	(void) input;
	rl_bind_keyseq (seq, rl_named_function (function_name));
}
static void
input_rl_bind_meta (void *input, char key, const char *function_name)
{
	// This one seems to actually work
	char keyseq[] = { '\\', 'e', key, 0 };
	input_rl_bind (input, 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- behaves exactly the same
	rl_bind_key (META (key), rl_named_function (function_name));
#endif
}
static void
input_rl_bind_control (void *input, char key, const char *function_name)
{
	char keyseq[] = { '\\', 'C', '-', key, 0 };
	input_rl_bind (input, keyseq, function_name);
}
static void
input_rl__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
{
	(void) cif;
	struct input_rl_fn *data = user_data;
	if (!data->callback
		(*(int *) args[0], UNMETA (*(int *) args[1]), data->user_data))
		rl_ding ();
	*(int *) ret = 0;
}
static void
input_rl_register_fn (void *input,
	const char *name, const char *help, input_fn callback, void *user_data)
{
	struct input_rl *self = input;
	(void) help;
	void *bound_fn = NULL;
	struct input_rl_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
	hard_assert (data);
	static ffi_cif cif;
	static ffi_type *args[2] = { &ffi_type_sint, &ffi_type_sint };
	hard_assert (ffi_prep_cif
		(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, args) == FFI_OK);
	data->prev = data->next = NULL;
	data->callback = callback;
	data->user_data = user_data;
	hard_assert (ffi_prep_closure_loc (&data->closure,
		&cif, input_rl__forward, data, bound_fn) == FFI_OK);
	rl_add_defun (name, (rl_command_func_t *) bound_fn, -1);
	LIST_PREPEND (self->fns, data);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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_rl_start (void *input, const char *program_name)
{
	struct input_rl *self = input;
	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_change_environment = false;
	rl_basic_word_break_characters = WORD_BREAKING_CHARS;
	rl_completer_word_break_characters = NULL;
	rl_attempted_completion_function = app_readline_completion;
	// We shouldn't produce any duplicates that the library would help us
	// autofilter, and we don't generally want alphabetic ordering at all
	rl_sort_completion_matches = false;
	hard_assert (self->prompt != NULL);
	// The inputrc is read before any callbacks are called, so we need to
	// register all functions that our user may want to map up front
	self->super.add_functions (self->super.user_data);
	rl_callback_handler_install (self->prompt, on_readline_input);
	self->prompt_shown = 1;
	self->active = true;
}
static void
input_rl_stop (void *input)
{
	struct input_rl *self = input;
	if (self->prompt_shown > 0)
		input_rl__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;
}
static void
input_rl_prepare (void *input, bool enabled)
{
	(void) input;
	if (enabled)
		rl_prep_terminal (true);
	else
		rl_deprep_terminal ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 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_rl__save_buffer (struct input_rl *self, struct input_rl_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;
	rl_replace_line ("", true);
	if (self->prompt_shown > 0)
		rl_redisplay ();
}
static void
input_rl__restore_buffer (struct input_rl *self, struct input_rl_buffer *buffer)
{
	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);
	}
	if (buffer->saved_line)
	{
		rl_replace_line (buffer->saved_line, true);
		rl_point = buffer->saved_point;
		rl_mark = buffer->saved_mark;
		cstr_set (&buffer->saved_line, NULL);
		if (self->prompt_shown > 0)
			rl_redisplay ();
	}
}
static void
input_rl_buffer_switch (void *input, input_buffer_t input_buffer)
{
	struct input_rl *self = input;
	struct input_rl_buffer *buffer = input_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_rl__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_rl__restore_buffer (self, buffer);
	self->current = buffer;
}
static struct strv
input_rl_buffer_history (void *input, input_buffer_t input_buffer)
{
	struct input_rl *self = input;
	struct input_rl_buffer *buffer = input_buffer;
	HIST_ENTRY **p =
		buffer->history ? buffer->history->entries : history_list();
	struct strv v = strv_make ();
	while (p && *p)
		strv_append (&v, (*p++)->line);
	return v;
}
static void
input_rl__buffer_destroy_wo_history (struct input_rl_buffer *self)
{
	free (self->history);
	free (self->saved_line);
	free (self);
}
static void
input_rl_buffer_destroy (void *input, input_buffer_t input_buffer)
{
	(void) input;
	struct input_rl_buffer *buffer = input_buffer;
	// 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_rl_buffer_switch() 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_rl__buffer_destroy_wo_history (buffer);
}
static input_buffer_t
input_rl_buffer_new (void *input)
{
	(void) input;
	struct input_rl_buffer *self = xcalloc (1, sizeof *self);
	return self;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Since {save,restore}_buffer() store history, we can't use them here like we
// do with libedit, because then buffer_destroy() can free memory that's still
// being used by readline.  This situation is bound to happen on quit.
static void
input_rl__save (struct input_rl *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_rl__restore (struct input_rl *self)
{
	hard_assert (self->saved_line);
	rl_set_prompt (self->prompt);
	rl_replace_line (self->saved_line, false);
	rl_point = self->saved_point;
	rl_mark = self->saved_mark;
	cstr_set (&self->saved_line, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_hide (void *input)
{
	struct input_rl *self = input;
	if (!self->active || self->prompt_shown-- < 1)
		return;
	input_rl__save (self);
	input_rl__erase (self);
}
static void
input_rl_show (void *input)
{
	struct input_rl *self = input;
	if (!self->active || ++self->prompt_shown < 1)
		return;
	input_rl__restore (self);
	rl_redisplay ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_on_tty_resized (void *input)
{
	(void) input;
	// 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_rl_on_tty_readable (void *input)
{
	(void) input;
	rl_callback_read_char ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_destroy (void *input)
{
	struct input_rl *self = input;
	free (self->saved_line);
	LIST_FOR_EACH (struct input_rl_fn, iter, self->fns)
		ffi_closure_free (iter);
	free (self->prompt);
	free (self);
}
#define XX(a) .a = input_rl_ ## a,
static struct input_vtable input_rl_vtable = { INPUT_VTABLE (XX) };
#undef XX
static struct input *
input_rl_new (void)
{
	struct input_rl *self = xcalloc (1, sizeof *self);
	self->super.vtable = &input_rl_vtable;
	return &self->super;
}
#define input_new input_rl_new
#endif // HAVE_READLINE
// ~~~ BSD Editline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#ifdef HAVE_EDITLINE
#include 
#define INPUT_START_IGNORE  '\x01'
#define INPUT_END_IGNORE    '\x01'
struct input_el_fn
{
	ffi_closure closure;                ///< Closure
	LIST_HEADER (struct input_el_fn)
	input_fn callback;                  ///< Real callback
	void *user_data;                    ///< Real callback user data
	wchar_t *name;                      ///< Function name
	wchar_t *help;                      ///< Function help
};
struct input_el_buffer
{
	HistoryW *history;                  ///< The history object
	wchar_t *saved_line;                ///< Saved line content
	int saved_len;                      ///< Length of the saved line
	int saved_point;                    ///< Saved cursor position
};
struct input_el
{
	struct input super;                 ///< Parent class
	EditLine *editline;                 ///< The EditLine object
	bool active;                        ///< Are we a thing?
	char *prompt;                       ///< The prompt we use
	int prompt_shown;                   ///< Whether the prompt is shown now
	struct input_el_fn *fns;            ///< Named functions
	struct input_el_buffer *current;    ///< Current input buffer
};
static void app_editline_init (struct input_el *self);
static void
input_el__redisplay (void *input)
{
	// See rl_redisplay(), however NetBSD editline's map.c v1.54 breaks VREPRINT
	// so we bind redisplay somewhere else in app_editline_init()
	struct input_el *self = input;
	wchar_t x[] = { L'q' & 31, 0 };
	el_wpush (self->editline, x);
	// We have to do this or it gets stuck and nothing is done
	int dummy_count = 0;
	(void) el_wgets (self->editline, &dummy_count);
}
static char *
input_el__make_prompt (EditLine *editline)
{
	struct input_el *self = NULL;
	el_get (editline, EL_CLIENTDATA, &self);
	if (!self->prompt)
		return "";
	return self->prompt;
}
static char *
input_el__make_empty_prompt (EditLine *editline)
{
	(void) editline;
	return "";
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_ding (void *input)
{
	// XXX: this isn't probably very portable;
	//   we could use "bell" from terminfo but that creates a dependency
	(void) input;
	write (STDOUT_FILENO, "\a", 1);
}
static const char *
input_el_get_prompt (void *input)
{
	struct input_el *self = input;
	return self->prompt;
}
static void
input_el_set_prompt (void *input, char *prompt)
{
	struct input_el *self = input;
	cstr_set (&self->prompt, prompt);
	if (self->prompt_shown > 0)
		input_el__redisplay (self);
}
static void
input_el_clear_line (void *input)
{
	struct input_el *self = input;
	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);
	input_el__redisplay (self);
}
static void
input_el__erase (struct input_el *self)
{
	el_set (self->editline, EL_PROMPT, input_el__make_empty_prompt);
	input_el_clear_line (self);
}
static bool
input_el_insert (void *input, const char *s)
{
	struct input_el *self = input;
	bool success = !*s || !el_insertstr (self->editline, s);
	if (self->prompt_shown > 0)
		input_el__redisplay (self);
	return success;
}
static char *
input_el_get_line (void *input, int *position)
{
	struct input_el *self = input;
	const LineInfo *info = el_line (self->editline);
	int point = info->cursor - info->buffer;
	if (position) *position = point;
	return xstrndup (info->buffer, info->lastchar - info->buffer);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_bind (void *input, const char *seq, const char *function_name)
{
	struct input_el *self = input;
	el_set (self->editline, EL_BIND, seq, function_name, NULL);
}
static void
input_el_bind_meta (void *input, char key, const char *function_name)
{
	char keyseq[] = { 'M', '-', key, 0 };
	input_el_bind (input, keyseq, function_name);
}
static void
input_el_bind_control (void *input, char key, const char *function_name)
{
	char keyseq[] = { '^', key, 0 };
	input_el_bind (input, keyseq, function_name);
}
static void
input_el__forward (ffi_cif *cif, void *ret, void **args, void *user_data)
{
	(void) cif;
	struct input_el_fn *data = user_data;
	*(unsigned char *) ret = data->callback
		(1, *(int *) args[1], data->user_data) ? CC_NORM : CC_ERROR;
}
static wchar_t *
ascii_to_wide (const char *ascii)
{
	size_t len = strlen (ascii) + 1;
	wchar_t *wide = xcalloc (sizeof *wide, len);
	while (len--)
		hard_assert ((wide[len] = (unsigned char) ascii[len]) < 0x80);
	return wide;
}
static void
input_el_register_fn (void *input,
	const char *name, const char *help, input_fn callback, void *user_data)
{
	void *bound_fn = NULL;
	struct input_el_fn *data = ffi_closure_alloc (sizeof *data, &bound_fn);
	hard_assert (data);
	static ffi_cif cif;
	static ffi_type *args[2] = { &ffi_type_pointer, &ffi_type_sint };
	hard_assert (ffi_prep_cif
		(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_uchar, args) == FFI_OK);
	data->user_data = user_data;
	data->callback = callback;
	data->name = ascii_to_wide (name);
	data->help = ascii_to_wide (help);
	hard_assert (ffi_prep_closure_loc (&data->closure,
		&cif, input_el__forward, data, bound_fn) == FFI_OK);
	struct input_el *self = input;
	el_wset (self->editline, EL_ADDFN, data->name, data->help, bound_fn);
	LIST_PREPEND (self->fns, data);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_start (void *input, const char *program_name)
{
	struct input_el *self = input;
	self->editline = el_init (program_name, stdin, stdout, stderr);
	el_set (self->editline, EL_CLIENTDATA, self);
	el_set (self->editline, EL_PROMPT_ESC,
		input_el__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_el_stop (void *input)
{
	struct input_el *self = input;
	if (self->prompt_shown > 0)
		input_el__erase (self);
	el_end (self->editline);
	self->editline = NULL;
	self->active = false;
	self->prompt_shown = false;
}
static void
input_el_prepare (void *input, bool enabled)
{
	struct input_el *self = input;
	el_set (self->editline, EL_PREP_TERM, enabled);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el__save_buffer (struct input_el *self, struct input_el_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_el__save (struct input_el *self)
{
	if (self->current)
		input_el__save_buffer (self, self->current);
}
static void
input_el__restore_buffer (struct input_el *self, struct input_el_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_el__restore (struct input_el *self)
{
	if (self->current)
		input_el__restore_buffer (self, self->current);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Editline keeping its own history position (look for "eventno" there).
// This is the only sane way of resetting it.
static void
input_el__start_over (struct input_el *self)
{
	wchar_t x[] = { L'c' & 31, 0 };
	el_wpush (self->editline, x);
	int dummy_count = 0;
	(void) el_wgets (self->editline, &dummy_count);
}
static void
input_el_buffer_switch (void *input, input_buffer_t input_buffer)
{
	struct input_el *self = input;
	struct input_el_buffer *buffer = input_buffer;
	if (!self->active)
		return;
	if (self->current)
		input_el__save_buffer (self, self->current);
	self->current = buffer;
	el_wset (self->editline, EL_HIST, history, buffer->history);
	input_el__start_over (self);
	input_el__restore_buffer (self, buffer);
}
static struct strv
input_el_buffer_history (void *input, input_buffer_t input_buffer)
{
	struct input_el *self = input;
	struct input_el_buffer *buffer = input_buffer;
	struct strv v = strv_make ();
	HistEventW ev;
	if (history_w (buffer->history, &ev, H_LAST) < 0)
		return v;
	do
	{
		size_t len = wcstombs (NULL, ev.str, 0);
		if (len++ == (size_t) -1)
			continue;
		char *mb = xmalloc (len);
		mb[wcstombs (mb, ev.str, len)] = 0;
		strv_append_owned (&v, mb);
	}
	while (history_w (buffer->history, &ev, H_PREV) >= 0);
	return v;
}
static void
input_el_buffer_destroy (void *input, input_buffer_t input_buffer)
{
	struct input_el *self = input;
	struct input_el_buffer *buffer = input_buffer;
	if (self->active && self->current == buffer)
	{
		el_wset (self->editline, EL_HIST, history, NULL);
		self->current = NULL;
	}
	history_wend (buffer->history);
	free (buffer->saved_line);
	free (buffer);
}
static input_buffer_t
input_el_buffer_new (void *input)
{
	(void) input;
	struct input_el_buffer *self = xcalloc (1, sizeof *self);
	self->history = history_winit ();
	HistEventW ev;
	history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
	return self;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_hide (void *input)
{
	struct input_el *self = input;
	if (!self->active || self->prompt_shown-- < 1)
		return;
	input_el__save (self);
	input_el__erase (self);
}
static void
input_el_show (void *input)
{
	struct input_el *self = input;
	if (!self->active || ++self->prompt_shown < 1)
		return;
	input_el__restore (self);
	el_set (self->editline,
		EL_PROMPT_ESC, input_el__make_prompt, INPUT_START_IGNORE);
	input_el__redisplay (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_on_tty_resized (void *input)
{
	struct input_el *self = input;
	el_resize (self->editline);
}
static void
input_el_on_tty_readable (void *input)
{
	// We bind the return key to process it how we need to
	struct input_el *self = input;
	// 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;
	if (count == 0 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in el_wgets() */)
	{
		el_deletestr (self->editline, 1);
		input_el__redisplay (self);
		input_el_ding (self);
	}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_destroy (void *input)
{
	struct input_el *self = input;
	LIST_FOR_EACH (struct input_el_fn, iter, self->fns)
	{
		free (iter->name);
		free (iter->help);
		ffi_closure_free (iter);
	}
	free (self->prompt);
	free (self);
}
#define XX(a) .a = input_el_ ## a,
static struct input_vtable input_el_vtable = { INPUT_VTABLE (XX) };
#undef XX
static struct input *
input_el_new (void)
{
	struct input_el *self = xcalloc (1, sizeof *self);
	self->super.vtable = &input_el_vtable;
	return &self->super;
}
#define input_new input_el_new
#endif // HAVE_EDITLINE
// --- Application data --------------------------------------------------------
// All text stored in our data structures is encoded in UTF-8.  Or at least
// should be--our only ways of retrieving strings are: via the command line
// (converted from locale, no room for errors), via the configuration file
// (restrictive ASCII grammar for bare words and an internal check for strings),
// and via plugins (meticulously validated).
//
// The only exception is IRC identifiers.
// ~~~ Scripting support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// We need a few reference countable objects with support for both strong
// and weak references (mainly used for scripted plugins).
//
// Beware that if you don't own the object, you will most probably want
// to keep the weak reference link so that you can get rid of it later.
// Also note that you have to make sure the user_data don't leak resources.
//
// Having a callback is more versatile than just nulling out a pointer.
/// Callback just before a reference counted object is destroyed
typedef void (*destroy_cb_fn) (void *object, void *user_data);
struct weak_ref_link
{
	LIST_HEADER (struct weak_ref_link)
	destroy_cb_fn on_destroy;           ///< Called when object is destroyed
	void *user_data;                    ///< User data
};
static struct weak_ref_link *
weak_ref (struct weak_ref_link **list, destroy_cb_fn cb, void *user_data)
{
	struct weak_ref_link *link = xcalloc (1, sizeof *link);
	link->on_destroy = cb;
	link->user_data = user_data;
	LIST_PREPEND (*list, link);
	return link;
}
static void
weak_unref (struct weak_ref_link **list, struct weak_ref_link **link)
{
	if (*link)
		LIST_UNLINK (*list, *link);
	free (*link);
	*link = NULL;
}
#define REF_COUNTABLE_HEADER                                                   \
	size_t ref_count;                   /**< Reference count                */ \
	struct weak_ref_link *weak_refs;    /**< To remove any weak references  */
#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;                                                            \
		LIST_FOR_EACH (struct weak_ref_link, iter, self->weak_refs)            \
		{                                                                      \
			iter->on_destroy (self, iter->user_data);                          \
			free (iter);                                                       \
		}                                                                      \
		name ## _destroy (self);                                               \
	}                                                                          \
																			   \
	static struct weak_ref_link *                                              \
	name ## _weak_ref (struct name *self, destroy_cb_fn cb, void *user_data)   \
	{ return weak_ref (&self->weak_refs, cb, user_data); }                     \
																			   \
	static void                                                                \
	name ## _weak_unref (struct name *self, struct weak_ref_link **link)       \
	{ weak_unref (&self->weak_refs, link); }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Simple introspection framework to simplify exporting stuff to Lua, since
// there is a lot of it.  While not fully automated, at least we have control
// over which fields are exported.
enum ispect_type
{
	ISPECT_BOOL, ISPECT_INT, ISPECT_UINT, ISPECT_SIZE, ISPECT_STRING,
	ISPECT_STR,                         ///< "struct str"
	ISPECT_STR_MAP,                     ///< "struct str_map"
	ISPECT_REF                          ///< Weakly referenced object
};
struct ispect_field
{
	const char *name;                   ///< Name of the field
	size_t offset;                      ///< Offset in the structure
	enum ispect_type type;              ///< Type of the field
	enum ispect_type subtype;           ///< STR_MAP subtype
	struct ispect_field *fields;        ///< REF target fields
	bool is_list;                       ///< REF target is a list
};
#define ISPECT(object, field, type)                                            \
	{ #field, offsetof (struct object, field), ISPECT_##type, 0, NULL, false },
#define ISPECT_REF(object, field, is_list, ref_type)                           \
	{ #field, offsetof (struct object, field), ISPECT_REF, 0,                  \
	  g_##ref_type##_ispect, is_list },
#define ISPECT_MAP(object, field, subtype)                                     \
	{ #field, offsetof (struct object, field), ISPECT_STR_MAP,                 \
	  ISPECT_##subtype, NULL, false },
#define ISPECT_MAP_REF(object, field, is_list, ref_type)                       \
	{ #field, offsetof (struct object, field), ISPECT_STR_MAP,                 \
	  ISPECT_REF, g_##ref_type##_ispect, is_list },
// ~~~ Chat ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
struct user_channel
{
	LIST_HEADER (struct user_channel)
	struct channel *channel;            ///< Reference to channel
};
static struct user_channel *
user_channel_new (struct channel *channel)
{
	struct user_channel *self = xcalloc (1, sizeof *self);
	self->channel = channel;
	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
	bool away;                          ///< User is away
	struct user_channel *channels;      ///< Channels the user is on (with us)
};
static struct ispect_field g_user_ispect[] =
{
	ISPECT( user, nickname, STRING )
	ISPECT( user, away,     BOOL   )
	{}
};
static struct user *
user_new (char *nickname)
{
	struct user *self = xcalloc (1, sizeof *self);
	self->ref_count = 1;
	self->nickname = nickname;
	return self;
}
static void
user_destroy (struct user *self)
{
	free (self->nickname);
	LIST_FOR_EACH (struct user_channel, iter, self->channels)
		user_channel_destroy (iter);
	free (self);
}
REF_COUNTABLE_METHODS (user)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct channel_user
{
	LIST_HEADER (struct channel_user)
	struct user *user;                  ///< Reference to user
	char *prefixes;                     ///< Ordered @+... characters
};
static struct channel_user *
channel_user_new (struct user *user, const char *prefixes)
{
	struct channel_user *self = xcalloc (1, sizeof *self);
	self->user = user;
	self->prefixes = xstrdup (prefixes);
	return self;
}
static void
channel_user_destroy (struct channel_user *self)
{
	user_unref (self->user);
	free (self->prefixes);
	free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// We keep references to channels in their buffers,
// and weak references in their users and the name lookup table.
struct channel
{
	REF_COUNTABLE_HEADER
	struct server *s;                   ///< Server
	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 strv names_buf;              ///< Buffer for RPL_NAMREPLY
	size_t users_len;                   ///< User count
	bool left_manually;                 ///< Don't rejoin on reconnect
	bool show_names_after_who;          ///< RPL_ENDOFWHO delays RPL_ENDOFNAMES
};
static struct ispect_field g_channel_ispect[] =
{
	ISPECT( channel, name,           STRING  )
	ISPECT( channel, topic,          STRING  )
	ISPECT( channel, no_param_modes, STR     )
	ISPECT_MAP( channel, param_modes, STRING )
	ISPECT( channel, users_len,      SIZE    )
	ISPECT( channel, left_manually,  BOOL    )
	{}
};
static struct channel *
channel_new (struct server *s, char *name)
{
	struct channel *self = xcalloc (1, sizeof *self);
	self->ref_count = 1;
	self->s = s;
	self->name = name;
	self->no_param_modes = str_make ();
	self->param_modes = str_map_make (free);
	self->names_buf = strv_make ();
	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 && !self->users_len);
	strv_free (&self->names_buf);
	free (self);
}
REF_COUNTABLE_METHODS (channel)
// ~~~ Attribute utilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enum
{
	TEXT_BOLD        = 1 << 0,
	TEXT_ITALIC      = 1 << 1,
	TEXT_UNDERLINE   = 1 << 2,
	TEXT_INVERSE     = 1 << 3,
	TEXT_BLINK       = 1 << 4,
	TEXT_CROSSED_OUT = 1 << 5,
	TEXT_MONOSPACE   = 1 << 6
};
// Similar to code in liberty-tui.c.
struct attrs
{
	short fg;                           ///< Foreground (256-colour cube or -1)
	short bg;                           ///< Background (256-colour cube or -1)
	unsigned attrs;                     ///< TEXT_* mask
};
/// Decode attributes in the value using a subset of the git config format,
/// ignoring all errors since it doesn't affect functionality
static struct attrs
attrs_decode (const char *value)
{
	struct strv v = strv_make ();
	cstr_split (value, " ", true, &v);
	int colors = 0;
	struct attrs attrs = { -1, -1, 0 };
	for (char **it = v.vector; *it; it++)
	{
		char *end = NULL;
		long n = strtol (*it, &end, 10);
		if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX)
		{
			if (colors == 0) attrs.fg = n;
			if (colors == 1) attrs.bg = n;
			colors++;
		}
		else if (!strcmp (*it, "bold"))    attrs.attrs |= TEXT_BOLD;
		else if (!strcmp (*it, "italic"))  attrs.attrs |= TEXT_ITALIC;
		else if (!strcmp (*it, "ul"))      attrs.attrs |= TEXT_UNDERLINE;
		else if (!strcmp (*it, "reverse")) attrs.attrs |= TEXT_INVERSE;
		else if (!strcmp (*it, "blink"))   attrs.attrs |= TEXT_BLINK;
		else if (!strcmp (*it, "strike"))  attrs.attrs |= TEXT_CROSSED_OUT;
	}
	strv_free (&v);
	return attrs;
}
// ~~~ Buffers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
enum formatter_item_type
{
	FORMATTER_ITEM_END,                 ///< Sentinel value for arrays
	FORMATTER_ITEM_TEXT,                ///< Text
	FORMATTER_ITEM_ATTR,                ///< Formatting attributes
	FORMATTER_ITEM_FG_COLOR,            ///< Foreground colour
	FORMATTER_ITEM_BG_COLOR,            ///< Background colour
	FORMATTER_ITEM_SIMPLE,              ///< Toggle IRC formatting
	FORMATTER_ITEM_IGNORE_ATTR          ///< Un/set attribute ignoration
};
struct formatter_item
{
	enum formatter_item_type type : 16; ///< Type of this item
	int attribute                 : 16; ///< Attribute ID or a TEXT_* mask
	int color;                          ///< Colour ([256 << 16] | 16)
	char *text;                         ///< String
};
static void
formatter_item_free (struct formatter_item *self)
{
	free (self->text);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct formatter
{
	struct app_context *ctx;            ///< Application context
	struct server *s;                   ///< Server
	bool clean;                         ///< Assume ATTR_RESET
	struct formatter_item *items;       ///< Items
	size_t items_len;                   ///< Items used
	size_t items_alloc;                 ///< Items allocated
};
static struct formatter
formatter_make (struct app_context *ctx, struct server *s)
{
	struct formatter self = { .ctx = ctx, .s = s, .clean = true };
	self.items = xcalloc (sizeof *self.items, (self.items_alloc = 16));
	return self;
}
static void
formatter_free (struct formatter *self)
{
	for (size_t i = 0; i < self->items_len; i++)
		formatter_item_free (&self->items[i]);
	free (self->items);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum buffer_line_flags
{
	BUFFER_LINE_SKIP_FILE   = 1 << 0,   ///< Don't log this to file
	BUFFER_LINE_UNIMPORTANT = 1 << 1,   ///< Joins, parts, similar spam
	BUFFER_LINE_HIGHLIGHT   = 1 << 2,   ///< The user was highlighted by this
};
// NOTE: This sequence must match up with xC-proto, only one lower.
enum buffer_line_rendition
{
	BUFFER_LINE_BARE,                   ///< Unadorned
	BUFFER_LINE_INDENT,                 ///< Just indent the line
	BUFFER_LINE_STATUS,                 ///< Status message
	BUFFER_LINE_ERROR,                  ///< Error message
	BUFFER_LINE_JOIN,                   ///< Join arrow
	BUFFER_LINE_PART,                   ///< Part arrow
	BUFFER_LINE_ACTION,                 ///< Highlighted asterisk
};
struct buffer_line
{
	LIST_HEADER (struct buffer_line)
	unsigned flags;                     ///< Functional flags
	enum buffer_line_rendition r;       ///< What the line should look like
	time_t when;                        ///< Time of the event
	struct formatter_item items[];      ///< Line data
};
/// Create a new buffer line stealing all data from the provided formatter
struct buffer_line *
buffer_line_new (struct formatter *f)
{
	// We make space for one more item that gets initialized to all zeros,
	// meaning FORMATTER_ITEM_END (because it's the first value in the enum)
	size_t items_size = f->items_len * sizeof *f->items;
	struct buffer_line *self =
		xcalloc (1, sizeof *self + items_size + sizeof *self->items);
	memcpy (self->items, f->items, items_size);
	// We've stolen pointers from the formatter, let's destroy it altogether
	free (f->items);
	memset (f, 0, sizeof *f);
	return self;
}
static void
buffer_line_destroy (struct buffer_line *self)
{
	for (struct formatter_item *iter = self->items; iter->type; iter++)
		formatter_item_free (iter);
	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)
	REF_COUNTABLE_HEADER
	enum buffer_type type;              ///< Type of the buffer
	char *name;                         ///< The name of the buffer
	struct input *input;                ///< API for "input_data"
	input_buffer_t 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 new_messages_count;        ///< # messages since last left
	unsigned new_unimportant_count;     ///< How much of that is unimportant
	bool highlighted;                   ///< We've been highlighted
	bool hide_unimportant;              ///< Hide unimportant messages
	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 ispect_field g_server_ispect[];
static struct ispect_field g_buffer_ispect[] =
{
	ISPECT( buffer, name,                  STRING )
	ISPECT( buffer, new_messages_count,    UINT   )
	ISPECT( buffer, new_unimportant_count, UINT   )
	ISPECT( buffer, highlighted,           BOOL   )
	ISPECT( buffer, hide_unimportant,      BOOL   )
	ISPECT_REF( buffer, server,  false, server    )
	ISPECT_REF( buffer, channel, false, channel   )
	ISPECT_REF( buffer, user,    false, user      )
	{}
};
static struct buffer *
buffer_new (struct input *input, enum buffer_type type, char *name)
{
	struct buffer *self = xcalloc (1, sizeof *self);
	self->ref_count = 1;
	self->input = input;
	self->input_data = CALL (input, buffer_new);
	self->type = type;
	self->name = name;
	return self;
}
static void
buffer_destroy (struct buffer *self)
{
	free (self->name);
	if (self->input_data)
	{
#ifdef HAVE_READLINE
		// FIXME: can't really free "history" contents from here, as we cannot
		//   be sure that the user interface pointer is valid and usable
		input_rl__buffer_destroy_wo_history (self->input_data);
#else // ! HAVE_READLINE
		CALL_ (self->input, buffer_destroy, self->input_data);
#endif // ! HAVE_READLINE
	}
	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);
}
REF_COUNTABLE_METHODS (buffer)
#define buffer_ref do_not_use_dangerous
// ~~~ Relay ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
struct client
{
	LIST_HEADER (struct client)
	struct app_context *ctx;            ///< Application context
	// TODO: Convert this all to TLS, and only TLS, with required client cert.
	//   That means replacing plumbing functions with the /other/ set from xD.
	int socket_fd;                      ///< The TCP socket
	struct str read_buffer;             ///< Unprocessed input
	struct str write_buffer;            ///< Output yet to be sent out
	uint32_t event_seq;                 ///< Outgoing message counter
	bool initialized;                   ///< Initial sync took place
	struct poller_fd socket_event;      ///< The socket can be read/written to
};
static struct client *
client_new (void)
{
	struct client *self = xcalloc (1, sizeof *self);
	self->socket_fd = -1;
	self->read_buffer = str_make ();
	self->write_buffer = str_make ();
	return self;
}
static void
client_destroy (struct client *self)
{
	if (!soft_assert (self->socket_fd == -1))
		xclose (self->socket_fd);
	str_free (&self->read_buffer);
	str_free (&self->write_buffer);
	free (self);
}
static void client_kill (struct client *c);
// ~~~ Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// The only real purpose of this is to abstract away TLS
struct transport
{
	/// Initialize the transport
	bool (*init) (struct server *s, const char *hostname, struct error **e);
	/// Destroy the user data pointer
	void (*cleanup) (struct server *s);
	/// The underlying socket may have become readable, update `read_buffer'
	enum socket_io_result (*try_read) (struct server *s);
	/// The underlying socket may have become writeable, flush `write_buffer'
	enum socket_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
{
	REF_COUNTABLE_HEADER
	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
	struct socks_connector *socks_conn; ///< SOCKS connection establisher
	unsigned reconnect_attempt;         ///< Number of reconnect attempt
	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
	struct poller_timer autojoin_tmr;   ///< Re/join channels as appropriate
	// 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_modes;          ///< Our current user modes
	char *irc_user_host;                ///< Our current user@host
	bool autoaway_active;               ///< Autoaway is currently active
	struct strv outstanding_joins;      ///< JOINs we expect a response to
	struct strv cap_ls_buf;             ///< Buffer for IRCv3.2 CAP LS
	bool cap_echo_message;              ///< Whether the server echoes messages
	bool cap_away_notify;               ///< Whether we get AWAY notifications
	bool cap_sasl;                      ///< Whether SASL is available
	// 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_extban_prefix;             ///< EXTBAN prefix or \0
	char *irc_extban_types;             ///< EXTBAN types
	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 struct ispect_field g_server_ispect[] =
{
	ISPECT( server, name,              STRING )
	ISPECT( server, state,             INT    )
	ISPECT( server, reconnect_attempt, UINT   )
	ISPECT( server, manual_disconnect, BOOL   )
	ISPECT( server, irc_user_host,     STRING )
	ISPECT( server, autoaway_active,   BOOL   )
	ISPECT( server, cap_echo_message,  BOOL   )
	ISPECT_REF( server, buffer, false, buffer )
	// TODO: either rename the underlying field or fix the plugins
	{ "user",      offsetof (struct server, irc_user),
	  ISPECT_REF, 0, g_user_ispect, false },
	{ "user_mode", offsetof (struct server, irc_user_modes),
	  ISPECT_STR, 0, NULL, false },
	{}
};
static void on_irc_timeout (void *user_data);
static void on_irc_ping_timeout (void *user_data);
static void on_irc_autojoin_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_extban_prefix             = 0;
	self->irc_extban_types              = 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_extban_types);
	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 struct server *
server_new (struct poller *poller)
{
	struct server *self = xcalloc (1, sizeof *self);
	self->ref_count = 1;
	self->socket = -1;
	self->read_buffer = str_make ();
	self->write_buffer = str_make ();
	self->state = IRC_DISCONNECTED;
	self->timeout_tmr = poller_timer_make (poller);
	self->timeout_tmr.dispatcher = on_irc_timeout;
	self->timeout_tmr.user_data = self;
	self->ping_tmr = poller_timer_make (poller);
	self->ping_tmr.dispatcher = on_irc_ping_timeout;
	self->ping_tmr.user_data = self;
	self->reconnect_tmr = poller_timer_make (poller);
	self->reconnect_tmr.dispatcher = (poller_timer_fn) irc_initiate_connect;
	self->reconnect_tmr.user_data = self;
	self->autojoin_tmr = poller_timer_make (poller);
	self->autojoin_tmr.dispatcher = on_irc_autojoin_timeout;
	self->autojoin_tmr.user_data = self;
	self->irc_users = str_map_make (NULL);
	self->irc_users.key_xfrm = irc_strxfrm;
	self->irc_channels = str_map_make (NULL);
	self->irc_channels.key_xfrm = irc_strxfrm;
	self->irc_buffer_map = str_map_make (NULL);
	self->irc_buffer_map.key_xfrm = irc_strxfrm;
	self->irc_user_modes = str_make ();
	self->outstanding_joins = strv_make ();
	self->cap_ls_buf = strv_make ();
	server_init_specifics (self);
	return self;
}
static void
server_destroy (struct server *self)
{
	free (self->name);
	if (self->connector)
	{
		connector_free (self->connector);
		free (self->connector);
	}
	if (self->socks_conn)
	{
		socks_connector_free (self->socks_conn);
		free (self->socks_conn);
	}
	if (self->transport
	 && self->transport->cleanup)
		self->transport->cleanup (self);
	if (self->socket != -1)
	{
		poller_fd_reset (&self->socket_event);
		xclose (self->socket);
	}
	str_free (&self->read_buffer);
	str_free (&self->write_buffer);
	poller_timer_reset (&self->ping_tmr);
	poller_timer_reset (&self->timeout_tmr);
	poller_timer_reset (&self->reconnect_tmr);
	poller_timer_reset (&self->autojoin_tmr);
	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_modes);
	free (self->irc_user_host);
	strv_free (&self->outstanding_joins);
	strv_free (&self->cap_ls_buf);
	server_free_specifics (self);
	free (self);
}
REF_COUNTABLE_METHODS (server)
#define server_ref do_not_use_dangerous
// ~~~ Scripting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
struct plugin
{
	LIST_HEADER (struct plugin)
	char *name;                         ///< Name of the plugin
	struct plugin_vtable *vtable;       ///< Methods
};
struct plugin_vtable
{
	/// Collect garbage
	void (*gc) (struct plugin *self);
	/// Unregister and free the plugin including all relevant resources
	void (*free) (struct plugin *self);
};
static void
plugin_destroy (struct plugin *self)
{
	self->vtable->free (self);
	free (self->name);
	free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// This is a bit ugly since insertion is O(n) and the need to get rid of the
// specific type because of list macros, however I don't currently posses any
// strictly better, ordered data structure
struct hook
{
	LIST_HEADER (struct hook)
	int priority;                       ///< The lesser the sooner
};
static struct hook *
hook_insert (struct hook *list, struct hook *item)
{
	// Corner cases: list is empty or we precede everything
	if (!list || item->priority < list->priority)
	{
		LIST_PREPEND (list, item);
		return list;
	}
	// Otherwise fast-forward to the last entry that precedes us
	struct hook *before = list;
	while (before->next && before->next->priority < item->priority)
		before = before->next;
	// And link ourselves in between it and its successor
	if ((item->next = before->next))
		item->next->prev = item;
	before->next = item;
	item->prev = before;
	return list;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct input_hook
{
	struct hook super;                  ///< Common hook fields
	/// Takes over the ownership of "input", returns either NULL if input
	/// was thrown away, or a possibly modified version of it
	char *(*filter) (struct input_hook *self,
		struct buffer *buffer, char *input);
};
struct irc_hook
{
	struct hook super;                  ///< Common hook fields
	/// Takes over the ownership of "message", returns either NULL if message
	/// was thrown away, or a possibly modified version of it
	char *(*filter) (struct irc_hook *self,
		struct server *server, char *message);
};
struct prompt_hook
{
	struct hook super;                  ///< Common hook fields
	/// Returns what the prompt should look like right now based on other state
	char *(*make) (struct prompt_hook *self);
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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
};
struct completion_hook
{
	struct hook super;                  ///< Common hook fields
	/// Tries to add possible completions of "word" to "output"
	void (*complete) (struct completion_hook *self,
		struct completion *data, const char *word, struct strv *output);
};
// ~~~ Main context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
struct app_context
{
	/// Default terminal attributes
	struct attrs theme_defaults[ATTR_COUNT];
	// Configuration:
	struct config config;               ///< Program configuration
	struct attrs theme[ATTR_COUNT];     ///< Terminal attributes
	bool isolate_buffers;               ///< Isolate global/server buffers
	bool beep_on_highlight;             ///< Beep on highlight
	bool logging;                       ///< Logging to file enabled
	bool show_all_prefixes;             ///< Show all prefixes before nicks
	bool word_wrapping;                 ///< Enable simple word wrapping
	struct str_map servers;             ///< Our servers
	// Relay:
	int relay_fd;                       ///< Listening socket FD
	struct client *clients;             ///< Our relay clients
	/// A single message buffer to prepare all outcoming messages within
	struct relay_event_message relay_message;
	// Events:
	struct poller_fd tty_event;         ///< Terminal input event
	struct poller_fd signal_event;      ///< Signal FD event
	struct poller_fd relay_event;       ///< New relay connection available
	struct poller_timer flush_timer;    ///< Flush all open files (e.g. logs)
	struct poller_timer date_chg_tmr;   ///< Print a date change
	struct poller_timer autoaway_tmr;   ///< Autoaway timer
	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
	struct str_map buffers_by_name;     ///< Buffers by name
	unsigned backlog_limit;             ///< Limit for buffer lines
	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
	struct input *input;                ///< User interface
	struct poller_idle prompt_event;    ///< Deferred prompt refresh
	struct poller_idle input_event;     ///< Pending input event
	struct strv pending_input;          ///< Pending input lines
	int *nick_palette;                  ///< A 256-colour palette for nicknames
	size_t nick_palette_len;            ///< Number of entries in nick_palette
	bool awaiting_formatting_escape;    ///< Awaiting an IRC formatting escape
	bool in_bracketed_paste;            ///< User is pasting some content
	struct str input_buffer;            ///< Buffered pasted content
	bool running_pager;                 ///< Running a pager for buffer history
	bool running_editor;                ///< Running editor for the input
	char *editor_filename;              ///< The file being edited by user
	int terminal_suspended;             ///< Terminal suspension level
	// Plugins:
	struct plugin *plugins;             ///< Loaded plugins
	struct hook *input_hooks;           ///< Input hooks
	struct hook *irc_hooks;             ///< IRC hooks
	struct hook *prompt_hooks;          ///< Prompt hooks
	struct hook *completion_hooks;      ///< Autocomplete hooks
}
*g_ctx;
static struct ispect_field g_ctx_ispect[] =
{
	ISPECT_MAP_REF( app_context, servers,    false, server )
	ISPECT_REF( app_context, buffers,        true,  buffer )
	ISPECT_REF( app_context, global_buffer,  false, buffer )
	ISPECT_REF( app_context, current_buffer, false, buffer )
	{}
};
static int *
filter_color_cube_for_acceptable_nick_colors (size_t *len)
{
	// This is a pure function and we don't use threads, static storage is fine
	static int table[6 * 6 * 6];
	size_t len_counter = 0;
	for (int x = 0; x < (int) N_ELEMENTS (table); x++)
	{
		int r =  x / 36;
		int g = (x / 6) % 6;
		int b = (x % 6);
		// The first step is 95/255, the rest are 40/255,
		// as an approximation we can double the first step
		double linear_R = pow ((r + !!r) / 6., 2.2);
		double linear_G = pow ((g + !!g) / 6., 2.2);
		double linear_B = pow ((b + !!b) / 6., 2.2);
		// Use the relative luminance of colours within the cube to filter
		// colours that look okay-ish on terminals with both black and white
		// backgrounds (use the test-nick-colors script to calibrate)
		double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B;
		if (Y >= .25 && Y <= .4)
			table[len_counter++] = 16 + x;
	}
	*len = len_counter;
	return table;
}
static bool
app_iconv_open (iconv_t *target, const char *to, const char *from)
{
	if (ICONV_ACCEPTS_TRANSLIT)
	{
		char *to_real = xstrdup_printf ("%s//TRANSLIT", to);
		*target = iconv_open (to_real, from);
		free (to_real);
	}
	else
		*target = iconv_open (to, from);
	return *target != (iconv_t) -1;
}
static void
app_context_init (struct app_context *self)
{
	memset (self, 0, sizeof *self);
	self->config = config_make ();
	poller_init (&self->poller);
	self->relay_fd = -1;
	self->servers = str_map_make ((str_map_free_fn) server_unref);
	self->servers.key_xfrm = tolower_ascii_strxfrm;
	self->buffers_by_name = str_map_make (NULL);
	self->buffers_by_name.key_xfrm = tolower_ascii_strxfrm;
	// So that we don't lose the logo shortly after startup
	self->backlog_limit = 1000;
	self->last_displayed_msg_time = time (NULL);
	char *native = nl_langinfo (CODESET);
	if (!app_iconv_open (&self->term_from_utf8, native, "UTF-8")
	 || !app_iconv_open (&self->term_to_utf8, "UTF-8", native))
		exit_fatal ("creating the UTF-8 conversion object failed: %s",
			strerror (errno));
	self->input = input_new ();
	self->input->user_data = self;
	self->pending_input = strv_make ();
	self->input_buffer = str_make ();
	self->nick_palette =
		filter_color_cube_for_acceptable_nick_colors (&self->nick_palette_len);
}
static void
app_context_relay_stop (struct app_context *self)
{
	if (self->relay_fd != -1)
	{
		poller_fd_reset (&self->relay_event);
		xclose (self->relay_fd);
		self->relay_fd = -1;
	}
}
static void
app_context_free (struct app_context *self)
{
	// Plugins can try to use of the other fields when destroyed
	LIST_FOR_EACH (struct plugin, iter, self->plugins)
		plugin_destroy (iter);
	config_free (&self->config);
	LIST_FOR_EACH (struct buffer, iter, self->buffers)
	{
#ifdef HAVE_READLINE
		// We can use the user interface here; see buffer_destroy()
		CALL_ (self->input, buffer_destroy, iter->input_data);
		iter->input_data = NULL;
#endif // HAVE_READLINE
		buffer_unref (iter);
	}
	str_map_free (&self->buffers_by_name);
	app_context_relay_stop (self);
	LIST_FOR_EACH (struct client, c, self->clients)
		client_kill (c);
	relay_event_message_free (&self->relay_message);
	str_map_free (&self->servers);
	poller_free (&self->poller);
	iconv_close (self->term_from_utf8);
	iconv_close (self->term_to_utf8);
	CALL (self->input, destroy);
	strv_free (&self->pending_input);
	str_free (&self->input_buffer);
	free (self->editor_filename);
}
static void
refresh_prompt (struct app_context *ctx)
{
	// XXX: the need for this conditional could probably be resolved
	//   by some clever reordering
	if (ctx->prompt_event.poller)
		poller_idle_set (&ctx->prompt_event);
}
// --- Configuration -----------------------------------------------------------
static void
on_config_debug_mode_change (struct config_item *item)
{
	g_debug_mode = item->value.boolean;
}
static void
on_config_show_all_prefixes_change (struct config_item *item)
{
	struct app_context *ctx = item->user_data;
	ctx->show_all_prefixes = item->value.boolean;
	refresh_prompt (ctx);
}
static void on_config_relay_bind_change (struct config_item *item);
static void on_config_backlog_limit_change (struct config_item *item);
static void on_config_theme_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)
TRIVIAL_BOOLEAN_ON_CHANGE (word_wrapping)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 (iscntrl_ascii (c))
		{
			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 },
	// XXX: if we add support for new capabilities, the value stays unchanged
	{ .name      = "capabilities",
	  .comment   = "Capabilities to use if supported by server",
	  .type      = CONFIG_ITEM_STRING_ARRAY,
	  .validate  = config_validate_nonjunk_string,
	  .default_  = "\"multi-prefix,invite-notify,server-time,echo-message,"
		"message-tags,away-notify,cap-notify,chghost\"" },
	{ .name      = "tls",
	  .comment   = "Whether to use TLS",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off" },
	{ .name      = "tls_cert",
	  .comment   = "Client TLS certificate (PEM)",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "tls_verify",
	  .comment   = "Whether to verify certificates",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on" },
	{ .name      = "tls_ca_file",
	  .comment   = "OpenSSL CA bundle file",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "tls_ca_path",
	  .comment   = "OpenSSL CA bundle path",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "tls_ciphers",
	  .comment   = "OpenSSL cipher preference list",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "\"DEFAULT:!MEDIUM:!LOW\"" },
	{ .name      = "autoconnect",
	  .comment   = "Connect automatically on startup",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on" },
	{ .name      = "autojoin",
	  .comment   = "Channels to join on start (e.g. \"#abc,#def key,#ghi\")",
	  .type      = CONFIG_ITEM_STRING_ARRAY,
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "command",
	  .comment   = "Command to execute after a successful connect",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "command_delay",
	  .comment   = "Delay between executing \"command\" and joining channels",
	  .type      = CONFIG_ITEM_INTEGER,
	  .validate  = config_validate_nonnegative,
	  .default_  = "0" },
	{ .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_general[] =
{
	{ .name      = "autosave",
	  .comment   = "Save configuration automatically after each change",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on" },
	{ .name      = "debug_mode",
	  .comment   = "Produce some debugging output",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off",
	  .on_change = on_config_debug_mode_change },
	{ .name      = "logging",
	  .comment   = "Log buffer contents to file",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off",
	  .on_change = on_config_logging_change },
	{ .name      = "plugin_autoload",
	  .comment   = "Plugins to automatically load on start",
	  .type      = CONFIG_ITEM_STRING_ARRAY,
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "relay_bind",
	  .comment   = "Address to bind to for a user interface relay point",
	  .type      = CONFIG_ITEM_STRING,
	  .validate  = config_validate_nonjunk_string,
	  .on_change = on_config_relay_bind_change },
	// Buffer history:
	{ .name      = "backlog_limit",
	  .comment   = "Maximum number of lines stored in the backlog",
	  .type      = CONFIG_ITEM_INTEGER,
	  .validate  = config_validate_nonnegative,
	  .default_  = "1000",
	  .on_change = on_config_backlog_limit_change },
	{ .name      = "pager",
	  .comment   = "Shell command to page buffer history (args: name [path])",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "`name=$(echo \"$1\" | sed 's/[%?:.]/\\\\&/g'); "
		"prompt='?f%F:'$name'. ?db- page %db?L of %D. .(?eEND:?PB%PB\\%..)'; "
		"LESSSECURE=1 less +Gb -Ps\"$prompt\" \"${2:--R}\"`" },
	{ .name      = "pager_strip_formatting",
	  .comment   = "Strip terminal formatting from pager input",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off" },
	// Output adjustments:
	{ .name      = "beep_on_highlight",
	  .comment   = "Ring the bell when highlighted or on a new invisible PM",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on",
	  .on_change = on_config_beep_on_highlight_change },
	{ .name      = "date_change_line",
	  .comment   = "Input to strftime(3) for the date change line",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "\"%F\"" },
	{ .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      = "read_marker_char",
	  .comment   = "The character to use for the read marker line",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "\"-\"",
	  .validate  = config_validate_nonjunk_string },
	{ .name      = "show_all_prefixes",
	  .comment   = "Show all prefixes in front of nicknames",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "off",
	  .on_change = on_config_show_all_prefixes_change },
	{ .name      = "word_wrapping",
	  .comment   = "Enable simple word wrapping in buffers",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on",
	  .on_change = on_config_word_wrapping_change },
	// User input:
	{ .name      = "editor",
	  .comment   = "VIM: \"vim +%Bgo %F\", Emacs: \"emacs -nw +%L:%C %F\", "
		"nano/micro/kakoune: \"nano/micro/kak +%L:%C %F\"",
	  .type      = CONFIG_ITEM_STRING },
	{ .name      = "process_pasted_text",
	  .comment   = "Normalize newlines and quote the command prefix in pastes",
	  .type      = CONFIG_ITEM_BOOLEAN,
	  .default_  = "on" },
	// Pan-server configuration:
	{ .name      = "autoaway_message",
	  .comment   = "Automated away message",
	  .type      = CONFIG_ITEM_STRING,
	  .default_  = "\"I'm not here right now\"" },
	{ .name      = "autoaway_delay",
	  .comment   = "Delay from the last keypress in seconds",
	  .type      = CONFIG_ITEM_INTEGER,
	  .validate  = config_validate_nonnegative,
	  .default_  = "1800" },
	{ .name      = "reconnect_delay_growing",
	  .comment   = "Growth factor for the reconnect delay",
	  .type      = CONFIG_ITEM_INTEGER,
	  .validate  = config_validate_nonnegative,
	  .default_  = "2" },
	{ .name      = "reconnect_delay_max",
	  .comment   = "Maximum reconnect delay in seconds",
	  .type      = CONFIG_ITEM_INTEGER,
	  .validate  = config_validate_nonnegative,
	  .default_  = "600" },
	{}
};
static struct config_schema g_config_theme[] =
{
#define XX(x, y, z) { .name = #y, .comment = #z, .type = CONFIG_ITEM_STRING, \
	.on_change = on_config_theme_change },
	ATTR_TABLE (XX)
#undef XX
	{}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_config_general (struct config_item *subtree, void *user_data)
{
	config_schema_apply_to_object (g_config_general, subtree, user_data);
}
static void
load_config_theme (struct config_item *subtree, void *user_data)
{
	config_schema_apply_to_object (g_config_theme, 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, "plugins", NULL, NULL);
	config_register_module (config, "general", load_config_general, ctx);
	config_register_module (config, "theme",   load_config_theme,   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 config_item *new_ = config_item_string_from_cstr (value);
	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 struct str_map *
get_servers_config (struct app_context *ctx)
{
	return &config_item_get (ctx->config.root, "servers", NULL)->value.object;
}
static struct str_map *
get_aliases_config (struct app_context *ctx)
{
	return &config_item_get (ctx->config.root, "aliases", NULL)->value.object;
}
static struct str_map *
get_plugins_config (struct app_context *ctx)
{
	return &config_item_get (ctx->config.root, "plugins", NULL)->value.object;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
serialize_configuration (struct config_item *root, 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 (root, true, output);
}
// --- Relay output ------------------------------------------------------------
static void
client_kill (struct client *c)
{
	struct app_context *ctx = c->ctx;
	poller_fd_reset (&c->socket_event);
	xclose (c->socket_fd);
	c->socket_fd = -1;
	LIST_UNLINK (ctx->clients, c);
	client_destroy (c);
}
static void
client_update_poller (struct client *c, const struct pollfd *pfd)
{
	int new_events = POLLIN;
	if (c->write_buffer.len)
		new_events |= POLLOUT;
	hard_assert (new_events != 0);
	if (!pfd || pfd->events != new_events)
		poller_fd_set (&c->socket_event, new_events);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
relay_send (struct client *c)
{
	struct relay_event_message *m = &c->ctx->relay_message;
	m->event_seq = c->event_seq++;
	// TODO: Also don't try sending anything if half-closed.
	if (!c->initialized || c->socket_fd == -1)
		return;
	// liberty has msg_{reader,writer} already, but they use 8-byte lengths.
	size_t frame_len_pos = c->write_buffer.len, frame_len = 0;
	str_pack_u32 (&c->write_buffer, 0);
	if (!relay_event_message_serialize (m, &c->write_buffer)
	 || (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX)
	{
		print_error ("serialization failed, killing client");
		client_kill (c);
		return;
	}
	uint32_t len = htonl (frame_len);
	memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len);
	client_update_poller (c, NULL);
}
static void
relay_broadcast (struct app_context *ctx)
{
	LIST_FOR_EACH (struct client, c, ctx->clients)
		relay_send (c);
}
static struct relay_event_message *
relay_prepare (struct app_context *ctx)
{
	struct relay_event_message *m = &ctx->relay_message;
	relay_event_message_free (m);
	memset (m, 0, sizeof *m);
	return m;
}
static void
relay_prepare_ping (struct app_context *ctx)
{
	relay_prepare (ctx)->data.event = RELAY_EVENT_PING;
}
static union relay_item_data *
relay_translate_formatter (struct app_context *ctx, union relay_item_data *p,
	const struct formatter_item *i)
{
	// XXX: See attr_printer_decode_color(), this is a footgun.
	int16_t c16  = i->color;
	int16_t c256 = i->color >> 16;
	unsigned attrs = i->attribute;
	switch (i->type)
	{
	case FORMATTER_ITEM_TEXT:
		p->text.text = str_from_cstr (i->text);
		(p++)->kind = RELAY_ITEM_TEXT;
		break;
	case FORMATTER_ITEM_FG_COLOR:
		p->fg_color.color = c256 <= 0 ? c16 : c256;
		(p++)->kind = RELAY_ITEM_FG_COLOR;
		break;
	case FORMATTER_ITEM_BG_COLOR:
		p->bg_color.color = c256 <= 0 ? c16 : c256;
		(p++)->kind = RELAY_ITEM_BG_COLOR;
		break;
	case FORMATTER_ITEM_ATTR:
		(p++)->kind = RELAY_ITEM_RESET;
		if ((c256 = ctx->theme[i->attribute].fg) >= 0)
		{
			p->fg_color.color = c256;
			(p++)->kind = RELAY_ITEM_FG_COLOR;
		}
		if ((c256 = ctx->theme[i->attribute].bg) >= 0)
		{
			p->bg_color.color = c256;
			(p++)->kind = RELAY_ITEM_BG_COLOR;
		}
		attrs = ctx->theme[i->attribute].attrs;
		// Fall-through
	case FORMATTER_ITEM_SIMPLE:
		if (attrs & TEXT_BOLD)
			(p++)->kind = RELAY_ITEM_FLIP_BOLD;
		if (attrs & TEXT_ITALIC)
			(p++)->kind = RELAY_ITEM_FLIP_ITALIC;
		if (attrs & TEXT_UNDERLINE)
			(p++)->kind = RELAY_ITEM_FLIP_UNDERLINE;
		if (attrs & TEXT_INVERSE)
			(p++)->kind = RELAY_ITEM_FLIP_INVERSE;
		if (attrs & TEXT_CROSSED_OUT)
			(p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT;
		if (attrs & TEXT_MONOSPACE)
			(p++)->kind = RELAY_ITEM_FLIP_MONOSPACE;
		break;
	default:
		break;
	}
	return p;
}
static union relay_item_data *
relay_items (struct app_context *ctx, const struct formatter_item *items,
	uint32_t *len)
{
	size_t items_len = 0;
	for (size_t i = 0; items[i].type; i++)
		items_len++;
	// Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
	union relay_item_data *a = xcalloc (items_len * 9, sizeof *a), *p = a;
	for (const struct formatter_item *i = items; items_len--; i++)
		p = relay_translate_formatter (ctx, p, i);
	*len = p - a;
	return a;
}
static void
relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer,
	struct buffer_line *line, bool leak_to_active)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_line *e = &m->data.buffer_line;
	e->event = RELAY_EVENT_BUFFER_LINE;
	e->buffer_name = str_from_cstr (buffer->name);
	e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT);
	e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT);
	e->rendition = 1 + line->r;
	e->when = line->when * 1000;
	e->leak_to_active = leak_to_active;
	e->items = relay_items (ctx, line->items, &e->items_len);
}
// TODO: Consider pushing this whole block of code much further down.
static void formatter_add (struct formatter *self, const char *format, ...);
static char *irc_to_utf8 (const char *text);
static void
relay_prepare_channel_buffer_update (struct app_context *ctx,
	struct buffer *buffer, struct relay_buffer_context_channel *e)
{
	struct channel *channel = buffer->channel;
	struct formatter f = formatter_make (ctx, buffer->server);
	if (channel->topic)
		formatter_add (&f, "#m", channel->topic);
	e->topic = relay_items (ctx, f.items, &e->topic_len);
	formatter_free (&f);
	// As in make_prompt(), conceal the last known channel modes.
	// XXX: This should use irc_channel_is_joined().
	if (!channel->users_len)
		return;
	struct str modes = str_make ();
	str_append_str (&modes, &channel->no_param_modes);
	struct str params = str_make ();
	struct str_map_iter iter = str_map_iter_make (&channel->param_modes);
	const char *param;
	while ((param = str_map_iter_next (&iter)))
	{
		str_append_c (&modes, iter.link->key[0]);
		str_append_c (¶ms, ' ');
		str_append (¶ms, param);
	}
	str_append_str (&modes, ¶ms);
	str_free (¶ms);
	char *modes_utf8 = irc_to_utf8 (modes.str);
	str_free (&modes);
	e->modes = str_from_cstr (modes_utf8);
	free (modes_utf8);
}
static void
relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_update *e = &m->data.buffer_update;
	e->event = RELAY_EVENT_BUFFER_UPDATE;
	e->buffer_name = str_from_cstr (buffer->name);
	e->hide_unimportant = buffer->hide_unimportant;
	struct str *server_name = NULL;
	switch (buffer->type)
	{
	case BUFFER_GLOBAL:
		e->context.kind = RELAY_BUFFER_KIND_GLOBAL;
		break;
	case BUFFER_SERVER:
		e->context.kind = RELAY_BUFFER_KIND_SERVER;
		server_name = &e->context.server.server_name;
		break;
	case BUFFER_CHANNEL:
		e->context.kind = RELAY_BUFFER_KIND_CHANNEL;
		server_name = &e->context.channel.server_name;
		relay_prepare_channel_buffer_update (ctx, buffer, &e->context.channel);
		break;
	case BUFFER_PM:
		e->context.kind = RELAY_BUFFER_KIND_PRIVATE_MESSAGE;
		server_name = &e->context.private_message.server_name;
		break;
	}
	if (server_name)
		*server_name = str_from_cstr (buffer->server->name);
}
static void
relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_stats *e = &m->data.buffer_stats;
	e->event = RELAY_EVENT_BUFFER_STATS;
	e->buffer_name = str_from_cstr (buffer->name);
	e->new_messages = MIN (UINT32_MAX,
		buffer->new_messages_count - buffer->new_unimportant_count);
	e->new_unimportant_messages = MIN (UINT32_MAX,
		buffer->new_unimportant_count);
	e->highlighted = buffer->highlighted;
}
static void
relay_prepare_buffer_rename (struct app_context *ctx, struct buffer *buffer,
	const char *new_name)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_rename *e = &m->data.buffer_rename;
	e->event = RELAY_EVENT_BUFFER_RENAME;
	e->buffer_name = str_from_cstr (buffer->name);
	e->new = str_from_cstr (new_name);
}
static void
relay_prepare_buffer_remove (struct app_context *ctx, struct buffer *buffer)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_remove *e = &m->data.buffer_remove;
	e->event = RELAY_EVENT_BUFFER_REMOVE;
	e->buffer_name = str_from_cstr (buffer->name);
}
static void
relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_activate *e = &m->data.buffer_activate;
	e->event = RELAY_EVENT_BUFFER_ACTIVATE;
	e->buffer_name = str_from_cstr (buffer->name);
}
static void
relay_prepare_buffer_input (struct app_context *ctx, struct buffer *buffer,
	const char *locale_input)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_input *e = &m->data.buffer_input;
	e->event = RELAY_EVENT_BUFFER_INPUT;
	e->buffer_name = str_from_cstr (buffer->name);
	char *input = iconv_xstrdup (ctx->term_to_utf8,
		(char *) locale_input, -1, NULL);
	e->text = str_from_cstr (input);
	free (input);
}
static void
relay_prepare_buffer_clear (struct app_context *ctx,
	struct buffer *buffer)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_buffer_clear *e = &m->data.buffer_clear;
	e->event = RELAY_EVENT_BUFFER_CLEAR;
	e->buffer_name = str_from_cstr (buffer->name);
}
enum relay_server_state
relay_server_state_for_server (struct server *s)
{
	switch (s->state)
	{
	case IRC_DISCONNECTED: return RELAY_SERVER_STATE_DISCONNECTED;
	case IRC_CONNECTING:   return RELAY_SERVER_STATE_CONNECTING;
	case IRC_CONNECTED:    return RELAY_SERVER_STATE_CONNECTED;
	case IRC_REGISTERED:   return RELAY_SERVER_STATE_REGISTERED;
	case IRC_CLOSING:
	case IRC_HALF_CLOSED:  return RELAY_SERVER_STATE_DISCONNECTING;
	}
	return 0;
}
static void
relay_prepare_server_update (struct app_context *ctx, struct server *s)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_server_update *e = &m->data.server_update;
	e->event = RELAY_EVENT_SERVER_UPDATE;
	e->server_name = str_from_cstr (s->name);
	e->data.state = relay_server_state_for_server (s);
	if (s->state == IRC_REGISTERED)
	{
		char *user_utf8 = irc_to_utf8 (s->irc_user->nickname);
		e->data.registered.user = str_from_cstr (user_utf8);
		free (user_utf8);
		char *user_modes_utf8 = irc_to_utf8 (s->irc_user_modes.str);
		e->data.registered.user_modes = str_from_cstr (user_modes_utf8);
		free (user_modes_utf8);
	}
}
static void
relay_prepare_server_rename (struct app_context *ctx, struct server *s,
	const char *new_name)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_server_rename *e = &m->data.server_rename;
	e->event = RELAY_EVENT_SERVER_RENAME;
	e->server_name = str_from_cstr (s->name);
	e->new = str_from_cstr (new_name);
}
static void
relay_prepare_server_remove (struct app_context *ctx, struct server *s)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_server_remove *e = &m->data.server_remove;
	e->event = RELAY_EVENT_SERVER_REMOVE;
	e->server_name = str_from_cstr (s->name);
}
static void
relay_prepare_error (struct app_context *ctx, uint32_t seq, const char *message)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_error *e = &m->data.error;
	e->event = RELAY_EVENT_ERROR;
	e->command_seq = seq;
	e->error = str_from_cstr (message);
}
static struct relay_event_data_response *
relay_prepare_response (struct app_context *ctx, uint32_t seq)
{
	struct relay_event_message *m = relay_prepare (ctx);
	struct relay_event_data_response *e = &m->data.response;
	e->event = RELAY_EVENT_RESPONSE;
	e->command_seq = seq;
	return e;
}
// --- Terminal output ---------------------------------------------------------
/// Default colour pair
#define COLOR_DEFAULT -1
/// Bright versions of the basic colour set
#define COLOR_BRIGHT(x) (COLOR_ ## x + 8)
/// Builds a colour pair for 256-colour terminals with a 16-colour backup value
#define COLOR_256(name, c256) \
	(((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16))
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;
}
// ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 colour pairs.
struct attr_printer
{
	struct attrs *attrs;                ///< Named attributes
	FILE *stream;                       ///< Output stream
	bool dirty;                         ///< Attributes are set
};
#define ATTR_PRINTER_INIT(attrs, stream) { attrs, stream, true }
static void
attr_printer_filtered_puts (FILE *stream, const char *attr)
{
	for (; *attr; attr++)
	{
		// sgr/set_attributes and sgr0/exit_attribute_mode like to enable or
		// disable the ACS with SO/SI (e.g. for TERM=screen), however `less -R`
		// does not skip over these characters and it screws up word wrapping
		if (*attr == 14 /* SO */ || *attr == 15 /* SI */)
			continue;
		// Trivially skip delay sequences intended to be processed by tputs()
		const char *end = NULL;
		if (attr[0] == '$' && attr[1] == '<' && (end = strchr (attr, '>')))
			attr = end;
		else
			fputc (*attr, stream);
	}
}
static void
attr_printer_tputs (struct attr_printer *self, const char *attr)
{
	terminal_printer_fn printer = get_attribute_printer (self->stream);
	if (printer)
		tputs (attr, 1, printer);
	else
		// We shouldn't really do this but we need it to output formatting
		// to the pager--it should be SGR-only
		attr_printer_filtered_puts (self->stream, attr);
}
static void
attr_printer_reset (struct attr_printer *self)
{
	if (self->dirty)
		attr_printer_tputs (self, exit_attribute_mode);
	self->dirty = false;
}
// NOTE: commonly terminals have:
//   8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK)
//   16 colours (okayish, we have the full basic range guaranteed)
//   88 colours (the same plus a 4^3 RGB cube and a few shades of grey)
//   256 colours (best, like above but with a larger cube and more grey)
/// Interpolate from the 256-colour palette to the 88-colour one
static int
attr_printer_256_to_88 (int color)
{
	// These colours are the same everywhere
	if (color < 16)
		return color;
	// 24 -> 8 extra shades of grey
	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
attr_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;
		}
		// Fall-through
	case 16:
		return c16;
	case 88:
		return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256);
	case 256:
		return c256 <= 0 ? c16 : c256;
	default:
		// Unsupported palette
		return -1;
	}
}
static void
attr_printer_apply (struct attr_printer *self,
	int text_attrs, int wanted_fg, int wanted_bg)
{
	bool fg_is_bright;
	int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright);
	bool bg_is_bright;
	int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright);
	bool have_inverse = !!(text_attrs & TEXT_INVERSE);
	if (have_inverse)
	{
		bool tmp = fg_is_bright;
		fg_is_bright = bg_is_bright;
		bg_is_bright = tmp;
	}
	// In 8 colour mode, some terminals don't support bright backgrounds.
	// However, we can make use of the fact that the brightness change caused
	// by the bold attribute is retained when inverting the colours.
	// This has the downside of making the text bold when it's not supposed
	// to be, and we still can't make both colours bright, so it's more of
	// an interesting hack rather than anything else.
	if (!fg_is_bright && bg_is_bright && have_inverse)
		text_attrs |= TEXT_BOLD;
	else if (!fg_is_bright && bg_is_bright
		&& !have_inverse && fg >= 0 && bg >= 0)
	{
		// As long as none of the colours is the default, we can swap them
		int tmp = fg; fg = bg; bg = tmp;
		text_attrs |= TEXT_BOLD | TEXT_INVERSE;
	}
	else
	{
		// This often works, however...
		if (fg_is_bright) text_attrs |= TEXT_BOLD;
		// this turns out to be annoying if implemented "correctly"
		if (bg_is_bright) text_attrs |= TEXT_BLINK;
	}
	attr_printer_reset (self);
	// TEXT_MONOSPACE is unimplemented, for obvious reasons
	if (text_attrs)
		attr_printer_tputs (self, tparm (set_attributes,
			0,   // standout
			text_attrs & TEXT_UNDERLINE,
			text_attrs & TEXT_INVERSE,
			text_attrs & TEXT_BLINK,
			0,   // dim
			text_attrs & TEXT_BOLD,
			0,   // blank
			0,   // protect
			0)); // acs
	if ((text_attrs & TEXT_ITALIC) && enter_italics_mode)
		attr_printer_tputs (self, enter_italics_mode);
	char *smxx = NULL;
	if ((text_attrs & TEXT_CROSSED_OUT)
	 && (smxx = tigetstr ("smxx")) && smxx != (char *) -1)
		attr_printer_tputs (self, smxx);
	if (fg >= 0)
		attr_printer_tputs (self, g_terminal.color_set_fg[fg]);
	if (bg >= 0)
		attr_printer_tputs (self, g_terminal.color_set_bg[bg]);
	self->dirty = true;
}
static void
attr_printer_apply_named (struct attr_printer *self, int attribute)
{
	attr_printer_reset (self);
	if (attribute == ATTR_RESET)
		return;
	// See the COLOR_256 macro or attr_printer_decode_color().
	struct attrs *a = &self->attrs[attribute];
	attr_printer_apply (self, a->attrs,
		a->fg < 16 ? a->fg : (a->fg << 16 | (-1 & 0xFFFF)),
		a->bg < 16 ? a->bg : (a->bg << 16 | (-1 & 0xFFFF)));
	self->dirty = true;
}
// ~~~ Logging redirect ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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;
	struct attr_printer state = ATTR_PRINTER_INIT (ctx->theme, stream);
	if (printer)
		attr_printer_apply_named (&state, attribute);
	vfprintf (stream, fmt, ap);
	if (printer)
		attr_printer_reset (&state);
}
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;
	CALL (ctx->input, hide);
	print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote);
	vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap);
	fputs ("\n", stream);
	CALL (ctx->input, show);
}
// ~~~ Theme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static ssize_t
attr_by_name (const char *name)
{
	static const char *table[ATTR_COUNT] =
	{
		NULL,
#define XX(x, y, z) [ATTR_ ## x] = #y,
		ATTR_TABLE (XX)
#undef XX
	};
	for (size_t i = 1; i < N_ELEMENTS (table); i++)
		if (!strcmp (name, table[i]))
			return i;
	return -1;
}
static void
on_config_theme_change (struct config_item *item)
{
	struct app_context *ctx = item->user_data;
	ssize_t id = attr_by_name (item->schema->name);
	if (id != -1)
	{
		// TODO: There should be a validator.
		ctx->theme[id] = item->type == CONFIG_ITEM_NULL
			? ctx->theme_defaults[id]
			: attrs_decode (item->value.string.str);
	}
}
static void
init_colors (struct app_context *ctx)
{
	bool have_ti = init_terminal ();
#define INIT_ATTR(id, ...) ctx->theme[ATTR_ ## id] = \
	ctx->theme_defaults[ATTR_ ## id] = (struct attrs) { __VA_ARGS__ }
	INIT_ATTR (PROMPT,      -1, -1, TEXT_BOLD);
	INIT_ATTR (RESET,       -1, -1, 0);
	INIT_ATTR (DATE_CHANGE, -1, -1, TEXT_BOLD);
	INIT_ATTR (READ_MARKER, COLOR_MAGENTA, -1, 0);
	INIT_ATTR (WARNING,     COLOR_YELLOW,  -1, 0);
	INIT_ATTR (ERROR,       COLOR_RED,     -1, 0);
	INIT_ATTR (EXTERNAL,    COLOR_WHITE,   -1, 0);
	INIT_ATTR (TIMESTAMP,   COLOR_WHITE,   -1, 0);
	INIT_ATTR (HIGHLIGHT,   COLOR_BRIGHT (YELLOW), COLOR_MAGENTA, TEXT_BOLD);
	INIT_ATTR (ACTION,      COLOR_RED,     -1, 0);
	INIT_ATTR (USERHOST,    COLOR_CYAN,    -1, 0);
	INIT_ATTR (JOIN,        COLOR_GREEN,   -1, 0);
	INIT_ATTR (PART,        COLOR_RED,     -1, 0);
#undef INIT_ATTR
	// This prevents formatters from obtaining an attribute printer function
	if (!have_ti)
	{
		g_terminal.stdout_is_tty = false;
		g_terminal.stderr_is_tty = false;
	}
	g_log_message_real = log_message_attributed;
}
// --- 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 cstr_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));
}
// Message targets can be prefixed by a character filtering their targets
static const char *
irc_skip_statusmsg (struct server *s, const char *target)
{
	return target + (*target && strchr (s->irc_statusmsg, *target));
}
static bool
irc_is_extban (struct server *s, const char *target)
{
	// Some servers have a prefix, and some support negation
	if (s->irc_extban_prefix && *target++ != s->irc_extban_prefix)
		return false;
	if (*target == '~')
		target++;
	// XXX: we don't know if it's supposed to have an argument, or not
	return *target && strchr (s->irc_extban_types, *target++)
		&& strchr (":\0", *target);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// As of 2020, 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 (const char *text)
{
	if (!text)
		return NULL;
	// XXX: the validation may be unnecessarily harsh, could do with a lenient
	//   first pass, then replace any errors with the replacement character
	size_t len = strlen (text) + 1;
	if (utf8_validate (text, len))
		return xstrdup (text);
	// Windows 1252 redefines several silly C1 control characters as glyphs
	static const char c1[32][4] =
	{
		"\xe2\x82\xac", "\xc2\x81",     "\xe2\x80\x9a", "\xc6\x92",
		"\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1",
		"\xcb\x86",     "\xe2\x80\xb0", "\xc5\xa0",     "\xe2\x80\xb9",
		"\xc5\x92",     "\xc2\x8d",     "\xc5\xbd",     "\xc2\x8f",
		"\xc2\x90",     "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c",
		"\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94",
		"\xcb\x9c",     "\xe2\x84\xa2", "\xc5\xa1",     "\xe2\x80\xba",
		"\xc5\x93",     "\xc2\x9d",     "\xc5\xbe",     "\xc5\xb8",
	};
	struct str s = str_make ();
	for (const char *p = text; *p; p++)
	{
		int c = *(unsigned char *) p;
		if (c < 0x80)
			str_append_c (&s, c);
		else if (c < 0xA0)
			str_append (&s, c1[c & 0x1f]);
		else
			str_append_data (&s,
				(char[]) {0xc0 | (c >> 6), 0x80 | (c & 0x3f)}, 2);
	}
	return str_steal (&s);
}
// --- 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
//   #l inserts a locale-encoded string
//
//   #S inserts a string from the server in an unknown encoding
//   #m inserts an IRC-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 colour
//   #C sets background colour
//
// Modifiers:
//    & free() the string argument after using it
static void
formatter_add_item (struct formatter *self, struct formatter_item template_)
{
	// Auto-resetting tends to create unnecessary items,
	// which also end up being relayed to frontends, so filter them out.
	bool reset =
		template_.type == FORMATTER_ITEM_ATTR &&
		template_.attribute == ATTR_RESET;
	if (self->clean && reset)
		return;
	self->clean = reset ||
		(self->clean && template_.type == FORMATTER_ITEM_TEXT);
	if (template_.text)
		template_.text = xstrdup (template_.text);
	if (self->items_len == self->items_alloc)
		self->items = xreallocarray
			(self->items, sizeof *self->items, (self->items_alloc <<= 1));
	self->items[self->items_len++] = template_;
}
#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_))
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 colour terminal palette, or the 256 colour 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),
};
// https://modern.ircdocs.horse/formatting.html
// http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html
static const int16_t g_extra_to_256[100 - 16] =
{
	 52,  94, 100,  58,  22,  29,  23,  24,  17,  54,  53,  89,
	 88, 130, 142,  64,  28,  35,  30,  25,  18,  91,  90, 125,
	124, 166, 184, 106,  34,  49,  37,  33,  19, 129, 127, 161,
	196, 208, 226, 154,  46,  86 , 51,  75,  21, 171, 201, 198,
	203, 215, 227, 191,  83, 122,  87, 111,  63, 177, 207, 205,
	217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
	 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,  -1
};
static const char *
irc_parse_mirc_color (const char *s, uint8_t *fg, uint8_t *bg)
{
	if (!isdigit_ascii (*s))
	{
		*fg = *bg = 99;
		return s;
	}
	*fg = *s++ - '0';
	if (isdigit_ascii (*s))
		*fg = *fg * 10 + (*s++ - '0');
	if (*s != ',' || !isdigit_ascii (s[1]))
		return s;
	s++;
	*bg = *s++ - '0';
	if (isdigit_ascii (*s))
		*bg = *bg * 10 + (*s++ - '0');
	return s;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct irc_char_attrs
{
	uint8_t fg, bg;                     ///< {Fore,back}ground colour or 99
	uint8_t attributes;                 ///< TEXT_* flags, except TEXT_BLINK
	uint8_t starts_at_boundary;         ///< Possible to split here?
};
static void
irc_serialize_char_attrs (const struct irc_char_attrs *attrs, struct str *out)
{
	soft_assert (attrs->fg < 100 && attrs->bg < 100);
	if (attrs->fg != 99 || attrs->bg != 99)
	{
		str_append_printf (out, "\x03%u", attrs->fg);
		if (attrs->bg != 99)
			str_append_printf (out, ",%02u", attrs->bg);
	}
	if (attrs->attributes & TEXT_BOLD)        str_append_c (out, '\x02');
	if (attrs->attributes & TEXT_ITALIC)      str_append_c (out, '\x1d');
	if (attrs->attributes & TEXT_UNDERLINE)   str_append_c (out, '\x1f');
	if (attrs->attributes & TEXT_INVERSE)     str_append_c (out, '\x16');
	if (attrs->attributes & TEXT_CROSSED_OUT) str_append_c (out, '\x1e');
	if (attrs->attributes & TEXT_MONOSPACE)   str_append_c (out, '\x11');
}
static int
irc_parse_attribute (char c)
{
	switch (c)
	{
	case '\x02' /* ^B */: return TEXT_BOLD;
	case '\x11' /* ^Q */: return TEXT_MONOSPACE;
	case '\x16' /* ^V */: return TEXT_INVERSE;
	case '\x1d' /* ^] */: return TEXT_ITALIC;
	case '\x1e' /* ^^ */: return TEXT_CROSSED_OUT;
	case '\x1f' /* ^_ */: return TEXT_UNDERLINE;
	case '\x0f' /* ^O */: return -1;
	}
	return 0;
}
// The text needs to be NUL-terminated, and a valid UTF-8 string
static struct irc_char_attrs *
irc_analyze_text (const char *text, size_t len)
{
	struct irc_char_attrs *attrs = xcalloc (len, sizeof *attrs),
		blank = { .fg = 99, .bg = 99, .starts_at_boundary = true },
		next = blank, cur = next;
	for (size_t i = 0; i != len; cur = next)
	{
		const char *start = text;
		hard_assert (utf8_decode (&text, len - i) >= 0);
		int attribute = irc_parse_attribute (*start);
		if (*start == '\x03')
			text = irc_parse_mirc_color (text, &next.fg, &next.bg);
		else if (attribute > 0)
			next.attributes ^= attribute;
		else if (attribute < 0)
			next = blank;
		while (start++ != text)
		{
			attrs[i++] = cur;
			cur.starts_at_boundary = false;
		}
	}
	return attrs;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *
formatter_parse_mirc_color (struct formatter *self, const char *s)
{
	uint8_t fg = 255, bg = 255;
	s = irc_parse_mirc_color (s, &fg, &bg);
	if (fg < 16)
		FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]);
	else if (fg < 100)
		FORMATTER_ADD_ITEM (self, FG_COLOR,
			.color = COLOR_256 (DEFAULT, g_extra_to_256[fg - 16]));
	if (bg < 16)
		FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]);
	else if (bg < 100)
		FORMATTER_ADD_ITEM (self, BG_COLOR,
			.color = COLOR_256 (DEFAULT, g_extra_to_256[bg - 16]));
	return s;
}
static void
formatter_parse_message (struct formatter *self, const char *s)
{
	FORMATTER_ADD_RESET (self);
	struct str buf = str_make ();
	unsigned char c;
	while ((c = *s++))
	{
		if (buf.len && c < 0x20)
		{
			FORMATTER_ADD_TEXT (self, buf.str);
			str_reset (&buf);
		}
		int attribute = irc_parse_attribute (c);
		if (c == '\x03')
			s = formatter_parse_mirc_color (self, s);
		else if (attribute > 0)
			FORMATTER_ADD_ITEM (self, SIMPLE, .attribute = attribute);
		else if (attribute < 0)
			FORMATTER_ADD_RESET (self);
		else
			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, const char *s)
{
	// For outgoing messages; maybe we should add a special #t for them
	// which would also make us not cut off the userhost part, ever
	if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s)))
	{
		char *tmp = irc_to_utf8 (s);
		FORMATTER_ADD_TEXT (self, tmp);
		free (tmp);
		return;
	}
	char *nick = irc_cut_nickname (s);
	int color = siphash_wrapper (nick, strlen (nick)) % 7;
	// Never use the black colour, could become transparent on black terminals;
	// white is similarly excluded from the range
	if (color == COLOR_BLACK)
		color = (uint16_t) -1;
	// Use a colour from the 256-colour cube if available
	color |= self->ctx->nick_palette[siphash_wrapper (nick,
		strlen (nick)) % self->ctx->nick_palette_len] << 16;
	// We always use the default colour for ourselves
	if (self->s && irc_is_this_us (self->s, nick))
		color = -1;
	FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color);
	char *x = irc_to_utf8 (nick);
	free (nick);
	FORMATTER_ADD_TEXT (self, x);
	free (x);
	// Need to reset the colour afterwards
	FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1);
}
static void
formatter_parse_nick_full (struct formatter *self, const 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 (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 'l':
		if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8,
			(s = va_arg (*ap, char *)), -1, NULL)))
			print_error ("character conversion failed for: %s", "output");
		else
			str_append (buf, tmp);
		free (tmp);
		break;
	case 'S':
		tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
		str_append (buf, tmp);
		free (tmp);
		break;
	case 'm':
		tmp = irc_to_utf8 ((s = va_arg (*ap, char *)));
		formatter_parse_message (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_make ();
	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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct line_char_attrs
{
	short named;                        ///< Named attribute or -1
	short text;                         ///< Text attributes
	int fg;                             ///< Foreground colour (-1 for default)
	int bg;                             ///< Background colour (-1 for default)
};
// We can get rid of the linked list and do this in one allocation (use strlen()
// for the upper bound)--since we only prepend and/or replace characters, add
// a member to specify the prepended character and how many times to repeat it.
// Tabs may nullify the wide character but it's not necessary.
//
// This would be slighly more optimal but it would also set the algorithm in
// stone and complicate flushing.
struct line_char
{
	LIST_HEADER (struct line_char)
	wchar_t wide;                       ///< The character as a wchar_t
	int width;                          ///< Width of the character in cells
	struct line_char_attrs attrs;       ///< Attributes
};
static struct line_char *
line_char_new (wchar_t wc)
{
	struct line_char *self = xcalloc (1, sizeof *self);
	self->width = wcwidth ((self->wide = wc));
	// Typically various control characters
	if (self->width < 0)
		self->width = 0;
	self->attrs.bg = self->attrs.fg = -1;
	self->attrs.named = ATTR_RESET;
	return self;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct line_wrap_mark
{
	struct line_char *start;            ///< First character
	int used;                           ///< Display cells used
};
static void
line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c)
{
	if (!mark->start)
		mark->start = c;
	mark->used += c->width;
}
struct line_wrap_state
{
	struct line_char *result;           ///< Head of result
	struct line_char *result_tail;      ///< Tail of result
	int line_used;                      ///< Line length before marks
	int line_max;                       ///< Maximum line length
	struct line_wrap_mark chunk;        ///< All buffered text
	struct line_wrap_mark overflow;     ///< Overflowing text
};
static void
line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before)
{
	struct line_char *nl = line_char_new (L'\n');
	LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start);
	s->line_used = before->used;
}
static void
line_wrap_flush (struct line_wrap_state *s, bool force_split)
{
	if (!s->overflow.start)
		s->line_used += s->chunk.used;
	else if (force_split || s->chunk.used > s->line_max)
	{
#ifdef WRAP_UNNECESSARILY
		// When the line wraps at the end of the screen and a background colour
		// is set, the terminal paints the entire new line with that colour.
		// Explicitly inserting a newline with the default attributes fixes it.
		line_wrap_flush_split (s, &s->overflow);
#else
		// Splitting here breaks link searching mechanisms in some terminals,
		// though, so we make a trade-off and let the chunk wrap naturally.
		// Fuck terminals, really.
		s->line_used = s->overflow.used;
#endif
	}
	else
		// Print the chunk in its entirety on a new line
		line_wrap_flush_split (s, &s->chunk);
	memset (&s->chunk,    0, sizeof s->chunk);
	memset (&s->overflow, 0, sizeof s->overflow);
}
static void
line_wrap_nl (struct line_wrap_state *s)
{
	line_wrap_flush (s, true);
	struct line_char *nl = line_char_new (L'\n');
	LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl);
	s->line_used = 0;
}
static void
line_wrap_tab (struct line_wrap_state *s, struct line_char *c)
{
	line_wrap_flush (s, true);
	if (s->line_used >= s->line_max)
		line_wrap_nl (s);
	// Compute the number of characters needed to get to the next tab stop
	int tab_width = ((s->line_used + 8) & ~7) - s->line_used;
	// On overflow just fill the rest of the line with spaces
	if (s->line_used + tab_width > s->line_max)
		tab_width = s->line_max - s->line_used;
	s->line_used += tab_width;
	while (tab_width--)
	{
		struct line_char *space = line_char_new (L' ');
		space->attrs = c->attrs;
		LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space);
	}
}
static void
line_wrap_push_char (struct line_wrap_state *s, struct line_char *c)
{
	// Note that when processing whitespace here, any non-WS chunk has already
	// been flushed, and thus it matters little if we flush with force split
	if (wcschr (L"\r\f\v", c->wide))
		/* Skip problematic characters */;
	else if (c->wide == L'\n')
		line_wrap_nl (s);
	else if (c->wide == L'\t')
		line_wrap_tab (s, c);
	else
		goto use_as_is;
	free (c);
	return;
use_as_is:
	if (s->overflow.start
	 || s->line_used + s->chunk.used + c->width > s->line_max)
	{
		if (s->overflow.used + c->width > s->line_max)
		{
#ifdef WRAP_UNNECESSARILY
			// If the overflow overflows, restart on a new line
			line_wrap_nl (s);
#else
			// See line_wrap_flush(), we would end up on a new line anyway
			line_wrap_flush (s, true);
			s->line_used = 0;
#endif
		}
		else
			line_wrap_mark_push (&s->overflow, c);
	}
	line_wrap_mark_push (&s->chunk, c);
	LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c);
}
/// Basic word wrapping that respects wcwidth(3) and expands tabs.
/// Besides making text easier to read, it also fixes the problem with
/// formatting spilling over the entire new line on line wrap.
static struct line_char *
line_wrap (struct line_char *line, int max_width)
{
	struct line_wrap_state s = { .line_max = max_width };
	bool last_was_word_char = false;
	LIST_FOR_EACH (struct line_char, c, line)
	{
		// Act on the right boundary of (\s*\S+) chunks
		bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide);
		if (last_was_word_char && !this_is_word_char)
			line_wrap_flush (&s, false);
		last_was_word_char = this_is_word_char;
		LIST_UNLINK (line, c);
		line_wrap_push_char (&s, c);
	}
	// Make sure to process the last word and return the modified list
	line_wrap_flush (&s, false);
	return s.result;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct exploder
{
	struct app_context *ctx;            ///< Application context
	struct line_char *result;           ///< Result
	struct line_char *result_tail;      ///< Tail of result
	struct line_char_attrs attrs;       ///< Current attributes
};
static bool
explode_formatter_attr (struct exploder *self, struct formatter_item *item)
{
	switch (item->type)
	{
	case FORMATTER_ITEM_ATTR:
		self->attrs.named = item->attribute;
		self->attrs.text = 0;
		self->attrs.fg = -1;
		self->attrs.bg = -1;
		return true;
	case FORMATTER_ITEM_SIMPLE:
		self->attrs.named = -1;
		self->attrs.text ^= item->attribute;
		return true;
	case FORMATTER_ITEM_FG_COLOR:
		self->attrs.named = -1;
		self->attrs.fg = item->color;
		return true;
	case FORMATTER_ITEM_BG_COLOR:
		self->attrs.named = -1;
		self->attrs.bg = item->color;
		return true;
	default:
		return false;
	}
}
static void
explode_text (struct exploder *self, const char *text)
{
	// Throw away any potentially harmful control characters first
	struct str filtered = str_make ();
	for (const char *p = text; *p; p++)
		if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p))
			str_append_c (&filtered, *p);
	size_t term_len = 0, processed = 0, len;
	char *term = iconv_xstrdup (self->ctx->term_from_utf8,
		filtered.str, filtered.len + 1, &term_len);
	str_free (&filtered);
	mbstate_t ps;
	memset (&ps, 0, sizeof ps);
	wchar_t wch;
	while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
	{
		hard_assert (len != (size_t) -2 && len != (size_t) -1);
		hard_assert ((processed += len) <= term_len);
		struct line_char *c = line_char_new (wch);
		c->attrs = self->attrs;
		LIST_APPEND_WITH_TAIL (self->result, self->result_tail, c);
	}
	free (term);
}
static struct line_char *
formatter_to_chars (struct formatter *formatter)
{
	struct exploder self = { .ctx = formatter->ctx };
	self.attrs.fg = self.attrs.bg = self.attrs.named = -1;
	int attribute_ignore = 0;
	for (size_t i = 0; i < formatter->items_len; i++)
	{
		struct formatter_item *iter = &formatter->items[i];
		if      (iter->type == FORMATTER_ITEM_TEXT)
			explode_text (&self, iter->text);
		else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR)
			attribute_ignore += iter->attribute;
		else if (attribute_ignore <= 0
			&& !explode_formatter_attr (&self, iter))
				hard_assert (!"unhandled formatter item type");
	}
	return self.result;
}
enum
{
	FLUSH_OPT_RAW    = (1 << 0),        ///< Print raw attributes
	FLUSH_OPT_NOWRAP = (1 << 1)         ///< Do not wrap
};
/// The input is a bunch of wide characters--respect shift state encodings
static void
formatter_putc (struct line_char *c, FILE *stream)
{
	static mbstate_t mb;
	char buf[MB_LEN_MAX] = {};
	size_t len = wcrtomb (buf, c ? c->wide : L'\0', &mb);
	if (len != (size_t) -1 && len)
		fwrite (buf, len - !c, 1, stream);
	free (c);
}
static void
formatter_flush (struct formatter *self, FILE *stream, int flush_opts)
{
	struct line_char *line = formatter_to_chars (self);
	bool is_tty = !!get_attribute_printer (stream);
	if (!is_tty && !(flush_opts & FLUSH_OPT_RAW))
	{
		LIST_FOR_EACH (struct line_char, c, line)
			formatter_putc (c, stream);
		formatter_putc (NULL, stream);
		return;
	}
	if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP))
		line = line_wrap (line, g_terminal.columns);
	struct attr_printer state = ATTR_PRINTER_INIT (self->ctx->theme, stream);
	struct line_char_attrs attrs = {};  // Won't compare equal to anything
	LIST_FOR_EACH (struct line_char, c, line)
	{
		if (attrs.fg    != c->attrs.fg
		 || attrs.bg    != c->attrs.bg
		 || attrs.named != c->attrs.named
		 || attrs.text  != c->attrs.text)
		{
			formatter_putc (NULL, stream);
			attrs = c->attrs;
			if (attrs.named == -1)
				attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg);
			else
				attr_printer_apply_named (&state, attrs.named);
		}
		formatter_putc (c, stream);
	}
	formatter_putc (NULL, stream);
	attr_printer_reset (&state);
}
// --- Buffers -----------------------------------------------------------------
static void
buffer_pop_excess_lines (struct app_context *ctx, struct buffer *self)
{
	int to_delete = (int) self->lines_count - (int) ctx->backlog_limit;
	while (to_delete-- > 0 && self->lines)
	{
		struct buffer_line *excess = self->lines;
		LIST_UNLINK_WITH_TAIL (self->lines, self->lines_tail, excess);
		buffer_line_destroy (excess);
		self->lines_count--;
	}
}
static void
on_config_backlog_limit_change (struct config_item *item)
{
	struct app_context *ctx = item->user_data;
	ctx->backlog_limit = MIN (item->value.integer, INT_MAX);
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
		buffer_pop_excess_lines (ctx, iter);
}
static void
buffer_update_time (struct app_context *ctx, time_t now, FILE *stream,
	int flush_opts)
{
	struct tm last, current;
	if (!localtime_r (&ctx->last_displayed_msg_time, &last)
	 || !localtime_r (&now, ¤t))
	{
		// 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[64] = "";
	const char *format =
		get_config_string (ctx->config.root, "general.date_change_line");
	if (!strftime (buf, sizeof buf, format, ¤t))
	{
		print_error ("%s: %s", "strftime", strerror (errno));
		return;
	}
	struct formatter f = formatter_make (ctx, NULL);
	formatter_add (&f, "#a#s\n", ATTR_DATE_CHANGE, buf);
	formatter_flush (&f, stream, flush_opts);
	// Flush the trailing formatting reset item
	fflush (stream);
	formatter_free (&f);
}
static void
buffer_line_flush (struct buffer_line *line, struct formatter *f, FILE *output,
	int flush_opts)
{
	switch (line->r)
	{
	case BUFFER_LINE_BARE:                               break;
	case BUFFER_LINE_INDENT:  formatter_add (f, "    "); break;
	case BUFFER_LINE_STATUS:  formatter_add (f, " -  "); break;
	case BUFFER_LINE_ERROR:   formatter_add (f, "#a=!=#r ", ATTR_ERROR);  break;
	case BUFFER_LINE_JOIN:    formatter_add (f, "#a-->#r ", ATTR_JOIN);   break;
	case BUFFER_LINE_PART:    formatter_add (f, "#a<--#r ", ATTR_PART);   break;
	case BUFFER_LINE_ACTION:  formatter_add (f, " #a*#r  ", ATTR_ACTION); break;
	}
	for (struct formatter_item *iter = line->items; iter->type; iter++)
		formatter_add_item (f, *iter);
	formatter_add (f, "\n");
	formatter_flush (f, output, flush_opts);
	formatter_free (f);
}
static void
buffer_line_write_time (struct formatter *f, struct buffer_line *line,
	FILE *stream, int flush_opts)
{
	// Normal timestamps don't include the date, make sure the user won't be
	// confused as to when an event has happened
	buffer_update_time (f->ctx, line->when, stream, flush_opts);
	struct tm current;
	char buf[9];
	if (!localtime_r (&line->when, ¤t))
		print_error ("%s: %s", "localtime_r", strerror (errno));
	else if (!strftime (buf, sizeof buf, "%T", ¤t))
		print_error ("%s: %s", "strftime", "buffer too small");
	else
		formatter_add (f, "#a#s#r ", ATTR_TIMESTAMP, buf);
}
#define buffer_line_will_show_up(buffer, line) \
	(!(buffer)->hide_unimportant || !((line)->flags & BUFFER_LINE_UNIMPORTANT))
static void
buffer_line_display (struct app_context *ctx,
	struct buffer *buffer, struct buffer_line *line, bool is_external)
{
	if (!buffer_line_will_show_up (buffer, line))
		return;
	CALL (ctx->input, hide);
	struct formatter f = formatter_make (ctx, NULL);
	buffer_line_write_time (&f, line, stdout, 0);
	// 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);
	}
	buffer_line_flush (line, &f, stdout, 0);
	// Flush the trailing formatting reset item
	fflush (stdout);
	CALL (ctx->input, show);
}
static void
buffer_line_write_to_backlog (struct app_context *ctx,
	struct buffer_line *line, FILE *log_file, int flush_opts)
{
	struct formatter f = formatter_make (ctx, NULL);
	buffer_line_write_time (&f, line, log_file, flush_opts);
	buffer_line_flush (line, &f, log_file, flush_opts);
}
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_make (ctx, NULL);
	struct tm current;
	char buf[20];
	if (!gmtime_r (&line->when, ¤t))
		print_error ("%s: %s", "gmtime_r", strerror (errno));
	else if (!strftime (buf, sizeof buf, "%F %T", ¤t))
		print_error ("%s: %s", "strftime", "buffer too small");
	else
		formatter_add (&f, "#s ", buf);
	// The target is not a terminal, thus it won't wrap in spite of the 0
	buffer_line_flush (line, &f, log_file, 0);
}
static void
log_formatter (struct app_context *ctx, struct buffer *buffer,
	unsigned flags, enum buffer_line_rendition r, struct formatter *f)
{
	if (!buffer)
		buffer = ctx->global_buffer;
	struct buffer_line *line = buffer_line_new (f);
	line->flags = flags;
	line->r = r;
	// TODO: allow providing custom time (IRCv3.2 server-time)
	line->when = time (NULL);
	buffer_pop_excess_lines (ctx, buffer);
	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);
	bool unseen_pm = buffer->type == BUFFER_PM
		&& buffer != ctx->current_buffer
		&& !(flags & BUFFER_LINE_UNIMPORTANT);
	bool important = (flags & BUFFER_LINE_HIGHLIGHT) || unseen_pm;
	if (ctx->beep_on_highlight && important)
		// XXX: this may disturb any other foreground process
		CALL (ctx->input, ding);
	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 = !ctx->isolate_buffers;
	bool leak_to_active = buffer != ctx->current_buffer && can_leak;
	relay_prepare_buffer_line (ctx, buffer, line, leak_to_active);
	relay_broadcast (ctx);
	bool visible = (buffer == ctx->current_buffer || leak_to_active)
		&& ctx->terminal_suspended <= 0;
	// Advance the unread marker but don't create a new one
	if (!visible || buffer->new_messages_count)
	{
		buffer->new_messages_count++;
		if ((flags & BUFFER_LINE_UNIMPORTANT) || leak_to_active)
			buffer->new_unimportant_count++;
	}
	if (visible)
		buffer_line_display (ctx, buffer, line, leak_to_active);
	else
	{
		buffer->highlighted |= important;
		refresh_prompt (ctx);
	}
}
static void
log_full (struct app_context *ctx, struct server *s, struct buffer *buffer,
	unsigned flags, enum buffer_line_rendition r, const char *format, ...)
{
	va_list ap;
	va_start (ap, format);
	struct formatter f = formatter_make (ctx, s);
	formatter_addv (&f, format, &ap);
	log_formatter (ctx, buffer, flags, r, &f);
	va_end (ap);
}
#define log_global(ctx, flags, r, ...)                                         \
	log_full ((ctx), NULL, (ctx)->global_buffer, (flags), (r), __VA_ARGS__)
#define log_server(s, buffer, flags, r, ...)                                   \
	log_full ((s)->ctx, (s), (buffer), (flags), (r), __VA_ARGS__)
#define log_global_status(ctx, ...)                                            \
	log_global ((ctx), 0, BUFFER_LINE_STATUS, __VA_ARGS__)
#define log_global_error(ctx, ...)                                             \
	log_global ((ctx), 0, BUFFER_LINE_ERROR,  __VA_ARGS__)
#define log_global_indent(ctx, ...)                                            \
	log_global ((ctx), 0, BUFFER_LINE_INDENT, __VA_ARGS__)
#define log_server_status(s, buffer, ...)                                      \
	log_server ((s), (buffer), 0, BUFFER_LINE_STATUS, __VA_ARGS__)
#define log_server_error(s, buffer, ...)                                       \
	log_server ((s), (buffer), 0, BUFFER_LINE_ERROR,  __VA_ARGS__)
#define log_global_debug(ctx, ...)                                             \
	BLOCK_START                                                                \
		if (g_debug_mode)                                                      \
			log_global ((ctx), 0, 0, "(*) " __VA_ARGS__);                      \
	BLOCK_END
#define log_server_debug(s, ...)                                               \
	BLOCK_START                                                                \
		if (g_debug_mode)                                                      \
			log_server ((s), (s)->buffer, 0, 0, "(*) " __VA_ARGS__);           \
	BLOCK_END
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Lines that are used in more than one place
#define log_nick_self(s, buffer, new_)                                         \
	log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS,    \
		"You are now known as #n", (new_))
#define log_nick(s, buffer, old, new_)                                         \
	log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS,    \
		"#n is now known as #n", (old), (new_))
#define log_chghost_self(s, buffer, new_)                                      \
	log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS,    \
		"You are now #N", (new_))
#define log_chghost(s, buffer, old, new_)                                      \
	log_server ((s), (buffer), BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS,    \
		"#N is now #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, 0, "<#s#n> #m", (prefixes), (who), (text))
#define log_outcoming_action(s, buffer, who, text)                             \
	log_server ((s), (buffer), 0, BUFFER_LINE_ACTION, "#n #m", (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 ((s), (s)->buffer, 0, BUFFER_LINE_STATUS,                       \
		"MSG(#n): #m", (target), (text))
#define log_outcoming_orphan_action(s, target, text)                           \
	log_server ((s), (s)->buffer, 0, BUFFER_LINE_ACTION,                       \
		"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 char *
buffer_get_log_path (struct buffer *buffer)
{
	struct str path = str_make ();
	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);
	str_append_c (&path, '/');
	// FIXME: This mixes up character encodings.
	make_log_filename (buffer->name, &path);
	str_append (&path, ".log");
	return str_steal (&path);
}
static void
buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
{
	if (!ctx->logging || buffer->log_file)
		return;
	// TODO: should we try to reopen files wrt. case mapping?
	//  - Need to read the whole directory and look for matches:
	//    irc_server_strcmp(buffer->s, d_name, make_log_filename())
	//    remember to strip the ".log" suffix from d_name, case-sensitively.
	//  - The tolower_ascii() in make_log_filename() is a perfect overlap,
	//    it may stay as-is.
	//  - buffer_get_log_path() will need to return a FILE *,
	//    or an error that includes the below message.
	char *path = buffer_get_log_path (buffer);
	if (!(buffer->log_file = fopen (path, "ab")))
		log_global_error (ctx, "Couldn't open log file `#l': #l",
			path, strerror (errno));
	else
		set_cloexec (fileno (buffer->log_file));
	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));
	relay_prepare_buffer_update (ctx, buffer);
	relay_broadcast (ctx);
	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);
	// Normally this doesn't cause changes in the prompt but a prompt hook
	// could decide to show some information for all buffers nonetheless
	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);
	relay_prepare_buffer_remove (ctx, buffer);
	relay_broadcast (ctx);
	CALL_ (ctx->input, buffer_destroy, 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;
	if (buffer->type == BUFFER_SERVER)
		buffer->server->buffer = NULL;
	str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
	LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
	buffer_unref (buffer);
	refresh_prompt (ctx);
}
static void
buffer_print_read_marker (struct app_context *ctx, FILE *stream, int flush_opts)
{
	struct formatter f = formatter_make (ctx, NULL);
	const int timestamp_width = 8;  // hardcoded to %T right now, simple
	const char *marker_char =
		get_config_string (ctx->config.root, "general.read_marker_char");
	// We could turn this off on FLUSH_OPT_NOWRAP, however our default pager
	// wraps lines for us even if we don't do it ourselves, and thus there's
	// no need to worry about inconsistency.
	if (*marker_char)
	{
		struct str s = str_make ();
		for (int i = 0; i < timestamp_width; i++)
			str_append (&s, marker_char);
		formatter_add (&f, "#a#s#r", ATTR_TIMESTAMP, s.str);
		str_reset (&s);
		for (int i = timestamp_width; i < g_terminal.columns; i++)
			str_append (&s, marker_char);
		formatter_add (&f, "#a#s#r\n", ATTR_READ_MARKER, s.str);
		str_free (&s);
	}
	else
		formatter_add (&f, "#a-- -- -- ---\n", ATTR_READ_MARKER);
	formatter_flush (&f, stream, flush_opts);
	// Flush the trailing formatting reset item
	fflush (stream);
	formatter_free (&f);
}
static void
buffer_print_backlog (struct app_context *ctx, struct buffer *buffer)
{
	// Buffers can be activated, or their lines modified, as automatic actions.
	if (ctx->terminal_suspended)
		return;
	// The prompt can take considerable time to redraw
	CALL (ctx->input, hide);
	// Simulate curses-like fullscreen buffers if the terminal allows it
	if (g_terminal.initialized && clear_screen)
	{
		terminal_printer_fn printer = get_attribute_printer (stdout);
		tputs (clear_screen, 1, printer);
		if (cursor_to_ll)
			tputs (cursor_to_ll, 1, printer);
		else if (row_address)
			tputs (tparm (row_address, g_terminal.lines - 1,
				0, 0, 0, 0, 0, 0, 0, 0), 1, printer);
		else if (cursor_address)
			tputs (tparm (cursor_address, g_terminal.lines - 1,
				0, 0, 0, 0, 0, 0, 0, 0), 1, printer);
		fflush (stdout);
		// We should update "last_displayed_msg_time" here just to be sure
		// that the first date marker, if necessary, is shown, but in practice
		// the value should always be from today when this function is called
	}
	else
	{
		char *buffer_name_localized =
			iconv_xstrdup (ctx->term_from_utf8, buffer->name, -1, NULL);
		print_status ("%s", buffer_name_localized);
		free (buffer_name_localized);
	}
	// That is, minus the readline prompt (taking at least one line)
	int display_limit = MAX (10, g_terminal.lines - 1);
	int to_display = 0;
	struct buffer_line *line;
	for (line = buffer->lines_tail; line; line = line->prev)
	{
		to_display++;
		if (buffer_line_will_show_up (buffer, line))
			display_limit--;
		if (!line->prev || display_limit <= 0)
			break;
	}
	// Once we've found where we want to start with the backlog, print it
	int until_marker = to_display - (int) buffer->new_messages_count;
	for (; line; line = line->next)
	{
		if (until_marker-- == 0
		 && buffer->new_messages_count != buffer->lines_count)
			buffer_print_read_marker (ctx, stdout, 0);
		buffer_line_display (ctx, buffer, line, 0);
	}
	// So that it is obvious if the last line in the buffer is not from today
	buffer_update_time (ctx, time (NULL), stdout, 0);
	refresh_prompt (ctx);
	CALL (ctx->input, show);
}
static void
buffer_activate (struct app_context *ctx, struct buffer *buffer)
{
	if (ctx->current_buffer == buffer)
		return;
	relay_prepare_buffer_activate (ctx, buffer);
	relay_broadcast (ctx);
	// This is the only place where the unread messages marker
	// and highlight indicator are reset
	if (ctx->current_buffer)
	{
		ctx->current_buffer->new_messages_count = 0;
		ctx->current_buffer->new_unimportant_count = 0;
		ctx->current_buffer->highlighted = false;
	}
	buffer_print_backlog (ctx, buffer);
	CALL_ (ctx->input, buffer_switch, 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, 0, 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;
	merged->lines_tail = start->prev;
	merged->lines_count -= n;
	// 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;
	// And since there is no log_*() call, send them to relays manually
	buffer->highlighted |= merged->highlighted;
	LIST_FOR_EACH (struct buffer_line, line, start)
	{
		if (buffer->new_messages_count)
		{
			buffer->new_messages_count++;
			if (line->flags & BUFFER_LINE_UNIMPORTANT)
				buffer->new_unimportant_count++;
		}
		relay_prepare_buffer_line (ctx, buffer, line, false);
		relay_broadcast (ctx);
	}
	log_full (ctx, NULL, buffer, BUFFER_LINE_SKIP_FILE, BUFFER_LINE_STATUS,
		"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);
	relay_prepare_buffer_rename (ctx, buffer, new_name);
	relay_broadcast (ctx);
	str_map_set (&ctx->buffers_by_name, buffer->name, NULL);
	str_map_set (&ctx->buffers_by_name, new_name, buffer);
	cstr_set (&buffer->name, xstrdup (new_name));
	buffer_close_log_file (buffer);
	buffer_open_log_file (ctx, buffer);
	// We might have renamed the current buffer
	refresh_prompt (ctx);
}
static void
buffer_clear (struct app_context *ctx, struct buffer *buffer)
{
	relay_prepare_buffer_clear (ctx, buffer);
	relay_broadcast (ctx);
	LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
		buffer_line_destroy (iter);
	buffer->lines = buffer->lines_tail = NULL;
	buffer->lines_count = 0;
}
static void
buffer_toggle_unimportant (struct app_context *ctx, struct buffer *buffer)
{
	buffer->hide_unimportant ^= true;
	relay_prepare_buffer_update (ctx, buffer);
	relay_broadcast (ctx);
	if (buffer == ctx->current_buffer)
		buffer_print_backlog (ctx, buffer);
}
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_count (struct app_context *ctx)
{
	int total = 0;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
		total++;
	return total;
}
static void
buffer_move (struct app_context *ctx, struct buffer *buffer, int n)
{
	hard_assert (n >= 1 && n <= buffer_count (ctx));
	LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer);
	struct buffer *following = ctx->buffers;
	while (--n && following)
		following = following->next;
	LIST_INSERT_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer, following);
	refresh_prompt (ctx);
}
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
buffer_remove_safe (struct app_context *ctx, struct buffer *buffer)
{
	if (buffer == ctx->current_buffer)
		buffer_activate (ctx, ctx->last_buffer
			? ctx->last_buffer
			: buffer_next (ctx, 1));
	buffer_remove (ctx, buffer);
}
static void
init_global_buffer (struct app_context *ctx)
{
	struct buffer *global = ctx->global_buffer =
		buffer_new (ctx->input, BUFFER_GLOBAL, xstrdup (PROGRAM_NAME));
	buffer_add (ctx, global);
	buffer_activate (ctx, global);
}
// --- Users, channels ---------------------------------------------------------
static char *
irc_make_buffer_name (struct server *s, const char *target)
{
	if (!target)
		return xstrdup (s->name);
	char *target_utf8 = irc_to_utf8 (target);
	char *name = xstrdup_printf ("%s.%s", s->name, target_utf8);
	free (target_utf8);
	struct buffer *conflict = buffer_by_name (s->ctx, name);
	if (!conflict)
		return name;
	hard_assert (conflict->server == s);
	// Fix up any conflicts.  Note that while parentheses aren't allowed
	// in IRC nicknames, they may occur in channel names.
	int i = 0;
	char *unique = xstrdup_printf ("%s(%d)", name, ++i);
	while (buffer_by_name (s->ctx, unique))
		cstr_set (&unique, xstrdup_printf ("%s(%d)", name, ++i));
	free (name);
	return unique;
}
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 (nickname);
	(void) user_weak_ref (user, irc_user_on_destroy, s);
	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 (s->ctx->input,
		BUFFER_PM, irc_make_buffer_name (s, nickname));
	buffer->server = s;
	buffer->user = user;
	str_map_set (&s->irc_buffer_map, user->nickname, buffer);
	buffer_add (s->ctx, buffer);
	return buffer;
}
static void
irc_get_channel_user_prefix (struct server *s,
	struct channel_user *channel_user, struct str *output)
{
	if (s->ctx->show_all_prefixes)
		str_append (output, channel_user->prefixes);
	else if (channel_user->prefixes[0])
		str_append_c (output, channel_user->prefixes[0]);
}
static bool
irc_channel_is_joined (struct channel *channel)
{
	// TODO: find a better way of checking if we're on a channel
	return !!channel->users_len;
}
// 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 (channel);
	LIST_PREPEND (user->channels, user_channel);
	struct channel_user *channel_user = channel_user_new (user, prefixes);
	LIST_PREPEND (channel->users, channel_user);
	channel->users_len++;
}
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);
		}
	// TODO: poll the away status for users we don't share a channel with.
	//   It might or might not be worth to auto-set this on with RPL_AWAY.
	if (!user->channels && user != channel->s->irc_user)
		user->away = false;
	// Then just unlink the user from the channel
	LIST_UNLINK (channel->users, channel_user);
	channel_user_destroy (channel_user);
	channel->users_len--;
}
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 (s, name);
	(void) channel_weak_ref (channel, irc_channel_on_destroy, s);
	str_map_set (&s->irc_channels, channel->name, channel);
	return channel;
}
static void
irc_channel_broadcast_buffer_update (const struct channel *channel)
{
	struct server *s = channel->s;
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
	if (buffer)
	{
		relay_prepare_buffer_update (s->ctx, buffer);
		relay_broadcast (s->ctx);
	}
}
static void
irc_channel_set_topic (struct channel *channel, const char *topic)
{
	cstr_set (&channel->topic, xstrdup (topic));
	irc_channel_broadcast_buffer_update (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)
{
	strv_reset (&channel->names_buf);
	channel->show_names_after_who = false;
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
		irc_channel_unlink_user (channel, iter);
	// Send empty channel modes.
	irc_channel_broadcast_buffer_update (channel);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 channel *channel, struct buffer *buffer)
{
	struct server *s = channel->s;
	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;
	s->irc_users      = str_map_make (NULL);
	s->irc_channels   = str_map_make (NULL);
	s->irc_buffer_map = str_map_make (NULL);
	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;
	iter = str_map_iter_make (&old_users);
	while ((user = str_map_iter_next (&iter)))
		irc_try_readd_user (s, user,
			str_map_find (&old_buffer_map, user->nickname));
	iter = str_map_iter_make (&old_channels);
	while ((channel = str_map_iter_next (&iter)))
		irc_try_readd_channel (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);
	poller_timer_reset (&s->autojoin_tmr);
}
static void
irc_reset_connection_timeouts (struct server *s)
{
	poller_timer_set (&s->timeout_tmr, 3 * 60 * 1000);
	poller_timer_set (&s->ping_tmr, (3 * 60 + 30) * 1000);
	poller_timer_reset (&s->reconnect_tmr);
}
static int64_t
irc_get_reconnect_delay (struct server *s)
{
	int64_t delay = get_config_integer (s->config, "reconnect_delay");
	int64_t delay_factor = get_config_integer
		(s->ctx->config.root, "general.reconnect_delay_growing");
	for (unsigned i = 0; i < s->reconnect_attempt; i++)
	{
		if (delay_factor && delay > INT64_MAX / delay_factor)
			break;
		delay *= delay_factor;
	}
	int64_t delay_max =
		get_config_integer (s->ctx->config.root, "general.reconnect_delay_max");
	return MIN (delay, delay_max);
}
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;
	// XXX: maybe add a state for when a connect is queued?
	hard_assert (s->state == IRC_DISCONNECTED);
	int64_t delay = irc_get_reconnect_delay (s);
	s->reconnect_attempt++;
	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_process_sent_message
	(const struct irc_message *msg, struct server *s);
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_make ();
	str_append_vprintf (&str, format, ap);
	va_end (ap);
	log_server_debug (s, "#a<< \"#S\"#r", ATTR_PART, str.str);
	struct irc_message msg;
	irc_parse_message (&msg, str.str);
	irc_process_sent_message (&msg, s);
	irc_free_message (&msg);
	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_set_state (struct server *s, enum server_state state)
{
	s->state = state;
	relay_prepare_server_update (s->ctx, s);
	relay_broadcast (s->ctx);
	refresh_prompt (s->ctx);
}
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)
		// XXX: we get ENOTCONN with OpenSSL (not plain) when a localhost
		// server is aborted, why?  strace says read 0, write 31, shutdown -1.
		if (!soft_assert (errno == EINTR))
			break;
	irc_set_state (s, 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
	irc_set_state (s, 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)
{
	if (s->connector)
		connector_free (s->connector);
	free (s->connector);
	s->connector = NULL;
	if (s->socks_conn)
		socks_connector_free (s->socks_conn);
	free (s->socks_conn);
	s->socks_conn = NULL;
	// Not connecting anymore
	irc_set_state (s, IRC_DISCONNECTED);
}
static void
try_finish_quit (struct app_context *ctx)
{
	if (!ctx->quitting)
		return;
	bool disconnected_all = true;
	struct str_map_iter iter = str_map_iter_make (&ctx->servers);
	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
irc_destroy_transport (struct server *s)
{
	if (s->transport
	 && s->transport->cleanup)
		s->transport->cleanup (s);
	s->transport = NULL;
	poller_fd_reset (&s->socket_event);
	xclose (s->socket);
	s->socket = -1;
	irc_set_state (s, IRC_DISCONNECTED);
	str_reset (&s->read_buffer);
	str_reset (&s->write_buffer);
}
static void
irc_destroy_state (struct server *s)
{
	struct str_map_iter iter = str_map_iter_make (&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_modes);
	cstr_set (&s->irc_user_host, NULL);
	strv_reset (&s->outstanding_joins);
	strv_reset (&s->cap_ls_buf);
	s->cap_away_notify = false;
	s->cap_echo_message = false;
	s->cap_sasl = 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);
}
static void
irc_disconnect (struct server *s)
{
	hard_assert (irc_is_connected (s));
	struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
	struct buffer *buffer;
	while ((buffer = str_map_iter_next (&iter)))
		log_server (s, buffer, BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_STATUS,
			"Disconnected from server");
	irc_cancel_timers (s);
	irc_destroy_transport (s);
	irc_destroy_state (s);
	// Take any relevant actions
	if (s->ctx->quitting)
		try_finish_quit (s->ctx);
	else if (s->manual_disconnect)
		s->manual_disconnect = false;
	else
	{
		s->reconnect_attempt = 0;
		irc_queue_reconnect (s);
	}
}
static void
irc_initiate_disconnect (struct server *s, const char *reason)
{
	hard_assert (irc_is_connected (s));
	// It can take a very long time for sending QUIT to take effect
	if (s->manual_disconnect)
	{
		log_server_error (s, s->buffer, "#s: #s", "Disconnected from server",
			"connection torn down early per user request");
		irc_disconnect (s);
		return;
	}
	if (reason)
		irc_send (s, "QUIT :%s", reason);
	else
		// TODO: make the default QUIT message customizable
		//   -> global/per server/both?
		//   -> implement it with an output hook in a plugin?
		irc_send (s, "QUIT :%s", PROGRAM_NAME " " PROGRAM_VERSION);
	s->manual_disconnect = true;
	irc_shutdown (s);
}
static void
request_quit (struct app_context *ctx, const char *message)
{
	if (!ctx->quitting)
	{
		log_global_status (ctx, "Shutting down");
		ctx->quitting = true;
		// Disable the user interface
		CALL (ctx->input, hide);
	}
	struct str_map_iter iter = str_map_iter_make (&ctx->servers);
	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_initiate_disconnect (s, message);
		else if (s->state == IRC_CONNECTING)
			irc_destroy_connector (s);
	}
	try_finish_quit (ctx);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
on_irc_ping_timeout (void *user_data)
{
	struct server *s = user_data;
	log_server_error (s, s->buffer,
		"#s: #s", "Disconnected from server", "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));
}
static void
on_irc_autojoin_timeout (void *user_data)
{
	struct server *s = user_data;
	// Since we may not have information from RPL_ISUPPORT yet,
	// it's our safest bet to send the channels one at a time
	struct str_map joins_sent = str_map_make (NULL);
	// We don't know the casemapping yet either, however ASCII should do
	joins_sent.key_xfrm = tolower_ascii_strxfrm;
	// First join autojoin channels in their given order
	const char *autojoin = get_config_string (s->config, "autojoin");
	if (autojoin)
	{
		struct strv v = strv_make ();
		cstr_split (autojoin, ",", true, &v);
		for (size_t i = 0; i < v.len; i++)
		{
			irc_send (s, "JOIN %s", v.vector[i]);
			str_map_set (&joins_sent, v.vector[i], (void *) 1);
		}
		strv_free (&v);
	}
	// Then also rejoin any channels from the last disconnect
	struct str_map_iter iter = str_map_iter_make (&s->irc_channels);
	struct channel *channel;
	while ((channel = str_map_iter_next (&iter)))
	{
		struct str target = str_make ();
		str_append (&target, channel->name);
		const char *key;
		if ((key = str_map_find (&channel->param_modes, "k")))
			str_append_printf (&target, " %s", key);
		// When a channel is both autojoined and rejoined, both keys are tried
		if (!channel->left_manually
		 && !str_map_find (&joins_sent, target.str))
			irc_send (s, "JOIN %s", target.str);
		str_free (&target);
	}
	str_map_free (&joins_sent);
}
// --- Server I/O --------------------------------------------------------------
static char *
irc_process_hooks (struct server *s, char *input)
{
	log_server_debug (s, "#a>> \"#S\"#r", ATTR_JOIN, input);
	uint64_t hash = siphash_wrapper (input, strlen (input));
	LIST_FOR_EACH (struct hook, iter, s->ctx->irc_hooks)
	{
		struct irc_hook *hook = (struct irc_hook *) iter;
		if (!(input = hook->filter (hook, s, input)))
		{
			log_server_debug (s, "#a>= #s#r", ATTR_JOIN, "thrown away by hook");
			return NULL;
		}
		// The old input may get freed, so we compare against a hash of it
		uint64_t new_hash = siphash_wrapper (input, strlen (input));
		if (new_hash != hash)
			log_server_debug (s, "#a>= \"#S\"#r", ATTR_JOIN, input);
		hash = new_hash;
	}
	return input;
}
static void irc_process_message
	(const struct irc_message *msg, struct server *s);
static void
irc_process_buffer_custom (struct server *s, struct str *buf)
{
	const char *start = buf->str, *end = start + buf->len;
	for (const char *p = start; p + 1 < end; p++)
	{
		// Split the input on newlines
		if (p[0] != '\r' || p[1] != '\n')
			continue;
		char *processed = irc_process_hooks (s, xstrndup (start, p - start));
		start = p + 2;
		if (!processed)
			continue;
		struct irc_message msg;
		irc_parse_message (&msg, processed);
		irc_process_message (&msg, s);
		irc_free_message (&msg);
		free (processed);
	}
	str_remove_slice (buf, 0, start - buf->str);
}
static enum socket_io_result
irc_try_read (struct server *s)
{
	enum socket_io_result result = s->transport->try_read (s);
	if (s->read_buffer.len >= (1 << 20))
	{
		// XXX: this is stupid; if anything, count it in dependence of time;
		//   we could make transport_tls_try_read() limit the immediate amount
		//   of data read like socket_io_try_read() does and remove this check
		log_server_error (s, s->buffer,
			"The IRC server seems to spew out data frantically");
		return SOCKET_IO_ERROR;
	}
	if (s->read_buffer.len)
		irc_process_buffer_custom (s, &s->read_buffer);
	return result;
}
static enum socket_io_result
irc_try_write (struct server *s)
{
	enum socket_io_result result = s->transport->try_write (s);
	if (result == SOCKET_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 socket_io_result read_result;
	enum socket_io_result write_result;
	if ((read_result  = irc_try_read  (s)) == SOCKET_IO_ERROR
	 || (write_result = irc_try_write (s)) == SOCKET_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  == SOCKET_IO_EOF
	 || write_result == SOCKET_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 == SOCKET_IO_EOF)
		return false;
	if (read_result  == SOCKET_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 socket_io_result
transport_plain_try_read (struct server *s)
{
	enum socket_io_result result =
		socket_io_try_read (s->socket, &s->read_buffer);
	if (result == SOCKET_IO_ERROR)
		print_debug ("%s: %s", __func__, strerror (errno));
	return result;
}
static enum socket_io_result
transport_plain_try_write (struct server *s)
{
	enum socket_io_result result =
		socket_io_try_write (s->socket, &s->write_buffer);
	if (result == SOCKET_IO_ERROR)
		print_debug ("%s: %s", __func__, strerror (errno));
	return result;
}
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,
};
// --- TLS transport -----------------------------------------------------------
struct transport_tls_data
{
	SSL_CTX *ssl_ctx;                   ///< SSL context
	SSL *ssl;                           ///< SSL connection
	bool ssl_rx_want_tx;                ///< SSL_read() wants to write
	bool ssl_tx_want_rx;                ///< SSL_write() wants to read
};
/// The index in SSL_CTX user data for a reference to the server
static int g_transport_tls_data_index = -1;
static int
transport_tls_verify_callback (int preverify_ok, X509_STORE_CTX *ctx)
{
	SSL *ssl = X509_STORE_CTX_get_ex_data
		(ctx, SSL_get_ex_data_X509_STORE_CTX_idx ());
	struct server *s = SSL_CTX_get_ex_data
		(SSL_get_SSL_CTX (ssl), g_transport_tls_data_index);
	X509 *cert = X509_STORE_CTX_get_current_cert (ctx);
	char *subject = X509_NAME_oneline (X509_get_subject_name (cert), NULL, 0);
	char *issuer  = X509_NAME_oneline (X509_get_issuer_name  (cert), NULL, 0);
	log_server_status (s, s->buffer, "Certificate subject: #s", subject);
	log_server_status (s, s->buffer, "Certificate issuer: #s", issuer);
	if (!preverify_ok)
	{
		log_server_error (s, s->buffer,
			"Certificate verification failed: #s",
			X509_verify_cert_error_string (X509_STORE_CTX_get_error (ctx)));
	}
	free (subject);
	free (issuer);
	return preverify_ok;
}
static bool
transport_tls_init_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path,
	struct error **e)
{
	ERR_clear_error ();
	if (file || path)
	{
		if (SSL_CTX_load_verify_locations (ssl_ctx, file, path))
			return true;
		return error_set (e, "%s: %s",
			"Failed to set locations for the CA certificate bundle",
			xerr_describe_error ());
	}
	if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
		return error_set (e, "%s: %s",
			"Couldn't load the default CA certificate bundle",
			xerr_describe_error ());
	return true;
}
static bool
transport_tls_init_ca (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
{
	const char *ca_file = get_config_string (s->config, "tls_ca_file");
	const char *ca_path = get_config_string (s->config, "tls_ca_path");
	char *full_ca_file = ca_file
		? resolve_filename (ca_file, resolve_relative_config_filename) : NULL;
	char *full_ca_path = ca_path
		? resolve_filename (ca_path, resolve_relative_config_filename) : NULL;
	bool ok = false;
	if      (ca_file && !full_ca_file)
		error_set (e, "Couldn't find the CA bundle file");
	else if (ca_path && !full_ca_path)
		error_set (e, "Couldn't find the CA bundle path");
	else
		ok = transport_tls_init_ca_set (ssl_ctx, full_ca_file, full_ca_path, e);
	free (full_ca_file);
	free (full_ca_path);
	return ok;
}
static bool
transport_tls_init_ctx (struct server *s, SSL_CTX *ssl_ctx, struct error **e)
{
	bool verify = get_config_boolean (s->config, "tls_verify");
	SSL_CTX_set_verify (ssl_ctx, verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE,
		transport_tls_verify_callback);
	if (g_transport_tls_data_index == -1)
		g_transport_tls_data_index =
			SSL_CTX_get_ex_new_index (0, "server", NULL, NULL, NULL);
	SSL_CTX_set_ex_data (ssl_ctx, g_transport_tls_data_index, s);
	const char *ciphers = get_config_string (s->config, "tls_ciphers");
	if (ciphers && !SSL_CTX_set_cipher_list (ssl_ctx, ciphers))
		log_server_error (s, s->buffer,
			"Failed to select any cipher from the cipher list");
	SSL_CTX_set_mode (ssl_ctx,
		SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
	// Disable deprecated protocols (see RFC 7568)
	SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
	// This seems to consume considerable amounts of memory while not giving
	// that much in return; in addition to that, I'm not sure about security
	// (see RFC 7525, section 3.3)
#ifdef SSL_OP_NO_COMPRESSION
	SSL_CTX_set_options (ssl_ctx, SSL_OP_NO_COMPRESSION);
#endif // SSL_OP_NO_COMPRESSION
#ifdef LOMEM
	SSL_CTX_set_mode (ssl_ctx, SSL_MODE_RELEASE_BUFFERS);
#endif // LOMEM
	struct error *error = NULL;
	if (!transport_tls_init_ca (s, ssl_ctx, &error))
	{
		if (verify)
		{
			error_propagate (e, error);
			return false;
		}
		// Just 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 *tls_cert = get_config_string (s->config, "tls_cert");
	if (!tls_cert)
		return true;
	ERR_clear_error ();
	bool result = false;
	char *path = resolve_filename (tls_cert, resolve_relative_config_filename);
	if (!path)
		error_set (e, "%s: %s", "Cannot open file", tls_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 TLS client certificate failed",
			xerr_describe_error ());
	else
		result = true;
	free (path);
	return result;
}
static bool
transport_tls_init (struct server *s, const char *hostname, struct error **e)
{
	ERR_clear_error ();
	struct error *error = 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, &error))
		goto error_ssl_2;
	SSL *ssl = SSL_new (ssl_ctx);
	if (!ssl)
		goto error_ssl_2;
	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);
		error = NULL;
	}
	SSL_set_connect_state (ssl);
	if (!SSL_set_fd (ssl, s->socket))
		goto error_ssl_3;
	// Enable SNI, FWIW; literal IP addresses aren't allowed
	struct in6_addr dummy;
	if (!inet_pton (AF_INET,  hostname, &dummy)
	 && !inet_pton (AF_INET6, hostname, &dummy))
		SSL_set_tlsext_host_name (ssl, hostname);
	struct transport_tls_data *data = xcalloc (1, sizeof *data);
	data->ssl_ctx = ssl_ctx;
	data->ssl = ssl;
	// Forces a handshake even if neither side wants to transmit data
	data->ssl_rx_want_tx = true;
	s->transport_data = data;
	return true;
error_ssl_3:
	SSL_free (ssl);
error_ssl_2:
	SSL_CTX_free (ssl_ctx);
error_ssl_1:
	if (!error)
		error_set (&error, "%s: %s", "Could not initialize TLS",
			xerr_describe_error ());
	error_propagate (e, error);
	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 socket_io_result
transport_tls_try_read (struct server *s)
{
	struct transport_tls_data *data = s->transport_data;
	if (data->ssl_tx_want_rx)
		return SOCKET_IO_OK;
	struct str *buf = &s->read_buffer;
	data->ssl_rx_want_tx = false;
	while (true)
	{
		ERR_clear_error ();
		str_reserve (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 SOCKET_IO_EOF;
		case SSL_ERROR_WANT_READ:
			return SOCKET_IO_OK;
		case SSL_ERROR_WANT_WRITE:
			data->ssl_rx_want_tx = true;
			return SOCKET_IO_OK;
		case XSSL_ERROR_TRY_AGAIN:
			continue;
		default:
			LOG_FUNC_FAILURE ("SSL_read", error_info);
			return SOCKET_IO_ERROR;
		}
	}
}
static enum socket_io_result
transport_tls_try_write (struct server *s)
{
	struct transport_tls_data *data = s->transport_data;
	if (data->ssl_rx_want_tx)
		return SOCKET_IO_OK;
	struct str *buf = &s->write_buffer;
	data->ssl_tx_want_rx = false;
	while (buf->len)
	{
		ERR_clear_error ();
		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 SOCKET_IO_EOF;
		case SSL_ERROR_WANT_WRITE:
			return SOCKET_IO_OK;
		case SSL_ERROR_WANT_READ:
			data->ssl_tx_want_rx = true;
			return SOCKET_IO_OK;
		case XSSL_ERROR_TRY_AGAIN:
			continue;
		default:
			LOG_FUNC_FAILURE ("SSL_write", error_info);
			return SOCKET_IO_ERROR;
		}
	}
	return SOCKET_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
	errno = 0;
	struct passwd *pwd = getpwuid (geteuid ());
	if (!pwd)
	{
		return error_set (e,
			"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 strv v = strv_make ();
	cstr_split (get_config_string (s->config, "nicks"), ",", true, &v);
	char *result = NULL;
	if (s->nick_counter >= 0 && (size_t) s->nick_counter < v.len)
		result = xstrdup (v.vector[s->nick_counter++]);
	if ((size_t) s->nick_counter >= v.len)
		// Exhausted all nicknames
		s->nick_counter = -1;
	strv_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 capability negotiation, with up to 3.2 features;
	// at worst the server will ignore this or send a harmless error message
	irc_send (s, "CAP LS 302");
	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, const char *hostname)
{
	struct app_context *ctx = s->ctx;
	// Most of our output comes from the user one full command at a time and we
	// use output buffering, so it makes a lot of sense to avoid these delays
	int yes = 1;
	soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY,
		&yes, sizeof yes) != -1);
	set_blocking (socket, false);
	s->socket = socket;
	s->transport = get_config_boolean (s->config, "tls")
		? &g_transport_tls
		: &g_transport_plain;
	struct error *e = NULL;
	if (s->transport->init && !s->transport->init (s, hostname, &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");
	irc_set_state (s, IRC_CONNECTED);
	s->socket_event = poller_fd_make (&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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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, const char *hostname)
{
	struct server *s = user_data;
	char *hostname_copy = xstrdup (hostname);
	irc_destroy_connector (s);
	irc_finish_connection (s, socket, hostname_copy);
	free (hostname_copy);
}
static void
irc_setup_connector (struct server *s, const struct strv *addresses)
{
	struct connector *connector = xmalloc (sizeof *connector);
	connector_init (connector, &s->ctx->poller);
	s->connector = connector;
	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;
	for (size_t i = 0; i < addresses->len; i++)
	{
		const char *port = "6667",
			*host = tokenize_host_port (addresses->vector[i], &port);
		connector_add_target (connector, host, port);
	}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// TODO: see if we can further merge code for the two connectors, for example
//   by making SOCKS 4A and 5 mere plugins for the connector, or by using
//   a virtual interface common to them both (seems more likely)
static void
irc_on_socks_connecting (void *user_data,
	const char *address, const char *via, const char *version)
{
	struct server *s = user_data;
	log_server_status (s, s->buffer,
		"Connecting to #s via #s (#s)...", address, via, version);
}
static bool
irc_setup_connector_socks (struct server *s, const struct strv *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");
	if (!socks_host)
		return false;
	struct socks_connector *connector = xmalloc (sizeof *connector);
	socks_connector_init (connector, &s->ctx->poller);
	s->socks_conn = connector;
	connector->user_data     = s;
	connector->on_connecting = irc_on_socks_connecting;
	connector->on_error      = irc_on_connector_error;
	connector->on_connected  = irc_on_connector_connected;
	connector->on_failure    = irc_on_connector_failure;
	for (size_t i = 0; i < addresses->len; i++)
	{
		const char *port = "6667",
			*host = tokenize_host_port (addresses->vector[i], &port);
		if (!socks_connector_add_target (connector, host, port, e))
			return false;
	}
	char *service = xstrdup_printf ("%" PRIi64, socks_port_int);
	socks_connector_run (connector, socks_host, service,
		get_config_string (s->config, "socks_username"),
		get_config_string (s->config, "socks_password"));
	free (service);
	// The SOCKS connector can have already failed; we mustn't return true then
	if (!s->socks_conn)
		return error_set (e, "SOCKS connection failed");
	return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 strv servers = strv_make ();
	cstr_split (addresses, ",", true, &servers);
	struct error *e = NULL;
	if (!irc_setup_connector_socks (s, &servers, &e) && !e)
		irc_setup_connector (s, &servers);
	strv_free (&servers);
	if (e)
	{
		irc_destroy_connector (s);
		log_server_error (s, s->buffer, "#s", e->message);
		error_free (e);
		irc_queue_reconnect (s);
	}
	else if (s->state != IRC_CONNECTED)
		irc_set_state (s, IRC_CONNECTING);
}
// --- Input prompt ------------------------------------------------------------
static void
make_unseen_prefix (struct app_context *ctx, struct str *active_buffers)
{
	size_t buffer_no = 0;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
	{
		buffer_no++;
		if (!(iter->new_messages_count - iter->new_unimportant_count)
		 || iter == ctx->current_buffer)
			continue;
		if (active_buffers->len)
			str_append_c (active_buffers, ',');
		if (iter->highlighted)
			str_append_c (active_buffers, '!');
		str_append_printf (active_buffers, "%zu", buffer_no);
	}
}
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_make (&channel->param_modes);
	const char *param;
	while ((param = str_map_iter_next (&iter)))
		str_append_c (modes, iter.link->key[0]);
}
static void
make_server_postfix_registered (struct buffer *buffer, struct str *output)
{
	struct server *s = buffer->server;
	if (buffer->type == BUFFER_CHANNEL)
	{
		struct channel_user *channel_user =
			irc_channel_get_user (buffer->channel, s->irc_user);
		if (channel_user)
			irc_get_channel_user_prefix (s, channel_user, output);
	}
	str_append (output, s->irc_user->nickname);
	if (s->irc_user_modes.len)
		str_append_printf (output, "(%s)", s->irc_user_modes.str);
}
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
		make_server_postfix_registered (buffer, output);
}
static void
make_prompt (struct app_context *ctx, struct str *output)
{
	LIST_FOR_EACH (struct hook, iter, ctx->prompt_hooks)
	{
		struct prompt_hook *hook = (struct prompt_hook *) iter;
		char *made = hook->make (hook);
		if (made)
		{
			str_append (output, made);
			free (made);
			return;
		}
	}
	struct buffer *buffer = ctx->current_buffer;
	if (!buffer)
		return;
	str_append_c (output, '[');
	struct str active_buffers = str_make ();
	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);
	// We remember old modes, don't show them while we're not on the channel
	if (buffer->type == BUFFER_CHANNEL
	 && irc_channel_is_joined (buffer->channel))
	{
		struct str modes = str_make ();
		make_chanmode_postfix (buffer->channel, &modes);
		if (modes.len)
			str_append_printf (output, "(+%s)", modes.str);
		str_free (&modes);
		str_append_printf (output, "{%zu}", buffer->channel->users_len);
	}
	if (buffer->hide_unimportant)
		str_append (output, "");
	if (buffer != ctx->global_buffer)
		make_server_postfix (buffer, output);
	str_append_c (output, ']');
	str_append_c (output, ' ');
}
static void
input_maybe_set_prompt (struct input *self, char *new_prompt)
{
	// Fix libedit's expectations to see a non-control character following
	// the end mark (see prompt.c and literal.c) by cleaning this up
	for (char *p = new_prompt; *p; )
		if (p[0] == INPUT_END_IGNORE && p[1] == INPUT_START_IGNORE)
			memmove (p, p + 2, strlen (p + 2) + 1);
		else
			p++;
	// Redisplay can be an expensive operation
	const char *prompt = CALL (self, get_prompt);
	if (prompt && !strcmp (new_prompt, prompt))
		free (new_prompt);
	else
		CALL_ (self, set_prompt, new_prompt);
}
static void
on_refresh_prompt (struct app_context *ctx)
{
	poller_idle_reset (&ctx->prompt_event);
	bool have_attributes = !!get_attribute_printer (stdout);
	struct str prompt = str_make ();
	make_prompt (ctx, &prompt);
	// libedit has a weird bug where it misapplies ignores when they're not
	// followed by anything else, so let's try to move a trailing space,
	// which will at least fix the default prompt.
	const char *attributed_suffix = "";
#ifdef HAVE_EDITLINE
	if (have_attributes && prompt.len && prompt.str[prompt.len - 1] == ' ')
	{
		prompt.str[--prompt.len] = 0;
		attributed_suffix = " ";
	}
	// Also enable a uniform interface for prompt hooks by assuming it uses
	// GNU Readline escapes: turn this into libedit's almost-flip-flop
	for (size_t i = 0; i < prompt.len; i++)
		if (prompt.str[i] == '\x01' || prompt.str[i] == '\x02')
			prompt.str[i] = INPUT_START_IGNORE /* == INPUT_END_IGNORE */;
#endif // HAVE_EDITLINE
	char *localized = iconv_xstrdup (ctx->term_from_utf8, prompt.str, -1, NULL);
	str_free (&prompt);
	// XXX: to be completely correct, we should use tputs, but we cannot
	if (have_attributes)
	{
		char buf[16384] = "";
		FILE *memfp = fmemopen (buf, sizeof buf - 1, "wb");
		struct attr_printer state = { ctx->theme, memfp, false };
		fputc (INPUT_START_IGNORE, memfp);
		attr_printer_apply_named (&state, ATTR_PROMPT);
		fputc (INPUT_END_IGNORE, memfp);
		fputs (localized, memfp);
		free (localized);
		fputc (INPUT_START_IGNORE, memfp);
		attr_printer_reset (&state);
		fputc (INPUT_END_IGNORE, memfp);
		fputs (attributed_suffix, memfp);
		fclose (memfp);
		input_maybe_set_prompt (ctx->input, xstrdup (buf));
	}
	else
		input_maybe_set_prompt (ctx->input, localized);
}
// --- Helpers -----------------------------------------------------------------
static struct buffer *
irc_get_buffer_for_message (struct server *s,
	const struct irc_message *msg, const char *target)
{
	// TODO: display such messages differently
	target = irc_skip_statusmsg (s, 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);
		// This is weird
		if (!channel)
			return NULL;
	}
	else if (!buffer)
	{
		// Outgoing messages needn't have a prefix, no buffer associated
		if (!msg->prefix)
			return NULL;
		// 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;
	// Strip formatting from the message so that it doesn't interfere
	// with nickname detection (colour sequences in particular)
	struct formatter f = formatter_make (s->ctx, NULL);
	formatter_parse_message (&f, message);
	struct str stripped = str_make ();
	for (size_t i = 0; i < f.items_len; i++)
	{
		if (f.items[i].type == FORMATTER_ITEM_TEXT)
			str_append (&stripped, f.items[i].text);
	}
	formatter_free (&f);
	// Well, this is rather crude but it should make most users happy.
	// We could do this in proper Unicode but that's two more conversions per
	// message when both the nickname and the message are likely valid UTF-8.
	char *copy = str_steal (&stripped);
	cstr_transform (copy, s->irc_tolower);
	char *nick = xstrdup (s->irc_user->nickname);
	cstr_transform (nick, s->irc_tolower);
	// Special characters allowed in nicknames by RFC 2812: []\`_^{|} and -
	// Also excluded from the ASCII: common user channel prefixes: +%@&~
	// XXX: why did I exclude those?  It won't match when IRC newbies use them.
	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 char *
irc_get_privmsg_prefix (struct server *s, struct user *user, const char *target)
{
	struct str prefix = str_make ();
	if (user && irc_is_channel (s, (target = irc_skip_statusmsg (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)))
			irc_get_channel_user_prefix (s, channel_user, &prefix);
	}
	return str_steal (&prefix);
}
// --- 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
	unsigned changes;                   ///< Count of all changes
	unsigned usermode_changes;          ///< Count of all usermode changes
};
/// 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;
	// Translate mode character to user prefix character
	const char *all_prefixes = self->s->irc_chanuser_prefixes;
	const char *all_modes    = self->s->irc_chanuser_modes;
	const char *mode = strchr (all_modes, self->mode_char);
	hard_assert (mode && (size_t) (mode - all_modes) < strlen (all_prefixes));
	char prefix = all_prefixes[mode - all_modes];
	char **prefixes = &channel_user->prefixes;
	char *pos = strchr (*prefixes, prefix);
	if (self->adding == !!pos)
		return;
	if (self->adding)
	{
		// Add the new mode prefix while retaining the right order
		struct str buf = str_make ();
		for (const char *p = all_prefixes; *p; p++)
			if (*p == prefix || strchr (*prefixes, *p))
				str_append_c (&buf, *p);
		cstr_set (prefixes, str_steal (&buf));
	}
	else
		memmove (pos, pos + 1, strlen (pos));
}
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)
{
	self->changes++;
	if      (strchr (self->s->irc_chanuser_modes,           self->mode_char))
	{
		self->usermode_changes++;
		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;
}
/// Returns whether the change has only affected channel user modes
static bool
irc_handle_mode_channel (struct channel *channel, char **params)
{
	struct mode_processor p = { .s = channel->s, .channel = channel };
	mode_processor_run (&p, params, mode_processor_apply_channel);
	if (p.changes == p.usermode_changes)
		return true;
	irc_channel_broadcast_buffer_update (channel);
	return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
mode_processor_apply_user (struct mode_processor *self)
{
	mode_processor_toggle (self, &self->s->irc_user_modes);
	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);
	relay_prepare_server_update (s->ctx, s);
	relay_broadcast (s->ctx);
}
// --- Output processing -------------------------------------------------------
// Both user and plugins can send whatever the heck they want to,
// we need to parse it back so that it's evident what's happening
static void
irc_handle_sent_cap (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 1)
		return;
	const char *subcommand = msg->params.vector[0];
	const char *args = (msg->params.len > 1) ? msg->params.vector[1] : "";
	if (!strcasecmp_ascii (subcommand, "REQ"))
		log_server_status (s, s->buffer,
			"#s: #S", "Capabilities requested", args);
}
static void
irc_handle_sent_join (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 1)
		return;
	if (strcmp (msg->params.vector[0], "0"))
		cstr_split (msg->params.vector[0], ",", true, &s->outstanding_joins);
}
static void
irc_handle_sent_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 && soft_assert (s->irc_user))
		log_outcoming_notice (s, buffer, s->irc_user->nickname, text->str);
	else
		log_outcoming_orphan_notice (s, target, text->str);
}
static void
irc_handle_sent_notice (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 2 || s->cap_echo_message)
		return;
	// This ignores empty messages which we should not normally send
	struct ctcp_chunk *chunks = ctcp_parse (msg->params.vector[1]);
	LIST_FOR_EACH (struct ctcp_chunk, iter, chunks)
	{
		if (iter->is_extended)
			log_ctcp_reply (s, msg->params.vector[0],
				xstrdup_printf ("%s %s", iter->tag.str, iter->text.str));
		else
			irc_handle_sent_notice_text (s, msg, &iter->text);
	}
	ctcp_destroy (chunks);
}
static void
irc_handle_sent_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 && soft_assert (s->irc_user))
	{
		char *prefixes = irc_get_privmsg_prefix (s, s->irc_user, target);
		if (is_action)
			log_outcoming_action (s, buffer, s->irc_user->nickname, text->str);
		else
			log_outcoming_privmsg (s, buffer,
				prefixes, s->irc_user->nickname, text->str);
		free (prefixes);
	}
	else if (is_action)
		log_outcoming_orphan_action (s, target, text->str);
	else
		log_outcoming_orphan_privmsg (s, target, text->str);
}
static void
irc_handle_sent_privmsg (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 2 || s->cap_echo_message)
		return;
	// This ignores empty messages which we should not normally send
	// and the server is likely going to reject with an error reply 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_sent_privmsg_text (s, msg, &iter->text, false);
		else if (!strcmp (iter->tag.str, "ACTION"))
			irc_handle_sent_privmsg_text (s, msg, &iter->text, true);
		else
			log_ctcp_query (s, msg->params.vector[0], iter->tag.str);
	}
	ctcp_destroy (chunks);
}
static struct irc_handler
{
	const char *name;
	void (*handler) (struct server *s, const struct irc_message *msg);
}
g_irc_sent_handlers[] =
{
	// This list needs to stay sorted
	{ "CAP",     irc_handle_sent_cap     },
	{ "JOIN",    irc_handle_sent_join    },
	{ "NOTICE",  irc_handle_sent_notice  },
	{ "PRIVMSG", irc_handle_sent_privmsg },
};
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 void
irc_process_sent_message (const struct irc_message *msg, struct server *s)
{
	// The server is free to reject even a matching prefix
	// XXX: even though no prefix should normally be present, this is racy
	if (msg->prefix && !irc_is_this_us (s, msg->prefix))
		return;
	struct irc_handler key = { .name = msg->command };
	struct irc_handler *handler = bsearch (&key, g_irc_sent_handlers,
		N_ELEMENTS (g_irc_sent_handlers), sizeof key, irc_handler_cmp_by_name);
	if (handler)
		handler->handler (s, msg);
}
// --- Input handling ----------------------------------------------------------
static void
irc_handle_authenticate (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 1)
		return;
	// Empty challenge -> empty response for e.g. SASL EXTERNAL,
	// abort anything else as it doesn't make much sense to let the user do it
	if (!strcmp (msg->params.vector[0], "+"))
		irc_send (s, "AUTHENTICATE +");
	else
		irc_send (s, "AUTHENTICATE *");
}
static void
irc_handle_away (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix)
		return;
	char *nickname = irc_cut_nickname (msg->prefix);
	struct user *user = str_map_find (&s->irc_users, nickname);
	free (nickname);
	// Let's allow the server to make us away
	if (user)
		user->away = !!msg->params.len;
}
static void
irc_process_cap_ls (struct server *s)
{
	log_server_status (s, s->buffer,
		"#s: #&S", "Capabilities supported", strv_join (&s->cap_ls_buf, " "));
	struct strv chosen = strv_make ();
	struct strv use    = strv_make ();
	cstr_split (get_config_string (s->config, "capabilities"), ",", true, &use);
	// Filter server capabilities for ones we can make use of
	for (size_t i = 0; i < s->cap_ls_buf.len; i++)
	{
		const char *cap = s->cap_ls_buf.vector[i];
		size_t cap_name_len = strcspn (cap, "=");
		for (size_t k = 0; k < use.len; k++)
			if (!strncasecmp_ascii (use.vector[k], cap, cap_name_len))
				strv_append_owned (&chosen, xstrndup (cap, cap_name_len));
	}
	strv_reset (&s->cap_ls_buf);
	char *chosen_str = strv_join (&chosen, " ");
	strv_free (&chosen);
	strv_free (&use);
	// XXX: with IRCv3.2, this may end up being too long for one message,
	// and we need to be careful with CAP END.  One probably has to count
	// the number of sent CAP REQ vs the number of received CAP ACK/NAK.
	if (s->state == IRC_CONNECTED)
		irc_send (s, "CAP REQ :%s", chosen_str);
	free (chosen_str);
}
static void
irc_toggle_cap (struct server *s, const char *cap, bool active)
{
	if (!strcasecmp_ascii (cap, "echo-message")) s->cap_echo_message = active;
	if (!strcasecmp_ascii (cap, "away-notify"))  s->cap_away_notify  = active;
	if (!strcasecmp_ascii (cap, "sasl"))         s->cap_sasl         = active;
}
static void
irc_try_finish_cap_negotiation (struct server *s)
{
	// It does not make sense to do this post-registration, although it would
	// not hurt either, as the server must ignore it in that case
	if (s->state == IRC_CONNECTED)
		irc_send (s, "CAP END");
}
static void
irc_handle_cap (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 2)
		return;
	struct strv v = strv_make ();
	const char *args = "";
	if (msg->params.len > 2)
		cstr_split ((args = msg->params.vector[2]), " ", true, &v);
	const char *subcommand = msg->params.vector[1];
	if (!strcasecmp_ascii (subcommand, "ACK"))
	{
		log_server_status (s, s->buffer,
			"#s: #S", "Capabilities acknowledged", args);
		for (size_t i = 0; i < v.len; i++)
		{
			const char *cap = v.vector[i];
			bool active = true;
			if (*cap == '-')
			{
				active = false;
				cap++;
			}
			irc_toggle_cap (s, cap, active);
		}
		if (s->cap_sasl && s->transport == &g_transport_tls)
			irc_send (s, "AUTHENTICATE EXTERNAL");
		else
			irc_try_finish_cap_negotiation (s);
	}
	else if (!strcasecmp_ascii (subcommand, "NAK"))
	{
		log_server_error (s, s->buffer,
			"#s: #S", "Capabilities not acknowledged", args);
		irc_try_finish_cap_negotiation (s);
	}
	else if (!strcasecmp_ascii (subcommand, "DEL"))
	{
		log_server_error (s, s->buffer,
			"#s: #S", "Capabilities deleted", args);
		for (size_t i = 0; i < v.len; i++)
			irc_toggle_cap (s, v.vector[i], false);
	}
	else if (!strcasecmp_ascii (subcommand, "LS"))
	{
		if (msg->params.len > 3 && !strcmp (args, "*"))
			cstr_split (msg->params.vector[3], " ", true, &s->cap_ls_buf);
		else
		{
			strv_append_vector (&s->cap_ls_buf, v.vector);
			irc_process_cap_ls (s);
		}
	}
	strv_free (&v);
}
static void
irc_handle_chghost (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 2)
		return;
	char *nickname = irc_cut_nickname (msg->prefix);
	struct user *user = str_map_find (&s->irc_users, nickname);
	free (nickname);
	if (!user)
		return;
	char *new_prefix = xstrdup_printf ("%s!%s@%s", user->nickname,
		msg->params.vector[0], msg->params.vector[1]);
	if (irc_is_this_us (s, msg->prefix))
	{
		cstr_set (&s->irc_user_host, xstrdup_printf ("%s@%s",
			msg->params.vector[0], msg->params.vector[1]));
		log_chghost_self (s, s->buffer, new_prefix);
		// Log a message in all open buffers on this server
		struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
		struct buffer *buffer;
		while ((buffer = str_map_iter_next (&iter)))
			log_chghost_self (s, buffer, new_prefix);
	}
	else
	{
		// Log a message in any PM buffer
		struct buffer *buffer =
			str_map_find (&s->irc_buffer_map, user->nickname);
		if (buffer)
			log_chghost (s, buffer, msg->prefix, new_prefix);
		// Log a message in all channels the user is in
		LIST_FOR_EACH (struct user_channel, iter, user->channels)
		{
			buffer = str_map_find (&s->irc_buffer_map, iter->channel->name);
			hard_assert (buffer != NULL);
			log_chghost (s, buffer, msg->prefix, new_prefix);
		}
	}
	free (new_prefix);
}
static void
irc_handle_error (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 1)
		return;
	log_server_error (s, s->buffer, "#m", msg->params.vector[0]);
}
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 bool
irc_satisfy_join (struct server *s, const char *target)
{
	// This queue could use some garbage collection,
	// but it's unlikely to pose problems.
	for (size_t i = 0; i < s->outstanding_joins.len; i++)
		if (!irc_server_strcmp (s, target, s->outstanding_joins.vector[i]))
		{
			strv_remove (&s->outstanding_joins, i);
			return true;
		}
	return false;
}
static void
irc_handle_join (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 1)
		return;
	// TODO: RFC 2812 doesn't guarantee that the argument isn't a target list.
	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);
	// We've joined a new channel
	if (!channel)
	{
		// This is weird, ignoring
		if (!irc_is_this_us (s, msg->prefix))
			return;
		buffer = buffer_new (s->ctx->input,
			BUFFER_CHANNEL, irc_make_buffer_name (s, 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);
		char *input = CALL_ (s->ctx->input, get_line, NULL);
		if (irc_satisfy_join (s, channel_name) && !*input)
			buffer_activate (s->ctx, buffer);
		else
			buffer->highlighted = true;
		free (input);
	}
	if (irc_is_this_us (s, msg->prefix))
	{
		// Reset the field so that we rejoin the channel after reconnecting
		channel->left_manually = false;
		// Request the channel mode as we don't get it automatically
		str_reset (&channel->no_param_modes);
		str_map_clear (&channel->param_modes);
		irc_send (s, "MODE %s", channel_name);
		if ((channel->show_names_after_who = s->cap_away_notify))
			irc_send (s, "WHO %s", channel_name);
	}
	// 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, BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_JOIN,
			"#N #a#s#r #S", 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);
	// It would be 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_make (s->ctx, s);
		formatter_add (&f, "#N #a#s#r #n",
			msg->prefix, ATTR_PART, "has kicked", target);
		if (message)
			formatter_add (&f, " (#m)", message);
		log_formatter (s->ctx, buffer, 0, BUFFER_LINE_PART, &f);
	}
}
static void
irc_handle_kill (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 *comment = msg->params.vector[1];
	if (irc_is_this_us (s, target))
		log_server_status (s, s->buffer,
			"You've been killed by #n (#m)", msg->prefix, comment);
}
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 strv copy = strv_make ();
	strv_append_vector (©, msg->params.vector + 1);
	char *modes = strv_join (©, " ");
	strv_free (©);
	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);
		int flags = 0;
		if (channel
		 && irc_handle_mode_channel (channel, msg->params.vector + 1))
			// This is 90% automode spam, let's not let it steal attention,
			// maybe this behaviour should be configurable though
			flags = BUFFER_LINE_UNIMPORTANT;
		if (buffer)
		{
			log_server (s, buffer, flags, BUFFER_LINE_STATUS,
				"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);
}
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);
	}
	if (pm_buffer && buffer_collision)
	{
		// 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 everything 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 = irc_make_buffer_name (s, new_nickname);
		buffer_rename (s->ctx, pm_buffer, x);
		free (x);
	}
	str_map_set (&s->irc_users, user->nickname, NULL);
	str_map_set (&s->irc_users, new_nickname, user);
	cstr_set (&user->nickname, xstrdup (new_nickname));
	// Finally broadcast the event to relay clients and secondary buffers
	if (irc_is_this_us (s, new_nickname))
	{
		relay_prepare_server_update (s->ctx, s);
		relay_broadcast (s->ctx);
		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_make (&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);
		}
	}
}
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_HIGHLIGHT, BUFFER_LINE_STATUS,
			"#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);
	// It would be 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_make (s->ctx, s);
		formatter_add (&f, "#N #a#s#r #S",
			msg->prefix, ATTR_PART, "has left", channel_name);
		if (message)
			formatter_add (&f, " (#m)", message);
		log_formatter (s->ctx, buffer,
			BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_PART, &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_make ();
	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);
	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))
	{
		if (s->cap_echo_message)
			log_ctcp_query (s, target, chunk->tag.str);
		if (!irc_is_this_us (s, target))
			return;
	}
	struct formatter f = formatter_make (s->ctx, s);
	formatter_add (&f, "CTCP requested by #n", msg->prefix);
	if (irc_is_channel (s, irc_skip_statusmsg (s, target)))
		formatter_add (&f, " (to #S)", target);
	formatter_add (&f, ": #S", chunk->tag.str);
	log_formatter (s->ctx, s->buffer, 0, BUFFER_LINE_STATUS, &f);
	char *nickname = irc_cut_nickname (msg->prefix);
	if (!strcmp (chunk->tag.str, "CLIENTINFO"))
		irc_send_ctcp_reply (s, nickname, "CLIENTINFO %s %s %s %s",
			"PING", "VERSION", "TIME", "CLIENTINFO");
	else if (!strcmp (chunk->tag.str, "PING"))
		irc_send_ctcp_reply (s, nickname, "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, nickname, "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, nickname, "TIME %s", buf);
	}
	free (nickname);
}
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);
	char *prefixes = irc_get_privmsg_prefix
		(s, str_map_find (&s->irc_users, nickname), target);
	// Make autocomplete offer recent speakers first on partial matches
	// (note that freshly joined users also move to the front)
	struct user *user;
	struct channel_user *channel_user;
	if (!irc_is_this_us (s, msg->prefix) && buffer->channel
	 && (user = str_map_find (&s->irc_users, nickname))
	 && (channel_user = irc_channel_get_user (buffer->channel, user)))
	{
		LIST_UNLINK (buffer->channel->users, channel_user);
		LIST_PREPEND (buffer->channel->users, channel_user);
	}
	// 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, BUFFER_LINE_ACTION,
			"#a#S#r #m", ATTR_HIGHLIGHT, nickname, text->str);
	else
		log_server (s, buffer, BUFFER_LINE_HIGHLIGHT, 0,
			"#a<#S#S>#r #m", ATTR_HIGHLIGHT, prefixes, nickname, text->str);
	free (nickname);
	free (prefixes);
}
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_make (s->ctx, s);
	formatter_add (&f, "#N #a#s#r", prefix, ATTR_PART, "has quit");
	if (reason)
		formatter_add (&f, " (#m)", reason);
	log_formatter (s->ctx, buffer,
		BUFFER_LINE_UNIMPORTANT, BUFFER_LINE_PART, &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_tagmsg (struct server *s, const struct irc_message *msg)
{
	// TODO: here we can process "typing" tags, once we find this useful
	(void) s;
	(void) msg;
}
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);
	// It would be weird for this to be false
	if (channel)
		irc_channel_set_topic (channel, topic);
	if (buffer)
	{
		log_server (s, buffer, 0, BUFFER_LINE_STATUS, "#n #s \"#m\"",
			msg->prefix, "has changed the topic to", topic);
	}
}
static void
irc_handle_wallops (struct server *s, const struct irc_message *msg)
{
	if (!msg->prefix || msg->params.len < 1)
		return;
	const char *message = msg->params.vector[0];
	log_server (s, s->buffer, 0, 0, "<#n> #m", msg->prefix, message);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct irc_handler g_irc_handlers[] =
{
	// This list needs to stay sorted
	{ "AUTHENTICATE", irc_handle_authenticate },
	{ "AWAY",         irc_handle_away         },
	{ "CAP",          irc_handle_cap          },
	{ "CHGHOST",      irc_handle_chghost      },
	{ "ERROR",        irc_handle_error        },
	{ "INVITE",       irc_handle_invite       },
	{ "JOIN",         irc_handle_join         },
	{ "KICK",         irc_handle_kick         },
	{ "KILL",         irc_handle_kill         },
	{ "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         },
	{ "TAGMSG",       irc_handle_tagmsg       },
	{ "TOPIC",        irc_handle_topic        },
	{ "WALLOPS",      irc_handle_wallops      },
};
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))
	{
		cstr_set (&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 strv v = strv_make ();
	cstr_split (m, " ", true, &v);
	for (size_t i = 0; i < v.len; i++)
		if (irc_try_parse_word_for_userhost (s, v.vector[i]))
			break;
	strv_free (&v);
}
static bool process_input_line
	(struct app_context *, struct buffer *, const char *, int);
static void on_autoaway_timer (struct app_context *ctx);
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_modes);
	cstr_set (&s->irc_user_host, NULL);
	irc_set_state (s, IRC_REGISTERED);
	// XXX: we can also use WHOIS if it's not supported (optional by RFC 2812)
	// TODO: maybe rather always use RPL_ISUPPORT NICKLEN & USERLEN & HOSTLEN
	//   since we don't seem to follow any subsequent changes in userhost;
	//   unrealircd sends RPL_HOSTHIDDEN (396), which has an optional user part,
	//   and there is also CAP CHGHOST which /may/ send it to ourselves
	irc_send (s, "USERHOST %s", s->irc_user->nickname);
	// A little hack that reinstates auto-away status when we get disconnected
	if (s->autoaway_active)
		on_autoaway_timer (s->ctx);
	const char *command = get_config_string (s->config, "command");
	if (command)
	{
		log_server_debug (s, "Executing \"#s\"", command);
		(void) process_input_line (s->ctx, s->buffer, command, 0);
	}
	int64_t command_delay = get_config_integer (s->config, "command_delay");
	log_server_debug (s, "Autojoining channels in #&s seconds...",
		xstrdup_printf ("%" PRId64, command_delay));
	poller_timer_set (&s->autojoin_tmr, command_delay * 1000);
}
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 strv v = strv_make ();
	cstr_split (response, " ", true, &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))
			cstr_set (&s->irc_user_host, xstrdup (userhost));
	}
	strv_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_modes);
	irc_handle_mode_user (s, msg->params.vector + 1);
	// XXX: do we want to log a message?
}
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)
		cstr_split (nicks, " ", true, &channel->names_buf);
	else
		log_server_status (s, s->buffer, "Users on #S: #S",
			channel_name, nicks);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct channel_user_sort_entry
{
	struct server *s;                   ///< Server (because of the qsort API)
	struct channel_user *channel_user;  ///< Channel user
};
static int
channel_user_sort_entry_cmp (const void *entry_a, const void *entry_b)
{
	const struct channel_user_sort_entry *a = entry_a;
	const struct channel_user_sort_entry *b = entry_b;
	struct server *s = a->s;
	// First order by the most significant channel user prefix
	const char *prio_a = strchr (s->irc_chanuser_prefixes,
		a->channel_user->prefixes[0]);
	const char *prio_b = strchr (s->irc_chanuser_prefixes,
		b->channel_user->prefixes[0]);
	// Put unrecognized prefixes at the end of the list
	if (prio_a || prio_b)
	{
		if (!prio_a) return  1;
		if (!prio_b) return -1;
		if (prio_a != prio_b)
			return prio_a - prio_b;
	}
	return irc_server_strcmp (s,
		a->channel_user->user->nickname,
		b->channel_user->user->nickname);
}
static void
irc_sort_channel_users (struct channel *channel)
{
	size_t n_users = channel->users_len;
	struct channel_user_sort_entry entries[n_users], *p = entries;
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
	{
		p->s = channel->s;
		p->channel_user = iter;
		p++;
	}
	qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp);
	channel->users = NULL;
	while (p-- != entries)
		LIST_PREPEND (channel->users, p->channel_user);
}
static char *
make_channel_users_list (struct channel *channel)
{
	size_t n_users = channel->users_len;
	struct channel_user_sort_entry entries[n_users], *p = entries;
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
	{
		p->s = channel->s;
		p->channel_user = iter;
		p++;
	}
	qsort (entries, n_users, sizeof *entries, channel_user_sort_entry_cmp);
	// Make names of users that are away italicised, constructing a formatter
	// and adding a new attribute seems like unnecessary work
	struct str list = str_make ();
	for (size_t i = 0; i < n_users; i++)
	{
		struct channel_user *channel_user = entries[i].channel_user;
		if (channel_user->user->away) str_append_c (&list, '\x1d');
		irc_get_channel_user_prefix (channel->s, channel_user, &list);
		str_append (&list, channel_user->user->nickname);
		if (channel_user->user->away) str_append_c (&list, '\x1d');
		str_append_c (&list, ' ');
	}
	if (list.len)
		list.str[--list.len] = '\0';
	return str_steal (&list);
}
static void
irc_sync_channel_user (struct channel *channel, const char *nickname,
	const char *prefixes)
{
	struct user *user = irc_get_or_make_user (channel->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[0] != prefixes[0])
		cstr_set (&channel_user->prefixes, xstrdup (prefixes));
}
static void
irc_process_names_finish (struct channel *channel)
{
	struct server *s = channel->s;
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, channel->name);
	if (buffer)
	{
		log_server_status (channel->s, buffer, "Users on #S: #&m",
			channel->name, make_channel_users_list (channel));
	}
}
static void
irc_process_names (struct channel *channel)
{
	struct str_map present = str_map_make (NULL);
	present.key_xfrm = channel->s->irc_strxfrm;
	// Either that, or there is no other inhabitant, and sorting does nothing
	bool we_have_just_joined = channel->users_len == 1;
	struct strv *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, channel->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 (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);
	strv_reset (&channel->names_buf);
	if (we_have_just_joined)
		irc_sort_channel_users (channel);
	if (!channel->show_names_after_who)
		irc_process_names_finish (channel);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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_make (&s->irc_channels);
		struct channel *channel;
		while ((channel = str_map_iter_next (&iter)))
			irc_process_names (channel);
	}
	else if (channel)
		irc_process_names (channel);
}
static bool
irc_handle_rpl_whoreply (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 7)
		return false;
	// Sequence: channel, user, host, server, nick, chars
	const char *channel_name = msg->params.vector[1];
	const char *nickname     = msg->params.vector[5];
	const char *chars        = msg->params.vector[6];
	struct channel *channel = str_map_find (&s->irc_channels, channel_name);
	struct user *user = str_map_find (&s->irc_users, nickname);
	// This makes sense to set only with the away-notify capability so far.
	if (!channel || !channel->show_names_after_who)
		return false;
	// We track ourselves by other means and we can't track PM-only users yet.
	if (user && user != s->irc_user && user->channels)
		user->away = *chars == 'G';
	return true;
}
static bool
irc_handle_rpl_endofwho (struct server *s, const struct irc_message *msg)
{
	if (msg->params.len < 2)
		return false;
	const char *target = msg->params.vector[1];
	struct channel *channel = str_map_find (&s->irc_channels, target);
	if (!channel || !channel->show_names_after_who)
		return false;
	irc_process_names_finish (channel);
	channel->show_names_after_who = false;
	return true;
}
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);
	if (channel)
		irc_channel_set_topic (channel, 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);
	if (channel)
	{
		str_reset (&channel->no_param_modes);
		str_map_clear (&channel->param_modes);
		irc_handle_mode_channel (channel, msg->params.vector + 1);
	}
	// XXX: do we want to log a message?
}
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);
	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);
	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 *nickname     = msg->params.vector[1];
	const char *channel_name = 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;
	cstr_set (&s->irc_chanuser_modes,    xstrndup (modes,    n_prefixes));
	cstr_set (&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)
{
	cstr_set (&s->irc_chantypes, xstrdup (value));
}
static void
irc_handle_isupport_idchan (struct server *s, char *value)
{
	struct str prefixes = str_make ();
	struct strv v = strv_make ();
	cstr_split (value, ",", true, &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);
	}
	strv_free (&v);
	cstr_set (&s->irc_idchan_prefixes, str_steal (&prefixes));
}
static void
irc_handle_isupport_statusmsg (struct server *s, char *value)
{
	cstr_set (&s->irc_statusmsg, xstrdup (value));
}
static void
irc_handle_isupport_extban (struct server *s, char *value)
{
	s->irc_extban_prefix = 0;
	if (*value && *value != ',')
		s->irc_extban_prefix = *value++;
	cstr_set (&s->irc_extban_types, xstrdup (*value == ',' ? ++value : ""));
}
static void
irc_handle_isupport_chanmodes (struct server *s, char *value)
{
	struct strv v = strv_make ();
	cstr_split (value, ",", true, &v);
	if (v.len >= 4)
	{
		cstr_set (&s->irc_chanmodes_list,           xstrdup (v.vector[0]));
		cstr_set (&s->irc_chanmodes_param_always,   xstrdup (v.vector[1]));
		cstr_set (&s->irc_chanmodes_param_when_set, xstrdup (v.vector[2]));
		cstr_set (&s->irc_chanmodes_param_never,    xstrdup (v.vector[3]));
	}
	strv_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 ("EXTBAN",      irc_handle_isupport_extban);
	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_make ();
		unescape_isupport_value (value, &value_unescaped);
		dispatch_isupport (s, param, value_unescaped.str);
		str_free (&value_unescaped);
	}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
irc_adjust_motd (char **motd)
{
	// Heuristic, force MOTD to be monospace in graphical frontends.
	if (!strchr (*motd, '\x11'))
	{
		struct str s = str_make ();
		str_append_c (&s, '\x11');
		for (const char *p = *motd; *p; p++)
		{
			str_append_c (&s, *p);
			if (*p == '\x0f')
				str_append_c (&s, '\x11');
		}
		cstr_set (motd, str_steal (&s));
	}
}
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 strv copy = strv_make ();
	strv_append_vector (©, msg->params.vector + !!msg->params.len);
	struct buffer *buffer = s->buffer;
	int flags = 0;
	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_MOTDSTART:
	case IRC_RPL_MOTD:
		if (copy.len)
			irc_adjust_motd (©.vector[0]);
		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;
		// Auto-away spams server buffers with activity
	case IRC_RPL_NOWAWAY:
		flags |= BUFFER_LINE_UNIMPORTANT;
		if (s->irc_user) s->irc_user->away = true;
		break;
	case IRC_RPL_UNAWAY:
		flags |= BUFFER_LINE_UNIMPORTANT;
		if (s->irc_user) s->irc_user->away = false;
		break;
	case IRC_RPL_WHOREPLY:
		if (irc_handle_rpl_whoreply (s, msg))  buffer = NULL;
		break;
	case IRC_RPL_ENDOFWHO:
		if (irc_handle_rpl_endofwho (s, msg))  buffer = NULL;
		break;
	case IRC_ERR_NICKLOCKED:
	case IRC_RPL_SASLSUCCESS:
	case IRC_ERR_SASLFAIL:
	case IRC_ERR_SASLTOOLONG:
	case IRC_ERR_SASLABORTED:
	case IRC_ERR_SASLALREADY:
		irc_try_finish_cap_negotiation (s);
		break;
	case IRC_RPL_LIST:
	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 < 2)
			break;
		struct buffer *x;
		if ((x = str_map_find (&s->irc_buffer_map, msg->params.vector[1])))
			buffer = x;
		// A JOIN request should be split at commas,
		// then for each element produce either a JOIN response, or a numeric.
		(void) irc_satisfy_join (s, msg->params.vector[1]);
	}
	if (buffer)
	{
		// Join the parameter vector back and send it to the server buffer
		log_server (s, buffer, flags, BUFFER_LINE_STATUS,
			"#&m", strv_join (©, " "));
	}
	strv_free (©);
}
static void
irc_sanitize_cut_off_utf8 (char **line)
{
	// A variation on utf8_validate(), we need to detect the -2 return
	const char *p = *line, *end = strchr (p, 0);
	int32_t codepoint;
	while ((codepoint = utf8_decode (&p, end - p)) >= 0
		&& utf8_validate_cp (codepoint))
		;
	if (codepoint != -2)
		return;
	struct str fixed_up = str_make ();
	str_append_data (&fixed_up, *line, p - *line);
	str_append (&fixed_up, "\xEF\xBF\xBD" /* U+FFFD */);
	cstr_set (line, str_steal (&fixed_up));
}
static void
irc_process_message (const struct irc_message *msg, struct server *s)
{
	if (msg->params.len)
		irc_sanitize_cut_off_utf8 (&msg->params.vector[msg->params.len - 1]);
	// TODO: make use of IRCv3.2 server-time (with fallback to unixtime_msec())
	//   -> change all calls to log_{server,nick,chghost,outcoming,ctcp}*()
	//   to take an extra numeric argument specifying time
	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);
	// Better always make sure everything is in sync rather than care about
	// each case explicitly whether anything might have changed
	refresh_prompt (s->ctx);
}
// --- Message autosplitting magic ---------------------------------------------
// This is a rather basic 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, struct irc_char_attrs *attrs,
	size_t text_len, size_t target_len, struct str *output)
{
	size_t 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 (target_len && word_len <= target_len)
	{
		if (word_len)
		{
			str_append_data (output, text, word_len);
			text += word_len;
			eaten += word_len;
			target_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
	for (size_t i = 1; i <= text_len && i <= target_len; i++)
		if (i == text_len || attrs[i].starts_at_boundary)
			eaten = i;
	str_append_data (output, text, eaten);
	return eaten;
}
// In practice, this should never fail at all, although it's not guaranteed
static bool
wrap_message (const char *message,
	int line_max, struct strv *output, struct error **e)
{
	size_t message_left = strlen (message), i = 0;
	struct irc_char_attrs *attrs = irc_analyze_text (message, message_left);
	struct str m = str_make ();
	if (line_max <= 0)
		goto error;
	while (m.len + message_left > (size_t) line_max)
	{
		size_t eaten = wrap_text_for_single_line
			(message + i, attrs + i, message_left, line_max - m.len, &m);
		if (!eaten)
			goto error;
		strv_append_owned (output, str_steal (&m));
		m = str_make ();
		i += eaten;
		if (!(message_left -= eaten))
			break;
		irc_serialize_char_attrs (attrs + i, &m);
		if (m.len >= (size_t) line_max)
		{
			print_debug ("formatting continuation too long");
			str_reset (&m);
		}
	}
	if (message_left)
		strv_append_owned (output,
			xstrdup_printf ("%s%s", m.str, message + i));
	free (attrs);
	str_free (&m);
	return true;
error:
	free (attrs);
	str_free (&m);
	return error_set (e,
		"Message splitting was unsuccessful as there was "
		"too little room for UTF-8 characters");
}
/// 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 strv *output, struct error **e)
{
	// :!@ 
	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;
	// Multiline messages can be triggered through hooks and plugins.
	struct strv lines = strv_make ();
	cstr_split (message, "\r\n", false, &lines);
	bool success = true;
	for (size_t i = 0; i < lines.len; i++)
	{
		// We don't always have the full info for message splitting.
		if (!space_in_one_message)
			strv_append (output, lines.vector[i]);
		else if (!(success =
			wrap_message (lines.vector[i], space_in_one_message, output, e)))
			break;
	}
	strv_free (&lines);
	return success;
}
static void
send_autosplit_message (struct server *s,
	const char *command, const char *target, const char *message,
	const char *prefix, const char *suffix)
{
	struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
	// "COMMAND target * :prefix*suffix"
	int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1
		+ strlen (prefix) + strlen (suffix);
	struct strv lines = strv_make ();
	struct error *e = NULL;
	if (!irc_autosplit_message (s, message, fixed_part, &lines, &e))
	{
		log_server_error (s, buffer ? buffer : s->buffer, "#s", e->message);
		error_free (e);
	}
	else
	{
		for (size_t i = 0; i < lines.len; i++)
			irc_send (s, "%s %s :%s%s%s", command, target,
				prefix, lines.vector[i], suffix);
	}
	strv_free (&lines);
}
#define SEND_AUTOSPLIT_ACTION(s, target, message)                              \
	send_autosplit_message ((s), "PRIVMSG", (target), (message),               \
		"\x01" "ACTION ", "\x01")
#define SEND_AUTOSPLIT_PRIVMSG(s, target, message)                             \
	send_autosplit_message ((s), "PRIVMSG", (target), (message), "", "")
#define SEND_AUTOSPLIT_NOTICE(s, target, message)                              \
	send_autosplit_message ((s), "NOTICE", (target), (message), "", "")
// --- Configuration dumper ----------------------------------------------------
struct config_dump_data
{
	struct strv path;                   ///< Levels
	struct strv *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 str_map_iter iter = str_map_iter_make (&object->value.object);
	struct config_item *child;
	while ((child = str_map_iter_next (&iter)))
	{
		strv_append_owned (&data->path, iter.link->key);
		config_dump_item (child, data);
		strv_steal (&data->path, data->path.len - 1);
	}
}
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;
	}
	// Currently there's no reason for us to dump unknown items
	struct config_schema *schema = item->schema;
	if (!schema)
		return;
	struct str line = str_make ();
	if (data->path.len)
		str_append (&line, data->path.vector[0]);
	for (size_t i = 1; i < data->path.len; i++)
		str_append_printf (&line, ".%s", data->path.vector[i]);
	struct str value = str_make ();
	config_item_write (item, false, &value);
	// Don't bother writing out null values everywhere
	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);
	strv_append_owned (data->output, str_steal (&line));
}
static void
config_dump (struct config_item *root, struct strv *output)
{
	struct config_dump_data data;
	data.path = strv_make ();
	data.output = output;
	config_dump_item (root, &data);
	hard_assert (!data.path.len);
	strv_free (&data.path);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
strv_sort_cb (const void *a, const void *b)
{
	return strcmp (*(const char **) a, *(const char **) b);
}
static void
strv_sort (struct strv *self)
{
	qsort (self->vector, self->len, sizeof *self->vector, strv_sort_cb);
}
static void
dump_matching_options
	(struct config_item *root, const char *mask, struct strv *output)
{
	config_dump (root, output);
	strv_sort (output);
	// Filter out results by wildcard matching
	for (size_t i = 0; i < output->len; i++)
	{
		// Yeah, I know
		char *key = cstr_cut_until (output->vector[i], " ");
		if (fnmatch (mask, key, 0))
			strv_remove (output, i--);
		free (key);
	}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
save_configuration (struct app_context *ctx)
{
	struct str data = str_make ();
	serialize_configuration (ctx->config.root, &data);
	struct error *e = NULL;
	char *filename = write_configuration_file (NULL, &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);
}
// --- Server management -------------------------------------------------------
static bool
validate_server_name (const char *name)
{
	for (const unsigned char *p = (const unsigned char *) name; *p; p++)
		if (iscntrl_ascii (*p) || *p == '.')
			return false;
	return true;
}
static const char *
check_server_name_for_addition (struct app_context *ctx, const char *name)
{
	if (!strcasecmp_ascii (name, ctx->global_buffer->name))
		return "name collides with the global buffer";
	if (str_map_find (&ctx->servers, name))
		return "server already exists";
	if (!validate_server_name (name))
		return "invalid server name";
	return NULL;
}
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 = server_new (&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 (ctx->input,
		BUFFER_SERVER, irc_make_buffer_name (s, NULL));
	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);
	if (get_config_boolean (s->config, "autoconnect"))
		// Connect to the server ASAP
		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);
	}
}
static void
server_remove (struct app_context *ctx, struct server *s)
{
	hard_assert (!irc_is_connected (s));
	if (s->buffer)
		buffer_remove_safe (ctx, s->buffer);
	relay_prepare_server_remove (ctx, s);
	relay_broadcast (ctx);
	struct str_map_unset_iter iter =
		str_map_unset_iter_make (&s->irc_buffer_map);
	struct buffer *buffer;
	while ((buffer = str_map_unset_iter_next (&iter)))
		buffer_remove_safe (ctx, buffer);
	str_map_unset_iter_free (&iter);
	hard_assert (!s->buffer);
	hard_assert (!s->irc_buffer_map.len);
	hard_assert (!s->irc_channels.len);
	soft_assert (!s->irc_users.len);
	str_map_set (get_servers_config (ctx), s->name, NULL);
	s->config = NULL;
	// This actually destroys the server as it's owned by the map
	str_map_set (&ctx->servers, s->name, NULL);
}
static void
server_rename (struct app_context *ctx, struct server *s, const char *new_name)
{
	hard_assert (!str_map_find (&ctx->servers, new_name));
	relay_prepare_server_rename (ctx, s, new_name);
	relay_broadcast (ctx);
	str_map_set (&ctx->servers, new_name,
		str_map_steal (&ctx->servers, s->name));
	struct str_map *servers = get_servers_config (ctx);
	str_map_set (servers, new_name, str_map_steal (servers, s->name));
	cstr_set (&s->name, xstrdup (new_name));
	buffer_rename (ctx, s->buffer, new_name);
	struct str_map_iter iter = str_map_iter_make (&s->irc_buffer_map);
	struct buffer *buffer;
	while ((buffer = str_map_iter_next (&iter)))
	{
		char *x = NULL;
		switch (buffer->type)
		{
		case BUFFER_PM:
			x = irc_make_buffer_name (s, buffer->user->nickname);
			break;
		case BUFFER_CHANNEL:
			x = irc_make_buffer_name (s, buffer->channel->name);
			break;
		default:
			hard_assert (!"unexpected type of server-related buffer");
		}
		buffer_rename (ctx, buffer, x);
		free (x);
	}
}
// --- Plugins -----------------------------------------------------------------
/// Returns the basename of the plugin's name without any extensions,
/// or NULL if the name isn't suitable (starts with a dot)
static char *
plugin_config_name (struct plugin *self)
{
	const char *begin = self->name;
	for (const char *p = begin; *p; )
		if (*p++ == '/')
			begin = p;
	size_t len = strcspn (begin, ".");
	if (!len)
		return NULL;
	// XXX: we might also allow arbitrary strings as object keys (except dots)
	char *copy = xstrndup (begin, len);
	for (char *p = copy; *p; p++)
		if (!config_tokenizer_is_word_char (*p))
			*p = '_';
	return copy;
}
// --- Lua ---------------------------------------------------------------------
// Each plugin has its own Lua state object, so that a/ they don't disturb each
// other and b/ unloading a plugin releases all resources.
//
// References to internal objects (buffers, servers) are all weak.
#ifdef HAVE_LUA
struct lua_plugin
{
	struct plugin super;                ///< The structure we're deriving
	struct app_context *ctx;            ///< Application context
	lua_State *L;                       ///< Lua state for the main thread
	struct lua_schema_item *schemas;    ///< Registered schema items
};
static void
lua_plugin_gc (struct plugin *self_)
{
	struct lua_plugin *self = (struct lua_plugin *) self_;
	lua_gc (self->L, LUA_GCCOLLECT, 0 /* Lua 5.3 required, 5.4 varargs */);
}
static void
lua_plugin_free (struct plugin *self_)
{
	struct lua_plugin *self = (struct lua_plugin *) self_;
	lua_close (self->L);
}
struct plugin_vtable lua_plugin_vtable =
{
	.gc   = lua_plugin_gc,
	.free = lua_plugin_free,
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// The registry can be used as a cache for weakly referenced objects
static bool
lua_cache_get (lua_State *L, void *object)
{
	lua_rawgetp (L, LUA_REGISTRYINDEX, object);
	if (lua_isnil (L, -1))
	{
		lua_pop (L, 1);
		return false;
	}
	return true;
}
static void
lua_cache_store (lua_State *L, void *object, int index)
{
	lua_pushvalue (L, index);
	lua_rawsetp (L, LUA_REGISTRYINDEX, object);
}
static void
lua_cache_invalidate (lua_State *L, void *object)
{
	lua_pushnil (L);
	lua_rawsetp (L, LUA_REGISTRYINDEX, object);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Append a traceback to all errors so that we can later extract it
static int
lua_plugin_error_handler (lua_State *L)
{
	luaL_traceback (L, L, luaL_checkstring (L, 1), 1);
	return 1;
}
static bool
lua_plugin_process_error (struct lua_plugin *self, const char *message,
	struct error **e)
{
	struct strv v = strv_make ();
	cstr_split (message, "\n", true, &v);
	if (v.len < 2)
		error_set (e, "%s", message);
	else
	{
		error_set (e, "%s", v.vector[0]);
		log_global_debug (self->ctx, "Lua: plugin \"#s\": #s",
			self->super.name, v.vector[1]);
		for (size_t i = 2; i < v.len; i++)
			log_global_debug (self->ctx, " #s", v.vector[i]);
	}
	strv_free (&v);
	return false;
}
/// Call a Lua function and process errors using our special error handler
static bool
lua_plugin_call (struct lua_plugin *self,
	int n_params, int n_results, struct error **e)
{
	// XXX: this may eventually be called from a thread, then this is wrong
	lua_State *L = self->L;
	// We need to pop the error handler at the end
	lua_pushcfunction (L, lua_plugin_error_handler);
	int error_handler_idx = -n_params - 2;
	lua_insert (L, error_handler_idx);
	if (!lua_pcall (L, n_params, n_results, error_handler_idx))
	{
		lua_remove (L, -n_results - 1);
		return true;
	}
	(void) lua_plugin_process_error (self, lua_tostring (L, -1), e);
	lua_pop (L, 2);
	return false;
}
/// Convenience function; replaces the "original" string or produces an error
static bool
lua_plugin_handle_string_filter_result (struct lua_plugin *self,
	char **original, bool utf8, struct error **e)
{
	lua_State *L = self->L;
	if (lua_isnil (L, -1))
	{
		cstr_set (original, NULL);
		return true;
	}
	if (!lua_isstring (L, -1))
		return error_set (e, "must return either a string or nil");
	size_t len;
	const char *processed = lua_tolstring (L, -1, &len);
	if (utf8 && !utf8_validate (processed, len))
		return error_set (e, "must return valid UTF-8");
	// Only replace the string if it's different
	if (strcmp (processed, *original))
		cstr_set (original, xstrdup (processed));
	return true;
}
static const char *
lua_plugin_check_utf8 (lua_State *L, int arg)
{
	size_t len;
	const char *s = luaL_checklstring (L, arg, &len);
	luaL_argcheck (L, utf8_validate (s, len), arg, "must be valid UTF-8");
	return s;
}
static void
lua_plugin_log_error
	(struct lua_plugin *self, const char *where, struct error *error)
{
	log_global_error (self->ctx, "Lua: plugin \"#s\": #s: #s",
		self->super.name, where, error->message);
	error_free (error);
}
/// Pop "n" values from the stack into a table, using their indexes as keys
static void
lua_plugin_pack (lua_State *L, int n)
{
	lua_createtable (L, n, 0);
	lua_insert (L, -n - 1);
	for (int i = n; i; i--)
		lua_rawseti (L, -i - 1, i);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
lua_plugin_kv (lua_State *L, const char *key, const char *value)
{
	lua_pushstring (L, value);
	lua_setfield (L, -2, key);
}
static void
lua_plugin_push_message (lua_State *L, const struct irc_message *msg)
{
	lua_createtable (L, 0, 4);
	lua_createtable (L, msg->tags.len, 0);
	struct str_map_iter iter = str_map_iter_make (&msg->tags);
	const char *value;
	while ((value = str_map_iter_next (&iter)))
		lua_plugin_kv (L, iter.link->key, value);
	lua_setfield (L, -2, "tags");
	// TODO: parse the prefix further?
	if (msg->prefix)   lua_plugin_kv (L, "prefix",  msg->prefix);
	if (msg->command)  lua_plugin_kv (L, "command", msg->command);
	lua_createtable (L, msg->params.len, 0);
	for (size_t i = 0; i < msg->params.len; i++)
	{
		lua_pushstring (L, msg->params.vector[i]);
		lua_rawseti (L, -2, i + 1);
	}
	lua_setfield (L, -2, "params");
}
static int
lua_plugin_parse (lua_State *L)
{
	struct irc_message msg;
	irc_parse_message (&msg, luaL_checkstring (L, 1));
	lua_plugin_push_message (L, &msg);
	irc_free_message (&msg);
	return 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Lua code can use weakly referenced wrappers for internal objects.
typedef struct weak_ref_link *
	(*lua_weak_ref_fn) (void *object, destroy_cb_fn cb, void *user_data);
typedef void (*lua_weak_unref_fn) (void *object, struct weak_ref_link **link);
struct lua_weak_info
{
	const char *name;                   ///< Metatable name
	struct ispect_field *ispect;        ///< Introspection data
	lua_weak_ref_fn ref;                ///< Weak link invalidator
	lua_weak_unref_fn unref;            ///< Weak link generator
};
struct lua_weak
{
	struct lua_plugin *plugin;          ///< The plugin we belong to
	struct lua_weak_info *info;         ///< Introspection data
	void *object;                       ///< The object
	struct weak_ref_link *weak_ref;     ///< A weak reference link
};
static void
lua_weak_invalidate (void *object, void *user_data)
{
	struct lua_weak *wrapper = user_data;
	wrapper->object = NULL;
	wrapper->weak_ref = NULL;
	// This can in theory call the GC, order isn't arbitrary here
	lua_cache_invalidate (wrapper->plugin->L, object);
}
static void
lua_weak_push (lua_State *L, struct lua_plugin *plugin, void *object,
	struct lua_weak_info *info)
{
	if (!object)
	{
		lua_pushnil (L);
		return;
	}
	if (lua_cache_get (L, object))
		return;
	struct lua_weak *wrapper = lua_newuserdata (L, sizeof *wrapper);
	luaL_setmetatable (L, info->name);
	wrapper->plugin = plugin;
	wrapper->info = info;
	wrapper->object = object;
	wrapper->weak_ref = NULL;
	if (info->ref)
		wrapper->weak_ref = info->ref (object, lua_weak_invalidate, wrapper);
	lua_cache_store (L, object, -1);
}
static int
lua_weak_gc (lua_State *L, const struct lua_weak_info *info)
{
	struct lua_weak *wrapper = luaL_checkudata (L, 1, info->name);
	if (wrapper->object)
	{
		lua_cache_invalidate (L, wrapper->object);
		if (info->unref)
			info->unref (wrapper->object, &wrapper->weak_ref);
		wrapper->object = NULL;
	}
	return 0;
}
static struct lua_weak *
lua_weak_deref (lua_State *L, const struct lua_weak_info *info)
{
	struct lua_weak *weak = luaL_checkudata (L, 1, info->name);
	luaL_argcheck (L, weak->object, 1, "dead reference used");
	return weak;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define LUA_WEAK_DECLARE(id, metatable_id)                                     \
	static struct lua_weak_info lua_ ## id ## _info =                          \
	{                                                                          \
		.name   = metatable_id,                                                \
		.ispect = g_ ## id ## _ispect,                                         \
		.ref    = (lua_weak_ref_fn)   id ## _weak_ref,                         \
		.unref  = (lua_weak_unref_fn) id ## _weak_unref,                       \
	};
#define XLUA_USER_METATABLE    "user"     ///< Identifier for Lua metatable
#define XLUA_CHANNEL_METATABLE "channel"  ///< Identifier for Lua metatable
#define XLUA_BUFFER_METATABLE  "buffer"   ///< Identifier for Lua metatable
#define XLUA_SERVER_METATABLE  "server"   ///< Identifier for Lua metatable
LUA_WEAK_DECLARE (user,    XLUA_USER_METATABLE)
LUA_WEAK_DECLARE (channel, XLUA_CHANNEL_METATABLE)
LUA_WEAK_DECLARE (buffer,  XLUA_BUFFER_METATABLE)
LUA_WEAK_DECLARE (server,  XLUA_SERVER_METATABLE)
// The global context is kind of fake and doesn't have any ref-counting,
// however it's still very much an object
static struct lua_weak_info lua_ctx_info =
{
	.name = PROGRAM_NAME,
	.ispect = g_ctx_ispect,
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_user_gc (lua_State *L)
{
	return lua_weak_gc (L, &lua_user_info);
}
static int
lua_user_get_channels (lua_State *L)
{
	struct lua_weak *wrapper = lua_weak_deref (L, &lua_user_info);
	struct user *user = wrapper->object;
	int i = 1;
	lua_newtable (L);
	LIST_FOR_EACH (struct user_channel, iter, user->channels)
	{
		lua_weak_push (L, wrapper->plugin, iter->channel, &lua_channel_info);
		lua_rawseti (L, -2, i++);
	}
	return 1;
}
static luaL_Reg lua_user_table[] =
{
	{ "__gc",         lua_user_gc           },
	{ "get_channels", lua_user_get_channels },
	{ NULL,           NULL                  }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_channel_gc (lua_State *L)
{
	return lua_weak_gc (L, &lua_channel_info);
}
static int
lua_channel_get_users (lua_State *L)
{
	struct lua_weak *wrapper = lua_weak_deref (L, &lua_channel_info);
	struct channel *channel = wrapper->object;
	int i = 1;
	lua_newtable (L);
	LIST_FOR_EACH (struct channel_user, iter, channel->users)
	{
		lua_createtable (L, 0, 2);
		lua_weak_push (L, wrapper->plugin, iter->user, &lua_user_info);
		lua_setfield (L, -2, "user");
		lua_plugin_kv (L, "prefixes", iter->prefixes);
		lua_rawseti (L, -2, i++);
	}
	return 1;
}
static luaL_Reg lua_channel_table[] =
{
	{ "__gc",      lua_channel_gc        },
	{ "get_users", lua_channel_get_users },
	{ NULL,        NULL                  }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_buffer_gc (lua_State *L)
{
	return lua_weak_gc (L, &lua_buffer_info);
}
static int
lua_buffer_log (lua_State *L)
{
	struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
	struct buffer *buffer = wrapper->object;
	const char *message = lua_plugin_check_utf8 (L, 2);
	log_full (wrapper->plugin->ctx, buffer->server, buffer,
		0, BUFFER_LINE_STATUS, "#s", message);
	return 0;
}
static int
lua_buffer_execute (lua_State *L)
{
	struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
	struct buffer *buffer = wrapper->object;
	const char *line = lua_plugin_check_utf8 (L, 2);
	(void) process_input_line (wrapper->plugin->ctx, buffer, line, 0);
	return 0;
}
static luaL_Reg lua_buffer_table[] =
{
	{ "__gc",        lua_buffer_gc          },
	{ "log",         lua_buffer_log         },
	{ "execute",     lua_buffer_execute     },
	{ NULL,          NULL                   }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_server_gc (lua_State *L)
{
	return lua_weak_gc (L, &lua_server_info);
}
static const char *
lua_server_state_to_string (enum server_state state)
{
	switch (state)
	{
	case IRC_DISCONNECTED: return "disconnected";
	case IRC_CONNECTING:   return "connecting";
	case IRC_CONNECTED:    return "connected";
	case IRC_REGISTERED:   return "registered";
	case IRC_CLOSING:      return "closing";
	case IRC_HALF_CLOSED:  return "half-closed";
	}
	return "?";
}
static int
lua_server_get_state (lua_State *L)
{
	struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
	struct server *server = wrapper->object;
	lua_pushstring (L, lua_server_state_to_string (server->state));
	return 1;
}
static int
lua_server_send (lua_State *L)
{
	struct lua_weak *wrapper = lua_weak_deref (L, &lua_server_info);
	struct server *server = wrapper->object;
	irc_send (server, "%s", luaL_checkstring (L, 2));
	return 0;
}
static luaL_Reg lua_server_table[] =
{
	{ "__gc",       lua_server_gc         },
	{ "get_state",  lua_server_get_state  },
	{ "send",       lua_server_send       },
	{ NULL,         NULL                  }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define XLUA_HOOK_METATABLE "hook"      ///< Identifier for the Lua metatable
enum lua_hook_type
{
	XLUA_HOOK_DEFUNCT,                  ///< No longer functional
	XLUA_HOOK_INPUT,                    ///< Input hook
	XLUA_HOOK_IRC,                      ///< IRC hook
	XLUA_HOOK_PROMPT,                   ///< Prompt hook
	XLUA_HOOK_COMPLETION,               ///< Autocomplete
};
struct lua_hook
{
	struct lua_plugin *plugin;          ///< The plugin we belong to
	enum lua_hook_type type;            ///< Type of the hook
	int ref_callback;                   ///< Reference to the callback
	union
	{
		struct hook hook;               ///< Hook base structure
		struct input_hook input_hook;   ///< Input hook
		struct irc_hook irc_hook;       ///< IRC hook
		struct prompt_hook prompt_hook; ///< IRC hook
		struct completion_hook c_hook;  ///< Autocomplete hook
	}
	data;                               ///< Hook data
};
static int
lua_hook_unhook (lua_State *L)
{
	struct lua_hook *hook = luaL_checkudata (L, 1, XLUA_HOOK_METATABLE);
	switch (hook->type)
	{
	case XLUA_HOOK_INPUT:
		LIST_UNLINK (hook->plugin->ctx->input_hooks,      &hook->data.hook);
		break;
	case XLUA_HOOK_IRC:
		LIST_UNLINK (hook->plugin->ctx->irc_hooks,        &hook->data.hook);
		break;
	case XLUA_HOOK_PROMPT:
		LIST_UNLINK (hook->plugin->ctx->prompt_hooks,     &hook->data.hook);
		refresh_prompt (hook->plugin->ctx);
		break;
	case XLUA_HOOK_COMPLETION:
		LIST_UNLINK (hook->plugin->ctx->completion_hooks, &hook->data.hook);
		break;
	default:
		hard_assert (!"invalid hook type");
	case XLUA_HOOK_DEFUNCT:
		break;
	}
	luaL_unref (L, LUA_REGISTRYINDEX, hook->ref_callback);
	hook->ref_callback = LUA_REFNIL;
	// The hook no longer has to stay alive
	hook->type = XLUA_HOOK_DEFUNCT;
	lua_cache_invalidate (L, hook);
	return 0;
}
// The hook dies either when the plugin requests it or at plugin unload
static luaL_Reg lua_hook_table[] =
{
	{ "unhook", lua_hook_unhook },
	{ "__gc",   lua_hook_unhook },
	{ NULL,     NULL            }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static char *
lua_input_hook_filter (struct input_hook *self, struct buffer *buffer,
	char *input)
{
	struct lua_hook *hook =
		CONTAINER_OF (self, struct lua_hook, data.input_hook);
	struct lua_plugin *plugin = hook->plugin;
	lua_State *L = plugin->L;
	lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
	lua_rawgetp (L, LUA_REGISTRYINDEX, hook);             // 1: hook
	lua_weak_push (L, plugin, buffer, &lua_buffer_info);  // 2: buffer
	lua_pushstring (L, input);                            // 3: input
	struct error *e = NULL;
	if (lua_plugin_call (plugin, 3, 1, &e))
	{
		lua_plugin_handle_string_filter_result (plugin, &input, true, &e);
		lua_pop (L, 1);
	}
	if (e)
		lua_plugin_log_error (plugin, "input hook", e);
	return input;
}
static char *
lua_irc_hook_filter (struct irc_hook *self, struct server *s, char *message)
{
	struct lua_hook *hook =
		CONTAINER_OF (self, struct lua_hook, data.irc_hook);
	struct lua_plugin *plugin = hook->plugin;
	lua_State *L = plugin->L;
	lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
	lua_rawgetp (L, LUA_REGISTRYINDEX, hook);        // 1: hook
	lua_weak_push (L, plugin, s, &lua_server_info);  // 2: server
	lua_pushstring (L, message);                     // 3: message
	struct error *e = NULL;
	if (lua_plugin_call (plugin, 3, 1, &e))
	{
		lua_plugin_handle_string_filter_result (plugin, &message, false, &e);
		lua_pop (L, 1);
	}
	if (e)
		lua_plugin_log_error (plugin, "IRC hook", e);
	return message;
}
static char *
lua_prompt_hook_make (struct prompt_hook *self)
{
	struct lua_hook *hook =
		CONTAINER_OF (self, struct lua_hook, data.prompt_hook);
	struct lua_plugin *plugin = hook->plugin;
	lua_State *L = plugin->L;
	lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
	lua_rawgetp (L, LUA_REGISTRYINDEX, hook);  // 1: hook
	struct error *e = NULL;
	char *prompt = xstrdup ("");
	if (lua_plugin_call (plugin, 1, 1, &e))
	{
		lua_plugin_handle_string_filter_result (plugin, &prompt, true, &e);
		lua_pop (L, 1);
	}
	if (e)
		lua_plugin_log_error (plugin, "prompt hook", e);
	return prompt;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
lua_plugin_push_completion (lua_State *L, struct completion *data)
{
	lua_createtable (L, 0, 3);
	lua_pushstring (L, data->line);
	lua_setfield (L, -2, "line");
	lua_createtable (L, data->words_len, 0);
	for (size_t i = 0; i < data->words_len; i++)
	{
		lua_pushlstring (L, data->line + data->words[i].start,
			data->words[i].end - data->words[i].start);
		lua_rawseti (L, -2, i + 1);
	}
	lua_setfield (L, -2, "words");
	lua_pushinteger (L, data->location);
	lua_setfield (L, -2, "location");
}
static bool
lua_completion_hook_process_value (lua_State *L, struct strv *output,
	struct error **e)
{
	if (lua_type (L, -1) != LUA_TSTRING)
	{
		return error_set (e,
			"%s: %s", "invalid type", lua_typename (L, lua_type (L, -1)));
	}
	size_t len;
	const char *value = lua_tolstring (L, -1, &len);
	if (!utf8_validate (value, len))
		return error_set (e, "must be valid UTF-8");
	strv_append (output, value);
	return true;
}
static bool
lua_completion_hook_process (lua_State *L, struct strv *output,
	struct error **e)
{
	if (lua_isnil (L, -1))
		return true;
	if (!lua_istable (L, -1))
		return error_set (e, "must return either a table or nil");
	bool success = true;
	for (lua_Integer i = 1; success && lua_rawgeti (L, -1, i); i++)
		if ((success = lua_completion_hook_process_value (L, output, e)))
			lua_pop (L, 1);
	lua_pop (L, 1);
	return success;
}
static void
lua_completion_hook_complete (struct completion_hook *self,
	struct completion *data, const char *word, struct strv *output)
{
	struct lua_hook *hook =
		CONTAINER_OF (self, struct lua_hook, data.c_hook);
	struct lua_plugin *plugin = hook->plugin;
	lua_State *L = plugin->L;
	lua_rawgeti (L, LUA_REGISTRYINDEX, hook->ref_callback);
	lua_rawgetp (L, LUA_REGISTRYINDEX, hook);  // 1: hook
	lua_plugin_push_completion (L, data);      // 2: data
	lua_weak_push (L, plugin, plugin->ctx->current_buffer, &lua_buffer_info);
	lua_setfield (L, -2, "buffer");
	lua_pushstring (L, word);                  // 3: word
	struct error *e = NULL;
	if (lua_plugin_call (plugin, 3, 1, &e))
	{
		lua_completion_hook_process (L, output, &e);
		lua_pop (L, 1);
	}
	if (e)
		lua_plugin_log_error (plugin, "autocomplete hook", e);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct lua_hook *
lua_plugin_push_hook (lua_State *L, struct lua_plugin *plugin,
	int callback_index, enum lua_hook_type type, int priority)
{
	luaL_checktype (L, callback_index, LUA_TFUNCTION);
	struct lua_hook *hook = lua_newuserdata (L, sizeof *hook);
	luaL_setmetatable (L, XLUA_HOOK_METATABLE);
	memset (hook, 0, sizeof *hook);
	hook->data.hook.priority = priority;
	hook->type = type;
	hook->plugin = plugin;
	lua_pushvalue (L, callback_index);
	hook->ref_callback = luaL_ref (L, LUA_REGISTRYINDEX);
	// Make sure the hook doesn't get garbage collected and return it
	lua_cache_store (L, hook, -1);
	return hook;
}
static int
lua_plugin_hook_input (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	struct lua_hook *hook = lua_plugin_push_hook
		(L, plugin, 1, XLUA_HOOK_INPUT, luaL_optinteger (L, 2, 0));
	hook->data.input_hook.filter = lua_input_hook_filter;
	plugin->ctx->input_hooks =
		hook_insert (plugin->ctx->input_hooks, &hook->data.hook);
	return 1;
}
static int
lua_plugin_hook_irc (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	struct lua_hook *hook = lua_plugin_push_hook
		(L, plugin, 1, XLUA_HOOK_IRC, luaL_optinteger (L, 2, 0));
	hook->data.irc_hook.filter = lua_irc_hook_filter;
	plugin->ctx->irc_hooks =
		hook_insert (plugin->ctx->irc_hooks, &hook->data.hook);
	return 1;
}
static int
lua_plugin_hook_prompt (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	struct lua_hook *hook = lua_plugin_push_hook
		(L, plugin, 1, XLUA_HOOK_PROMPT, luaL_optinteger (L, 2, 0));
	hook->data.prompt_hook.make = lua_prompt_hook_make;
	plugin->ctx->prompt_hooks =
		hook_insert (plugin->ctx->prompt_hooks, &hook->data.hook);
	refresh_prompt (plugin->ctx);
	return 1;
}
static int
lua_plugin_hook_completion (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	struct lua_hook *hook = lua_plugin_push_hook
		(L, plugin, 1, XLUA_HOOK_COMPLETION, luaL_optinteger (L, 2, 0));
	hook->data.c_hook.complete = lua_completion_hook_complete;
	plugin->ctx->completion_hooks =
		hook_insert (plugin->ctx->completion_hooks, &hook->data.hook);
	return 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define XLUA_SCHEMA_METATABLE "schema"  ///< Identifier for the Lua metatable
struct lua_schema_item
{
	LIST_HEADER (struct lua_schema_item)
	struct lua_plugin *plugin;          ///< The plugin we belong to
	struct config_item *item;           ///< The item managed by the schema
	struct config_schema schema;        ///< Schema itself
	int ref_validate;                   ///< Reference to "validate" callback
	int ref_on_change;                  ///< Reference to "on_change" callback
};
static void
lua_schema_item_discard (struct lua_schema_item *self)
{
	if (self->item)
	{
		self->item->schema = NULL;
		self->item->user_data = NULL;
		self->item = NULL;
		LIST_UNLINK (self->plugin->schemas, self);
	}
	// Now that we've disconnected from the item, allow garbage collection
	lua_cache_invalidate (self->plugin->L, self);
}
static int
lua_schema_item_gc (lua_State *L)
{
	struct lua_schema_item *self =
		luaL_checkudata (L, 1, XLUA_SCHEMA_METATABLE);
	lua_schema_item_discard (self);
	free ((char *) self->schema.name);
	free ((char *) self->schema.comment);
	free ((char *) self->schema.default_);
	luaL_unref (L, LUA_REGISTRYINDEX, self->ref_validate);
	luaL_unref (L, LUA_REGISTRYINDEX, self->ref_on_change);
	return 0;
}
static luaL_Reg lua_schema_table[] =
{
	{ "__gc", lua_schema_item_gc },
	{ NULL,   NULL               }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Unfortunately this has the same problem as JSON libraries in that Lua
/// cannot store null values in containers (it has no distinct "undefined" type)
static void
lua_plugin_push_config_item (lua_State *L, const struct config_item *item)
{
	switch (item->type)
	{
	case CONFIG_ITEM_NULL:
		lua_pushnil (L);
		break;
	case CONFIG_ITEM_OBJECT:
	{
		lua_createtable (L, 0, item->value.object.len);
		struct str_map_iter iter = str_map_iter_make (&item->value.object);
		struct config_item *child;
		while ((child = str_map_iter_next (&iter)))
		{
			lua_plugin_push_config_item (L, child);
			lua_setfield (L, -2, iter.link->key);
		}
		break;
	}
	case CONFIG_ITEM_BOOLEAN:
		lua_pushboolean (L, item->value.boolean);
		break;
	case CONFIG_ITEM_INTEGER:
		lua_pushinteger (L, item->value.integer);
		break;
	case CONFIG_ITEM_STRING:
	case CONFIG_ITEM_STRING_ARRAY:
		lua_pushlstring (L, item->value.string.str, item->value.string.len);
		break;
	}
}
static bool
lua_schema_item_validate (const struct config_item *item, struct error **e)
{
	struct lua_schema_item *self = item->user_data;
	if (self->ref_validate == LUA_REFNIL)
		return true;
	lua_State *L = self->plugin->L;
	lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_validate);
	lua_plugin_push_config_item (L, item);
	// The callback can make use of error("...", 0) to produce nice messages
	return lua_plugin_call (self->plugin, 1, 0, e);
}
static void
lua_schema_item_on_change (struct config_item *item)
{
	struct lua_schema_item *self = item->user_data;
	if (self->ref_on_change == LUA_REFNIL)
		return;
	lua_State *L = self->plugin->L;
	lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_on_change);
	lua_plugin_push_config_item (L, item);
	struct error *e = NULL;
	if (!lua_plugin_call (self->plugin, 1, 0, &e))
		lua_plugin_log_error (self->plugin, "schema on_change", e);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_plugin_decode_config_item_type (const char *type)
{
	if (!strcmp (type, "null"))         return CONFIG_ITEM_NULL;
	if (!strcmp (type, "object"))       return CONFIG_ITEM_OBJECT;
	if (!strcmp (type, "boolean"))      return CONFIG_ITEM_BOOLEAN;
	if (!strcmp (type, "integer"))      return CONFIG_ITEM_INTEGER;
	if (!strcmp (type, "string"))       return CONFIG_ITEM_STRING;
	if (!strcmp (type, "string_array")) return CONFIG_ITEM_STRING_ARRAY;
	return -1;
}
static bool
lua_plugin_check_field (lua_State *L, int idx, const char *name,
	int expected, bool optional)
{
	int found = lua_getfield (L, idx, name);
	if (found == expected)
		return true;
	if (optional && found == LUA_TNIL)
		return false;
	const char *message = optional
		? "invalid field \"%s\" (found: %s, expected: %s or nil)"
		: "invalid or missing field \"%s\" (found: %s, expected: %s)";
	return luaL_error (L, message, name,
		lua_typename (L, found), lua_typename (L, expected));
}
static int
lua_plugin_add_config_schema (lua_State *L, struct lua_plugin *plugin,
	struct config_item *subtree, const char *name)
{
	struct config_item *item = str_map_find (&subtree->value.object, name);
	// This should only ever happen because of a conflict with another plugin;
	// this is the price we pay for simplicity
	if (item && item->schema)
	{
		struct lua_schema_item *owner = item->user_data;
		return luaL_error (L, "conflicting schema item: %s (owned by: %s)",
			name, owner->plugin->super.name);
	}
	// Create and initialize a full userdata wrapper for the schema item
	struct lua_schema_item *self = lua_newuserdata (L, sizeof *self);
	luaL_setmetatable (L, XLUA_SCHEMA_METATABLE);
	memset (self, 0, sizeof *self);
	self->plugin = plugin;
	self->ref_on_change = LUA_REFNIL;
	self->ref_validate = LUA_REFNIL;
	struct config_schema *schema = &self->schema;
	schema->name      = xstrdup (name);
	schema->comment   = NULL;
	schema->default_  = NULL;
	schema->type      = CONFIG_ITEM_NULL;
	schema->on_change = lua_schema_item_on_change;
	schema->validate  = lua_schema_item_validate;
	// Try to update the defaults with values provided by the plugin
	int values = lua_absindex (L, -2);
	(void) lua_plugin_check_field (L, values, "type", LUA_TSTRING, false);
	int item_type = schema->type =
		lua_plugin_decode_config_item_type (lua_tostring (L, -1));
	if (item_type == -1)
		return luaL_error (L, "invalid type of schema item");
	if (lua_plugin_check_field (L, values, "comment",   LUA_TSTRING, true))
		schema->comment = xstrdup (lua_tostring (L, -1));
	if (lua_plugin_check_field (L, values, "default",   LUA_TSTRING, true))
		schema->default_ = xstrdup (lua_tostring (L, -1));
	lua_pop (L, 3);
	(void) lua_plugin_check_field (L, values, "on_change", LUA_TFUNCTION, true);
	self->ref_on_change = luaL_ref (L, LUA_REGISTRYINDEX);
	(void) lua_plugin_check_field (L, values, "validate",  LUA_TFUNCTION, true);
	self->ref_validate = luaL_ref (L, LUA_REGISTRYINDEX);
	// Try to install the created schema item into our configuration
	struct error *warning = NULL, *e = NULL;
	item = config_schema_initialize_item
		(&self->schema, subtree, self, &warning, &e);
	if (warning)
	{
		log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s",
			plugin->super.name, warning->message);
		error_free (warning);
	}
	if (e)
	{
		const char *error = lua_pushstring (L, e->message);
		error_free (e);
		return luaL_error (L, "%s", error);
	}
	self->item = item;
	LIST_PREPEND (plugin->schemas, self);
	// On the stack there should be the schema table and the resulting object;
	// we need to make sure Lua doesn't GC the second and get rid of them both
	lua_cache_store (L, self, -1);
	lua_pop (L, 2);
	return 0;
}
static int
lua_plugin_setup_config (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	luaL_checktype (L, 1, LUA_TTABLE);
	struct app_context *ctx = plugin->ctx;
	char *config_name = plugin_config_name (&plugin->super);
	if (!config_name)
		return luaL_error (L, "unsuitable plugin name");
	struct str_map *plugins = get_plugins_config (ctx);
	struct config_item *subtree = str_map_find (plugins, config_name);
	if (!subtree || subtree->type != CONFIG_ITEM_OBJECT)
		str_map_set (plugins, config_name, (subtree = config_item_object ()));
	free (config_name);
	LIST_FOR_EACH (struct lua_schema_item, iter, plugin->schemas)
		lua_schema_item_discard (iter);
	// Load all schema items and apply them to the plugin's subtree
	lua_pushnil (L);
	while (lua_next (L, 1))
	{
		if (lua_type (L, -2) != LUA_TSTRING
		 || lua_type (L, -1) != LUA_TTABLE)
			return luaL_error (L, "%s: %s -> %s", "invalid types",
				lua_typename (L, lua_type (L, -2)),
				lua_typename (L, lua_type (L, -1)));
		lua_plugin_add_config_schema (L, plugin, subtree, lua_tostring (L, -2));
	}
	// Let the plugin read out configuration via on_change callbacks
	config_schema_call_changed (subtree);
	return 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// Identifier for the Lua metatable
#define XLUA_CONNECTION_METATABLE "connection"
struct lua_connection
{
	struct lua_plugin *plugin;          ///< The plugin we belong to
	struct poller_fd socket_event;      ///< Socket is ready
	int socket_fd;                      ///< Underlying connected socket
	bool got_eof;                       ///< Half-closed by remote host
	bool closing;                       ///< We're closing the connection
	struct str read_buffer;             ///< Read buffer
	struct str write_buffer;            ///< Write buffer
};
static void
lua_connection_update_poller (struct lua_connection *self)
{
	poller_fd_set (&self->socket_event,
		self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
}
static int
lua_connection_send (lua_State *L)
{
	struct lua_connection *self =
		luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE);
	if (self->socket_fd == -1)
		return luaL_error (L, "connection has been closed");
	size_t len;
	const char *s = luaL_checklstring (L, 2, &len);
	str_append_data (&self->write_buffer, s, len);
	lua_connection_update_poller (self);
	return 0;
}
static void
lua_connection_discard (struct lua_connection *self)
{
	if (self->socket_fd != -1)
	{
		poller_fd_reset (&self->socket_event);
		xclose (self->socket_fd);
		self->socket_fd = -1;
		str_free (&self->read_buffer);
		str_free (&self->write_buffer);
	}
	// Connection is dead, we don't need to hold onto any resources anymore
	lua_cache_invalidate (self->plugin->L, self);
}
static int
lua_connection_close (lua_State *L)
{
	struct lua_connection *self =
		luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE);
	if (self->socket_fd != -1)
	{
		self->closing = true;
		// NOTE: this seems to do nothing on Linux
		(void) shutdown (self->socket_fd, SHUT_RD);
		// Right now we want to wait until all data is flushed to the socket
		// and can't call close() here immediately -- a rewrite to use async
		// would enable the user to await on either :send() or :flush();
		// a successful send() doesn't necessarily mean anything though
		if (!self->write_buffer.len)
			lua_connection_discard (self);
	}
	return 0;
}
static int
lua_connection_gc (lua_State *L)
{
	lua_connection_discard (luaL_checkudata (L, 1, XLUA_CONNECTION_METATABLE));
	return 0;
}
static luaL_Reg lua_connection_table[] =
{
	{ "send",  lua_connection_send  },
	{ "close", lua_connection_close },
	{ "__gc",  lua_connection_gc    },
	{ NULL,    NULL                 }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_connection_check_fn (lua_State *L)
{
	lua_plugin_check_field (L, 1, luaL_checkstring (L, 2), LUA_TFUNCTION, true);
	return 1;
}
// We need to run it in a protected environment because of lua_getfield()
static bool
lua_connection_cb_lookup (struct lua_connection *self, const char *name,
	struct error **e)
{
	lua_State *L = self->plugin->L;
	lua_pushcfunction (L, lua_connection_check_fn);
	hard_assert (lua_cache_get (L, self));
	lua_pushstring (L, name);
	return lua_plugin_call (self->plugin, 2, 1, e);
}
// Ideally lua_connection_cb_lookup() would return a ternary value
static bool
lua_connection_eat_nil (struct lua_connection *self)
{
	if (lua_toboolean (self->plugin->L, -1))
		return false;
	lua_pop (self->plugin->L, 1);
	return true;
}
static bool
lua_connection_invoke_on_data (struct lua_connection *self, struct error **e)
{
	if (!lua_connection_cb_lookup (self, "on_data", e))
		return false;
	if (lua_connection_eat_nil (self))
		return true;
	lua_pushlstring (self->plugin->L,
		self->read_buffer.str, self->read_buffer.len);
	return lua_plugin_call (self->plugin, 1, 0, e);
}
static bool
lua_connection_invoke_on_eof (struct lua_connection *self, struct error **e)
{
	if (!lua_connection_cb_lookup (self, "on_eof", e))
		return false;
	if (lua_connection_eat_nil (self))
		return true;
	return lua_plugin_call (self->plugin, 0, 0, e);
}
static bool
lua_connection_invoke_on_error (struct lua_connection *self,
	const char *error, struct error **e)
{
	// XXX: not sure if ignoring errors after :close() is always desired;
	//   code might want to make sure that data are transferred successfully
	if (!self->closing
	 && lua_connection_cb_lookup (self, "on_error", e)
	 && !lua_connection_eat_nil (self))
	{
		lua_pushstring (self->plugin->L, error);
		lua_plugin_call (self->plugin, 1, 0, e);
	}
	return false;
}
static bool
lua_connection_try_read (struct lua_connection *self, struct error **e)
{
	// Avoid the read call when it's obviously not going to return any data
	// and would only cause unwanted invocation of callbacks
	if (self->closing || self->got_eof)
		return true;
	enum socket_io_result read_result =
		socket_io_try_read (self->socket_fd, &self->read_buffer);
	const char *error = strerror (errno);
	// Dispatch any data that we got before an EOF or any error
	if (self->read_buffer.len)
	{
		if (!lua_connection_invoke_on_data (self, e))
			return false;
		str_reset (&self->read_buffer);
	}
	if (read_result == SOCKET_IO_EOF)
	{
		if (!lua_connection_invoke_on_eof (self, e))
			return false;
		self->got_eof = true;
	}
	if (read_result == SOCKET_IO_ERROR)
		return lua_connection_invoke_on_error (self, error, e);
	return true;
}
static bool
lua_connection_try_write (struct lua_connection *self, struct error **e)
{
	enum socket_io_result write_result =
		socket_io_try_write (self->socket_fd, &self->write_buffer);
	const char *error = strerror (errno);
	if (write_result == SOCKET_IO_ERROR)
		return lua_connection_invoke_on_error (self, error, e);
	return !self->closing || self->write_buffer.len;
}
static void
lua_connection_on_ready (const struct pollfd *pfd, struct lua_connection *self)
{
	(void) pfd;
	// Hold a reference so that it doesn't get collected on close()
	hard_assert (lua_cache_get (self->plugin->L, self));
	struct error *e = NULL;
	bool keep = lua_connection_try_read (self, &e)
		&& lua_connection_try_write (self, &e);
	if (e)
		lua_plugin_log_error (self->plugin, "network I/O", e);
	if (keep)
		lua_connection_update_poller (self);
	else
		lua_connection_discard (self);
	lua_pop (self->plugin->L, 1);
}
static struct lua_connection *
lua_plugin_push_connection (struct lua_plugin *plugin, int socket_fd)
{
	lua_State *L = plugin->L;
	struct lua_connection *self = lua_newuserdata (L, sizeof *self);
	luaL_setmetatable (L, XLUA_CONNECTION_METATABLE);
	memset (self, 0, sizeof *self);
	self->plugin = plugin;
	set_blocking (socket_fd, false);
	self->socket_event = poller_fd_make
		(&plugin->ctx->poller, (self->socket_fd = socket_fd));
	self->socket_event.dispatcher = (poller_fd_fn) lua_connection_on_ready;
	self->socket_event.user_data = self;
	poller_fd_set (&self->socket_event, POLLIN);
	self->read_buffer = str_make ();
	self->write_buffer = str_make ();
	// Make sure the connection doesn't get garbage collected and return it
	lua_cache_store (L, self, -1);
	return self;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// The script can create as many wait channels as wanted.  They only actually
// do anything once they get yielded to the main lua_resume() call.
/// Identifier for the Lua metatable
#define XLUA_WCHANNEL_METATABLE "wchannel"
struct lua_wait_channel
{
	LIST_HEADER (struct lua_wait_channel)
	struct lua_task *task;              ///< The task we're active in
	/// Check if the event is ready and eventually push values to the thread;
	/// the channel then may release any resources
	bool (*check) (struct lua_wait_channel *self);
	/// Release all resources held by the subclass
	void (*cleanup) (struct lua_wait_channel *self);
};
static int
lua_wchannel_gc (lua_State *L)
{
	struct lua_wait_channel *self =
		luaL_checkudata (L, 1, XLUA_WCHANNEL_METATABLE);
	if (self->cleanup)
		self->cleanup (self);
	return 0;
}
static luaL_Reg lua_wchannel_table[] =
{
	{ "__gc", lua_wchannel_gc },
	{ NULL,   NULL            }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// A task encapsulates a thread so that wait channels yielded from its main
// function get waited upon by the event loop
#define XLUA_TASK_METATABLE  "task"     ///< Identifier for the Lua metatable
struct lua_task
{
	LIST_HEADER (struct lua_task)
	struct lua_plugin *plugin;          ///< The plugin we belong to
	lua_State *thread;                  ///< Lua thread
	struct lua_wait_channel *active;    ///< Channels we're waiting on
	struct poller_idle idle;            ///< Idle job
};
static void
lua_task_unregister_channels (struct lua_task *self)
{
	LIST_FOR_EACH (struct lua_wait_channel, iter, self->active)
	{
		iter->task = NULL;
		LIST_UNLINK (self->active, iter);
		lua_cache_invalidate (self->plugin->L, iter);
	}
}
static void
lua_task_cancel_internal (struct lua_task *self)
{
	if (self->thread)
	{
		lua_cache_invalidate (self->plugin->L, self->thread);
		self->thread = NULL;
	}
	lua_task_unregister_channels (self);
	poller_idle_reset (&self->idle);
	// The task no longer has to stay alive
	lua_cache_invalidate (self->plugin->L, self);
}
static int
lua_task_cancel (lua_State *L)
{
	struct lua_task *self = luaL_checkudata (L, 1, XLUA_TASK_METATABLE);
	// We could also yield and make lua_task_resume() check "self->thread",
	// however the main issue here is that the script should just return
	luaL_argcheck (L, L != self->thread, 1,
		"cannot cancel task from within itself");
	lua_task_cancel_internal (self);
	return 0;
}
#define lua_task_wakeup(self) poller_idle_set (&(self)->idle)
static bool
lua_task_schedule (struct lua_task *self, int n, struct error **e)
{
	lua_State *L = self->thread;
	for (int i = -1; -i <= n; i--)
	{
		struct lua_wait_channel *channel =
			luaL_testudata (L, i, XLUA_WCHANNEL_METATABLE);
		if (!channel)
			return error_set (e, "bad argument #%d to yield: %s", -i + n + 1,
				"tasks can only yield wait channels");
		if (channel->task)
			return error_set (e, "bad argument #%d to yield: %s", -i + n + 1,
				"wait channels can only be active in one task at most");
	}
	for (int i = -1; -i <= n; i--)
	{
		// Quietly ignore duplicate channels
		struct lua_wait_channel *channel = lua_touserdata (L, i);
		if (channel->task)
			continue;
		// By going in reverse the list ends up in the right order
		channel->task = self;
		LIST_PREPEND (self->active, channel);
		lua_cache_store (self->plugin->L, channel, i);
	}
	lua_pop (L, n);
	// There doesn't have to be a single channel
	// We can also be waiting on a channel that is already ready
	lua_task_wakeup (self);
	return true;
}
static void
lua_task_resume (struct lua_task *self, int index)
{
	lua_State *L = self->thread;
	bool waiting_on_multiple = self->active && self->active->next;
	// Since we've ended the wait, we don't need to hold on to them anymore
	lua_task_unregister_channels (self);
	// On the first run we also have the main function on the stack,
	// before any initial arguments
	int n = lua_gettop (L) - (lua_status (L) == LUA_OK);
	// Pack the values in a table and prepend the index of the channel, so that
	// the caller doesn't need to care about the number of return values
	if (waiting_on_multiple)
	{
		lua_plugin_pack (L, n);
		lua_pushinteger (L, index);
		lua_insert (L, -2);
		n = 2;
	}
#if LUA_VERSION_NUM >= 504
	int nresults = 0;
	int res = lua_resume (L, NULL, n, &nresults);
#else
	int res = lua_resume (L, NULL, n);
	int nresults = lua_gettop (L);
#endif
	struct error *error = NULL;
	if (res == LUA_YIELD)
	{
		// AFAIK we don't get any good error context information from here
		if (lua_task_schedule (self, nresults, &error))
			return;
	}
	// For simplicity ignore any results from successful returns
	else if (res != LUA_OK)
	{
		luaL_traceback (L, L, lua_tostring (L, -1), 0 /* or 1? */);
		lua_plugin_process_error (self->plugin, lua_tostring (L, -1), &error);
		lua_pop (L, 2);
	}
	if (error)
		lua_plugin_log_error (self->plugin, "task", error);
	lua_task_cancel_internal (self);
}
static void
lua_task_check (struct lua_task *self)
{
	poller_idle_reset (&self->idle);
	lua_Integer i = 0;
	LIST_FOR_EACH (struct lua_wait_channel, iter, self->active)
	{
		i++;
		if (iter->check (iter))
		{
			lua_task_resume (self, i);
			return;
		}
	}
	if (!self->active)
		lua_task_resume (self, i);
}
// The task dies either when it finishes, it is cancelled, or at plugin unload
static luaL_Reg lua_task_table[] =
{
	{ "cancel", lua_task_cancel },
	{ "__gc",   lua_task_cancel },
	{ NULL,     NULL            }
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct lua_wait_timer
{
	struct lua_wait_channel super;      ///< The structure we're deriving
	struct poller_timer timer;          ///< Timer event
	bool expired;                       ///< Whether the timer has expired
};
static bool
lua_wait_timer_check (struct lua_wait_channel *wchannel)
{
	struct lua_wait_timer *self =
		CONTAINER_OF (wchannel, struct lua_wait_timer, super);
	return self->super.task && self->expired;
}
static void
lua_wait_timer_cleanup (struct lua_wait_channel *wchannel)
{
	struct lua_wait_timer *self =
		CONTAINER_OF (wchannel, struct lua_wait_timer, super);
	poller_timer_reset (&self->timer);
}
static void
lua_wait_timer_dispatch (struct lua_wait_timer *self)
{
	self->expired = true;
	if (self->super.task)
		lua_task_wakeup (self->super.task);
}
static int
lua_plugin_push_wait_timer (struct lua_plugin *plugin, lua_State *L,
	lua_Integer timeout)
{
	struct lua_wait_timer *self = lua_newuserdata (L, sizeof *self);
	luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE);
	memset (self, 0, sizeof *self);
	self->super.check = lua_wait_timer_check;
	self->super.cleanup = lua_wait_timer_cleanup;
	self->timer = poller_timer_make (&plugin->ctx->poller);
	self->timer.dispatcher = (poller_timer_fn) lua_wait_timer_dispatch;
	self->timer.user_data = self;
	if (timeout)
		poller_timer_set (&self->timer, timeout);
	else
		self->expired = true;
	return 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct lua_wait_dial
{
	struct lua_wait_channel super;      ///< The structure we're deriving
	struct lua_plugin *plugin;          ///< The plugin we belong to
	struct connector connector;         ///< Connector object
	bool active;                        ///< Whether the connector is alive
	struct lua_connection *connection;  ///< Established connection
	char *hostname;                     ///< Target hostname
	char *last_error;                   ///< Connecting error, if any
};
static bool
lua_wait_dial_check (struct lua_wait_channel *wchannel)
{
	struct lua_wait_dial *self =
		CONTAINER_OF (wchannel, struct lua_wait_dial, super);
	lua_State *L = self->super.task->thread;
	if (self->connection)
	{
		// FIXME: this way the connection may leak -- we pass the value to the
		//   task manager on the stack and forget about it but still leave the
		//   connection in the cache.  That is because right now, when Lua code
		//   sets up callbacks in the connection object and returns, it might
		//   get otherwise GC'd since nothing else keeps referencing it.
		//   By rewriting lua_connection using async, tasks and wait channels
		//   would hold a reference, allowing us to remove it from the cache.
		lua_cache_get (L, self->connection);
		lua_pushstring (L, self->hostname);
		self->connection = NULL;
	}
	else if (self->last_error)
	{
		lua_pushnil (L);
		lua_pushnil (L);
		lua_pushstring (L, self->last_error);
	}
	else
		return false;
	return true;
}
static void
lua_wait_dial_cancel (struct lua_wait_dial *self)
{
	if (self->active)
	{
		connector_free (&self->connector);
		self->active = false;
	}
}
static void
lua_wait_dial_cleanup (struct lua_wait_channel *wchannel)
{
	struct lua_wait_dial *self =
		CONTAINER_OF (wchannel, struct lua_wait_dial, super);
	lua_wait_dial_cancel (self);
	if (self->connection)
		lua_connection_discard (self->connection);
	free (self->hostname);
	free (self->last_error);
}
static void
lua_wait_dial_on_connected (void *user_data, int socket, const char *hostname)
{
	struct lua_wait_dial *self = user_data;
	if (self->super.task)
		lua_task_wakeup (self->super.task);
	self->connection = lua_plugin_push_connection (self->plugin, socket);
	// TODO: use the hostname for SNI once TLS is implemented
	self->hostname = xstrdup (hostname);
	lua_wait_dial_cancel (self);
}
static void
lua_wait_dial_on_failure (void *user_data)
{
	struct lua_wait_dial *self = user_data;
	if (self->super.task)
		lua_task_wakeup (self->super.task);
	lua_wait_dial_cancel (self);
}
static void
lua_wait_dial_on_error (void *user_data, const char *error)
{
	struct lua_wait_dial *self = user_data;
	cstr_set (&self->last_error, xstrdup (error));
}
static int
lua_plugin_push_wait_dial (struct lua_plugin *plugin, lua_State *L,
	const char *host, const char *service)
{
	struct lua_wait_dial *self = lua_newuserdata (L, sizeof *self);
	luaL_setmetatable (L, XLUA_WCHANNEL_METATABLE);
	memset (self, 0, sizeof *self);
	self->super.check = lua_wait_dial_check;
	self->super.cleanup = lua_wait_dial_cleanup;
	struct connector *connector = &self->connector;
	connector_init (connector, &plugin->ctx->poller);
	connector_add_target (connector, host, service);
	connector->on_connected  = lua_wait_dial_on_connected;
	connector->on_connecting = NULL;
	connector->on_error      = lua_wait_dial_on_error;
	connector->on_failure    = lua_wait_dial_on_failure;
	connector->user_data     = self;
	self->plugin = plugin;
	self->active = true;
	return 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_async_go (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	luaL_checktype (L, 1, LUA_TFUNCTION);
	lua_State *thread = lua_newthread (L);
	lua_cache_store (L, thread, -1);
	lua_pop (L, 1);
	// Move the main function w/ arguments to the thread
	lua_xmove (L, thread, lua_gettop (L));
	struct lua_task *task = lua_newuserdata (L, sizeof *task);
	luaL_setmetatable (L, XLUA_TASK_METATABLE);
	memset (task, 0, sizeof *task);
	task->plugin = plugin;
	task->thread = thread;
	task->idle = poller_idle_make (&plugin->ctx->poller);
	task->idle.dispatcher = (poller_idle_fn) lua_task_check;
	task->idle.user_data = task;
	poller_idle_set (&task->idle);
	// Make sure the task doesn't get garbage collected and return it
	lua_cache_store (L, task, -1);
	return 1;
}
static int
lua_async_timer_ms (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	lua_Integer timeout = luaL_checkinteger (L, 1);
	if (timeout < 0)
		luaL_argerror (L, 1, "timeout mustn't be negative");
	return lua_plugin_push_wait_timer (plugin, L, timeout);
}
static int
lua_async_dial (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	return lua_plugin_push_wait_dial (plugin, L,
		luaL_checkstring (L, 1), luaL_checkstring (L, 2));
}
static luaL_Reg lua_async_library[] =
{
	{ "go",       lua_async_go       },
	{ "timer_ms", lua_async_timer_ms },
	{ "dial",     lua_async_dial     },
	{ NULL,       NULL               },
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static int
lua_plugin_get_screen_size (lua_State *L)
{
	lua_pushinteger (L, g_terminal.lines);
	lua_pushinteger (L, g_terminal.columns);
	return 2;
}
static int
lua_plugin_measure (lua_State *L)
{
	struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
	const char *line = lua_plugin_check_utf8 (L, 1);
	size_t term_len = 0, processed = 0, width = 0, len;
	char *term = iconv_xstrdup (plugin->ctx->term_from_utf8,
		(char *) line, strlen (line) + 1, &term_len);
	mbstate_t ps;
	memset (&ps, 0, sizeof ps);
	wchar_t wch;
	while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps)))
	{
		hard_assert (len != (size_t) -2 && len != (size_t) -1);
		hard_assert ((processed += len) <= term_len);
		int wch_width = wcwidth (wch);
		width += MAX (0, wch_width);
	}
	free (term);
	lua_pushinteger (L, width);
	return 1;
}
static int
lua_ctx_gc (lua_State *L)
{
	return lua_weak_gc (L, &lua_ctx_info);
}
static luaL_Reg lua_plugin_library[] =
{
	// These are pseudo-global functions:
	{ "measure",         lua_plugin_measure         },
	{ "parse",           lua_plugin_parse           },
	{ "hook_input",      lua_plugin_hook_input      },
	{ "hook_irc",        lua_plugin_hook_irc        },
	{ "hook_prompt",     lua_plugin_hook_prompt     },
	{ "hook_completion", lua_plugin_hook_completion },
	{ "setup_config",    lua_plugin_setup_config    },
	// And these are methods:
	// Note that this only returns the height when used through an accessor.
	{ "get_screen_size", lua_plugin_get_screen_size },
	{ "__gc",            lua_ctx_gc                 },
	{ NULL,              NULL                       },
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void *
lua_plugin_alloc (void *ud, void *ptr, size_t o_size, size_t n_size)
{
	(void) ud;
	(void) o_size;
	if (n_size)
		return realloc (ptr, n_size);
	free (ptr);
	return NULL;
}
static int
lua_plugin_panic (lua_State *L)
{
	// XXX: we might be able to do something better
	print_fatal ("Lua panicked: %s", lua_tostring (L, -1));
	lua_close (L);
	exit (EXIT_FAILURE);
	return 0;
}
static void
lua_plugin_push_ref (lua_State *L, struct lua_plugin *self, void *object,
	struct ispect_field *field)
{
	// We create a mapping on object type registration
	hard_assert (lua_rawgetp (L, LUA_REGISTRYINDEX, field->fields));
	struct lua_weak_info *info = lua_touserdata (L, -1);
	lua_pop (L, 1);
	if (!field->is_list)
	{
		lua_weak_push (L, self, object, info);
		return;
	}
	// As a rule in this codebase, these fields are right at the top of structs
	struct list_header { LIST_HEADER (void) };
	int i = 1;
	lua_newtable (L);
	LIST_FOR_EACH (struct list_header, iter, object)
	{
		lua_weak_push (L, self, iter, info);
		lua_rawseti (L, -2, i++);
	}
}
static void lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
	const char *key, void *p, struct ispect_field *field);
static void
lua_plugin_push_struct (lua_State *L, struct lua_plugin *self,
	enum ispect_type type, void *value, struct ispect_field *field)
{
	if (type == ISPECT_STR)
	{
		const struct str *s = value;
		lua_pushlstring (L, s->str, s->len);
		return;
	}
	if (type == ISPECT_STR_MAP)
	{
		struct str_map_iter iter = str_map_iter_make (value);
		void *value;
		lua_newtable (L);
		while ((value = str_map_iter_next (&iter)))
			lua_plugin_push_map_field (L, self, iter.link->key, value, field);
		return;
	}
	hard_assert (!"unhandled introspection object type");
}
static void
lua_plugin_push_map_field (lua_State *L, struct lua_plugin *self,
	const char *key, void *p, struct ispect_field *field)
{
	// That would mean maps in maps ad infinitum
	hard_assert (field->subtype != ISPECT_STR_MAP);
	intptr_t n = (intptr_t) p;
	switch (field->subtype)
	{
	// Here the types are generally casted to a void pointer
	case ISPECT_BOOL:   lua_pushboolean (L, (bool     ) n); break;
	case ISPECT_INT:    lua_pushinteger (L, (int      ) n); break;
	case ISPECT_UINT:   lua_pushinteger (L, (unsigned ) n); break;
	case ISPECT_SIZE:   lua_pushinteger (L, (size_t   ) n); break;
	case ISPECT_STRING: lua_pushstring  (L,             p); break;
	case ISPECT_REF: lua_plugin_push_ref (L, self, p, field); break;
	default: lua_plugin_push_struct (L, self, field->subtype, p, field);
	}
	lua_setfield (L, -2, key);
}
static bool
lua_plugin_property_get_ispect (lua_State *L, const char *property_name)
{
	struct lua_weak_info *info = lua_touserdata (L, lua_upvalueindex (1));
	if (!info || !info->ispect)
		return false;
	struct lua_weak *weak = luaL_checkudata (L, 1, info->name);
	// TODO: I think we can do better than this, maybe binary search at least?
	struct ispect_field *field;
	for (field = info->ispect; field->name; field++)
		if (!strcmp (property_name, field->name))
			break;
	if (!field->name)
		return false;
	struct lua_plugin *self = weak->plugin;
	void *p = (uint8_t *) weak->object + field->offset;
	switch (field->type)
	{
	// Here the types are really what's under the pointer
	case ISPECT_BOOL:   lua_pushboolean (L, *(bool     *) p); break;
	case ISPECT_INT:    lua_pushinteger (L, *(int      *) p); break;
	case ISPECT_UINT:   lua_pushinteger (L, *(unsigned *) p); break;
	case ISPECT_SIZE:   lua_pushinteger (L, *(size_t   *) p); break;
	case ISPECT_STRING: lua_pushstring  (L, *(char    **) p); break;
	case ISPECT_REF: lua_plugin_push_ref (L, self, *(void **) p, field); break;
	default: lua_plugin_push_struct (L, self, field->type, p, field);
	}
	return true;
}
static int
lua_plugin_property_get (lua_State *L)
{
	luaL_checktype (L, 1, LUA_TUSERDATA);
	const char *property_name = luaL_checkstring (L, 2);
	// Either it's directly present in the metatable
	if (luaL_getmetafield (L, 1, property_name))
		return 1;
	// Or we try to find and eventually call a getter method
	char *getter_name = xstrdup_printf ("get_%s", property_name);
	bool found = luaL_getmetafield (L, 1, getter_name);
	free (getter_name);
	if (found)
	{
		lua_pushvalue (L, 1);
		lua_call (L, 1, 1);
		return 1;
	}
	// Maybe we can find it via introspection
	if (lua_plugin_property_get_ispect (L, property_name))
		return 1;
	// Or we look for a property set by the user (__gc cannot be overriden)
	if (lua_getuservalue (L, 1) != LUA_TTABLE)
		lua_pushnil (L);
	else
		lua_getfield (L, -1, property_name);
	return 1;
}
static int
lua_plugin_property_set (lua_State *L)
{
	luaL_checktype (L, 1, LUA_TUSERDATA);
	const char *property_name = luaL_checkstring (L, 2);
	luaL_checkany (L, 3);
	// We use the associated value to store user-defined properties
	int type = lua_getuservalue (L, 1);
	if (type == LUA_TNIL)
	{
		lua_pop (L, 1);
		lua_newtable (L);
		lua_pushvalue (L, -1);
		lua_setuservalue (L, 1);
	}
	else if (type != LUA_TTABLE)
		return luaL_error (L, "associated value is not a table");
	// Beware that we do not check for conflicts here;
	// if Lua code writes a conflicting field, it is effectively ignored
	lua_pushvalue (L, 3);
	lua_setfield (L, -2, property_name);
	return 0;
}
static void
lua_plugin_add_accessors (lua_State *L, struct lua_weak_info *info)
{
	// Emulate properties for convenience
	lua_pushlightuserdata (L, info);
	lua_pushcclosure (L, lua_plugin_property_get, 1);
	lua_setfield (L, -2, "__index");
	lua_pushcfunction (L, lua_plugin_property_set);
	lua_setfield (L, -2, "__newindex");
}
static void
lua_plugin_reg_meta (lua_State *L, const char *name, luaL_Reg *fns)
{
	luaL_newmetatable (L, name);
	luaL_setfuncs (L, fns, 0);
	lua_plugin_add_accessors (L, NULL);
	lua_pop (L, 1);
}
static void
lua_plugin_reg_weak (lua_State *L, struct lua_weak_info *info, luaL_Reg *fns)
{
	// Create a mapping from the object type (info->ispect) back to metadata
	// so that we can figure out what to create from ISPECT_REF fields
	lua_pushlightuserdata (L, info);
	lua_rawsetp (L, LUA_REGISTRYINDEX, info->ispect);
	luaL_newmetatable (L, info->name);
	luaL_setfuncs (L, fns, 0);
	lua_plugin_add_accessors (L, info);
	lua_pop (L, 1);
}
static struct plugin *
lua_plugin_load (struct app_context *ctx, const char *filename,
	struct error **e)
{
	lua_State *L = lua_newstate (lua_plugin_alloc, NULL);
	if (!L)
	{
		error_set (e, "Lua initialization failed");
		return NULL;
	}
	lua_atpanic (L, lua_plugin_panic);
	luaL_openlibs (L);
	struct lua_plugin *plugin = xcalloc (1, sizeof *plugin);
	plugin->super.name = xstrdup (filename);
	plugin->super.vtable = &lua_plugin_vtable;
	plugin->ctx = ctx;
	plugin->L = L;
	luaL_checkversion (L);
	// Register the xC library as a singleton with "plugin" as an upvalue
	// (mostly historical, but rather convenient)
	luaL_newmetatable (L, lua_ctx_info.name);
	lua_pushlightuserdata (L, plugin);
	luaL_setfuncs (L, lua_plugin_library, 1);
	lua_plugin_add_accessors (L, &lua_ctx_info);
	// Add the asynchronous library underneath
	lua_newtable (L);
	lua_pushlightuserdata (L, plugin);
	luaL_setfuncs (L, lua_async_library, 1);
	lua_setfield (L, -2, "async");
	lua_pop (L, 1);
	lua_weak_push (L, plugin, ctx, &lua_ctx_info);
	lua_setglobal (L, lua_ctx_info.name);
	// Create metatables for our objects
	lua_plugin_reg_meta (L, XLUA_HOOK_METATABLE,       lua_hook_table);
	lua_plugin_reg_weak (L, &lua_user_info,            lua_user_table);
	lua_plugin_reg_weak (L, &lua_channel_info,         lua_channel_table);
	lua_plugin_reg_weak (L, &lua_buffer_info,          lua_buffer_table);
	lua_plugin_reg_weak (L, &lua_server_info,          lua_server_table);
	lua_plugin_reg_meta (L, XLUA_SCHEMA_METATABLE,     lua_schema_table);
	lua_plugin_reg_meta (L, XLUA_CONNECTION_METATABLE, lua_connection_table);
	lua_plugin_reg_meta (L, XLUA_TASK_METATABLE,       lua_task_table);
	lua_plugin_reg_meta (L, XLUA_WCHANNEL_METATABLE,   lua_wchannel_table);
	struct error *error = NULL;
	if (luaL_loadfile (L, filename))
		error_set (e, "%s: %s", "Lua", lua_tostring (L, -1));
	else if (!lua_plugin_call (plugin, 0, 0, &error))
	{
		error_set (e, "%s: %s", "Lua", error->message);
		error_free (error);
	}
	else
		return &plugin->super;
	plugin_destroy (&plugin->super);
	return NULL;
}
#endif // HAVE_LUA
// --- Plugins -----------------------------------------------------------------
typedef struct plugin *(*plugin_load_fn)
	(struct app_context *ctx, const char *filename, struct error **e);
// We can potentially add support for other scripting languages if so desired,
// however this possibility is just a byproduct of abstraction
static plugin_load_fn g_plugin_loaders[] =
{
#ifdef HAVE_LUA
	lua_plugin_load,
#endif // HAVE_LUA
};
static struct plugin *
plugin_load_from_filename (struct app_context *ctx, const char *filename,
	struct error **e)
{
	struct plugin *plugin = NULL;
	struct error *error = NULL;
	for (size_t i = 0; i < N_ELEMENTS (g_plugin_loaders); i++)
		if ((plugin = g_plugin_loaders[i](ctx, filename, &error)) || error)
			break;
	if (error)
		error_propagate (e, error);
	else if (!plugin)
	{
		error_set (e, "no plugin handler for \"%s\"", filename);
		return NULL;
	}
	return plugin;
}
static struct plugin *
plugin_find (struct app_context *ctx, const char *name)
{
	LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
		if (!strcmp (name, iter->name))
			return iter;
	return NULL;
}
static char *
plugin_resolve_relative_filename (const char *filename)
{
	struct strv paths = strv_make ();
	get_xdg_data_dirs (&paths);
	strv_append (&paths, PROJECT_DATADIR);
	char *result = resolve_relative_filename_generic
		(&paths, PROGRAM_NAME "/plugins/", filename);
	strv_free (&paths);
	return result;
}
static struct plugin *
plugin_load_by_name (struct app_context *ctx, const char *name,
	struct error **e)
{
	struct plugin *plugin = plugin_find (ctx, name);
	if (plugin)
	{
		error_set (e, "plugin already loaded");
		return NULL;
	}
	// As a side effect, a plugin can be loaded multiple times by giving
	// various relative or non-relative paths to the function; this is not
	// supposed to be fool-proof though, that requires other mechanisms
	char *filename = resolve_filename (name, plugin_resolve_relative_filename);
	if (!filename)
	{
		error_set (e, "file not found");
		return NULL;
	}
	plugin = plugin_load_from_filename (ctx, filename, e);
	free (filename);
	return plugin;
}
static void
plugin_load (struct app_context *ctx, const char *name)
{
	struct error *e = NULL;
	struct plugin *plugin = plugin_load_by_name (ctx, name, &e);
	if (plugin)
	{
		// FIXME: this way the real name isn't available to the plugin on load,
		//   which has effect on e.g. plugin_config_name()
		cstr_set (&plugin->name, xstrdup (name));
		log_global_status (ctx, "Plugin \"#s\" loaded", name);
		LIST_PREPEND (ctx->plugins, plugin);
	}
	else
	{
		log_global_error (ctx, "Can't load plugin \"#s\": #s",
			name, e->message);
		error_free (e);
	}
}
static void
plugin_unload (struct app_context *ctx, const char *name)
{
	struct plugin *plugin = plugin_find (ctx, name);
	if (!plugin)
		log_global_error (ctx, "Can't unload plugin \"#s\": #s",
			name, "plugin not loaded");
	else
	{
		log_global_status (ctx, "Plugin \"#s\" unloaded", name);
		LIST_UNLINK (ctx->plugins, plugin);
		plugin_destroy (plugin);
	}
}
static void
load_plugins (struct app_context *ctx)
{
	const char *plugins =
		get_config_string (ctx->config.root, "general.plugin_autoload");
	if (plugins)
	{
		struct strv v = strv_make ();
		cstr_split (plugins, ",", true, &v);
		for (size_t i = 0; i < v.len; i++)
			plugin_load (ctx, v.vector[i]);
		strv_free (&v);
	}
}
// --- 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
	// Contrary to maybe_cut_word(), we ignore all whitespace at the end
	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)))
		return buffer;
	// Basic case insensitive partial matching -- at most one buffer can match
	int n_matches = 0;
	LIST_FOR_EACH (struct buffer, iter, ctx->buffers)
	{
		char *string  = xstrdup (iter->name);
		char *pattern = xstrdup_printf ("*%s*", word);
		for (char *p = string;  *p; p++) *p = tolower_ascii (*p);
		for (char *p = pattern; *p; p++) *p = tolower_ascii (*p);
		if (!fnmatch (pattern, string, 0))
		{
			n_matches++;
			buffer = iter;
		}
		free (string);
		free (pattern);
	}
	return n_matches == 1 ? buffer : NULL;
}
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)
	{
		struct str s = str_make ();
		int new = iter->new_messages_count - iter->new_unimportant_count;
		if (new && iter != ctx->current_buffer)
			str_append_printf (&s, " (%d%s)", new, &"!"[!iter->highlighted]);
		log_global_indent (ctx,
			"  [#d] #s#&s", i++, iter->name, str_steal (&s));
	}
}
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);
	struct channel *channel;
	if ((channel = str_map_find (&s->irc_channels, channel_name)))
		channel->left_manually = true;
}
static bool
handle_buffer_goto (struct app_context *ctx, struct handler_args *a)
{
	if (!*a->arguments)
		return false;
	const char *which = cut_word (&a->arguments);
	struct buffer *buffer = try_decode_buffer (ctx, which);
	if (buffer)
		buffer_activate (ctx, buffer);
	else
		log_global_error (ctx, "#s: #s", "No such buffer", which);
	return true;
}
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_channel_is_joined (buffer->channel))
			part_channel (buffer->server, buffer->channel->name, "");
		buffer_remove_safe (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;
	if (request == 0 || request > (unsigned long) buffer_count (ctx))
	{
		log_global_error (ctx, "#s: #s",
			"Can't move buffer", "requested position is out of range");
		return true;
	}
	buffer_move (ctx, a->buffer, request);
	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;
	bool result = true;
	if (!*action || !strcasecmp_ascii (action, "list"))
		show_buffers_list (ctx);
	else if (!strcasecmp_ascii (action, "clear"))
	{
		buffer_clear (ctx, a->buffer);
		if (a->buffer == ctx->current_buffer)
			buffer_print_backlog (ctx, a->buffer);
	}
	else if (!strcasecmp_ascii (action, "move"))
		result = handle_buffer_move (ctx, a);
	else if (!strcasecmp_ascii (action, "goto"))
		result = handle_buffer_goto (ctx, a);
	else if (!strcasecmp_ascii (action, "close"))
		handle_buffer_close (ctx, a);
	else
		result = false;
	return result;
}
static bool
handle_command_set_add
	(struct strv *items, const struct strv *values, struct error **e)
{
	for (size_t i = 0; i < values->len; i++)
	{
		const char *value = values->vector[i];
		if (strv_find (items, values->vector[i]) != -1)
			return error_set (e, "already present in the array: %s", value);
		strv_append (items, value);
	}
	return true;
}
static bool
handle_command_set_remove
	(struct strv *items, const struct strv *values, struct error **e)
{
	for (size_t i = 0; i < values->len; i++)
	{
		const char *value = values->vector[i];
		ssize_t i = strv_find (items, value);
		if (i == -1)
			return error_set (e, "not present in the array: %s", value);
		strv_remove (items, i);
	}
	return true;
}
static bool
handle_command_set_modify
	(struct config_item *item, const char *value, bool add, struct error **e)
{
	struct strv items = strv_make ();
	if (item->type != CONFIG_ITEM_NULL)
		cstr_split (item->value.string.str, ",", false, &items);
	if (items.len == 1 && !*items.vector[0])
		strv_reset (&items);
	struct strv values = strv_make ();
	cstr_split (value, ",", false, &values);
	bool result = add
		? handle_command_set_add (&items, &values, e)
		: handle_command_set_remove (&items, &values, e);
	if (result)
	{
		char *changed = strv_join (&items, ",");
		struct str tmp = { .str = changed, .len = strlen (changed) };
		result = config_item_set_from (item,
			config_item_string_array (&tmp), e);
		free (changed);
	}
	strv_free (&items);
	strv_free (&values);
	return result;
}
static bool
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)
		config_item_set_from (item, config_item_clone (new_), &e);
	else if (item->schema->type != CONFIG_ITEM_STRING_ARRAY)
		error_set (&e, "not a string array");
	else
		handle_command_set_modify (item, new_->value.string.str, add, &e);
	if (e)
	{
		log_global_error (ctx,
			"Failed to set option \"#s\": #s", key, e->message);
		error_free (e);
		return false;
	}
	struct strv tmp = strv_make ();
	dump_matching_options (ctx->config.root, key, &tmp);
	log_global_status (ctx, "Option changed: #s", tmp.vector[0]);
	strv_free (&tmp);
	return true;
}
static bool
handle_command_set_assign
	(struct app_context *ctx, struct strv *all, char *arguments)
{
	hard_assert (all->len > 0);
	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;
	}
	bool changed = false;
	for (size_t i = 0; i < all->len; i++)
	{
		char *key = cstr_cut_until (all->vector[i], " ");
		if (handle_command_set_assign_item (ctx, key, new_, add, remove))
			changed = true;
		free (key);
	}
	config_item_destroy (new_);
	if (changed && get_config_boolean (ctx->config.root, "general.autosave"))
		save_configuration (ctx);
	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 strv all = strv_make ();
	dump_matching_options (ctx->config.root, 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);
	strv_free (&all);
	return result;
}
static bool
handle_command_save (struct handler_args *a)
{
	if (*a->arguments)
		return false;
	save_configuration (a->ctx);
	return true;
}
static void
show_plugin_list (struct app_context *ctx)
{
	log_global_indent (ctx, "");
	log_global_indent (ctx, "Plugins:");
	LIST_FOR_EACH (struct plugin, iter, ctx->plugins)
		log_global_indent (ctx, "  #s", iter->name);
}
static bool
handle_command_plugin (struct handler_args *a)
{
	char *action = cut_word (&a->arguments);
	if (!*action || !strcasecmp_ascii (action, "list"))
		show_plugin_list (a->ctx);
	else if (!strcasecmp_ascii (action, "load"))
	{
		if (!*a->arguments)
			return false;
		plugin_load (a->ctx, cut_word (&a->arguments));
	}
	else if (!strcasecmp_ascii (action, "unload"))
	{
		if (!*a->arguments)
			return false;
		plugin_unload (a->ctx, cut_word (&a->arguments));
	}
	else
		return false;
	return true;
}
static bool
handle_command_relay (struct handler_args *a)
{
	if (*a->arguments)
		return false;
	int len = 0;
	LIST_FOR_EACH (struct client, c, a->ctx->clients)
		len++;
	if (a->ctx->relay_fd == -1)
		log_global_status (a->ctx, "The relay is not enabled");
	else
		log_global_status (a->ctx, "The relay has #d clients", len);
	return true;
}
static bool
show_aliases_list (struct app_context *ctx)
{
	log_global_indent (ctx, "");
	log_global_indent (ctx, "Aliases:");
	struct str_map *aliases = get_aliases_config (ctx);
	if (!aliases->len)
	{
		log_global_indent (ctx, "  (none)");
		return true;
	}
	struct str_map_iter iter = str_map_iter_make (aliases);
	struct config_item *alias;
	while ((alias = str_map_iter_next (&iter)))
	{
		struct str definition = str_make ();
		if (config_item_type_is_string (alias->type))
			config_item_write_string (&definition, &alias->value.string);
		else
			str_append (&definition, "alias definition is not a string");
		log_global_indent (ctx, "  /#s: #s", iter.link->key, definition.str);
		str_free (&definition);
	}
	return true;
}
static bool
handle_command_alias (struct handler_args *a)
{
	if (!*a->arguments)
		return show_aliases_list (a->ctx);
	char *name = cut_word (&a->arguments);
	if (!*a->arguments)
		return false;
	if (*name == '/')
		name++;
	struct config_item *alias = config_item_string_from_cstr (a->arguments);
	struct str definition = str_make ();
	config_item_write_string (&definition, &alias->value.string);
	str_map_set (get_aliases_config (a->ctx), name, alias);
	log_global_status (a->ctx, "Created alias /#s: #s", name, definition.str);
	str_free (&definition);
	return true;
}
static bool
handle_command_unalias (struct handler_args *a)
{
	if (!*a->arguments)
		return false;
	struct str_map *aliases = get_aliases_config (a->ctx);
	while (*a->arguments)
	{
		char *name = cut_word (&a->arguments);
		if (!str_map_find (aliases, name))
			log_global_error (a->ctx, "No such alias: #s", name);
		else
		{
			str_map_set (aliases, name, NULL);
			log_global_status (a->ctx, "Alias removed: #s", name);
		}
	}
	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, irc_skip_statusmsg (a->s, target)))
		log_server_error (a->s, a->s->buffer, "Cannot query a channel");
	else if (!*a->arguments)
		buffer_activate (a->ctx, irc_get_or_make_user_buffer (a->s, target));
	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_squery (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
		irc_send (a->s, "SQUERY %s :%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);
	cstr_transform (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);
	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)
{
	request_quit (a->ctx, *a->arguments ? a->arguments : NULL);
	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");
	else if (irc_channel_is_joined (a->buffer->channel))
		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 bool
handle_command_part (struct handler_args *a)
{
	if (irc_is_channel (a->s, a->arguments))
	{
		struct strv v = strv_make ();
		cstr_split (cut_word (&a->arguments), ",", true, &v);
		for (size_t i = 0; i < v.len; i++)
			part_channel (a->s, v.vector[i], a->arguments);
		strv_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");
	else if (!irc_channel_is_joined (a->buffer->channel))
		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 strv v = strv_make ();
		cstr_split (cut_word (&a->arguments), ",", true, &v);
		for (size_t i = 0; i < v.len; i++)
			cycle_channel (a->s, v.vector[i], a->arguments);
		strv_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");
	else if (!irc_channel_is_joined (a->buffer->channel))
		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 start the topic with whitespace
		// FIXME: there's no way to unset the topic;
		//   we could adopt the Tcl style of "-switches" with "--" sentinels,
		//   or we could accept "strings" in the config format
		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 strv *v)
{
	size_t n;
	for (size_t i = 0; i < v->len; i += n)
	{
		struct str modes  = str_make ();
		struct str params = str_make ();
		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 (¶ms, " %s", v->vector[i + k]);
		}
		irc_send (s, "%s%s", modes.str, params.str);
		str_free (&modes);
		str_free (¶ms);
	}
}
static void
mass_channel_mode_mask_list
	(struct handler_args *a, bool adding, char mode_char)
{
	struct strv v = strv_make ();
	cstr_split (a->arguments, " ", true, &v);
	// XXX: this may be a bit too trivial; we could also map 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, "!@*?") || irc_is_extban (a->s, target))
			continue;
		v.vector[i] = xstrdup_printf ("%s!*@*", target);
		free (target);
	}
	mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
	strv_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 strv v = strv_make ();
	cstr_split (a->arguments, " ", true, &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);
	strv_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);
	s->reconnect_attempt = 0;
	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 (poller_timer_is_active (&s->reconnect_tmr))
	{
		log_server_status (s, s->buffer, "Connecting aborted");
		poller_timer_reset (&s->reconnect_tmr);
	}
	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 bool
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_make (&ctx->servers);
	struct server *s;
	while ((s = str_map_iter_next (&iter)))
		log_global_indent (ctx, "  #s", s->name);
	return true;
}
static bool
handle_server_add (struct handler_args *a)
{
	if (!*a->arguments)
		return false;
	struct app_context *ctx = a->ctx;
	char *name = cut_word (&a->arguments);
	const char *err;
	if ((err = check_server_name_for_addition (ctx, name)))
		log_global_error (ctx, "Cannot create server `#s': #s", name, err);
	else
	{
		server_add_new (ctx, name);
		log_global_status (ctx, "Server added: #s", name);
	}
	return true;
}
static bool
handle_server_remove (struct handler_args *a)
{
	struct app_context *ctx = a->ctx;
	struct server *s = NULL;
	if (!(s = resolve_server (ctx, a, "server")))
		return true;
	if (irc_is_connected (s))
		log_server_error (s, s->buffer, "Can't remove a connected server");
	else
	{
		char *name = xstrdup (s->name);
		server_remove (ctx, s);
		log_global_status (ctx, "Server removed: #s", name);
		free (name);
	}
	return true;
}
static bool
handle_server_rename (struct handler_args *a)
{
	struct app_context *ctx = a->ctx;
	if (!*a->arguments)
		return false;
	char *old_name = cut_word (&a->arguments);
	if (!*a->arguments)
		return false;
	char *new_name = cut_word (&a->arguments);
	struct server *s;
	const char *err;
	if (!(s = str_map_find (&ctx->servers, old_name)))
		log_global_error (ctx, "/#s: #s: #s",
			"server", "no such server", old_name);
	else if ((err = check_server_name_for_addition (ctx, new_name)))
		log_global_error (ctx,
			"Cannot rename server to `#s': #s", new_name, err);
	else
	{
		server_rename (ctx, s, new_name);
		log_global_status (ctx, "Server renamed: #s to #s", old_name, new_name);
	}
	return true;
}
static bool
handle_command_server (struct handler_args *a)
{
	if (!*a->arguments)
		return show_servers_list (a->ctx);
	char *action = cut_word (&a->arguments);
	if (!strcasecmp_ascii (action, "list"))
		return show_servers_list (a->ctx);
	if (!strcasecmp_ascii (action, "add"))
		return handle_server_add (a);
	if (!strcasecmp_ascii (action, "remove"))
		return handle_server_remove (a);
	if (!strcasecmp_ascii (action, "rename"))
		return handle_server_rename (a);
	return false;
}
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 neither 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_kill (struct handler_args *a)
{
	if (!*a->arguments)
		return false;
	char *target = cut_word (&a->arguments);
	if (*a->arguments)
		irc_send (a->s, "KILL %s :%s", target, a->arguments);
	else
		irc_send (a->s, "KILL %s", target);
	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)
{
	const char *targets = a->arguments;
	if (!*targets)
	{
		if (adding)
			return false;
		targets = a->s->irc_user->nickname;
	}
	struct strv v = strv_make ();
	cstr_split (targets, " ", true, &v);
	mass_channel_mode (a->s, a->channel_name, adding, mode_char, &v);
	strv_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 (oper,  "OPER")
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",
	  "[ |