/*
 * Copyright (C) 2018 Guido Berhoerster <guido+xwrited@berhoerster.name>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <errno.h>
#include <fcntl.h>
#include <glib-unix.h>
#include <glib/gi18n.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "xwd-application.h"
#include "xwd-utmp.h"

#define	BUFFER_TIMEOUT	500 /* ms */

struct _XwdApplication {
	GApplication	parent_instance;
	gint		masterfd;
	gint		slavefd;
	GIOChannel	*master_pty_chan;
	guint		buffer_timeout_id;
	GString		*message_buf;
};

G_DEFINE_TYPE(XwdApplication, xwd_application, G_TYPE_APPLICATION)

static void	xwd_application_quit(GSimpleAction *, GVariant *, gpointer);

static const GOptionEntry cmd_options[] = {
    { "debug", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
    N_("Enable debugging messages"), NULL },
    { "quit", 'q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
    N_("Quit running instance of xwrited"), NULL },
    { "version", 'V', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
    N_("Print the version number and quit"), NULL },
    { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, NULL, NULL },
    { NULL }
};

static const GActionEntry xwd_application_actions[] = {
    { "quit", xwd_application_quit }
};

static void
xwd_application_quit(GSimpleAction *simple, GVariant *parameter,
    gpointer user_data)
{
	XwdApplication	*self = user_data;

	g_application_quit(G_APPLICATION(self));
}

static gboolean
on_signal(gpointer user_data)
{
	XwdApplication	*self = user_data;

	g_debug("received signal, exiting");
	g_action_group_activate_action(G_ACTION_GROUP(G_APPLICATION(self)),
	    "quit", NULL);

	return (TRUE);
}

static GString *
string_copy(GString *dest, GString *src)
{
	g_string_truncate(dest, 0);
	g_string_append(dest, src->str);

	return (dest);
}

static void
string_decode_octal(GString *string)
{
	GString	*result;
	gsize	remaining = string->len;
	gchar	*p = string->str;
	gchar	oct[4] = { '\0' };
	guint64	num;
	char	*end;

	result = g_string_sized_new(string->len);

	while (remaining > 0) {
		if ((remaining >= 4) && (*p == '\\')) {
			/* put octal number in NUL-terminated buffer */
			memcpy(oct, p + 1, 3);
			/* convert valid octal number to byte */
			num = g_ascii_strtoull(oct, &end, 8);
			if ((*end == '\0') && (num <= 0xff)) {
				/* conversion succeeded */
				p += 4;
				remaining -= 4;
				g_string_append_c(result, num);
				continue;
			}
		}

		g_string_append_c(result, *p++);
		remaining--;
	}

	string_copy(string, result);
	g_string_free(result, TRUE);
}

static void
string_decode_meta_ascii(GString *string)
{
	GString		*result;
	gchar		*p = string->str;
	gsize		remaining = string->len;

	result = g_string_sized_new(string->len);

	while (remaining > 0) {
		if ((remaining >= 3) && (*p == 'M') && (*(p + 1) == '-') &&
		    ((*(p + 2) & (1 << 7)) == 0)) {
			/* restore 8th bit */
			g_string_append_c(result, *(p + 2) | (1 << 7));
			remaining -= 3;
			p += 3;
			continue;
		}

		g_string_append_c(result, *p++);
		remaining--;
	}

	string_copy(string, result);
	g_string_free(result, TRUE);
}

static void
string_to_valid_utf8(GString *string)
{
	GString		*result;
	gchar		*start = string->str;
	gchar		*end;
	gsize		remaining = string->len;

	result = g_string_sized_new(string->len);

	while (remaining > 0) {
		if (g_utf8_validate(start, remaining, (const gchar **)&end)) {
			/* remaining part is valid */
			g_string_append_len(result, start, remaining);
			break;
		}

		/* append valid part */
		g_string_append_len(result, start, end - start);
		/*
		 * replace first invalid byte with Unicode "REPLACEMENT
		 * CHARACTER" (U+FFFD)
		 */
		g_string_append(result, "\357\277\275");
		remaining -= (end - start) + 1;
		start = end + 1;
	}

	string_copy(string, result);
	g_string_free(result, TRUE);
}

