/*
 * Copyright (C) 2004 John Ellis
 * Copyright (C) 2008 - 2016 The Geeqie Team
 *
 * Author: John Ellis
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "remote.h"

#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>

#include <algorithm>
#include <cerrno>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>

#include <gtk/gtk.h>

#include <config.h>

#include "cache-maint.h"
#include "collect-io.h"
#include "collect.h"
#include "compat.h"
#include "debug.h"
#include "exif.h"
#include "filedata.h"
#include "filefilter.h"
#include "glua.h"
#include "image.h"
#include "img-view.h"
#include "intl.h"
#include "layout-image.h"
#include "layout-util.h"
#include "layout.h"
#include "main-defines.h"
#include "main.h"
#include "misc.h"
#include "options.h"
#include "pixbuf-renderer.h"
#include "rcfile.h"
#include "slideshow.h"
#include "typedefs.h"
#include "ui-fileops.h"
#include "ui-misc.h"
#include "utilops.h"
#include "view-file.h"

#ifndef UNIX_PATH_MAX
#define UNIX_PATH_MAX 108
#endif

namespace
{

constexpr guint SERVER_MAX_CLIENTS = 8;
constexpr int REMOTE_SERVER_BACKLOG = 4;

} // namespace

static RemoteConnection *remote_client_open(const gchar *path);
static gint remote_client_send(RemoteConnection *rc, const gchar *text);
static void gr_raise(const gchar *text, GIOChannel *channel, gpointer data);

static LayoutWindow *lw_id = nullptr; /* points to the window set by the --id option */

struct RemoteClient {
	gint fd;
	guint channel_id; /* event source id */
	RemoteConnection *rc;
};

struct RemoteData {
	CollectionData *command_collection;
	GList *file_list;
	gboolean single_dir;
};

/* To enable file names containing newlines to be processed correctly,
 * the --print0 remote option sets returned data to be terminated with a null
 * character rather a newline
 */
static gboolean print0 = FALSE;

/* Remote commands from main.cc are prepended with the current dir the remote
 * command was made from. Some remote commands require this. The
 * value is stored here
 */
static gchar *pwd = nullptr;

/**
 * @brief Ensures file path is absolute.
 * @param[in] filename Filepath, absolute or relative to calling directory
 * @returns absolute path
 *
 * If first character of input filepath is not the directory
 * separator, assume it as a relative path and prepend
 * the directory the remote command was initiated from
 *
 * Return value must be freed with g_free()
 */
static gchar *set_pwd(gchar *filename)
{
	gchar *temp;

	if (strncmp(filename, G_DIR_SEPARATOR_S, 1) != 0)
		{
		temp = g_build_filename(pwd, filename, NULL);
		}
	else
		{
		temp = g_strdup(filename);
		}

	return temp;
}

static gboolean remote_server_client_cb(GIOChannel *source, GIOCondition condition, gpointer data)
{
	auto client = static_cast<RemoteClient *>(data);
	RemoteConnection *rc;
	GIOStatus status = G_IO_STATUS_NORMAL;

	lw_id = nullptr;
	rc = client->rc;

	if (condition & G_IO_IN)
		{
		gchar *buffer = nullptr;
		GError *error = nullptr;
		gsize termpos;
		/** @FIXME it should be possible to terminate the command with a null character */
		g_io_channel_set_line_term(source, "<gq_end_of_command>", -1);
		while ((status = g_io_channel_read_line(source, &buffer, nullptr, &termpos, &error)) == G_IO_STATUS_NORMAL)
			{
			if (buffer)
				{
				buffer[termpos] = '\0';

				if (buffer[0] != '\0')
					{
					if (rc->read_func) rc->read_func(rc, buffer, source, rc->read_data);
					g_io_channel_write_chars(source, "<gq_end_of_command>", -1, nullptr, nullptr); /* empty line finishes the command */
					g_io_channel_flush(source, nullptr);
					}
				g_free(buffer);

				buffer = nullptr;
				}
			}

		if (error)
			{
			log_printf("error reading socket: %s\n", error->message);
			g_error_free(error);
			}
		}

	if (condition & G_IO_HUP || status == G_IO_STATUS_EOF || status == G_IO_STATUS_ERROR)
		{
		rc->clients = g_list_remove(rc->clients, client);

		DEBUG_1("HUP detected, closing client.");
		DEBUG_1("client count %u", g_list_length(rc->clients));

		g_source_remove(client->channel_id);
		close(client->fd);
		g_free(client);
		}

	return TRUE;
}

static void remote_server_client_add(RemoteConnection *rc, gint fd)
{
	RemoteClient *client;
	GIOChannel *channel;

	if (g_list_length(rc->clients) > SERVER_MAX_CLIENTS)
		{
		log_printf("maximum remote clients of %d exceeded, closing connection\n", SERVER_MAX_CLIENTS);
		close(fd);
		return;
		}

	client = g_new0(RemoteClient, 1);
	client->rc = rc;
	client->fd = fd;

	channel = g_io_channel_unix_new(fd);
	client->channel_id = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, static_cast<GIOCondition>(G_IO_IN | G_IO_HUP),
						 remote_server_client_cb, client, nullptr);
	g_io_channel_unref(channel);

	rc->clients = g_list_append(rc->clients, client);
	DEBUG_1("client count %u", g_list_length(rc->clients));
}

static void remote_server_clients_close(RemoteConnection *rc)
{
	while (rc->clients)
		{
		auto client = static_cast<RemoteClient *>(rc->clients->data);

		rc->clients = g_list_remove(rc->clients, client);

		g_source_remove(client->channel_id);
		close(client->fd);
		g_free(client);
		}
}

static gboolean remote_server_read_cb(GIOChannel *, GIOCondition, gpointer data)
{
	auto rc = static_cast<RemoteConnection *>(data);
	gint fd;
	guint alen;

	fd = accept(rc->fd, nullptr, &alen);
	if (fd == -1)
		{
		log_printf("error accepting socket: %s\n", strerror(errno));
		return TRUE;
		}

	remote_server_client_add(rc, fd);

	return TRUE;
}

gboolean remote_server_exists(const gchar *path)
{
	RemoteConnection *rc;

	/* verify server up */
	rc = remote_client_open(path);
	remote_close(rc);

	if (rc) return TRUE;

	/* unable to connect, remove socket file to free up address */
	unlink(path);
	return FALSE;
}

static RemoteConnection *remote_server_open(const gchar *path)
{
	RemoteConnection *rc;
	struct sockaddr_un addr;
	gint fd;
	GIOChannel *channel;

	if (strlen(path) >= sizeof(addr.sun_path))
		{
		log_printf("Address is too long: %s\n", path);
		return nullptr;
		}

	if (remote_server_exists(path))
		{
		log_printf("Address already in use: %s\n", path);
		return nullptr;
		}

	fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (fd == -1) return nullptr;

	addr.sun_family = AF_UNIX;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstringop-truncation"
	strncpy(addr.sun_path, path, sizeof(addr.sun_path));
#pragma GCC diagnostic pop
	if (bind(fd, reinterpret_cast<const struct sockaddr*>(&addr), sizeof(addr)) == -1 ||
	    listen(fd, REMOTE_SERVER_BACKLOG) == -1)
		{
		log_printf("error subscribing to socket: %s\n", strerror(errno));
		close(fd);
		return nullptr;
		}

	rc = g_new0(RemoteConnection, 1);

	rc->server = TRUE;
	rc->fd = fd;
	rc->path = g_strdup(path);

	channel = g_io_channel_unix_new(rc->fd);
	g_io_channel_set_flags(channel, G_IO_FLAG_NONBLOCK, nullptr);

	rc->channel_id = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, G_IO_IN,
					     remote_server_read_cb, rc, nullptr);
	g_io_channel_unref(channel);

	return rc;
}

