/*
 * dwmstatus.c: simple PulseAudio-enabled dwmstatus
 *
 * Copyright (c) 2015 - 2016, Přemysl Janouch <p.janouch@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

#define LIBERTY_WANT_POLLER
#define LIBERTY_WANT_ASYNC

#define _GNU_SOURCE // openat

#include "config.h"
#undef PROGRAM_NAME
#define PROGRAM_NAME "dwmstatus"
#include "liberty/liberty.c"

#include <dirent.h>
#include <sys/un.h>
#include <spawn.h>

#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/XF86keysym.h>

#include <pulse/mainloop.h>
#include <pulse/context.h>
#include <pulse/error.h>
#include <pulse/introspect.h>
#include <pulse/subscribe.h>

// --- Utilities ---------------------------------------------------------------

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

	fprintf (stream, PROGRAM_NAME ": ");
	fputs (quote, stream);
	vfprintf (stream, fmt, ap);
	fputs ("\n", stream);
}

static void
set_dwm_status (Display *dpy, const char *str)
{
	print_debug ("setting status to: %s", str);
	XStoreName (dpy, DefaultRootWindow (dpy), str);
	XSync (dpy, False);
}

// --- Simple network I/O ------------------------------------------------------

enum socket_io_result
{
	SOCKET_IO_OK = 0,                   ///< Completed successfully
	SOCKET_IO_EOF,                      ///< Connection shut down by peer
	SOCKET_IO_ERROR                     ///< Connection error
};

static enum socket_io_result
socket_io_try_read (int socket_fd, struct str *rb)
{
	ssize_t n_read;
	while (true)
	{
		str_ensure_space (rb, 512);
		n_read = recv (socket_fd, rb->str + rb->len,
			rb->alloc - rb->len - 1 /* null byte */, 0);

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

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

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

static enum socket_io_result
socket_io_try_write (int socket_fd, struct str *wb)
{
	ssize_t n_written;
	while (wb->len)
	{
		n_written = send (socket_fd, wb->str, wb->len, 0);
		if (n_written >= 0)
		{
			str_remove_slice (wb, 0, n_written);
			continue;
		}

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

		LOG_LIBC_FAILURE ("send");
		return SOCKET_IO_ERROR;
	}
	return SOCKET_IO_OK;
}

// --- PulseAudio mainloop abstraction -----------------------------------------

struct pa_io_event
{
	LIST_HEADER (pa_io_event)

	pa_mainloop_api *api;               ///< Parent structure
	struct poller_fd fd;                ///< Underlying FD event

	pa_io_event_cb_t dispatch;          ///< Dispatcher
	pa_io_event_destroy_cb_t free;      ///< Destroyer
	void *user_data;                    ///< User data
};

struct pa_time_event
{
	LIST_HEADER (pa_time_event)

	pa_mainloop_api *api;               ///< Parent structure
	struct poller_timer timer;          ///< Underlying timer event

	pa_time_event_cb_t dispatch;        ///< Dispatcher
	pa_time_event_destroy_cb_t free;    ///< Destroyer
	void *user_data;                    ///< User data
};

struct pa_defer_event
{
	LIST_HEADER (pa_defer_event)

	pa_mainloop_api *api;               ///< Parent structure
	struct poller_idle idle;            ///< Underlying idle event

	pa_defer_event_cb_t dispatch;       ///< Dispatcher
	pa_defer_event_destroy_cb_t free;   ///< Destroyer
	void *user_data;                    ///< User data
};

struct poller_pa
{
	struct poller *poller;              ///< The underlying event loop
	int result;                         ///< Result on quit
	bool running;                       ///< Not quitting

	pa_io_event *io_list;               ///< I/O events
	pa_time_event *time_list;           ///< Timer events
	pa_defer_event *defer_list;         ///< Deferred events
};

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

static short
poller_pa_flags_to_events (pa_io_event_flags_t flags)
{
	short result = 0;
	if (flags & PA_IO_EVENT_ERROR)   result |= POLLERR;
	if (flags & PA_IO_EVENT_HANGUP)  result |= POLLHUP;
	if (flags & PA_IO_EVENT_INPUT)   result |= POLLIN;
	if (flags & PA_IO_EVENT_OUTPUT)  result |= POLLOUT;
	return result;
}

static pa_io_event_flags_t
poller_pa_events_to_flags (short events)
{
	pa_io_event_flags_t result = 0;
	if (events & POLLERR)  result |= PA_IO_EVENT_ERROR;
	if (events & POLLHUP)  result |= PA_IO_EVENT_HANGUP;
	if (events & POLLIN)   result |= PA_IO_EVENT_INPUT;
	if (events & POLLOUT)  result |= PA_IO_EVENT_OUTPUT;
	return result;
}

static struct timeval
poller_pa_get_current_time (void)
{
	struct timeval tv;
#ifdef _POSIX_TIMERS
	struct timespec tp;
	hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1);
	tv.tv_sec = tp.tv_sec;
	tv.tv_usec = tp.tv_nsec / 1000;
#else
	gettimeofday (&tv, NULL);
#endif
	return tv;
}

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

static void
poller_pa_io_dispatcher (const struct pollfd *pfd, void *user_data)
{
	pa_io_event *self = user_data;
	self->dispatch (self->api, self,
		pfd->fd, poller_pa_events_to_flags (pfd->revents), self->user_data);
}

static void
poller_pa_io_enable (pa_io_event *self, pa_io_event_flags_t events)
{
	struct poller_fd *fd = &self->fd;
	if (events)
		poller_fd_set (fd, poller_pa_flags_to_events (events));
	else
		poller_fd_reset (fd);
}

static pa_io_event *
poller_pa_io_new (pa_mainloop_api *api, int fd_, pa_io_event_flags_t events,
	pa_io_event_cb_t cb, void *userdata)
{
	pa_io_event *self = xcalloc (1, sizeof *self);
	self->api = api;
	self->dispatch = cb;
	self->user_data = userdata;

	struct poller_pa *data = api->userdata;
	struct poller_fd *fd = &self->fd;
	poller_fd_init (fd, data->poller, fd_);
	fd->user_data = self;
	fd->dispatcher = poller_pa_io_dispatcher;

	poller_pa_io_enable (self, events);
	LIST_PREPEND (data->io_list, self);
	return self;
}

static void
poller_pa_io_free (pa_io_event *self)
{
	if (self->free)
		self->free (self->api, self, self->user_data);

	struct poller_pa *data = self->api->userdata;
	poller_fd_reset (&self->fd);
	LIST_UNLINK (data->io_list, self);
	free (self);
}

static void
poller_pa_io_set_destroy (pa_io_event *self, pa_io_event_destroy_cb_t cb)
{
	self->free = cb;
}

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

static void
poller_pa_time_dispatcher (void *user_data)
{
	pa_time_event *self = user_data;
	// XXX: the meaning of the time argument is undocumented,
	//   so let's just put current Unix time in there
	struct timeval now = poller_pa_get_current_time ();
	self->dispatch (self->api, self, &now, self->user_data);
}

static void
poller_pa_time_restart (pa_time_event *self, const struct timeval *tv)
{
	struct poller_timer *timer = &self->timer;
	if (tv)
	{
		struct timeval now = poller_pa_get_current_time ();
		poller_timer_set (timer,
			(tv->tv_sec  - now.tv_sec)  * 1000 +
			(tv->tv_usec - now.tv_usec) / 1000);
	}
	else
		poller_timer_reset (timer);
}

static pa_time_event *
poller_pa_time_new (pa_mainloop_api *api, const struct timeval *tv,
	pa_time_event_cb_t cb, void *userdata)
{
	pa_time_event *self = xcalloc (1, sizeof *self);
	self->api = api;
	self->dispatch = cb;
	self->user_data = userdata;

	struct poller_pa *data = api->userdata;
	struct poller_timer *timer = &self->timer;
	poller_timer_init (timer, data->poller);
	timer->user_data = self;
	timer->dispatcher = poller_pa_time_dispatcher;

	poller_pa_time_restart (self, tv);
	LIST_PREPEND (data->time_list, self);
	return self;
}

static void
poller_pa_time_free (pa_time_event *self)
{
	if (self->free)
		self->free (self->api, self, self->user_data);

	struct poller_pa *data = self->api->userdata;
	poller_timer_reset (&self->timer);
	LIST_UNLINK (data->time_list, self);
	free (self);
}

static void
poller_pa_time_set_destroy (pa_time_event *self, pa_time_event_destroy_cb_t cb)
{
	self->free = cb;
}

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

static void
poller_pa_defer_dispatcher (void *user_data)
{
	pa_defer_event *self = user_data;
	self->dispatch (self->api, self, self->user_data);
}

static pa_defer_event *
poller_pa_defer_new (pa_mainloop_api *api,
	pa_defer_event_cb_t cb, void *userdata)
{
	pa_defer_event *self = xcalloc (1, sizeof *self);
	self->api = api;
	self->dispatch = cb;
	self->user_data = userdata;

	struct poller_pa *data = api->userdata;
	struct poller_idle *idle = &self->idle;
	poller_idle_init (idle, data->poller);
	idle->user_data = self;
	idle->dispatcher = poller_pa_defer_dispatcher;

	poller_idle_set (idle);
	LIST_PREPEND (data->defer_list, self);
	return self;
}

static void
poller_pa_defer_enable (pa_defer_event *self, int enable)
{
	struct poller_idle *idle = &self->idle;
	if (enable)
		poller_idle_set (idle);
	else
		poller_idle_reset (idle);
}

static void
poller_pa_defer_free (pa_defer_event *self)
{
	if (self->free)
		self->free (self->api, self, self->user_data);

	struct poller_pa *data = self->api->userdata;
	poller_idle_reset (&self->idle);
	LIST_UNLINK (data->defer_list, self);
	free (self);
}