static void
string_trim_lines(GString *string)
{
	GString		*result;
	gchar		*p = string->str;
	gchar		*q;
	gsize		line_len;
	gsize		remaining = string->len;

	result = g_string_sized_new(string->len);

	while (remaining > 0) {
		q = memchr(p, '\n', remaining);
		if (q == NULL) {
			g_string_append_len(result, p, remaining);
			break;
		}
		line_len = q - p - 1;
		/* convert \r\n to \n */
		if ((line_len > 0) && (p[line_len - 1] == '\r')) {
			line_len--;
		}
		/* trim spaces on the right */
		while ((line_len > 0) && (p[line_len - 1] == ' ')) {
			line_len--;
		}
		g_string_append_len(result, p, line_len);
		g_string_append_c(result, '\n');
		remaining -= q + 1 - p;
		p = q + 1;
	}

	string_copy(string, result);
	g_string_free(result, TRUE);
}

static void
string_filter_nonprintable(GString *string)
{
	GString		*result;
	const gchar	*p;
	gunichar	c;

	result = g_string_sized_new(string->len);

	for (p = string->str; *p != '\0'; p = g_utf8_next_char(p)) {
		c = g_utf8_get_char(p);
		if (g_unichar_isprint(c) || g_unichar_isspace(c)) {
			g_string_append_unichar(result, c);
		}
	}

	string_copy(string, result);
	g_string_free(result, TRUE);
}

static gchar *
hexdumpa(const void *mem, gsize n)
{
	const guchar	*bytes = mem;
	GString		*string;
	gsize		i;
	gsize		j;

	string = g_string_sized_new((n / 16 + (n % 16 > 0)) * 76);

	for (i = 0; i < n; i += 16) {
		g_string_append_printf(string, "%08zx ", i);

		for (j = 0; (i + j < n) && (j < 16); j++) {
			g_string_append_printf(string, " %02x", bytes[i + j]);
		}
		for (; j < 16; j++) {
			g_string_append(string, "   ");
		}

		g_string_append(string, "  ");
		for (j = 0; (i + j < n) && (j < 16); j++) {
			g_string_append_printf(string, "%c",
			    g_ascii_isprint(bytes[i + j]) ? bytes[i + j] : '.');
		}

		g_string_append_c(string, '\n');
	}

	return (g_string_free(string, FALSE));
}

static void
display_message(XwdApplication *self)
{
	gboolean	enable_debug_logging;
	gchar		*message_dump;
	GString		*message;
	GIcon		*icon;
	GNotification	*notification;

	if (self->message_buf->len == 0) {
		return;
	}

	enable_debug_logging = (g_getenv("G_MESSAGES_DEBUG") != NULL);
	if (enable_debug_logging)  {
		message_dump = hexdumpa(self->message_buf->str,
		    self->message_buf->len);
		g_debug("raw message:\n%s", message_dump);
		g_free(message_dump);
	}

	/*
	 * There is no reliable way to determine the character encoding of the
	 * received message which, depending on the locale of the sender, may
	 * even differ for different messages. A user could even send binary
	 * data. It is thus assumed that messages are in UTF-8 encoding which
	 * should be the most common case on modern systems. Any invalid
	 * sequences are replaced with the Unicode "REPLACEMENT CHARACTER"
	 * (U+FFFD) and non-printable characters are removed. Additionally,
	 * padding typically added by wall(1) implementations is removed in
	 * order to improve readability.
	 * Some write(1) and wall(1) implementations encode non-ASCII
	 * characters, in particular UTF-8 sequences, by prefixing them with
	 * "M-" and clearing the 8th bit while others (e.g. util-linux) use
	 * octal escape sequences. These encodings are reversed before messages
	 * are processed further. However some implementations such as NetBSD
	 * write(1) uncoditionally process each byte with toascii(3) which
	 * makes it impossible to restore the original value.
	 */
	message = g_string_new_len(self->message_buf->str,
	    self->message_buf->len);
	string_decode_octal(message);
	string_decode_meta_ascii(message);
	string_to_valid_utf8(message);
	string_filter_nonprintable(message);
	string_trim_lines(message);

	if (enable_debug_logging)  {
		message_dump = hexdumpa(message->str, message->len);
		g_debug("message:\n%s", message_dump);
		g_free(message_dump);
	}

	icon = g_themed_icon_new("utilities-terminal");

	notification = g_notification_new(_("Message received"));
	g_notification_set_icon(notification, icon);
	g_notification_set_body(notification, message->str);
	g_application_send_notification(G_APPLICATION(self), NULL,
	    notification);

	g_object_unref(notification);
	g_object_unref(icon);
	g_string_free(message, TRUE);
	g_string_truncate(self->message_buf, 0);
}