static void remote_server_subscribe(RemoteConnection *rc, RemoteConnection::ReadFunc *func, gpointer data)
{
	if (!rc || !rc->server) return;

	rc->read_func = func;
	rc->read_data = data;
}


static RemoteConnection *remote_client_open(const gchar *path)
{
	RemoteConnection *rc;
	struct stat st;
	struct sockaddr_un addr;
	gint fd;

	if (strlen(path) >= sizeof(addr.sun_path)) return nullptr;

	if (stat(path, &st) != 0 || !S_ISSOCK(st.st_mode)) return nullptr;

	fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (fd == -1) return nullptr;

	addr.sun_family = AF_UNIX;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstringop-truncation"
	strncpy(addr.sun_path, path, sizeof(addr.sun_path));
#pragma GCC diagnostic pop
	if (connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) == -1)
		{
		DEBUG_1("error connecting to socket: %s", strerror(errno));
		close(fd);
		return nullptr;
		}

	rc = g_new0(RemoteConnection, 1);
	rc->server = FALSE;
	rc->fd = fd;
	rc->path = g_strdup(path);

	return rc;
}

static sig_atomic_t sigpipe_occurred = FALSE;

static void sighandler_sigpipe(gint)
{
	sigpipe_occurred = TRUE;
}

static gboolean remote_client_send(RemoteConnection *rc, const gchar *text)
{
	struct sigaction new_action;
	struct sigaction old_action;
	gboolean ret = FALSE;
	GError *error = nullptr;
	GIOChannel *channel;

	if (!rc || rc->server) return FALSE;
	if (!text) return TRUE;

	sigpipe_occurred = FALSE;

	new_action.sa_handler = sighandler_sigpipe;
	sigemptyset(&new_action.sa_mask);
	new_action.sa_flags = 0;

	/* setup our signal handler */
	sigaction(SIGPIPE, &new_action, &old_action);

	channel = g_io_channel_unix_new(rc->fd);

	g_io_channel_write_chars(channel, text, -1, nullptr, &error);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, &error);
	g_io_channel_flush(channel, &error);

	if (error)
		{
		log_printf("error reading socket: %s\n", error->message);
		g_error_free(error);
		ret = FALSE;;
		}
	else
		{
		ret = TRUE;
		}

	if (ret)
		{
		gchar *buffer = nullptr;
		gsize termpos;
		g_io_channel_set_line_term(channel, "<gq_end_of_command>", -1);
		while (g_io_channel_read_line(channel, &buffer, nullptr, &termpos, &error) == G_IO_STATUS_NORMAL)
			{
			if (buffer)
				{
				if (g_strstr_len(buffer, -1, "<gq_end_of_command>") == buffer) /* empty line finishes the command */
					{
					g_free(buffer);
					fflush(stdout);
					break;
					}
				buffer[termpos] = '\0';
				if (g_strstr_len(buffer, -1, "print0") != nullptr)
					{
					print0 = TRUE;
					}
				else
					{
					if (print0)
						{
						printf("%s%c", buffer, 0);
						}
					else
						{
						printf("%s\n", buffer);
						}
					}
				g_free(buffer);
				buffer = nullptr;
				}
			}

		if (error)
			{
			log_printf("error reading socket: %s\n", error->message);
			g_error_free(error);
			ret = FALSE;
			}
		}


	/* restore the original signal handler */
	sigaction(SIGPIPE, &old_action, nullptr);
	g_io_channel_unref(channel);
	return ret;
}

void remote_close(RemoteConnection *rc)
{
	if (!rc) return;

	if (rc->server)
		{
		remote_server_clients_close(rc);

		g_source_remove(rc->channel_id);
		unlink(rc->path);
		}

	g_free(rc->read_data);

	close(rc->fd);

	g_free(rc->path);
	g_free(rc);
}

/*
 *-----------------------------------------------------------------------------
 * remote functions
 *-----------------------------------------------------------------------------
 */

static void gr_image_next(const gchar *, GIOChannel *, gpointer)
{
	layout_image_next(lw_id);
}

static void gr_new_window(const gchar *, GIOChannel *, gpointer)
{
	LayoutWindow *lw = nullptr;

	if (!layout_valid(&lw)) return;

	lw_id = layout_new_from_default();

	layout_set_path(lw_id, pwd);
}

static gboolean gr_close_window_cb(gpointer)
{
	if (!layout_valid(&lw_id)) return FALSE;

	layout_menu_close_cb(nullptr, lw_id);

	return G_SOURCE_REMOVE;
}

static void gr_close_window(const gchar *, GIOChannel *, gpointer)
{
	g_idle_add((gr_close_window_cb), nullptr);
}

static void gr_image_prev(const gchar *, GIOChannel *, gpointer)
{
	layout_image_prev(lw_id);
}

static void gr_image_first(const gchar *, GIOChannel *, gpointer)
{
	layout_image_first(lw_id);
}

static void gr_image_last(const gchar *, GIOChannel *, gpointer)
{
	layout_image_last(lw_id);
}

static void gr_fullscreen_toggle(const gchar *, GIOChannel *, gpointer)
{
	layout_image_full_screen_toggle(lw_id);
}

static void gr_fullscreen_start(const gchar *, GIOChannel *, gpointer)
{
	layout_image_full_screen_start(lw_id);
}

static void gr_fullscreen_stop(const gchar *, GIOChannel *, gpointer)
{
	layout_image_full_screen_stop(lw_id);
}

static void gr_lw_id(const gchar *text, GIOChannel *, gpointer)
{
	lw_id = layout_find_by_layout_id(text);
	if (!lw_id)
		{
		log_printf("remote sent window ID that does not exist:\"%s\"\n",text);
		}
	layout_valid(&lw_id);
}

static void gr_slideshow_start_rec(const gchar *text, GIOChannel *, gpointer)
{
	GList *list;
	gchar *tilde_filename;

	tilde_filename = expand_tilde(text);

	FileData *dir_fd = file_data_new_dir(tilde_filename);
	g_free(tilde_filename);

	layout_valid(&lw_id);
	list = filelist_recursive_full(dir_fd, lw_id->options.file_view_list_sort.method, lw_id->options.file_view_list_sort.ascend, lw_id->options.file_view_list_sort.case_sensitive);
	file_data_unref(dir_fd);
	if (!list) return;

	layout_image_slideshow_stop(lw_id);
	layout_image_slideshow_start_from_list(lw_id, list);
}

static void gr_cache_thumb(const gchar *text, GIOChannel *, gpointer)
{
	if (!g_strcmp0(text, "clear"))
		{
		cache_maintain_home_remote(FALSE, TRUE, nullptr);
		}
	else if (!g_strcmp0(text, "clean"))
		{
		cache_maintain_home_remote(FALSE, FALSE, nullptr);
		}
}

static void gr_cache_shared(const gchar *text, GIOChannel *, gpointer)
{
	if (!g_strcmp0(text, "clear"))
		cache_manager_standard_process_remote(TRUE);
	else if (!g_strcmp0(text, "clean"))
		cache_manager_standard_process_remote(FALSE);
}

static void gr_cache_metadata(const gchar *, GIOChannel *, gpointer)
{
	cache_maintain_home_remote(TRUE, FALSE, nullptr);
}

static void gr_cache_render(const gchar *text, GIOChannel *, gpointer)
{
	cache_manager_render_remote(text, FALSE, FALSE, nullptr);
}

static void gr_cache_render_recurse(const gchar *text, GIOChannel *, gpointer)
{
	cache_manager_render_remote(text, TRUE, FALSE, nullptr);
}

static void gr_cache_render_standard(const gchar *text, GIOChannel *, gpointer)
{
	if(options->thumbnails.spec_standard)
		{
		cache_manager_render_remote(text, FALSE, TRUE, nullptr);
		}
}