static void
poller_pa_defer_set_destroy (pa_defer_event *self,
	pa_defer_event_destroy_cb_t cb)
{
	self->free = cb;
}

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

static void
poller_pa_quit (pa_mainloop_api *api, int retval)
{
	struct poller_pa *data = api->userdata;
	data->result = retval;
	data->running = false;
}

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

static struct pa_mainloop_api g_poller_pa_template =
{
	.io_new            = poller_pa_io_new,
	.io_enable         = poller_pa_io_enable,
	.io_free           = poller_pa_io_free,
	.io_set_destroy    = poller_pa_io_set_destroy,

	.time_new          = poller_pa_time_new,
	.time_restart      = poller_pa_time_restart,
	.time_free         = poller_pa_time_free,
	.time_set_destroy  = poller_pa_time_set_destroy,

	.defer_new         = poller_pa_defer_new,
	.defer_enable      = poller_pa_defer_enable,
	.defer_free        = poller_pa_defer_free,
	.defer_set_destroy = poller_pa_defer_set_destroy,

	.quit              = poller_pa_quit,
};

static struct pa_mainloop_api *
poller_pa_new (struct poller *self)
{
	struct poller_pa *data = xcalloc (1, sizeof *data);
	data->poller = self;

	struct pa_mainloop_api *api = xmalloc (sizeof *api);
	*api = g_poller_pa_template;
	api->userdata = data;
	return api;
}

static void
poller_pa_destroy (struct pa_mainloop_api *api)
{
	struct poller_pa *data = api->userdata;

	LIST_FOR_EACH (pa_io_event, iter, data->io_list)
		poller_pa_io_free (iter);
	LIST_FOR_EACH (pa_time_event, iter, data->time_list)
		poller_pa_time_free (iter);
	LIST_FOR_EACH (pa_defer_event, iter, data->defer_list)
		poller_pa_defer_free (iter);

	free (data);
	free (api);
}

/// Since our poller API doesn't care much about continuous operation,
/// we need to provide that in the PulseAudio abstraction itself
static int
poller_pa_run (struct pa_mainloop_api *api)
{
	struct poller_pa *data = api->userdata;
	data->running = true;
	while (data->running)
		poller_run (data->poller);
	return data->result;
}

// --- MPD client interface ----------------------------------------------------

// This is a rather thin MPD client interface intended for basic tasks

#define MPD_SUBSYSTEM_TABLE(XX)                 \
	XX (DATABASE,         0, "database")        \
	XX (UPDATE,           1, "update")          \
	XX (STORED_PLAYLIST,  2, "stored_playlist") \
	XX (PLAYLIST,         3, "playlist")        \
	XX (PLAYER,           4, "player")          \
	XX (MIXER,            5, "mixer")           \
	XX (OUTPUT,           6, "output")          \
	XX (OPTIONS,          7, "options")         \
	XX (STICKER,          8, "sticker")         \
	XX (SUBSCRIPTION,     9, "subscription")    \
	XX (MESSAGE,         10, "message")

enum mpd_subsystem
{
#define XX(a, b, c) MPD_SUBSYSTEM_ ## a = (1 << b),
	MPD_SUBSYSTEM_TABLE (XX)
#undef XX
	MPD_SUBSYSTEM_MAX
};

static const char *mpd_subsystem_names[] =
{
#define XX(a, b, c) [b] = c,
	MPD_SUBSYSTEM_TABLE (XX)
#undef XX
};

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

enum mpd_client_state
{
	MPD_DISCONNECTED,                   ///< Not connected
	MPD_CONNECTING,                     ///< Currently connecting
	MPD_CONNECTED                       ///< Connected
};

struct mpd_response
{
	bool success;                       ///< OK or ACK

	// ACK-only fields:

	int error;                          ///< Numeric error value (ack.h)
	int list_offset;                    ///< Offset of command in list
	char *current_command;              ///< Name of the erroring command
	char *message_text;                 ///< Error message
};

/// Task completion callback
typedef void (*mpd_client_task_cb) (const struct mpd_response *response,
	const struct str_vector *data, void *user_data);

struct mpd_client_task
{
	LIST_HEADER (struct mpd_client_task)

	mpd_client_task_cb callback;        ///< Callback on completion
	void *user_data;                    ///< User data
};

struct mpd_client
{
	struct poller *poller;              ///< Poller

	// Connection:

	enum mpd_client_state state;        ///< Connection state
	struct connector *connector;        ///< Connection establisher

	int socket;                         ///< MPD socket
	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 poller_timer timeout_timer;  ///< Connection seems to be dead

	// Protocol:

	bool got_hello;                     ///< Got the OK MPD hello message

	bool idling;                        ///< Sent idle as the last command
	unsigned idling_subsystems;         ///< Subsystems we're idling for
	bool in_list;                       ///< We're inside a command list

	struct mpd_client_task *tasks;      ///< Task queue
	struct mpd_client_task *tasks_tail; ///< Tail of task queue
	struct str_vector data;             ///< Data from last command

	// User configuration:

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

	/// Callback after connection has been successfully established
	void (*on_connected) (void *user_data);

	/// Callback for general failures or even normal disconnection;
	/// the interface is reinitialized
	void (*on_failure) (void *user_data);

	/// Callback to receive "idle" updates.
	/// Remember to restart the idle if needed.
	void (*on_event) (unsigned subsystems, void *user_data);
};

static void mpd_client_reset (struct mpd_client *self);
static void mpd_client_destroy_connector (struct mpd_client *self);

static void
mpd_client_init (struct mpd_client *self, struct poller *poller)
{
	memset (self, 0, sizeof *self);

	self->poller = poller;
	self->socket = -1;

	str_init (&self->read_buffer);
	str_init (&self->write_buffer);

	str_vector_init (&self->data);

	poller_fd_init (&self->socket_event, poller, -1);
	poller_timer_init (&self->timeout_timer, poller);
}

static void
mpd_client_free (struct mpd_client *self)
{
	// So that we don't have to repeat most of the stuff
	mpd_client_reset (self);

	str_free (&self->read_buffer);
	str_free (&self->write_buffer);

	str_vector_free (&self->data);
}

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

/// Reinitialize the interface so that you can reconnect anew
static void
mpd_client_reset (struct mpd_client *self)
{
	if (self->state == MPD_CONNECTING)
		mpd_client_destroy_connector (self);

	if (self->socket != -1)
		xclose (self->socket);
	self->socket = -1;

	self->socket_event.closed = true;
	poller_fd_reset (&self->socket_event);
	poller_timer_reset (&self->timeout_timer);

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

	str_vector_reset (&self->data);

	self->got_hello = false;
	self->idling = false;
	self->idling_subsystems = 0;
	self->in_list = false;

	LIST_FOR_EACH (struct mpd_client_task, iter, self->tasks)
		free (iter);
	self->tasks = self->tasks_tail = NULL;

	self->state = MPD_DISCONNECTED;
}

static void
mpd_client_fail (struct mpd_client *self)
{
	mpd_client_reset (self);
	if (self->on_failure)
		self->on_failure (self->user_data);
}

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

static bool
mpd_client_parse_response (const char *p, struct mpd_response *response)
{
	if (!strcmp (p, "OK"))
		return response->success = true;
	if (!strcmp (p, "list_OK"))
		// TODO: either implement this or fail the connection properly
		hard_assert (!"command_list_ok_begin not implemented");

	char *end = NULL;
	if (*p++ != 'A' || *p++ != 'C' || *p++ != 'K' || *p++ != ' ' || *p++ != '[')
		return false;

	errno = 0;
	response->error = strtoul (p, &end, 10);
	if (errno != 0 || end == p)
		return false;
	p = end;
	if (*p++ != '@')
		return false;

	errno = 0;
	response->list_offset = strtoul (p, &end, 10);
	if (errno != 0 || end == p)
		return false;
	p = end;
	if (*p++ != ']' || *p++ != ' ' || *p++ != '{' || !(end = strchr (p, '}')))
		return false;

	response->current_command = xstrndup (p, end - p);
	p = end + 1;

	if (*p++ != ' ')
		return false;

	response->message_text = xstrdup (p);
	response->success = false;
	return true;
}

static void
mpd_client_dispatch (struct mpd_client *self, struct mpd_response *response)
{
	struct mpd_client_task *task;
	if (!(task = self->tasks))
		return;

	if (task->callback)
		task->callback (response, &self->data, task->user_data);
	str_vector_reset (&self->data);

	LIST_UNLINK_WITH_TAIL (self->tasks, self->tasks_tail, task);
	free (task);
}

static bool
mpd_client_parse_hello (struct mpd_client *self, const char *line)
{
	const char hello[] = "OK MPD ";
	if (strncmp (line, hello, sizeof hello - 1))
	{
		print_debug ("invalid MPD hello message");
		return false;
	}

	// TODO: call "on_connected" now.  We should however also set up a timer
	//   so that we don't wait on this message forever.
	return self->got_hello = true;
}

static bool
mpd_client_parse_line (struct mpd_client *self, const char *line)
{
	print_debug ("MPD >> %s", line);

	if (!self->got_hello)
		return mpd_client_parse_hello (self, line);

	struct mpd_response response;
	memset (&response, 0, sizeof response);
	if (mpd_client_parse_response (line, &response))
	{
		mpd_client_dispatch (self, &response);
		free (response.current_command);
		free (response.message_text);
	}
	else
		str_vector_add (&self->data, line);
	return true;
}

/// All output from MPD commands seems to be in a trivial "key: value" format
static char *
mpd_client_parse_kv (char *line, char **value)
{
	char *sep;
	if (!(sep = strstr (line, ": ")))
		return NULL;

	*sep = 0;
	*value = sep + 2;
	return line;
}

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