static gboolean
on_buffer_timeout(gpointer user_data)
{
	XwdApplication	*self = user_data;

	display_message(self);

	self->buffer_timeout_id = 0;

	return (FALSE);
}

static gboolean
on_master_pty_readable(GIOChannel *source, GIOCondition cond,
    gpointer user_data)
{
	XwdApplication	*self = user_data;
	GIOStatus	status;
	gchar		buf[BUFSIZ];
	gsize		buf_len = 0;
	GError		*error = NULL;

	if (cond & G_IO_IN) {
		/* read message data from master pty */
		memset(buf, 0, sizeof (buf));
		while ((status = g_io_channel_read_chars(source, (gchar *)&buf,
		    sizeof (buf), &buf_len, &error)) == G_IO_STATUS_NORMAL) {
			if (buf_len > 0) {
				g_debug("read %" G_GSSIZE_FORMAT " bytes from "
				    "master pty", buf_len);
				g_string_append_len(self->message_buf, buf,
				    buf_len);
			}
		}
		if (error != NULL) {
			g_critical("failed to read from master pty: %s",
			    error->message);
			g_error_free(error);
			return (FALSE);
		}

		/*
		 * a message might be read in several parts and it is not
		 * possible to reliably detect the beginning or end of a
		 * message, so buffer read data until a short timeout is
		 * reached as this works well for a single message which should
		 * be the most common case
		 */
		if (self->buffer_timeout_id == 0) {
			self->buffer_timeout_id = g_timeout_add(BUFFER_TIMEOUT,
			    on_buffer_timeout, self);
		}
	}

	if (cond & (G_IO_HUP | G_IO_ERR)) {
		g_critical("connection to master pty broken");
		return (FALSE);
	}

	return (TRUE);
}

static void
xwd_application_startup(GApplication *application)
{
	XwdApplication	*self = XWD_APPLICATION(application);
	gchar		*slave_name;
	GIOFlags	flags;
	GError		*error = NULL;

	G_APPLICATION_CLASS(xwd_application_parent_class)->startup(application);

	/* create actions */
	g_action_map_add_action_entries(G_ACTION_MAP(self),
	    xwd_application_actions, G_N_ELEMENTS(xwd_application_actions),
	    self);

	/* create signal watchers */
	g_unix_signal_add(SIGINT, on_signal, self);
	g_unix_signal_add(SIGTERM, on_signal, self);
	g_unix_signal_add(SIGHUP, on_signal, self);

	/* open master pty */
	self->masterfd = posix_openpt(O_RDWR | O_NOCTTY);
	if (self->masterfd == -1) {
		g_critical("failed to open master pty: %s", g_strerror(errno));
		return;
	}

	/* create slave pty */
	if ((grantpt(self->masterfd) == -1) ||
	    (unlockpt(self->masterfd) == -1)) {
		g_critical("failed to create slave pty: %s", g_strerror(errno));
		return;
	}
	slave_name = ptsname(self->masterfd);
	if (slave_name == NULL) {
		g_critical("failed to obtain name of slave pty");
		return;
	}

	/*
	 * keep an open fd around order to prevent closing the master fd when
	 * receiving an EOF
	 */
	self->slavefd = open(slave_name, O_RDWR);
	if (self->slavefd == -1) {
		g_critical("failed to open slave pty: %s", g_strerror(errno));
		return;
	}

	/* create a GIOChannel for monitoring the master pty for messages */
	self->master_pty_chan = g_io_channel_unix_new(self->masterfd);
	/* make it non-blocking */
	flags = g_io_channel_get_flags(self->master_pty_chan);
	if (g_io_channel_set_flags(self->master_pty_chan,
	    flags | G_IO_FLAG_NONBLOCK, &error) != G_IO_STATUS_NORMAL) {
		g_critical("failed set flags on the master pty channel: %s",
		    error->message);
		g_error_free(error);
		return;
	}
	/* make the channel safe for encodings other than UTF-8 */
	if (g_io_channel_set_encoding(self->master_pty_chan, NULL, &error) !=
	    G_IO_STATUS_NORMAL) {
		g_critical("failed set encoding on the master pty channel: %s",
		    error->message);
		g_error_free(error);
		return;
	}
	if (!g_io_add_watch(self->master_pty_chan,
	    G_IO_IN | G_IO_HUP | G_IO_ERR, on_master_pty_readable, self)) {
		g_critical("failed to add watch on master pty channel");
		return;
	}

	xwd_utmp_add_entry(self->masterfd);

	/* keep GApplication running */
	g_application_hold(application);
}