static void gr_cache_render_standard_recurse(const gchar *text, GIOChannel *, gpointer)
{
	if(options->thumbnails.spec_standard)
		{
		cache_manager_render_remote(text, TRUE, TRUE, nullptr);
		}
}

static void gr_slideshow_toggle(const gchar *, GIOChannel *, gpointer)
{
	layout_image_slideshow_toggle(lw_id);
}

static void gr_slideshow_start(const gchar *, GIOChannel *, gpointer)
{
	layout_image_slideshow_start(lw_id);
}

static void gr_slideshow_stop(const gchar *, GIOChannel *, gpointer)
{
	layout_image_slideshow_stop(lw_id);
}

static void gr_slideshow_delay(const gchar *text, GIOChannel *, gpointer)
{
	gdouble t1;
	gdouble t2;
	gdouble t3;
	gdouble n;
	gint res;

	res = sscanf(text, "%lf:%lf:%lf", &t1, &t2, &t3);
	if (res == 3)
		{
		n = (t1 * 3600) + (t2 * 60) + t3;
		if (n < SLIDESHOW_MIN_SECONDS || n > SLIDESHOW_MAX_SECONDS ||
				t1 >= 24 || t2 >= 60 || t3 >= 60)
			{
			printf_term(TRUE, "Remote slideshow delay out of range (%.1f to %.1f)\n",
								SLIDESHOW_MIN_SECONDS, SLIDESHOW_MAX_SECONDS);
			return;
			}
		}
	else if (res == 2)
		{
		n = t1 * 60 + t2;
		if (n < SLIDESHOW_MIN_SECONDS || n > SLIDESHOW_MAX_SECONDS ||
				t1 >= 60 || t2 >= 60)
			{
			printf_term(TRUE, "Remote slideshow delay out of range (%.1f to %.1f)\n",
								SLIDESHOW_MIN_SECONDS, SLIDESHOW_MAX_SECONDS);
			return;
			}
		}
	else if (res == 1)
		{
		n = t1;
		if (n < SLIDESHOW_MIN_SECONDS || n > SLIDESHOW_MAX_SECONDS)
			{
			printf_term(TRUE, "Remote slideshow delay out of range (%.1f to %.1f)\n",
								SLIDESHOW_MIN_SECONDS, SLIDESHOW_MAX_SECONDS);
			return;
			}
		}
	else
		{
		n = 0;
		}

	options->slideshow.delay = static_cast<gint>(n * 10.0 + 0.01);
}

static void gr_tools_show(const gchar *, GIOChannel *, gpointer)
{
	gboolean popped;
	gboolean hidden;

	if (layout_tools_float_get(lw_id, &popped, &hidden) && hidden)
		{
		layout_tools_float_set(lw_id, popped, FALSE);
		}
}

static void gr_tools_hide(const gchar *, GIOChannel *, gpointer)
{
	gboolean popped;
	gboolean hidden;

	if (layout_tools_float_get(lw_id, &popped, &hidden) && !hidden)
		{
		layout_tools_float_set(lw_id, popped, TRUE);
		}
}

static gboolean gr_quit_idle_cb(gpointer)
{
	exit_program();

	return G_SOURCE_REMOVE;
}

static void gr_quit(const gchar *, GIOChannel *, gpointer)
{
	/* schedule exit when idle, if done from within a
	 * remote handler remote_close will crash
	 */
	g_idle_add(gr_quit_idle_cb, nullptr);
}

static void gr_file_load_no_raise(const gchar *text, GIOChannel *, gpointer)
{
	gchar *filename;
	gchar *tilde_filename;

	if (!download_web_file(text, TRUE, nullptr))
		{
		tilde_filename = expand_tilde(text);
		filename = set_pwd(tilde_filename);

		if (isfile(filename))
			{
			if (file_extension_match(filename, GQ_COLLECTION_EXT))
				{
				collection_window_new(filename);
				}
			else
				{
				layout_set_path(lw_id, filename);
				}
			}
		else if (isdir(filename))
			{
			layout_set_path(lw_id, filename);
			}
		else
			{
			log_printf("remote sent filename that does not exist:\"%s\"\n", filename);
			layout_set_path(lw_id, homedir());
			}

		g_free(filename);
		g_free(tilde_filename);
		}
}

static void gr_file_load(const gchar *text, GIOChannel *channel, gpointer data)
{
	gr_file_load_no_raise(text, channel, data);

	gr_raise(text, channel, data);
}

static void gr_pixel_info(const gchar *, GIOChannel *channel, gpointer)
{
	gchar *pixel_info;
	gint x_pixel;
	gint y_pixel;
	gint width;
	gint height;
	gint r_mouse;
	gint g_mouse;
	gint b_mouse;
	gint a_mouse;
	PixbufRenderer *pr;

	if (!layout_valid(&lw_id)) return;

	pr = PIXBUF_RENDERER(lw_id->image->pr);

	if (pr)
		{
		pixbuf_renderer_get_image_size(pr, &width, &height);
		if (width < 1 || height < 1) return;

		pixbuf_renderer_get_mouse_position(pr, &x_pixel, &y_pixel);

		if (x_pixel >= 0 && y_pixel >= 0)
			{
			pixbuf_renderer_get_pixel_colors(pr, x_pixel, y_pixel, &r_mouse, &g_mouse, &b_mouse, &a_mouse);

			if (gdk_pixbuf_get_has_alpha(pr->pixbuf))
				{
				pixel_info = g_strdup_printf(_("[%d,%d]: RGBA(%3d,%3d,%3d,%3d)"),
				x_pixel, y_pixel,
				r_mouse, g_mouse, b_mouse, a_mouse);
				}
			else
				{
				pixel_info = g_strdup_printf(_("[%d,%d]: RGB(%3d,%3d,%3d)"),
				x_pixel, y_pixel,
				r_mouse, g_mouse, b_mouse);
				}

			g_io_channel_write_chars(channel, pixel_info, -1, nullptr, nullptr);
			g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

			g_free(pixel_info);
			}
		else
			{
			return;
			}
		}
	else
		{
		return;
		}
}