static void
mpd_client_update_poller (struct mpd_client *self)
{
	poller_fd_set (&self->socket_event,
		self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
}

static bool
mpd_client_process_input (struct mpd_client *self)
{
	// Split socket input at newlines and process them separately
	struct str *rb = &self->read_buffer;
	char *start = rb->str, *end = start + rb->len;
	for (char *p = start; p < end; p++)
	{
		if (*p != '\n')
			continue;

		*p = 0;
		if (!mpd_client_parse_line (self, start))
			return false;
		start = p + 1;
	}

	str_remove_slice (rb, 0, start - rb->str);
	return true;
}

static void
mpd_client_on_ready (const struct pollfd *pfd, void *user_data)
{
	(void) pfd;

	struct mpd_client *self = user_data;
	if (socket_io_try_read  (self->socket, &self->read_buffer)  != SOCKET_IO_OK
	 || !mpd_client_process_input (self)
	 || socket_io_try_write (self->socket, &self->write_buffer) != SOCKET_IO_OK)
		mpd_client_fail (self);
	else
		mpd_client_update_poller (self);
}

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

static bool
mpd_client_must_quote_char (char c)
{
	return (unsigned char) c <= ' ' || c == '"' || c == '\'';
}

static bool
mpd_client_must_quote (const char *s)
{
	if (!*s)
		return true;
	for (; *s; s++)
		if (mpd_client_must_quote_char (*s))
			return true;
	return false;
}

static void
mpd_client_quote (const char *s, struct str *output)
{
	str_append_c (output, '"');
	for (; *s; s++)
	{
		if (mpd_client_must_quote_char (*s))
			str_append_c (output, '\\');
		str_append_c (output, *s);
	}
	str_append_c (output, '"');
}

/// Beware that delivery of the event isn't deferred and you musn't make
/// changes to the interface while processing the event!
static void
mpd_client_add_task
	(struct mpd_client *self, mpd_client_task_cb cb, void *user_data)
{
	// This only has meaning with command_list_ok_begin, and then it requires
	// special handling (all in-list tasks need to be specially marked and
	// later flushed if an early ACK or OK arrives).
	hard_assert (!self->in_list);

	struct mpd_client_task *task = xcalloc (1, sizeof *self);
	task->callback = cb;
	task->user_data = user_data;
	LIST_APPEND_WITH_TAIL (self->tasks, self->tasks_tail, task);
}

/// Send a command.  Remember to call mpd_client_add_task() to handle responses,
/// unless the command is being sent in a list.
static void mpd_client_send_command
	(struct mpd_client *self, const char *command, ...) ATTRIBUTE_SENTINEL;

static void
mpd_client_send_commandv (struct mpd_client *self, char **commands)
{
	// Automatically interrupt idle mode
	if (self->idling)
	{
		poller_timer_reset (&self->timeout_timer);

		self->idling = false;
		self->idling_subsystems = 0;
		mpd_client_send_command (self, "noidle", NULL);
	}

	struct str line;
	str_init (&line);

	for (; *commands; commands++)
	{
		if (line.len)
			str_append_c (&line, ' ');

		if (mpd_client_must_quote (*commands))
			mpd_client_quote (*commands, &line);
		else
			str_append (&line, *commands);
	}

	print_debug ("MPD << %s", line.str);
	str_append_c (&line, '\n');
	str_append_str (&self->write_buffer, &line);
	str_free (&line);

	mpd_client_update_poller (self);
}

static void
mpd_client_send_command (struct mpd_client *self, const char *command, ...)
{
	struct str_vector v;
	str_vector_init (&v);

	va_list ap;
	va_start (ap, command);
	for (; command; command = va_arg (ap, const char *))
		str_vector_add (&v, command);
	va_end (ap);

	mpd_client_send_commandv (self, v.vector);
	str_vector_free (&v);
}

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

static void
mpd_client_list_begin (struct mpd_client *self)
{
	hard_assert (!self->in_list);
	mpd_client_send_command (self, "command_list_begin", NULL);
	self->in_list = true;
}

/// End a list of commands.  Remember to call mpd_client_add_task()
/// to handle the summary response.
static void
mpd_client_list_end (struct mpd_client *self)
{
	hard_assert (self->in_list);
	mpd_client_send_command (self, "command_list_end", NULL);
	self->in_list = false;
}

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

static bool
mpd_resolve_subsystem (const char *name, unsigned *output)
{
	for (size_t i = 0; i < N_ELEMENTS (mpd_subsystem_names); i++)
		if (!strcasecmp_ascii (name, mpd_subsystem_names[i]))
		{
			*output |= 1 << i;
			return true;
		}
	return false;
}

static void
mpd_client_on_idle_return (const struct mpd_response *response,
	const struct str_vector *data, void *user_data)
{
	(void) response;

	struct mpd_client *self = user_data;
	unsigned subsystems = 0;
	for (size_t i = 0; i < data->len; i++)
	{
		char *value, *key;
		if (!(key = mpd_client_parse_kv (data->vector[i], &value)))
			print_debug ("%s: %s", "erroneous MPD output", data->vector[i]);
		else if (strcasecmp_ascii (key, "changed"))
			print_debug ("%s: %s", "unexpected idle key", key);
		else if (!mpd_resolve_subsystem (value, &subsystems))
			print_debug ("%s: %s", "unknown subsystem", value);
	}

	// Not resetting "idling" here, we may send an extra "noidle" no problem
	if (self->on_event && subsystems)
		self->on_event (subsystems, self->user_data);
}

static void mpd_client_idle (struct mpd_client *self, unsigned subsystems);

static void
mpd_client_on_timeout (void *user_data)
{
	struct mpd_client *self = user_data;
	unsigned subsystems = self->idling_subsystems;

	// Just sending this out should bring a dead connection down over TCP
	// TODO: set another timer to make sure the ping reply arrives
	mpd_client_send_command (self, "ping", NULL);
	mpd_client_add_task (self, NULL, NULL);

	// Restore the incriminating idle immediately
	mpd_client_idle (self, subsystems);
}

/// When not expecting to send any further commands, you should call this
/// in order to keep the connection alive.  Or to receive updates.
static void
mpd_client_idle (struct mpd_client *self, unsigned subsystems)
{
	hard_assert (!self->in_list);

	struct str_vector v;
	str_vector_init (&v);

	str_vector_add (&v, "idle");
	for (size_t i = 0; i < N_ELEMENTS (mpd_subsystem_names); i++)
		if (subsystems & (1 << i))
			str_vector_add (&v, mpd_subsystem_names[i]);

	mpd_client_send_commandv (self, v.vector);
	str_vector_free (&v);

	self->timeout_timer.dispatcher = mpd_client_on_timeout;
	self->timeout_timer.user_data = self;
	poller_timer_set (&self->timeout_timer, 5 * 60 * 1000);

	mpd_client_add_task (self, mpd_client_on_idle_return, self);
	self->idling = true;
	self->idling_subsystems = subsystems;
}

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

static void
mpd_client_finish_connection (struct mpd_client *self, int socket)
{
	set_blocking (socket, false);
	self->socket = socket;
	self->state = MPD_CONNECTED;

	poller_fd_init (&self->socket_event, self->poller, self->socket);
	self->socket_event.dispatcher = mpd_client_on_ready;
	self->socket_event.user_data = self;

	mpd_client_update_poller (self);

	if (self->on_connected)
		self->on_connected (self->user_data);
}

static void
mpd_client_destroy_connector (struct mpd_client *self)
{
	if (self->connector)
		connector_free (self->connector);
	free (self->connector);
	self->connector = NULL;

	// Not connecting anymore
	self->state = MPD_DISCONNECTED;
}

static void
mpd_client_on_connector_failure (void *user_data)
{
	struct mpd_client *self = user_data;
	mpd_client_destroy_connector (self);
	mpd_client_fail (self);
}

static void
mpd_client_on_connector_connected
	(void *user_data, int socket, const char *host)
{
	(void) host;

	struct mpd_client *self = user_data;
	mpd_client_destroy_connector (self);
	mpd_client_finish_connection (self, socket);
}

static bool
mpd_client_connect_unix (struct mpd_client *self, const char *address,
	struct error **e)
{
	int fd = socket (AF_UNIX, SOCK_STREAM, 0);
	if (fd == -1)
	{
		error_set (e, "%s: %s", "socket", strerror (errno));
		return false;
	}

	// Expand tilde if needed
	char *expanded = resolve_filename (address, xstrdup);

	struct sockaddr_un sun;
	sun.sun_family = AF_UNIX;
	strncpy (sun.sun_path, expanded, sizeof sun.sun_path);
	sun.sun_path[sizeof sun.sun_path - 1] = 0;

	free (expanded);

	if (connect (fd, (struct sockaddr *) &sun, sizeof sun))
	{
		error_set (e, "%s: %s", "connect", strerror (errno));
		return false;
	}

	mpd_client_finish_connection (self, fd);
	return true;
}

static bool
mpd_client_connect (struct mpd_client *self, const char *address,
	const char *service, struct error **e)
{
	hard_assert (self->state == MPD_DISCONNECTED);

	// If it looks like a path, assume it's a UNIX socket
	if (strchr (address, '/'))
		return mpd_client_connect_unix (self, address, e);

	struct connector *connector = xmalloc (sizeof *connector);
	connector_init (connector, self->poller);
	self->connector = connector;

	connector->user_data    = self;
	connector->on_connected = mpd_client_on_connector_connected;
	connector->on_failure   = mpd_client_on_connector_failure;

	connector_add_target (connector, address, service);
	self->state = MPD_CONNECTING;
	return true;
}

// --- NUT ---------------------------------------------------------------------

// More or less copied and pasted from the MPD client.  This code doesn't even
// deserve much love, the protocol is somehow even worse than MPD's.
//
// http://www.networkupstools.org/docs/developer-guide.chunked/ar01s09.html

// This was written by loosely following the top comment in NUT's parseconf.c.

enum nut_parser_state
{
	NUT_STATE_START_LINE,               ///< Start of a line
	NUT_STATE_BETWEEN,                  ///< Between words, expecting non-WS
	NUT_STATE_UNQUOTED,                 ///< Within unquoted word
	NUT_STATE_UNQUOTED_ESCAPE,          ///< Dtto after a backslash
	NUT_STATE_QUOTED,                   ///< Within a quoted word
	NUT_STATE_QUOTED_ESCAPE,            ///< Dtto after a backslash
	NUT_STATE_QUOTED_END                ///< End of word, expecting WS
};

struct nut_parser
{
	enum nut_parser_state state;        ///< Parser state
	struct str current_field;           ///< Current field

	// Public:

	struct str_vector fields;           ///< Line fields
};

static void
nut_parser_init (struct nut_parser *self)
{
	self->state = NUT_STATE_START_LINE;
	str_init (&self->current_field);
	str_vector_init (&self->fields);
}

static void
nut_parser_free (struct nut_parser *self)
{
	str_free (&self->current_field);
	str_vector_free (&self->fields);
}

static int
nut_parser_end_field (struct nut_parser *self, char c)
{
	str_vector_add (&self->fields, self->current_field.str);
	str_reset (&self->current_field);

	if (c == '\n')
	{
		self->state = NUT_STATE_START_LINE;
		return 1;
	}

	self->state = NUT_STATE_BETWEEN;
	return 0;
}

/// Returns 1 if a complete line has been read, -1 on error, 0 otherwise
static int
nut_parser_push (struct nut_parser *self, char c)
{
	switch (self->state)
	{
	case NUT_STATE_START_LINE:
		str_vector_reset (&self->fields);
		str_reset (&self->current_field);
		self->state = NUT_STATE_BETWEEN;
		// Fall-through

	case NUT_STATE_BETWEEN:
		if (c == '\\')
			self->state = NUT_STATE_UNQUOTED_ESCAPE;
		else if (c == '"')
			self->state = NUT_STATE_QUOTED;
		else if (c == '\n' && self->fields.len)
		{
			self->state = NUT_STATE_START_LINE;
			return 1;
		}
		else if (!isspace_ascii (c))
		{
			str_append_c (&self->current_field, c);
			self->state = NUT_STATE_UNQUOTED;
		}
		return 0;

	case NUT_STATE_UNQUOTED:
		if (c == '\\')
			self->state = NUT_STATE_UNQUOTED_ESCAPE;
		else if (c == '"')
			return -1;
		else if (!isspace_ascii (c))
			str_append_c (&self->current_field, c);
		else
			return nut_parser_end_field (self, c);
		return 0;

	case NUT_STATE_UNQUOTED_ESCAPE:
		str_append_c (&self->current_field, c);
		self->state = NUT_STATE_UNQUOTED;
		return 0;

	case NUT_STATE_QUOTED:
		if (c == '\\')
			self->state = NUT_STATE_QUOTED_ESCAPE;
		else if (c == '"')
			self->state = NUT_STATE_QUOTED_END;
		else
			str_append_c (&self->current_field, c);
		return 0;

	case NUT_STATE_QUOTED_ESCAPE:
		str_append_c (&self->current_field, c);
		self->state = NUT_STATE_QUOTED;
		return 0;

	case NUT_STATE_QUOTED_END:
		if (!isspace_ascii (c))
			return -1;
		return nut_parser_end_field (self, c);
	}

	// Silence the compiler
	hard_assert (!"unhandled NUT parser state");
	return -1;
}

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

struct nut_line
{
	LIST_HEADER (struct nut_line)

	struct str_vector fields;           ///< Parsed fields from the line
};

struct nut_response
{
	struct nut_line *data;              ///< Raw result data

	bool success;                       ///< Whether a general failure occured
	char *message;                      ///< Eventually an error ID string
};

/// Task completion callback
typedef void (*nut_client_task_cb)
	(const struct nut_response *response, void *user_data);

struct nut_client_task
{
	LIST_HEADER (struct nut_client_task)

	nut_client_task_cb callback;        ///< Callback on completion
	void *user_data;                    ///< User data
};

enum nut_client_state
{
	NUT_DISCONNECTED,                   ///< Not connected
	NUT_CONNECTING,                     ///< Currently connecting
	NUT_CONNECTED                       ///< Connected
};

struct nut_client
{
	struct poller *poller;              ///< Poller

	// Connection:

	enum nut_client_state state;        ///< Connection state
	struct connector *connector;        ///< Connection establisher

	int socket;                         ///< MPD socket
	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

	// Protocol:

	struct nut_parser parser;           ///< Protocol parser
	struct nut_line *data;              ///< Data from last command
	struct nut_line *data_tail;         ///< Tail of data list
	bool in_list;                       ///< Currently within a list

	struct nut_client_task *tasks;      ///< Task queue
	struct nut_client_task *tasks_tail; ///< Tail of task queue

	// User configuration:

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

	/// Callback after connection has been successfully established
	void (*on_connected) (void *user_data);

	/// Callback for general failures or even normal disconnection;
	/// the interface is reinitialized
	void (*on_failure) (void *user_data);
};

static void nut_client_reset (struct nut_client *self);
static void nut_client_destroy_connector (struct nut_client *self);

static void
nut_client_init (struct nut_client *self, struct poller *poller)
{
	memset (self, 0, sizeof *self);

	self->poller = poller;
	self->socket = -1;

	str_init (&self->read_buffer);
	str_init (&self->write_buffer);

	nut_parser_init (&self->parser);

	poller_fd_init (&self->socket_event, poller, -1);
}

static void
nut_client_free (struct nut_client *self)
{
	// So that we don't have to repeat most of the stuff
	nut_client_reset (self);

	str_free (&self->read_buffer);
	str_free (&self->write_buffer);

	nut_parser_free (&self->parser);
}

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

static void
nut_client_flush_data (struct nut_client *self)
{
	LIST_FOR_EACH (struct nut_line, iter, self->data)
	{
		str_vector_free (&iter->fields);
		free (iter);
	}
	self->data = self->data_tail = NULL;
}

/// Reinitialize the interface so that you can reconnect anew
static void
nut_client_reset (struct nut_client *self)
{
	if (self->state == NUT_CONNECTING)
		nut_client_destroy_connector (self);

	if (self->socket != -1)
		xclose (self->socket);
	self->socket = -1;

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

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

	self->parser.state = NUT_STATE_START_LINE;
	nut_client_flush_data (self);
	self->in_list = false;

	LIST_FOR_EACH (struct nut_client_task, iter, self->tasks)
		free (iter);
	self->tasks = self->tasks_tail = NULL;

	self->state = NUT_DISCONNECTED;
}

static void
nut_client_fail (struct nut_client *self)
{
	nut_client_reset (self);
	if (self->on_failure)
		self->on_failure (self->user_data);
}

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

static void
nut_client_quote (const char *s, struct str *output)
{
	str_append_c (output, '"');
	for (; *s; s++)
	{
		if (*s == '"' || *s == '\\')
			str_append_c (output, '\\');
		str_append_c (output, *s);
	}
	str_append_c (output, '"');
}

static bool
nut_client_must_quote (const char *s)
{
	if (!*s)
		return true;
	for (; *s; s++)
		if ((unsigned char) *s <= ' ' || *s == '"' || *s == '\\')
			return true;
	return false;
}

static void
nut_client_serialize (char **commands, struct str *line)
{
	for (; *commands; commands++)
	{
		if (line->len)
			str_append_c (line, ' ');

		if (nut_client_must_quote (*commands))
			nut_client_quote (*commands, line);
		else
			str_append (line, *commands);
	}
}

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

static void
nut_client_dispatch (struct nut_client *self, struct nut_response *response)
{
	struct nut_client_task *task;
	if (!(task = self->tasks))
		return;

	if (task->callback)
		task->callback (response, task->user_data);
	nut_client_flush_data (self);

	LIST_UNLINK_WITH_TAIL (self->tasks, self->tasks_tail, task);
	free (task);
}

static bool
nut_client_parse_line (struct nut_client *self)
{
	struct str reconstructed;
	str_init (&reconstructed);
	nut_client_serialize (self->parser.fields.vector, &reconstructed);
	print_debug ("NUT >> %s", reconstructed.str);
	str_free (&reconstructed);

	struct str_vector *fields = &self->parser.fields;
	hard_assert (fields->len != 0);

	// Lists are always dispatched as their innards (and they can be empty)
	if (fields->len >= 2
	 && !strcmp (fields->vector[0], "BEGIN")
	 && !strcmp (fields->vector[1], "LIST"))
		self->in_list = true;
	else if (fields->len >= 2
	 && !strcmp (fields->vector[0], "END")
	 && !strcmp (fields->vector[1], "LIST"))
		self->in_list = false;
	else
	{
		struct nut_line *line = xcalloc (1, sizeof *line);
		str_vector_init (&line->fields);
		str_vector_add_vector (&line->fields, fields->vector);
		LIST_APPEND_WITH_TAIL (self->data, self->data_tail, line);
	}

	if (!self->in_list)
	{
		struct nut_response response;
		memset (&response, 0, sizeof response);
		response.success = true;
		response.data = self->data;

		if (!strcmp (fields->vector[0], "ERR"))
		{
			response.success = false;
			if (fields->len < 2)
				return false;
			response.message = xstrdup (fields->vector[1]);
		}

		nut_client_dispatch (self, &response);
		free (response.message);
	}
	return true;
}

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

static void
nut_client_update_poller (struct nut_client *self)
{
	poller_fd_set (&self->socket_event,
		self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
}

static bool
nut_client_process_input (struct nut_client *self)
{
	struct str *rb = &self->read_buffer;
	for (size_t i = 0; i < rb->len; i++)
	{
		int res = nut_parser_push (&self->parser, rb->str[i]);
		if (res == -1 || (res == 1 && !nut_client_parse_line (self)))
			return false;
	}

	str_reset (rb);
	return true;
}

static void
nut_client_on_ready (const struct pollfd *pfd, void *user_data)
{
	(void) pfd;

	struct nut_client *self = user_data;
	bool read_succeeded = socket_io_try_read
		(self->socket, &self->read_buffer) == SOCKET_IO_OK;

	// Whether or not the read was successful, we need to process all data
	if (!nut_client_process_input (self) || !read_succeeded
	 || socket_io_try_write (self->socket, &self->write_buffer) != SOCKET_IO_OK)
		nut_client_fail (self);
	else
		nut_client_update_poller (self);
}

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

/// Beware that delivery of the event isn't deferred and you musn't make
/// changes to the interface while processing the event!
static void
nut_client_add_task
	(struct nut_client *self, nut_client_task_cb cb, void *user_data)
{
	struct nut_client_task *task = xcalloc (1, sizeof *self);
	task->callback = cb;
	task->user_data = user_data;
	LIST_APPEND_WITH_TAIL (self->tasks, self->tasks_tail, task);
}

/// Send a command.  Remember to call nut_client_add_task() to handle responses,
/// unless the command generates none.
static void nut_client_send_command
	(struct nut_client *self, const char *command, ...) ATTRIBUTE_SENTINEL;

static void
nut_client_send_commandv (struct nut_client *self, char **commands)
{
	struct str line;
	str_init (&line);
	nut_client_serialize (commands, &line);

	print_debug ("NUT << %s", line.str);
	str_append_c (&line, '\n');
	str_append_str (&self->write_buffer, &line);
	str_free (&line);

	nut_client_update_poller (self);
}

static void
nut_client_send_command (struct nut_client *self, const char *command, ...)
{
	struct str_vector v;
	str_vector_init (&v);

	va_list ap;
	va_start (ap, command);
	for (; command; command = va_arg (ap, const char *))
		str_vector_add (&v, command);
	va_end (ap);

	nut_client_send_commandv (self, v.vector);
	str_vector_free (&v);
}

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

static void
nut_client_finish_connection (struct nut_client *self, int socket)
{
	set_blocking (socket, false);
	self->socket = socket;
	self->state = NUT_CONNECTED;

	poller_fd_init (&self->socket_event, self->poller, self->socket);
	self->socket_event.dispatcher = nut_client_on_ready;
	self->socket_event.user_data = self;

	nut_client_update_poller (self);

	if (self->on_connected)
		self->on_connected (self->user_data);
}

static void
nut_client_destroy_connector (struct nut_client *self)
{
	if (self->connector)
		connector_free (self->connector);
	free (self->connector);
	self->connector = NULL;

	// Not connecting anymore
	self->state = NUT_DISCONNECTED;
}

static void
nut_client_on_connector_failure (void *user_data)
{
	struct nut_client *self = user_data;
	nut_client_destroy_connector (self);
	nut_client_fail (self);
}

static void
nut_client_on_connector_connected
	(void *user_data, int socket, const char *host)
{
	(void) host;

	struct nut_client *self = user_data;
	nut_client_destroy_connector (self);
	nut_client_finish_connection (self, socket);
}

static void
nut_client_connect
	(struct nut_client *self, const char *address, const char *service)
{
	hard_assert (self->state == NUT_DISCONNECTED);

	struct connector *connector = xmalloc (sizeof *connector);
	connector_init (connector, self->poller);
	self->connector = connector;

	connector->user_data    = self;
	connector->on_connected = nut_client_on_connector_connected;
	connector->on_failure   = nut_client_on_connector_failure;

	connector_add_target (connector, address, service);
	self->state = NUT_CONNECTING;
}

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

static struct simple_config_item g_config_table[] =
{
	{ "mpd_address",     "localhost",       "MPD host or socket"             },
	{ "mpd_service",     "6600",            "MPD service name or port"       },
	{ "mpd_password",    NULL,              "MPD password"                   },

	{ "nut_enabled",     "off",             "NUT UPS status reading enabled" },
	{ "nut_load_thld",   "50",              "NUT threshold for load display" },

	{ NULL,              NULL,              NULL                             }
};

// --- Application -------------------------------------------------------------

struct app_context
{
	struct str_map config;              ///< Program configuration

	Display *dpy;                       ///< X display handle
	const char *prefix;                 ///< User-defined prefix

	struct poller poller;               ///< Poller
	struct poller_timer time_changed;   ///< Time change timer
	struct poller_timer make_context;   ///< Start PulseAudio communication
	struct poller_timer refresh_rest;   ///< Refresh unpollable information

	// Hotkeys:

	struct poller_fd x_event;           ///< X11 event

	// MPD:

	struct poller_timer mpd_reconnect;  ///< Start MPD communication
	struct mpd_client mpd_client;       ///< MPD client

	char *mpd_song;                     ///< MPD current song
	char *mpd_status;                   ///< MPD status (overrides song)

	// NUT:

	struct poller_timer nut_reconnect;  ///< Start NUT communication
	struct nut_client nut_client;       ///< NUT client
	struct str_map nut_ups_info;        ///< Per-UPS information
	bool nut_success;                   ///< Information retrieved successfully

	char *nut_status;                   ///< NUT status

	// PulseAudio:

	pa_mainloop_api *api;               ///< PulseAudio event loop proxy
	pa_context *context;                ///< PulseAudio connection context

	bool failed;                        ///< General PulseAudio failure

	char *sink_name;                    ///< The sink to watch volume of
	pa_cvolume volume;                  ///< Current volume
	bool muted;                         ///< Currently muted?
};

static void
str_map_destroy (void *self)
{
	str_map_free (self);
	free (self);
}

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

	str_map_init (&self->config);
	self->config.free = free;
	simple_config_load_defaults (&self->config, g_config_table);

	if (!(self->dpy = XOpenDisplay (NULL)))
		exit_fatal ("cannot open display");

	poller_init (&self->poller);
	self->api = poller_pa_new (&self->poller);

	poller_fd_init (&self->x_event, &self->poller,
		ConnectionNumber (self->dpy));

	mpd_client_init (&self->mpd_client, &self->poller);

	nut_client_init (&self->nut_client, &self->poller);
	str_map_init (&self->nut_ups_info);
	self->nut_ups_info.free = str_map_destroy;
}

static void
app_context_free (struct app_context *self)
{
	str_map_free (&self->config);

	poller_fd_reset (&self->x_event);

	if (self->context)  pa_context_unref (self->context);
	if (self->dpy)      XCloseDisplay (self->dpy);

	poller_pa_destroy (self->api);
	poller_free (&self->poller);

	mpd_client_free (&self->mpd_client);
	free (self->mpd_song);
	free (self->mpd_status);

	nut_client_free (&self->nut_client);
	str_map_free (&self->nut_ups_info);

	free (self->sink_name);
}

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

static char *
read_value (int dir, const char *filename, struct error **e)
{
	int fd = openat (dir, filename, O_RDONLY);
	if (fd < 0)
	{
		error_set (e, "%s: %s: %s", filename, "openat", strerror (errno));
		return NULL;
	}

	FILE *fp = fdopen (fd, "r");
	if (!fp)
	{
		error_set (e, "%s: %s: %s", filename, "fdopen", strerror (errno));
		close (fd);
		return NULL;
	}

	struct str s;
	str_init (&s);
	bool success = read_line (fp, &s);
	fclose (fp);

	if (!success)
	{
		error_set (e, "%s: %s", filename, "read failed");
		return NULL;
	}
	return str_steal (&s);
}

static unsigned long
read_number (int dir, const char *filename, struct error **e)
{
	char *value;
	if (!(value = read_value (dir, filename, e)))
		return false;

	unsigned long number = 0;
	if (!xstrtoul (&number, value, 10))
		error_set (e, "%s: %s", filename, "doesn't contain an unsigned number");
	free (value);
	return number;
}

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

static char *
read_battery_status (int dir, struct error **e)
{
	char *result = NULL;
	struct error *error = NULL;

	char *status;
	double charge_now;
	double charge_full;

	if ((status      = read_value  (dir, "status",      &error), error)
	 || (charge_now  = read_number (dir, "charge_now",  &error), error)
	 || (charge_full = read_number (dir, "charge_full", &error), error))
		error_propagate (e, error);
	else
		result = xstrdup_printf ("%s (%u%%)",
			status, (unsigned) (charge_now / charge_full * 100 + 0.5));

	free (status);
	return result;
}

static char *
try_power_supply (int dir, struct error **e)
{
	char *type;
	struct error *error = NULL;
	if (!(type = read_value (dir, "type", &error)))
	{
		error_propagate (e, error);
		return NULL;
	}

	bool is_relevant =
		!strcmp (type, "Battery") ||
		!strcmp (type, "UPS");

	char *result = NULL;
	if (is_relevant)
	{
		char *status = read_battery_status (dir, &error);
		if (error)
			error_propagate (e, error);
		if (status)
			result = xstrdup_printf ("%s %s", type, status);
		free (status);
	}
	free (type);
	return result;
}

static char *
make_battery_status (void)
{
	DIR *power_supply = opendir ("/sys/class/power_supply");
	if (!power_supply)
	{
		print_debug ("cannot access %s: %s: %s",
			"/sys/class/power_supply", "opendir", strerror (errno));
		return NULL;
	}

	struct dirent *entry;
	char *status = NULL;
	while (!status && (entry = readdir (power_supply)))
	{
		const char *device_name = entry->d_name;
		if (device_name[0] == '.')
			continue;

		int dir = openat (dirfd (power_supply), device_name, O_RDONLY);
		if (dir < 0)
		{
			print_error ("%s: %s: %s", device_name, "openat", strerror (errno));
			continue;
		}

		struct error *error = NULL;
		status = try_power_supply (dir, &error);
		close (dir);
		if (error)
		{
			print_error ("%s: %s", device_name, error->message);
			error_free (error);
		}
	}
	closedir (power_supply);
	return status;
}

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

static char *
make_time_status (char *fmt)
{
	char buf[129] = "";
	time_t now = time (NULL);
	struct tm *local = localtime (&now);

	if (local == NULL)
		exit_fatal ("%s: %s", "localtime", strerror (errno));
	if (!strftime (buf, sizeof buf, fmt, local))
		exit_fatal ("strftime == 0");

	return xstrdup (buf);
}

#define VOLUME_PERCENT(x) (((x) * 100 + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM)

static char *
make_volume_status (struct app_context *ctx)
{
	if (!ctx->volume.channels)
		return xstrdup ("");

	struct str s;
	str_init (&s);
	str_append_printf (&s, "%u%%", VOLUME_PERCENT (ctx->volume.values[0]));
	if (!pa_cvolume_channels_equal_to (&ctx->volume, ctx->volume.values[0]))
		for (size_t i = 1; i < ctx->volume.channels; i++)
			str_append_printf (&s, " / %u%%",
				VOLUME_PERCENT (ctx->volume.values[i]));
	return str_steal (&s);
}

static void
refresh_status (struct app_context *ctx)
{
	struct str status;
	str_init (&status);

	if (ctx->prefix)
		str_append_printf (&status, "%s   ", ctx->prefix);

	if (ctx->mpd_status)
		str_append_printf (&status, "%s   ", ctx->mpd_status);
	else if (ctx->mpd_song)
		str_append_printf (&status, "%s   ", ctx->mpd_song);

	if (ctx->failed)
		str_append_printf (&status, "%s   ", "PA failure");
	else
	{
		char *volumes = make_volume_status (ctx);
		str_append_printf (&status, "%s %s   ",
			ctx->muted ? "Muted" : "Volume", volumes);
		free (volumes);
	}

	char *battery = make_battery_status ();
	if (battery)
		str_append_printf (&status, "%s   ", battery);
	free (battery);

	if (ctx->nut_status)
		str_append_printf (&status, "%s   ", ctx->nut_status);

	char *times = make_time_status ("Week %V, %a %d %b %Y %H:%M %Z");
	str_append (&status, times);
	free (times);

	set_dwm_status (ctx->dpy, status.str);
	str_free (&status);
}

static void
on_time_changed (void *user_data)
{
	struct app_context *ctx = user_data;
	refresh_status (ctx);

	const time_t now = time (NULL);
	const time_t next = (now / 60 + 1) * 60;
	poller_timer_set (&ctx->time_changed, (next - now) * 1000);
}

static void
on_refresh_rest (void *user_data)
{
	struct app_context *ctx = user_data;

	// We cannot use poll() on most sysfs entries, including battery charge

	refresh_status (ctx);
	poller_timer_set (&ctx->refresh_rest, 5000);
}

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

// Sometimes it's not that easy and there can be repeating entries
static void
mpd_vector_to_map (const struct str_vector *data, struct str_map *map)
{
	str_map_init (map);
	map->key_xfrm = tolower_ascii_strxfrm;
	map->free = free;

	char *key, *value;
	for (size_t i = 0; i < data->len; i++)
	{
		if ((key = mpd_client_parse_kv (data->vector[i], &value)))
			str_map_set (map, key, xstrdup (value));
		else
			print_debug ("%s: %s", "erroneous MPD output", data->vector[i]);
	}
}

static void
mpd_on_info_response (const struct mpd_response *response,
	const struct str_vector *data, void *user_data)
{
	if (!response->success)
	{
		print_debug ("%s: %s",
			"retrieving MPD info failed", response->message_text);
		return;
	}

	struct app_context *ctx = user_data;
	struct str_map map;
	mpd_vector_to_map (data, &map);

	free (ctx->mpd_status);
	ctx->mpd_status = NULL;

	const char *value;
	if ((value = str_map_find (&map, "state")))
	{
		if (!strcmp (value, "stop"))
			ctx->mpd_status = xstrdup ("MPD stopped");
		if (!strcmp (value, "pause"))
			ctx->mpd_status = xstrdup ("MPD paused");
	}

	struct str s;
	str_init (&s);

	str_append (&s, "Playing: ");
	if ((value = str_map_find (&map, "title"))
	 || (value = str_map_find (&map, "name"))
	 || (value = str_map_find (&map, "file")))
		str_append_printf (&s, "\"%s\"", value);
	if ((value = str_map_find (&map, "artist")))
		str_append_printf (&s, " by \"%s\"", value);
	if ((value = str_map_find (&map, "album")))
		str_append_printf (&s, " from \"%s\"", value);

	free (ctx->mpd_song);
	ctx->mpd_song = str_steal (&s);

	refresh_status (ctx);
	str_map_free (&map);
}

static void
mpd_request_info (struct app_context *ctx)
{
	struct mpd_client *c = &ctx->mpd_client;

	mpd_client_list_begin (c);
	mpd_client_send_command (c, "currentsong", NULL);
	mpd_client_send_command (c, "status", NULL);
	mpd_client_list_end (c);
	mpd_client_add_task (c, mpd_on_info_response, ctx);

	mpd_client_idle (c, 0);
}

static void
mpd_on_events (unsigned subsystems, void *user_data)
{
	struct app_context *ctx = user_data;
	struct mpd_client *c = &ctx->mpd_client;

	if (subsystems & (MPD_SUBSYSTEM_PLAYER | MPD_SUBSYSTEM_PLAYLIST))
		mpd_request_info (ctx);
	else
		mpd_client_idle (c, 0);
}

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

static void
mpd_queue_reconnect (struct app_context *ctx)
{
	poller_timer_set (&ctx->mpd_reconnect, 30 * 1000);
}

static void
mpd_on_password_response (const struct mpd_response *response,
	const struct str_vector *data, void *user_data)
{
	(void) data;
	struct app_context *ctx = user_data;
	struct mpd_client *c = &ctx->mpd_client;

	if (response->success)
		mpd_request_info (ctx);
	else
	{
		print_error ("%s: %s",
			"couldn't authenticate to MPD", response->message_text);
		mpd_client_send_command (c, "close", NULL);
	}
}

static void
mpd_on_connected (void *user_data)
{
	struct app_context *ctx = user_data;
	struct mpd_client *c = &ctx->mpd_client;

	const char *password = str_map_find (&ctx->config, "mpd_password");
	if (password)
	{
		mpd_client_send_command (c, "password", password, NULL);
		mpd_client_add_task (c, mpd_on_password_response, ctx);
	}
	else
		mpd_request_info (ctx);
}

static void
mpd_on_failure (void *user_data)
{
	// This is also triggered both by a failed connect and a clean disconnect
	struct app_context *ctx = user_data;
	print_error ("connection to MPD failed");
	mpd_queue_reconnect (ctx);
}

static void
on_mpd_reconnect (void *user_data)
{
	struct app_context *ctx = user_data;

	struct mpd_client *c = &ctx->mpd_client;
	c->user_data    = ctx;
	c->on_failure   = mpd_on_failure;
	c->on_connected = mpd_on_connected;
	c->on_event     = mpd_on_events;

	struct error *e = NULL;
	if (!mpd_client_connect (&ctx->mpd_client,
		str_map_find (&ctx->config, "mpd_address"),
		str_map_find (&ctx->config, "mpd_service"), &e))
	{
		print_error ("%s: %s", "cannot connect to MPD", e->message);
		error_free (e);
		mpd_queue_reconnect (ctx);
	}
}

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

static bool
nut_common_handler (const struct nut_response *response)
{
	if (response->success)
		return true;

	print_error ("%s: %s", "retrieving NUT info failed", response->message);
	return false;
}

static void
nut_translate_status (const char *status, struct str_vector *out)
{
	// https://github.com/networkupstools/nut/blob/master/clients/status.h
	if (!strcmp (status, "OL"))      str_vector_add (out, "on-line");
	if (!strcmp (status, "OB"))      str_vector_add (out, "on battery");
	if (!strcmp (status, "LB"))      str_vector_add (out, "low battery");
	if (!strcmp (status, "RB"))      str_vector_add (out, "replace battery");
	if (!strcmp (status, "CHRG"))    str_vector_add (out, "charging");
	if (!strcmp (status, "DISCHRG")) str_vector_add (out, "discharging");
	if (!strcmp (status, "OVER"))    str_vector_add (out, "overload");
	if (!strcmp (status, "OFF"))     str_vector_add (out, "off");
	if (!strcmp (status, "TRIM"))    str_vector_add (out, "voltage trim");
	if (!strcmp (status, "BOOST"))   str_vector_add (out, "voltage boost");
	if (!strcmp (status, "BYPASS"))  str_vector_add (out, "bypass");
}

static char *
interval_string (unsigned long seconds)
{
	unsigned long hours = seconds / 3600; seconds %= 3600;
	unsigned long mins  = seconds /   60; seconds %=   60;
	return xstrdup_printf ("%lu:%02lu:%02lu", hours, mins, seconds);
}

static void
nut_process_ups (struct app_context *ctx, struct str_vector *ups_list,
	const char *ups_name, struct str_map *dict)
{
	// Not currently interested in this kind of information;
	// maybe if someone had more than one UPS installed
	(void) ups_name;

	// http://www.networkupstools.org/docs/developer-guide.chunked/apas01.html
	const char *status  = str_map_find (dict, "ups.status");
	const char *charge  = str_map_find (dict, "battery.charge");
	const char *runtime = str_map_find (dict, "battery.runtime");
	const char *load    = str_map_find (dict, "ups.load");

	if (!soft_assert (status && charge && runtime))
		return;

	unsigned long runtime_sec;
	if (!soft_assert (xstrtoul (&runtime_sec, runtime, 10)))
		return;

	struct str_vector items;
	str_vector_init (&items);

	bool running_on_batteries = false;

	struct str_vector v;
	str_vector_init (&v);
	cstr_split_ignore_empty (status, ' ', &v);
	for (size_t i = 0; i < v.len; i++)
	{
		const char *status = v.vector[i];
		nut_translate_status (status, &items);

		if (!strcmp (status, "OB"))
			running_on_batteries = true;
	}
	str_vector_free (&v);

	if (running_on_batteries || strcmp (charge, "100"))
		str_vector_add_owned (&items, xstrdup_printf ("%s%%", charge));
	if (running_on_batteries)
		str_vector_add_owned (&items, interval_string (runtime_sec));

	// Only show load if it's higher than the threshold so as to not distract
	const char *threshold = str_map_find (&ctx->config, "nut_load_thld");
	unsigned long load_n, threshold_n;
	if (load
	 && xstrtoul (&load_n,      load,      10)
	 && xstrtoul (&threshold_n, threshold, 10)
	 && load_n >= threshold_n)
		str_vector_add_owned (&items, xstrdup_printf ("load %s%%", load));

	struct str result;
	str_init (&result);
	str_append (&result, "UPS: ");

	for (size_t i = 0; i < items.len; i++)
	{
		if (i) str_append (&result, "; ");
		str_append (&result, items.vector[i]);
	}
	str_vector_free (&items);
	str_vector_add_owned (ups_list, str_steal (&result));
}

static void
nut_on_logout_response (const struct nut_response *response, void *user_data)
{
	if (!nut_common_handler (response))
		return;

	struct app_context *ctx = user_data;
	struct str_vector ups_list;
	str_vector_init (&ups_list);

	struct str_map_iter iter;
	str_map_iter_init (&iter, &ctx->nut_ups_info);
	struct str_map *dict;
	while ((dict = str_map_iter_next (&iter)))
		nut_process_ups (ctx, &ups_list, iter.link->key, dict);

	free (ctx->nut_status);
	ctx->nut_status = NULL;

	if (ups_list.len)
	{
		struct str status;
		str_init (&status);
		str_append (&status, ups_list.vector[0]);
		for (size_t i = 1; i < ups_list.len; i++)
			str_append_printf (&status, "   %s", ups_list.vector[0]);
		ctx->nut_status = str_steal (&status);
	}

	ctx->nut_success = true;
	str_vector_free (&ups_list);
	refresh_status (ctx);
}

static void
nut_store_var (struct app_context *ctx,
	const char *ups_name, const char *key, const char *value)
{
	struct str_map *map;
	if (!(map = str_map_find (&ctx->nut_ups_info, ups_name)))
	{
		str_map_init ((map = xcalloc (1, sizeof *map)));
		map->free = free;
		str_map_set (&ctx->nut_ups_info, ups_name, map);
	}

	str_map_set (map, key, xstrdup (value));
}

static void
nut_on_var_response (const struct nut_response *response, void *user_data)
{
	if (!nut_common_handler (response))
		return;

	struct app_context *ctx = user_data;
	LIST_FOR_EACH (struct nut_line, iter, response->data)
	{
		const struct str_vector *fields = &iter->fields;
		if (!soft_assert (fields->len >= 4
		 && !strcmp (fields->vector[0], "VAR")))
			continue;

		nut_store_var (ctx, fields->vector[1],
			fields->vector[2], fields->vector[3]);
	}
}

static void
nut_on_list_ups_response (const struct nut_response *response, void *user_data)
{
	if (!nut_common_handler (response))
		return;

	struct app_context *ctx = user_data;
	struct nut_client *c = &ctx->nut_client;

	// Then we list all their properties and terminate the connection
	LIST_FOR_EACH (struct nut_line, iter, response->data)
	{
		const struct str_vector *fields = &iter->fields;
		if (!soft_assert (fields->len >= 2
		 && !strcmp (fields->vector[0], "UPS")))
			continue;

		nut_client_send_command (c, "LIST", "VAR", fields->vector[1], NULL);
		nut_client_add_task (c, nut_on_var_response, ctx);
	}

	nut_client_send_command (c, "LOGOUT", NULL);
	nut_client_add_task (c, nut_on_logout_response, ctx);
}

static void
nut_on_connected (void *user_data)
{
	struct app_context *ctx = user_data;
	struct nut_client *c = &ctx->nut_client;

	// First we list all available UPS devices
	nut_client_send_command (c, "LIST", "UPS", NULL);
	nut_client_add_task (c, nut_on_list_ups_response, ctx);
}

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

static void
nut_indicate_failure (struct app_context *ctx)
{
	free (ctx->nut_status);
	ctx->nut_status = xstrdup ("NUT failure");

	refresh_status (ctx);
}

static void
nut_on_failure (void *user_data)
{
	struct app_context *ctx = user_data;

	// This is also triggered both by a failed connect and a clean disconnect
	if (!ctx->nut_success)
	{
		print_error ("connection to NUT failed");
		nut_indicate_failure (ctx);
	}
}

static void
on_nut_reconnect (void *user_data)
{
	struct app_context *ctx = user_data;

	bool want_nut = false;
	if (!set_boolean_if_valid (&want_nut,
		str_map_find (&ctx->config, "nut_enabled")))
		print_error ("invalid configuration value for `%s'", "nut_enabled");
	if (!want_nut)
		return;

	struct nut_client *c = &ctx->nut_client;
	c->user_data    = ctx;
	c->on_failure   = nut_on_failure;
	c->on_connected = nut_on_connected;

	// So that we don't have to maintain a separate timeout timer,
	// we keep a simple periodic reconnect timer
	if (c->state != NUT_DISCONNECTED)
	{
		print_error ("failed to retrieve NUT status within the interval");
		nut_indicate_failure (ctx);
		nut_client_reset (c);
	}

	str_map_clear (&ctx->nut_ups_info);

	nut_client_connect (&ctx->nut_client, "localhost", "3493");
	ctx->nut_success = false;
	poller_timer_set (&ctx->nut_reconnect, 10 * 1000);
}

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

static void
on_sink_info
	(pa_context *context, const pa_sink_info *info, int eol, void *userdata)
{
	(void) context;

	if (!info && eol)
		return;

	struct app_context *ctx = userdata;
	ctx->volume = info->volume;
	ctx->muted = !!info->mute;
	refresh_status (ctx);
}

static void
update_volume (struct app_context *ctx)
{
	pa_operation_unref (pa_context_get_sink_info_by_name
		(ctx->context, ctx->sink_name, on_sink_info, ctx));
}

static void
on_server_info (pa_context *context, const pa_server_info *info, void *userdata)
{
	(void) context;

	struct app_context *ctx = userdata;
	free (ctx->sink_name);
	ctx->sink_name = strdup (info->default_sink_name);
	update_volume (ctx);
}

static void
on_event (pa_context *context, pa_subscription_event_type_t event,
	uint32_t index, void *userdata)
{
	(void) context;
	(void) index;

	struct app_context *ctx = userdata;
	pa_subscription_event_type_t facility =
		event & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
	pa_subscription_event_type_t type =
		event & PA_SUBSCRIPTION_EVENT_TYPE_MASK;

	// XXX: can the default sink be removed before being changed?
	if (facility == PA_SUBSCRIPTION_EVENT_SINK
	 && type == PA_SUBSCRIPTION_EVENT_CHANGE)
		update_volume (ctx);

	// Default sink could change
	if (facility == PA_SUBSCRIPTION_EVENT_SERVER)
		pa_operation_unref (pa_context_get_server_info (context,
			on_server_info, userdata));
}

static void
on_subscribe_finish (pa_context *context, int success, void *userdata)
{
	(void) context;

	struct app_context *ctx = userdata;
	if (!success)
	{
		ctx->failed = true;
		refresh_status (ctx);
	}
}

static void
on_context_state_change (pa_context *context, void *userdata)
{
	struct app_context *ctx = userdata;
	switch (pa_context_get_state (context))
	{
	case PA_CONTEXT_FAILED:
	case PA_CONTEXT_TERMINATED:
		ctx->failed = true;
		refresh_status (ctx);

		pa_context_unref (context);
		ctx->context = NULL;

		// Retry after an arbitrary delay of 5 seconds
		poller_timer_set (&ctx->make_context, 5000);
		return;
	case PA_CONTEXT_READY:
		ctx->failed = false;
		refresh_status (ctx);

		pa_context_get_server_info (context, on_server_info, userdata);
		pa_context_set_subscribe_callback (context, on_event, userdata);
		pa_operation_unref (pa_context_subscribe (context,
			PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER,
			on_subscribe_finish, userdata));
	default:
		return;
	}
}

static void
on_make_context (void *user_data)
{
	struct app_context *ctx = user_data;
	ctx->context = pa_context_new (ctx->api, PROGRAM_NAME);
	pa_context_set_state_callback (ctx->context, on_context_state_change, ctx);
	pa_context_connect (ctx->context, NULL, PA_CONTEXT_NOFLAGS, NULL);
}

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

static void
spawn (struct app_context *ctx, char *argv[])
{
	posix_spawn_file_actions_t actions;
	posix_spawn_file_actions_init (&actions);

	posix_spawn_file_actions_addclose (&actions, ConnectionNumber (ctx->dpy));
	if (ctx->mpd_client.socket != -1)
		posix_spawn_file_actions_addclose (&actions, ctx->mpd_client.socket);
	if (ctx->nut_client.socket != -1)
		posix_spawn_file_actions_addclose (&actions, ctx->nut_client.socket);

	posix_spawnattr_t attr;
	posix_spawnattr_init (&attr);
	posix_spawnattr_setpgroup (&attr, 0);

	posix_spawnp (NULL, argv[0], &actions, &attr, argv, environ);

	posix_spawn_file_actions_destroy (&actions);
	posix_spawnattr_destroy (&attr);
}

#define MPD_SIMPLE(name, ...)                              \
	static void                                            \
	on_mpd_ ## name (struct app_context *ctx, int arg)     \
	{                                                      \
		(void) arg;                                        \
		struct mpd_client *c = &ctx->mpd_client;           \
		if (c->state != MPD_CONNECTED)                     \
			return;                                        \
		mpd_client_send_command (c, __VA_ARGS__);          \
		mpd_client_add_task (c, NULL, NULL);               \
		mpd_client_idle (c, 0);                            \
	}

// XXX: pause without argument is deprecated, we can watch play state
//   if we want to have the toggle pause/play functionality
MPD_SIMPLE (play,     "pause",    NULL)
MPD_SIMPLE (stop,     "stop",     NULL)
MPD_SIMPLE (prev,     "previous", NULL)
MPD_SIMPLE (next,     "next",     NULL)
MPD_SIMPLE (forward,  "seekcur", "+10", NULL)
MPD_SIMPLE (backward, "seekcur", "-10", NULL)

static void
on_volume_finish (pa_context *context, int success, void *userdata)
{
	(void) context;
	(void) success;
	(void) userdata;

	// Just like... whatever, man
}

static void
on_volume_mute (struct app_context *ctx, int arg)
{
	(void) arg;

	if (!ctx->context)
		return;

	pa_operation_unref (pa_context_set_sink_mute_by_name (ctx->context,
		ctx->sink_name, !ctx->muted, on_volume_finish, ctx));
}

static void
on_volume_set (struct app_context *ctx, int arg)
{
	if (!ctx->context)
		return;

	pa_cvolume volume = ctx->volume;
	if (arg > 0)
		pa_cvolume_inc (&volume, (pa_volume_t)  arg * PA_VOLUME_NORM / 100);
	else
		pa_cvolume_dec (&volume, (pa_volume_t) -arg * PA_VOLUME_NORM / 100);
	pa_operation_unref (pa_context_set_sink_volume_by_name (ctx->context,
		ctx->sink_name, &volume, on_volume_finish, ctx));
}

static void
on_brightness (struct app_context *ctx, int arg)
{
	char *value = xstrdup_printf ("%d", arg);
	char *argv[] = { "brightness", value, NULL };
	spawn (ctx, argv);
	free (value);
}

struct
{
	unsigned mod;
	KeySym keysym;
	void (*handler) (struct app_context *ctx, int arg);
	int arg;
}
g_keys[] =
{
	// MPD
	{ Mod4Mask,            XK_Up,        on_mpd_play,      0 },
	{ Mod4Mask,            XK_Down,      on_mpd_stop,      0 },
	{ Mod4Mask,            XK_Left,      on_mpd_prev,      0 },
	{ Mod4Mask,            XK_Right,     on_mpd_next,      0 },
	/* xmodmap | grep -e Alt_R -e Meta_R -e ISO_Level3_Shift -e Mode_switch */
	{ Mod4Mask | Mod5Mask, XK_Left,      on_mpd_backward,  0 },
	{ Mod4Mask | Mod5Mask, XK_Right,     on_mpd_forward,   0 },

	// Brightness
	{ Mod4Mask,            XK_Home,      on_brightness,   10 },
	{ Mod4Mask,            XK_End,       on_brightness,  -10 },
	{ 0, XF86XK_MonBrightnessUp,         on_brightness,   10 },
	{ 0, XF86XK_MonBrightnessDown,       on_brightness,  -10 },

	// Volume
	{ Mod4Mask,            XK_Delete,    on_volume_mute,   0 },
	{ Mod4Mask,            XK_Page_Up,   on_volume_set,   10 },
	{ Mod4Mask | Mod5Mask, XK_Page_Up,   on_volume_set,    1 },
	{ Mod4Mask,            XK_Page_Down, on_volume_set,  -10 },
	{ Mod4Mask | Mod5Mask, XK_Page_Down, on_volume_set,   -1 },
	{ 0, XF86XK_AudioMute,               on_volume_mute,   0 },
	{ 0, XF86XK_AudioRaiseVolume,        on_volume_set,   10 },
	{ 0, XF86XK_AudioLowerVolume,        on_volume_set,  -10 },
};

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

static unsigned
get_numlock_mask (struct app_context *ctx)
{
	unsigned result = 0;
	XModifierKeymap *modmap = XGetModifierMapping (ctx->dpy);
	for (unsigned i = 0; i < 8; i++)
		for (int j = 0; j < modmap->max_keypermod; j++)
			if (modmap->modifiermap[i * modmap->max_keypermod + j] ==
				XKeysymToKeycode (ctx->dpy, XK_Num_Lock))
				result = (1 << i);
	XFreeModifiermap(modmap);
	return result;
}

#define CLEANMASK(mask) (mask & ~(numlock_mask | LockMask))

static void
on_x_keypress (struct app_context *ctx, XEvent *e)
{
	unsigned numlock_mask = get_numlock_mask (ctx);

	XKeyEvent *ev = &e->xkey;
	KeySym keysym = XKeycodeToKeysym (ctx->dpy, (KeyCode) ev->keycode, 0);
	for (size_t i = 0; i < N_ELEMENTS (g_keys); i++)
		if (keysym == g_keys[i].keysym
		 && CLEANMASK (g_keys[i].mod) == CLEANMASK (ev->state)
		 && g_keys[i].handler)
			g_keys[i].handler (ctx, g_keys[i].arg);
}

static void
on_x_ready (const struct pollfd *pfd, void *user_data)
{
	(void) pfd;
	struct app_context *ctx = user_data;

	XEvent ev;
	while (XPending (ctx->dpy))
	{
		if (XNextEvent (ctx->dpy, &ev))
			exit_fatal ("XNextEvent returned non-zero");
		if (ev.type == KeyPress)
			on_x_keypress (ctx, &ev);
	}
}

static void
grab_keys (struct app_context *ctx)
{
	unsigned numlock_mask = get_numlock_mask (ctx);
	unsigned modifiers[] =
		{ 0, LockMask, numlock_mask, numlock_mask | LockMask };

	KeyCode code;
	Window root = DefaultRootWindow (ctx->dpy);
	for (size_t i = 0; i < N_ELEMENTS (g_keys); i++)
		if ((code = XKeysymToKeycode (ctx->dpy, g_keys[i].keysym)))
			for (size_t j = 0; j < N_ELEMENTS (modifiers); j++)
				XGrabKey (ctx->dpy, code, g_keys[i].mod | modifiers[j], root,
					 False /* ? */, GrabModeAsync, GrabModeAsync);

	XSelectInput (ctx->dpy, root, KeyPressMask);
	XSync (ctx->dpy, False);

	ctx->x_event.dispatcher = on_x_ready;
	ctx->x_event.user_data = ctx;
	poller_fd_set (&ctx->x_event, POLLIN);
}

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

static void
poller_timer_init_and_set (struct poller_timer *self, struct poller *poller,
	poller_timer_fn cb, void *user_data)
{
	poller_timer_init (self, poller);
	self->dispatcher = cb;
	self->user_data = user_data;

	poller_timer_set (self, 0);
}

int
main (int argc, char *argv[])
{
	g_log_message_real = log_message_custom;

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

	struct opt_handler oh;
	opt_handler_init (&oh, argc, argv, opts, NULL, "Set root window name.");

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

	argc -= optind;
	argv += optind;

	opt_handler_free (&oh);

	// We don't need to retrieve exit statuses of anything, avoid zombies
	struct sigaction sa;
	sa.sa_flags = SA_RESTART | SA_NOCLDWAIT;
	sigemptyset (&sa.sa_mask);
	sa.sa_handler = SIG_IGN;
	if (sigaction (SIGCHLD, &sa, NULL) == -1)
		print_error ("%s: %s", "sigaction", strerror (errno));

	struct app_context ctx;
	app_context_init (&ctx);
	ctx.prefix = argc > 1 ? argv[1] : NULL;

	struct error *e = NULL;
	if (!simple_config_update_from_file (&ctx.config, &e))
		exit_fatal ("%s", e->message);

	poller_timer_init_and_set (&ctx.time_changed, &ctx.poller,
		on_time_changed, &ctx);
	poller_timer_init_and_set (&ctx.make_context, &ctx.poller,
		on_make_context, &ctx);
	poller_timer_init_and_set (&ctx.refresh_rest, &ctx.poller,
		on_refresh_rest, &ctx);
	poller_timer_init_and_set (&ctx.mpd_reconnect, &ctx.poller,
		on_mpd_reconnect, &ctx);
	poller_timer_init_and_set (&ctx.nut_reconnect, &ctx.poller,
		on_nut_reconnect, &ctx);

	grab_keys (&ctx);

	poller_pa_run (ctx.api);
	app_context_free (&ctx);

	return 0;
}