static void
xwd_application_shutdown(GApplication *application)
{
	XwdApplication	*self = XWD_APPLICATION(application);
	GApplicationClass *application_class =
	    G_APPLICATION_CLASS(xwd_application_parent_class);

	/* display any buffered data before exiting */
	display_message(self);

	if (self->master_pty_chan != NULL) {
		g_io_channel_shutdown(self->master_pty_chan, FALSE, NULL);
		g_clear_pointer(&self->master_pty_chan,
		    (GDestroyNotify)g_io_channel_unref);
	}

	if (self->slavefd != -1) {
		close(self->slavefd);
		self->slavefd = -1;
	}

	if (self->masterfd != -1) {
		close(self->masterfd);
		self->masterfd = -1;
	}

	xwd_utmp_remove_entry(self->masterfd);

	/* remove signal watches and buffer timeout */
	while (g_source_remove_by_user_data(self)) {
		continue;
	}
	self->buffer_timeout_id = 0;

	application_class->shutdown(application);
}

static gint
xwd_application_handle_local_options(GApplication *application,
    GVariantDict *options)
{
	gchar		**args = NULL;
	gchar		*messages_debug;
	GError		*error = NULL;

	/* filename arguments are not allowed */
	if (g_variant_dict_lookup(options, G_OPTION_REMAINING, "^a&ay",
	    &args)) {
		g_printerr("invalid argument: \"%s\"\n", args[0]);
		g_free(args);
		return (1);
	}

	if (g_variant_dict_contains(options, "version")) {
		g_print("%s %s\n", PACKAGE, VERSION);

		/* quit */
		return (0);
	}

	if (g_variant_dict_contains(options, "debug")) {
		/* enable debug logging */
		messages_debug = g_strjoin(":", G_LOG_DOMAIN,
		    g_getenv("G_MESSAGES_DEBUG"), NULL);
		g_setenv("G_MESSAGES_DEBUG", messages_debug, TRUE);
		g_free(messages_debug);
	}

	/*
	 * register with the session bus so that it is possible to discern
	 * between remote and primary instance and that remote actions can be
	 * invoked, this causes the startup signal to be emitted which, in case
	 * of the primary instance, starts to instantiate the
	 * backend with the given values
	 */
	if (!g_application_register(application, NULL, &error)) {
		g_critical("g_application_register: %s", error->message);
		g_error_free(error);
		return (1);
	}

	if (g_variant_dict_contains(options, "quit")) {
		/* only valid if a remote instance is running */
		if (!g_application_get_is_remote(application)) {
			g_printerr("%s is not running\n", g_get_prgname());
			return (1);
		}

		/* signal remote instance to quit */
		g_action_group_activate_action(G_ACTION_GROUP(application),
		    "quit", NULL);

		/* quit local instance */
		return (0);
	}

	/* proceed with default command line processing */
	return (-1);
}

static void
xwd_application_activate(GApplication *application) {
	GApplicationClass *application_class =
	    G_APPLICATION_CLASS(xwd_application_parent_class);

	/* do nothing, implementation required by GApplication */

	application_class->activate(application);
}

static void
xwd_application_finalize(GObject *object)
{
	XwdApplication	*self = XWD_APPLICATION(object);

	g_string_free(self->message_buf, TRUE);

	G_OBJECT_CLASS(xwd_application_parent_class)->finalize(object);
}

static void
xwd_application_class_init(XwdApplicationClass *klass)
{
	GObjectClass	*object_class = G_OBJECT_CLASS(klass);
	GApplicationClass *application_class = G_APPLICATION_CLASS(klass);

	object_class->finalize = xwd_application_finalize;

	application_class->startup = xwd_application_startup;
	application_class->shutdown = xwd_application_shutdown;
	application_class->handle_local_options =
	    xwd_application_handle_local_options;
	application_class->activate = xwd_application_activate;
}

static void
xwd_application_init(XwdApplication *self)
{
	g_application_add_main_option_entries(G_APPLICATION(self),
	    cmd_options);

	self->masterfd = -1;
	self->slavefd = -1;

	self->message_buf = g_string_sized_new(BUFSIZ);
}

XwdApplication *
xwd_application_new(void)
{
	return (g_object_new(XWD_TYPE_APPLICATION, "application-id",
	    APPLICATION_ID, NULL));
}