static void gr_rectangle(const gchar *, GIOChannel *channel, gpointer)
{
	if (!options->draw_rectangle) return;
	if (!layout_valid(&lw_id)) return;

	auto *pr = PIXBUF_RENDERER(lw_id->image->pr);
	if (!pr) return;

	gint x1;
	gint y1;
	gint x2;
	gint y2;
	image_get_rectangle(x1, y1, x2, y2);

	gchar *rectangle_info = g_strdup_printf(_("%dx%d+%d+%d"),
	                                        std::abs(x1 - x2),
	                                        std::abs(y1 - y2),
	                                        std::min(x1, x2),
	                                        std::min(y1, y2));

	g_io_channel_write_chars(channel, rectangle_info, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_free(rectangle_info);
}

static void gr_render_intent(const gchar *, GIOChannel *channel, gpointer)
{
	gchar *render_intent;

	switch (options->color_profile.render_intent)
		{
		case 0:
			render_intent = g_strdup("Perceptual");
			break;
		case 1:
			render_intent = g_strdup("Relative Colorimetric");
			break;
		case 2:
			render_intent = g_strdup("Saturation");
			break;
		case 3:
			render_intent = g_strdup("Absolute Colorimetric");
			break;
		default:
			render_intent = g_strdup("none");
			break;
		}

	g_io_channel_write_chars(channel, render_intent, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_free(render_intent);
}

static void get_filelist(const gchar *text, GIOChannel *channel, gboolean recurse)
{
	GList *list = nullptr;
	FileFormatClass format_class;
	FileData *dir_fd;
	FileData *fd;
	GList *work;
	gchar *tilde_filename;

	if (strcmp(text, "") == 0)
		{
		if (layout_valid(&lw_id))
			{
			dir_fd = file_data_new_dir(lw_id->dir_fd->path);
			}
		else
			{
			return;
			}
		}
	else
		{
		tilde_filename = expand_tilde(text);
		if (isdir(tilde_filename))
			{
			dir_fd = file_data_new_dir(tilde_filename);
			}
		else
			{
			g_free(tilde_filename);
			return;
			}
		g_free(tilde_filename);
		}

	if (recurse)
		{
		list = filelist_recursive(dir_fd);
		}
	else
		{
		filelist_read(dir_fd, &list, nullptr);
		}

	GString *out_string = g_string_new(nullptr);
	work = list;
	while (work)
		{
		fd = static_cast<FileData *>(work->data);
		g_string_append(out_string, fd->path);
		format_class = filter_file_get_class(fd->path);

		switch (format_class)
			{
			case FORMAT_CLASS_IMAGE:
				out_string = g_string_append(out_string, "    Class: Image");
				break;
			case FORMAT_CLASS_RAWIMAGE:
				out_string = g_string_append(out_string, "    Class: RAW image");
				break;
			case FORMAT_CLASS_META:
				out_string = g_string_append(out_string, "    Class: Metadata");
				break;
			case FORMAT_CLASS_VIDEO:
				out_string = g_string_append(out_string, "    Class: Video");
				break;
			case FORMAT_CLASS_COLLECTION:
				out_string = g_string_append(out_string, "    Class: Collection");
				break;
			case FORMAT_CLASS_DOCUMENT:
				out_string = g_string_append(out_string, "    Class: Document");
				break;
			case FORMAT_CLASS_ARCHIVE:
				out_string = g_string_append(out_string, "    Class: Archive");
				break;
			case FORMAT_CLASS_UNKNOWN:
				out_string = g_string_append(out_string, "    Class: Unknown");
				break;
			default:
				out_string = g_string_append(out_string, "    Class: Unknown");
				break;
			}
		out_string = g_string_append(out_string, "\n");
		work = work->next;
		}

	g_io_channel_write_chars(channel, out_string->str, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_string_free(out_string, TRUE);
	filelist_free(list);
	file_data_unref(dir_fd);
}

static void gr_get_selection(const gchar *, GIOChannel *channel, gpointer)
{
	if (!layout_valid(&lw_id)) return;

	GList *selected = layout_selection_list(lw_id);  // Keep copy to free.
	GString *out_string = g_string_new(nullptr);

	GList *work = selected;
	while (work)
		{
		auto fd = static_cast<FileData *>(work->data);
		g_assert(fd->magick == FD_MAGICK);

		g_string_append_printf(out_string, "%s    %s\n",
				       fd->path,
				       format_class_list[filter_file_get_class(fd->path)]);

		work = work->next;
		}

	g_io_channel_write_chars(channel, out_string->str, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	filelist_free(selected);
	g_string_free(out_string, TRUE);
}

static void gr_selection_add(const gchar *text, GIOChannel *, gpointer)
{
	if (!layout_valid(&lw_id)) return;

	FileData *fd_to_select = nullptr;
	if (strcmp(text, "") == 0)
		{
		// No file specified, use current fd.
		fd_to_select = layout_image_get_fd(lw_id);
		}
	else
		{
		// Search through the current file list for a file matching the specified path.
		// "Match" is either a basename match or a file path match.
		gchar *path = expand_tilde(text);
		gchar *filename = g_path_get_basename(path);
		gchar *slash_plus_filename = g_strdup_printf("%s%s", G_DIR_SEPARATOR_S, filename);

		GList *file_list = layout_list(lw_id);
		for (GList *work = file_list; work && !fd_to_select; work = work->next)
			{
			auto fd = static_cast<FileData *>(work->data);
			if (!strcmp(path, fd->path) || g_str_has_suffix(fd->path, slash_plus_filename))
				{
				fd_to_select = file_data_ref(fd);
				continue;  // will exit loop.
				}

			for (GList *sidecar = fd->sidecar_files; sidecar && !fd_to_select; sidecar = sidecar->next)
				{
				auto side_fd = static_cast<FileData *>(sidecar->data);
				if (!strcmp(path, side_fd->path)
	                            || g_str_has_suffix(side_fd->path, slash_plus_filename))
					{
					fd_to_select = file_data_ref(side_fd);
					continue;  // will exit both nested loops.
					}
				}
			}

		if (!fd_to_select)
			{
			log_printf("remote sent --selection-add filename that could not be found: \"%s\"\n",
				   filename);
			}

		filelist_free(file_list);
		g_free(slash_plus_filename);
		g_free(filename);
		g_free(path);
		}

	if (fd_to_select)
		{
		GList *to_select = g_list_append(nullptr, fd_to_select);
		// Using the "_list" variant doesn't clear the existing selection.
		layout_select_list(lw_id, to_select);
		filelist_free(to_select);
		}
}

static void gr_selection_clear(const gchar *, GIOChannel *, gpointer)
{
	layout_select_none(lw_id);  // Checks lw_id validity internally.
}

static void gr_selection_remove(const gchar *text, GIOChannel *, gpointer)
{
	if (!layout_valid(&lw_id)) return;

	GList *selected = layout_selection_list(lw_id);  // Keep copy to free.
	if (!selected)
		{
		log_printf("remote sent --selection-remove with empty selection.");
		return;
		}

	FileData *fd_to_deselect = nullptr;
	gchar *path = nullptr;
	gchar *filename = nullptr;
	gchar *slash_plus_filename = nullptr;
	if (strcmp(text, "") == 0)
		{
		// No file specified, use current fd.
		fd_to_deselect = layout_image_get_fd(lw_id);
		if (!fd_to_deselect)
			{
			log_printf("remote sent \"--selection-remove:\" with no current image");
			filelist_free(selected);
			return;
			}
		}
	else
		{
		// Search through the selection list for a file matching the specified path.
		// "Match" is either a basename match or a file path match.
		path = expand_tilde(text);
		filename = g_path_get_basename(path);
		slash_plus_filename = g_strdup_printf("%s%s", G_DIR_SEPARATOR_S, filename);
		}

	GList *prior_link = nullptr;  // Stash base for link removal to avoid a second traversal.
	GList *link_to_remove = nullptr;
	for (GList *work = selected; work; prior_link = work, work = work->next)
		{
		auto fd = static_cast<FileData *>(work->data);
		if (fd_to_deselect)
			{
			if (fd == fd_to_deselect)
				{
				link_to_remove = work;
				break;
				}
			}
		else
			{
			// path, filename, and slash_plus_filename should be defined.

			if (!strcmp(path, fd->path) || g_str_has_suffix(fd->path, slash_plus_filename))
				{
				link_to_remove = work;
				break;
				}
			}
		}

	if (!link_to_remove)
		{
		if (fd_to_deselect)
			{
			log_printf("remote sent \"--selection-remove=\" but current image is not selected");
			}
		else
			{
			log_printf("remote sent \"--selection-remove=%s\" but that filename is not selected",
				   filename);
			}
		}
	else
		{
		if (link_to_remove == selected)
			{
			// Remove first link.
			selected = g_list_remove_link(selected, link_to_remove);
			filelist_free(link_to_remove);
			link_to_remove = nullptr;
			}
		else
			{
			// Remove a subsequent link.
			prior_link = g_list_remove_link(prior_link, link_to_remove);
			filelist_free(link_to_remove);
			link_to_remove = nullptr;
			}

		// Re-select all but the deselected item.
		layout_select_none(lw_id);
		layout_select_list(lw_id, selected);
		}

	filelist_free(selected);
	file_data_unref(fd_to_deselect);
	g_free(slash_plus_filename);
	g_free(filename);
	g_free(path);
}

static void gr_collection(const gchar *text, GIOChannel *channel, gpointer)
{
	GString *contents = g_string_new(nullptr);

	if (is_collection(text))
		{
		collection_contents(text, &contents);
		}
	else
		{
		return;
		}

	g_io_channel_write_chars(channel, contents->str, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_string_free(contents, TRUE);
}

static void gr_collection_list(const gchar *, GIOChannel *channel, gpointer)
{

	GList *collection_list = nullptr;
	GList *work;
	GString *out_string = g_string_new(nullptr);

	collect_manager_list(&collection_list, nullptr, nullptr);

	work = collection_list;
	while (work)
		{
		auto collection_name = static_cast<const gchar *>(work->data);
		out_string = g_string_append(out_string, collection_name);
		out_string = g_string_append(out_string, "\n");

		work = work->next;
		}

	g_io_channel_write_chars(channel, out_string->str, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_list_free_full(collection_list, g_free);
	g_string_free(out_string, TRUE);
}

static gboolean wait_cb(gpointer data)
{
	gint position = GPOINTER_TO_INT(data);
	gint x = position >> 16;
	gint y = position - (x << 16);

	gq_gtk_window_move(GTK_WINDOW(lw_id->window), x, y);

	return G_SOURCE_REMOVE;
}

static void gr_geometry(const gchar *text, GIOChannel *, gpointer)
{
	gchar **geometry;

	if (!layout_valid(&lw_id) || !text)
		{
		return;
		}

	if (text[0] == '+')
		{
		geometry = g_strsplit_set(text, "+", 3);
		if (geometry[1] != nullptr && geometry[2] != nullptr )
			{
			gq_gtk_window_move(GTK_WINDOW(lw_id->window), atoi(geometry[1]), atoi(geometry[2]));
			}
		}
	else
		{
		geometry = g_strsplit_set(text, "+x", 4);
		if (geometry[0] != nullptr && geometry[1] != nullptr)
			{
			gtk_window_resize(GTK_WINDOW(lw_id->window), atoi(geometry[0]), atoi(geometry[1]));
			}
		if (geometry[2] != nullptr && geometry[3] != nullptr)
			{
			/* There is an occasional problem with a window_move immediately after a window_resize */
			g_idle_add(wait_cb, GINT_TO_POINTER((atoi(geometry[2]) << 16) + atoi(geometry[3])));
			}
		}
	g_strfreev(geometry);
}

static void gr_filelist(const gchar *text, GIOChannel *channel, gpointer)
{
	get_filelist(text, channel, FALSE);
}

static void gr_filelist_recurse(const gchar *text, GIOChannel *channel, gpointer)
{
	get_filelist(text, channel, TRUE);
}

static void gr_file_tell(const gchar *, GIOChannel *channel, gpointer)
{
	gchar *out_string;

	if (!layout_valid(&lw_id)) return;

	const gchar *filename = image_get_path(lw_id->image);
	if (filename)
		{
		if (lw_id->image->collection && lw_id->image->collection->name)
			{
			g_autofree gchar *collection_name = remove_extension_from_path(lw_id->image->collection->name);
			out_string = g_strconcat(filename, "    Collection: ", collection_name, NULL);
			}
		else
			{
			out_string = g_strconcat(filename, NULL);
			}
		}
	else
		{
		out_string = g_strconcat(lw_id->dir_fd->path, G_DIR_SEPARATOR_S, NULL);
		}

	g_io_channel_write_chars(channel, out_string, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_free(out_string);
}

static void gr_file_info(const gchar *, GIOChannel *channel, gpointer)
{
	FileData *fd;
	GString *out_string;
	FileFormatClass format_class;

	if (!layout_valid(&lw_id)) return;

	const gchar *filename = image_get_path(lw_id->image);
	if (filename)
		{
		fd = file_data_new_group(filename);
		out_string = g_string_new(nullptr);

		if (fd->pixbuf)
			{
			format_class = filter_file_get_class(filename);
			}
		else
			{
			format_class = FORMAT_CLASS_UNKNOWN;
			}

		g_string_append_printf(out_string, _("Class: %s\n"), format_class_list[format_class]);

		if (fd->page_total > 1)
			{
			g_string_append_printf(out_string, _("Page no: %d/%d\n"), fd->page_num + 1, fd->page_total);
			}

		if (fd->exif)
			{
			g_autofree gchar *country_name = exif_get_data_as_text(fd->exif, "formatted.countryname");
			if (country_name)
				{
				g_string_append_printf(out_string, _("Country name: %s\n"), country_name);
				}

			g_autofree gchar *country_code = exif_get_data_as_text(fd->exif, "formatted.countrycode");
			if (country_code)
				{
				g_string_append_printf(out_string, _("Country code: %s\n"), country_code);
				}

			g_autofree gchar *timezone = exif_get_data_as_text(fd->exif, "formatted.timezone");
			if (timezone)
				{
				g_string_append_printf(out_string, _("Timezone: %s\n"), timezone);
				}

			g_autofree gchar *local_time = exif_get_data_as_text(fd->exif, "formatted.localtime");
			if (local_time)
				{
				g_string_append_printf(out_string, ("Local time: %s\n"), local_time);
				}
			}

		g_io_channel_write_chars(channel, out_string->str, -1, nullptr, nullptr);
		g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

		g_string_free(out_string, TRUE);
		file_data_unref(fd);
		}
}

static gchar *config_file_path(const gchar *param)
{
	gchar *path = nullptr;
	gchar *full_name = nullptr;

	if (file_extension_match(param, ".xml"))
		{
		path = g_build_filename(get_window_layouts_dir(), param, NULL);
		}
	else if (file_extension_match(param, nullptr))
		{
		full_name = g_strconcat(param, ".xml", NULL);
		path = g_build_filename(get_window_layouts_dir(), full_name, NULL);
		}

	if (!isfile(path))
		{
		g_free(path);
		path = nullptr;
		}

	g_free(full_name);
	return path;
}

static gboolean is_config_file(const gchar *param)
{
	gchar *name = nullptr;

	name = config_file_path(param);
	if (name)
		{
		g_free(name);
		return TRUE;
		}
	return FALSE;
}

static void gr_config_load(const gchar *text, GIOChannel *, gpointer)
{
	gchar *filename = expand_tilde(text);

	if (!g_strstr_len(filename, -1, G_DIR_SEPARATOR_S))
		{
		if (is_config_file(filename))
			{
			gchar *tmp = config_file_path(filename);
			g_free(filename);
			filename = tmp;
			}
		}

	if (isfile(filename))
		{
		load_config_from_file(filename, FALSE);
		}
	else
		{
		log_printf("remote sent filename that does not exist:\"%s\"\n", filename ? filename : "(null)");
		layout_set_path(nullptr, homedir());
		}

	g_free(filename);
}

static void gr_window_list(const gchar *, GIOChannel *channel, gpointer)
{
	GString *window_list;

	window_list = layout_get_window_list();

	g_io_channel_write_chars(channel, window_list->str, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_string_free(window_list, TRUE);
}

static void gr_get_sidecars(const gchar *text, GIOChannel *channel, gpointer)
{
	gchar *filename = expand_tilde(text);
	FileData *fd = file_data_new_group(filename);

	GList *work;
	if (fd->parent) fd = fd->parent;

	g_io_channel_write_chars(channel, fd->path, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	work = fd->sidecar_files;

	while (work)
		{
		fd = static_cast<FileData *>(work->data);
		work = work->next;
		g_io_channel_write_chars(channel, fd->path, -1, nullptr, nullptr);
		g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);
		}
	g_free(filename);
}

static void gr_get_destination(const gchar *text, GIOChannel *channel, gpointer)
{
	gchar *filename = expand_tilde(text);
	FileData *fd = file_data_new_group(filename);

	if (fd->change && fd->change->dest)
		{
		g_io_channel_write_chars(channel, fd->change->dest, -1, nullptr, nullptr);
		g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);
		}
	g_free(filename);
}

static void gr_file_view(const gchar *text, GIOChannel *, gpointer)
{
	gchar *filename;
	gchar *tilde_filename = expand_tilde(text);

	filename = set_pwd(tilde_filename);

	view_window_new(file_data_new_group(filename));
	g_free(filename);
	g_free(tilde_filename);
}

static void gr_list_clear(const gchar *, GIOChannel *, gpointer data)
{
	auto remote_data = static_cast<RemoteData *>(data);

	remote_data->command_collection = nullptr;
	remote_data->file_list = nullptr;
	remote_data->single_dir = TRUE;
}

static void gr_list_add(const gchar *text, GIOChannel *, gpointer data)
{
	auto remote_data = static_cast<RemoteData *>(data);
	gboolean is_new = TRUE;
	gchar *path = nullptr;
	FileData *fd;
	FileData *first;

	/** @FIXME Should check if file is in current dir, has tilde or is relative */
	if (!isfile(text))
		{
		log_printf("Warning: File does not exist --remote --list-add:%s", text);

		return;
		}

	/* If there is a files list on the command line
	 * check if they are all in the same folder
	 */
	if (remote_data->single_dir)
		{
		GList *work;
		work = remote_data->file_list;
		while (work && remote_data->single_dir)
			{
			gchar *dirname;
			dirname = g_path_get_dirname((static_cast<FileData *>(work->data))->path);
			if (!path)
				{
				path = g_strdup(dirname);
				}
			else
				{
				if (g_strcmp0(path, dirname) != 0)
					{
					remote_data->single_dir = FALSE;
					}
				}
			g_free(dirname);
			work = work->next;
			}
		g_free(path);
		}

	gchar *pathname = g_path_get_dirname(text);
	layout_set_path(lw_id, pathname);
	g_free(pathname);

	fd = file_data_new_simple(text);
	remote_data->file_list = g_list_append(remote_data->file_list, fd);
	file_data_unref(fd);

	vf_select_none(lw_id->vf);
	remote_data->file_list = g_list_reverse(remote_data->file_list);

	layout_select_list(lw_id, remote_data->file_list);
	layout_refresh(lw_id);
	first = static_cast<FileData *>(g_list_first(vf_selection_get_list(lw_id->vf))->data);
	layout_set_fd(lw_id, first);

		CollectionData *cd;
		CollectWindow *cw;

	if (!remote_data->command_collection && !remote_data->single_dir)
		{
		cw = collection_window_new(nullptr);
		cd = cw->cd;

		collection_path_changed(cd);

		remote_data->command_collection = cd;
		}
	else if (!remote_data->single_dir)
		{
		is_new = (!collection_get_first(remote_data->command_collection));
		}

	if (!remote_data->single_dir)
		{
		layout_image_set_collection(lw_id, remote_data->command_collection, collection_get_first(remote_data->command_collection));
		if (collection_add(remote_data->command_collection, file_data_new_group(text), FALSE) && is_new)
			{
			layout_image_set_collection(lw_id, remote_data->command_collection, collection_get_first(remote_data->command_collection));
			}
		}
}

static void gr_action(const gchar *text, GIOChannel *, gpointer)
{
	GtkAction *action;

	if (!layout_valid(&lw_id))
		{
		return;
		}

	if (g_strstr_len(text, -1, ".desktop") != nullptr)
		{
		file_util_start_editor_from_filelist(text, layout_selection_list(lw_id), layout_get_path(lw_id), lw_id->window);
		}
	else
		{
		action = gq_gtk_action_group_get_action(lw_id->action_group, text);
		if (action)
			{
			gq_gtk_action_activate(action);
			}
		else
			{
			log_printf("Action %s unknown", text);
			}
		}
}

static void gr_action_list(const gchar *, GIOChannel *channel, gpointer)
{
	gint max_length = 0;

	if (!layout_valid(&lw_id))
		{
		return;
		}

	std::vector<ActionItem> list = get_action_items();

	/* Get the length required for padding */
	for (const ActionItem &action_item : list)
		{
		const auto length = g_utf8_strlen(action_item.name, -1);
		max_length = MAX(length, max_length);
		}

	/* Pad the action names to the same column for readable output */
	g_autoptr(GString) out_string = g_string_new(nullptr);
	for (const ActionItem &action_item : list)
		{
		g_string_append_printf(out_string, "%-*s", max_length + 4, action_item.name);
		out_string = g_string_append(out_string, action_item.label);
		out_string = g_string_append(out_string, "\n");
		}

	g_io_channel_write_chars(channel, out_string->str, -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);
}

static void gr_raise(const gchar *, GIOChannel *, gpointer)
{
	if (layout_valid(&lw_id))
		{
		gtk_window_present(GTK_WINDOW(lw_id->window));
		}
}

static void gr_pwd(const gchar *text, GIOChannel *, gpointer)
{
	LayoutWindow *lw = nullptr;

	layout_valid(&lw);

	g_free(pwd);
	pwd = g_strdup(text);
	lw_id = lw;
}

static void gr_print0(const gchar *, GIOChannel *channel, gpointer)
{
	g_io_channel_write_chars(channel, "print0", -1, nullptr, nullptr);
	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);
}

#if HAVE_LUA
static void gr_lua(const gchar *text, GIOChannel *channel, gpointer)
{
	gchar *result = nullptr;
	gchar **lua_command;

	lua_command = g_strsplit(text, ",", 2);

	if (lua_command[0] && lua_command[1])
		{
		FileData *fd = file_data_new_group(lua_command[0]);
		result = g_strdup(lua_callvalue(fd, lua_command[1], nullptr));
		if (result)
			{
			g_io_channel_write_chars(channel, result, -1, nullptr, nullptr);
			}
		else
			{
			g_io_channel_write_chars(channel, N_("lua error: no data"), -1, nullptr, nullptr);
			}
		}
	else
		{
		g_io_channel_write_chars(channel, N_("lua error: no data"), -1, nullptr, nullptr);
		}

	g_io_channel_write_chars(channel, "<gq_end_of_command>", -1, nullptr, nullptr);

	g_strfreev(lua_command);
	g_free(result);
}
#endif

struct RemoteCommandEntry {
	const gchar *opt_s;
	const gchar *opt_l;
	void (*func)(const gchar *text, GIOChannel *channel, gpointer data);
	gboolean needs_extra;
	gboolean prefer_command_line;
	const gchar *parameter;
	const gchar *description;
};

static RemoteCommandEntry remote_commands[] = {
	/* short,     long                    callback,                extra, prefer,  parameter,               description */
	{ nullptr, "--action=",               gr_action,               TRUE,  FALSE, N_("<ACTION>"),          N_("execute keyboard action (See Help/Reference/Remote Keyboard Actions)") },
	{ nullptr, "--action-list",           gr_action_list,          FALSE, FALSE, nullptr,                 N_("list available keyboard actions (some are redundant)") },
	{ "-b",    "--back",                  gr_image_prev,           FALSE, FALSE, nullptr,                 N_("previous image") },
	{ nullptr, "--close-window",          gr_close_window,         FALSE, FALSE, nullptr,                 N_("close window") },
	{ nullptr, "--config-load=",          gr_config_load,          TRUE,  FALSE, N_("<FILE>|layout ID"),  N_("load configuration from FILE") },
	{ nullptr, "--cache-metadata",        gr_cache_metadata,       FALSE, FALSE, nullptr,                 N_("clean the metadata cache") },
	{ nullptr, "--cache-render=",         gr_cache_render,         TRUE,  FALSE, N_("<folder>  "),        N_(" render thumbnails") },
	{ nullptr, "--cache-render-recurse=", gr_cache_render_recurse, TRUE,  FALSE, N_("<folder> "),         N_("render thumbnails recursively") },
	{ nullptr, "--cache-render-shared=",  gr_cache_render_standard,TRUE,  FALSE, N_("<folder> "),         N_(" render thumbnails (see Help)") },
	{ nullptr, "--cache-render-shared-recurse=", gr_cache_render_standard_recurse, TRUE, FALSE, N_("<folder>"), N_(" render thumbnails recursively (see Help)") },
	{ nullptr, "--cache-shared=",         gr_cache_shared,         TRUE,  FALSE, N_("clean|clear"),       N_("clean or clear shared thumbnail cache") },
	{ nullptr, "--cache-thumbs=",         gr_cache_thumb,          TRUE,  FALSE, N_("clean|clear"),       N_("clean or clear thumbnail cache") },
	{ "-d",    "--delay=",                gr_slideshow_delay,      TRUE,  FALSE, N_("<[H:][M:][N][.M]>"), N_("set slide show delay to Hrs Mins N.M seconds") },
	{ nullptr, "--first",                 gr_image_first,          FALSE, FALSE, nullptr,                 N_("first image") },
	{ "-f",    "--fullscreen",            gr_fullscreen_toggle,    FALSE, TRUE,  nullptr,                 N_("toggle full screen") },
	{ nullptr, "--file=",                 gr_file_load,            TRUE,  FALSE, N_("<FILE>|<URL>"),      N_("open FILE or URL, bring Geeqie window to the top") },
	{ nullptr, "file=",                   gr_file_load,            TRUE,  FALSE, N_("<FILE>|<URL>"),      N_("open FILE or URL, bring Geeqie window to the top") },
	{ nullptr, "--File=",                 gr_file_load_no_raise,   TRUE,  FALSE, N_("<FILE>|<URL>"),      N_("open FILE or URL, do not bring Geeqie window to the top") },
	{ nullptr, "File=",                   gr_file_load_no_raise,   TRUE,  FALSE, N_("<FILE>|<URL>"),      N_("open FILE or URL, do not bring Geeqie window to the top") },
	{ nullptr, "--fullscreen-start",      gr_fullscreen_start,     FALSE, FALSE, nullptr,                 N_("start full screen") },
	{ nullptr, "--fullscreen-stop",       gr_fullscreen_stop,      FALSE, FALSE, nullptr,                 N_("stop full screen") },
	{ nullptr, "--geometry=",             gr_geometry,             TRUE,  FALSE, N_("<GEOMETRY>"),        N_("set window geometry") },
	{ nullptr, "--get-collection=",       gr_collection,           TRUE,  FALSE, N_("<COLLECTION>"),      N_("get collection content") },
	{ nullptr, "--get-collection-list",   gr_collection_list,      FALSE, FALSE, nullptr,                 N_("get collection list") },
	{ nullptr, "--get-destination=",      gr_get_destination,      TRUE,  FALSE, N_("<FILE>"),            N_("get destination path of FILE (See Plugins Configuration)") },
	{ nullptr, "--get-file-info",         gr_file_info,            FALSE, FALSE, nullptr,                 N_("get file info") },
	{ nullptr, "--get-filelist=",         gr_filelist,             TRUE,  FALSE, N_("[<FOLDER>]"),        N_("get list of files and class") },
	{ nullptr, "--get-filelist-recurse=", gr_filelist_recurse,     TRUE,  FALSE, N_("[<FOLDER>]"),        N_("get list of files and class recursive") },
	{ nullptr, "--get-rectangle",         gr_rectangle,            FALSE, FALSE, nullptr,                 N_("get rectangle co-ordinates") },
	{ nullptr, "--get-render-intent",     gr_render_intent,        FALSE, FALSE, nullptr,                 N_("get render intent") },
	{ nullptr, "--get-selection",         gr_get_selection,        FALSE, FALSE, nullptr,                 N_("get list of selected files") },
	{ nullptr, "--get-sidecars=",         gr_get_sidecars,         TRUE,  FALSE, N_("<FILE>"),            N_("get list of sidecars of FILE") },
	{ nullptr, "--get-window-list",       gr_window_list,          FALSE, FALSE, nullptr,                 N_("get window list") },
	{ nullptr, "--id=",                   gr_lw_id,                TRUE,  FALSE, N_("<ID>"),              N_("window id for following commands") },
	{ nullptr, "--last",                  gr_image_last,           FALSE, FALSE, nullptr,                 N_("last image") },
	{ nullptr, "--list-add=",             gr_list_add,             TRUE,  FALSE, N_("<FILE>"),            N_("add FILE to command line collection list") },
	{ nullptr, "--list-clear",            gr_list_clear,           FALSE, FALSE, nullptr,                 N_("clear command line collection list") },
#if HAVE_LUA
	{ nullptr, "--lua=",                  gr_lua,                  TRUE,  FALSE, N_("<FILE>,<lua script>"), N_("run lua script on FILE") },
#endif
	{ nullptr, "--new-window",            gr_new_window,           FALSE, FALSE, nullptr,                 N_("new window") },
	{ "-n",    "--next",                  gr_image_next,           FALSE, FALSE, nullptr,                 N_("next image") },
	{ nullptr, "--pixel-info",            gr_pixel_info,           FALSE, FALSE, nullptr,                 N_("print pixel info of mouse pointer on current image") },
	{ nullptr, "--print0",                gr_print0,               TRUE,  FALSE, nullptr,                 N_("terminate returned data with null character instead of newline") },
	{ nullptr, "--PWD=",                  gr_pwd,                  TRUE,  FALSE, N_("<PWD>"),             N_("use PWD as working directory for following commands") },
	{ "-q",    "--quit",                  gr_quit,                 FALSE, FALSE, nullptr,                 N_("quit") },
	{ nullptr, "--raise",                 gr_raise,                FALSE, FALSE, nullptr,                 N_("bring the Geeqie window to the top") },
	{ nullptr, "raise",                   gr_raise,                FALSE, FALSE, nullptr,                 N_("bring the Geeqie window to the top") },
	{ nullptr, "--selection-add=",        gr_selection_add,        TRUE,  FALSE, N_("[<FILE>]"),          N_("adds the current file (or the specified file) to the current selection") },
	{ nullptr, "--selection-clear",       gr_selection_clear,      FALSE, FALSE, nullptr,                 N_("clears the current selection") },
	{ nullptr, "--selection-remove=",     gr_selection_remove,     TRUE,  FALSE, N_("[<FILE>]"),          N_("removes the current file (or the specified file) from the current selection") },
	{ "-s",    "--slideshow",             gr_slideshow_toggle,     FALSE, TRUE,  nullptr,                 N_("toggle slide show") },
	{ nullptr, "--slideshow-recurse=",    gr_slideshow_start_rec,  TRUE,  FALSE, N_("<FOLDER>"),          N_("start recursive slide show in FOLDER") },
	{ nullptr, "--slideshow-start",       gr_slideshow_start,      FALSE, FALSE, nullptr,                 N_("start slide show") },
	{ nullptr, "--slideshow-stop",        gr_slideshow_stop,       FALSE, FALSE, nullptr,                 N_("stop slide show") },
	{ nullptr, "--tell",                  gr_file_tell,            FALSE, FALSE, nullptr,                 N_("print filename [and Collection] of current image") },
	{ "-T",    "--tools-show",            gr_tools_show,           FALSE, TRUE,  nullptr,                 N_("show tools") },
	{ "-t",    "--tools-hide",	          gr_tools_hide,           FALSE, TRUE,  nullptr,                 N_("hide tools") },
	{ nullptr, "--view=",                 gr_file_view,            TRUE,  FALSE, N_("<FILE>"),            N_("open FILE in new window") },
	{ nullptr, "view=",                   gr_file_view,            TRUE,  FALSE, N_("<FILE>"),            N_("open FILE in new window") },
	{ nullptr, nullptr, nullptr, FALSE, FALSE, nullptr, nullptr }
};

static RemoteCommandEntry *remote_command_find(const gchar *text, const gchar **offset)
{
	gint i = 0;

	while (remote_commands[i].func != nullptr)
		{
		if (remote_commands[i].needs_extra)
			{
			if (remote_commands[i].opt_s &&
			    strncmp(remote_commands[i].opt_s, text, strlen(remote_commands[i].opt_s)) == 0)
				{
				if (offset) *offset = text + strlen(remote_commands[i].opt_s);
				return &remote_commands[i];
				}

			if (remote_commands[i].opt_l &&
				 strncmp(remote_commands[i].opt_l, text, strlen(remote_commands[i].opt_l)) == 0)
				{
				if (offset) *offset = text + strlen(remote_commands[i].opt_l);
				return &remote_commands[i];
				}
			}
		else
			{
			if ((remote_commands[i].opt_s && strcmp(remote_commands[i].opt_s, text) == 0) ||
			    (remote_commands[i].opt_l && strcmp(remote_commands[i].opt_l, text) == 0))
				{
				if (offset) *offset = text;
				return &remote_commands[i];
				}
			}

		i++;
		}

	return nullptr;
}

gboolean is_remote_command(const gchar *text)
{
	RemoteCommandEntry *entry = nullptr;

	entry = remote_command_find(text, nullptr);

	return entry ? TRUE : FALSE;
}

static void remote_cb(RemoteConnection *, const gchar *text, GIOChannel *channel, gpointer data)
{
	RemoteCommandEntry *entry;
	const gchar *offset;

	entry = remote_command_find(text, &offset);
	if (entry && entry->func)
		{
		entry->func(offset, channel, data);
		}
	else
		{
		log_printf("unknown remote command:%s\n", text);
		}
}

void remote_help()
{
	gint i;
	gchar *s_opt_param;
	gchar *l_opt_param;

	print_term(FALSE, _("Remote command list:\n"));

	i = 0;
	while (remote_commands[i].func != nullptr)
		{
		if (remote_commands[i].description)
			{
			s_opt_param = g_strdup(remote_commands[i].opt_s  ? remote_commands[i].opt_s : "" );
			l_opt_param = g_strconcat(remote_commands[i].opt_l, remote_commands[i].parameter, NULL);

			if (g_str_has_prefix(l_opt_param, "--"))
				{
				printf_term(FALSE, "  %-4s %-40s%-s\n", s_opt_param, l_opt_param, _(remote_commands[i].description));
				}
			g_free(s_opt_param);
			g_free(l_opt_param);
			}
		i++;
		}
	printf_term(FALSE, "%s", _("\n\n  All other command line parameters are used as plain files if they exist.\n\n  The name of a collection, with or without either path or extension (.gqv) may be used.\n"));
}

GList *remote_build_list(GList *list, gint argc, gchar *argv[], GList **errors)
{
	gint i;

	i = 1;
	while (i < argc)
		{
		RemoteCommandEntry *entry;

		entry = remote_command_find(argv[i], nullptr);
		if (entry)
			{
			list = g_list_append(list, argv[i]);
			}
		else if (errors && !isname(argv[i]))
			{
			*errors = g_list_append(*errors, argv[i]);
			}
		i++;
		}

	return list;
}

/**
 * @param arg_exec Binary (argv0)
 * @param remote_list Evaluated and recognized remote commands
 * @param path The current path
 * @param cmd_list List of all non collections in Path (gchar *path)
 * @param collection_list List of all collections in argv
 */
void remote_control(const gchar *arg_exec, GList *remote_list, const gchar *path,
		    GList *cmd_list, GList *collection_list)
{
	RemoteConnection *rc;
	gboolean started = FALSE;
	gchar *buf;

	buf = g_build_filename(get_rc_dir(), ".command", NULL);
	rc = remote_client_open(buf);
	if (!rc)
		{
		GString *command;
		GList *work;
		gint retry_count = 12;
		gboolean blank = FALSE;

		printf_term(FALSE, _("Remote %s not running, starting..."), GQ_APPNAME);

		command = g_string_new(arg_exec);

		work = remote_list;
		while (work)
			{
			gchar *text;
			RemoteCommandEntry *entry;

			text = static_cast<gchar *>(work->data);
			work = work->next;

			entry = remote_command_find(text, nullptr);
			if (entry)
				{
				/* If Geeqie is not running, stop the --new-window command opening a second window */
				if (g_strcmp0(text, "--new-window") == 0)
					{
					remote_list = g_list_remove(remote_list, text);
					}
				if (entry->prefer_command_line)
					{
					remote_list = g_list_remove(remote_list, text);
					g_string_append(command, " ");
					g_string_append(command, text);
					}
				if (entry->opt_l && strcmp(entry->opt_l, "file=") == 0)
					{
					blank = TRUE;
					}
				}
			}

		if (blank || cmd_list || path) g_string_append(command, " --blank");
		if (get_debug_level()) g_string_append(command, " --debug");

		g_string_append(command, " &");
		runcmd(command->str);
		g_string_free(command, TRUE);

		while (!rc && retry_count > 0)
			{
			usleep((retry_count > 10) ? 500000 : 1000000);
			rc = remote_client_open(buf);
			if (!rc) print_term(FALSE, ".");
			retry_count--;
			}

		print_term(FALSE, "\n");

		started = TRUE;
		}
	g_free(buf);

	if (rc)
		{
		GList *work;
		const gchar *prefix;
		gboolean use_path = TRUE;
		gboolean sent = FALSE;

		work = remote_list;
		while (work)
			{
			gchar *text;
			RemoteCommandEntry *entry;

			text = static_cast<gchar *>(work->data);
			work = work->next;

			entry = remote_command_find(text, nullptr);
			if (entry &&
			    entry->opt_l &&
			    strcmp(entry->opt_l, "file=") == 0) use_path = FALSE;

			remote_client_send(rc, text);

			sent = TRUE;
			}

		if (cmd_list && cmd_list->next)
			{
			prefix = "--list-add=";
			remote_client_send(rc, "--list-clear");
			}
		else
			{
			prefix = "file=";
			}

		work = cmd_list;
		while (work)
			{
			gchar *text;

			text = g_strconcat(prefix, work->data, NULL);
			remote_client_send(rc, text);
			g_free(text);
			work = work->next;

			sent = TRUE;
			}

		if (path && !cmd_list && use_path)
			{
			gchar *text;

			text = g_strdup_printf("file=%s", path);
			remote_client_send(rc, text);
			g_free(text);

			sent = TRUE;
			}

		work = collection_list;
		while (work)
			{
			const gchar *name;
			gchar *text;

			name = static_cast<const gchar *>(work->data);
			work = work->next;

			text = g_strdup_printf("file=%s", name);
			remote_client_send(rc, text);
			g_free(text);

			sent = TRUE;
			}

		if (!started && !sent)
			{
			remote_client_send(rc, "raise");
			}
		}
	else
		{
		print_term(TRUE, _("Remote not available\n"));
		}

	_exit(0);
}

RemoteConnection *remote_server_init(gchar *path, CollectionData *command_collection)
{
	RemoteConnection *remote_connection = remote_server_open(path);
	auto remote_data = g_new(RemoteData, 1);

	remote_data->command_collection = command_collection;

	remote_server_subscribe(remote_connection, remote_cb, remote_data);
	return remote_connection;
}
/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */
