/*-
 * redshift-ng - Automatically adjust display colour temperature according the Sun
 * 
 * Copyright (c) 2009-2018        Jon Lund Steffensen <jonlst@gmail.com>
 * Copyright (c) 2014-2016, 2025  Mattias Andrée <m@maandree.se>
 * 
 * redshift-ng 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 3 of the License, or
 * (at your option) any later version.
 * 
 * redshift-ng 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 redshift-ng.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "common.h"

#if defined(__clang__)
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wreserved-identifier"
# pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# pragma clang diagnostic ignored "-Wdocumentation-unknown-command"
# pragma clang diagnostic ignored "-Wdocumentation"
# pragma clang diagnostic ignored "-Wpadded"
#endif
#include <glib.h>
#include <glib/gprintf.h>
#include <gio/gio.h>
#if defined(__clang__)
# pragma clang diagnostic pop
#endif


/**
 * D-Bus error indicating denial of access
 */
#define DBUS_ACCESS_ERROR  "org.freedesktop.DBus.Error.AccessDenied"


/**
 * Location data
 */
struct location_data {
	/**
	 * The user's geographical location
	 */
	struct location location;

	/**
	 * Whether the location provider is available
	 */
	int available;

	/**
	 * Whether an unrecoverable error has occurred
	 */
	int error;
};


struct location_state {
	GMainLoop *loop;

	/**
	 * Slave thread, used to receive location updates
	 */
	GThread *thread;

	/**
	 * Read-end of piped used to send location data
	 * from the slave thread to the master thread
	 */
	int pipe_fd_read;

	/**
	 * Write-end of piped used to send location data
	 * from the slave thread to the master thread
	 */
	int pipe_fd_write;

	/**
	 * Location data available from the slave thread
	 */
	struct location_data data;

	/**
	 * Location data sent to the master thread
	 */
	struct location_data saved_data;
};


/* Print the message explaining denial from GeoClue */
static void
print_denial_message(void)
{
	g_printerr(_(
		"Access to the current location was denied by GeoClue!\n"
		"Make sure that location services are enabled and that"
		" Redshift is permitted\nto use location services."
		" See https://github.com/jonls/redshift#faq for more\n"
		"information.\n"));
}


static void
send_data(struct location_state *state)
{
	while (write(state->pipe_fd_write, &state->data, sizeof(state->data)) == -1 && errno == EINTR);
}


/* Indicate an unrecoverable error during GeoClue2 communication */
static void
mark_error(struct location_state *state)
{
	state->data.error = 1;
	send_data(state);
}


/* Handle position change callbacks */
static void
geoclue_client_signal_cb(GDBusProxy *client, gchar *sender_name, gchar *signal_name, GVariant *parameters, gpointer user_data)
{
	struct location_state *state = user_data;
	const gchar *location_path;
	GDBusProxy *location;
	GError *error;
	GVariant *lat_v, *lon_v;

	(void) sender_name;

	/* Only handle LocationUpdated signals */
	if (g_strcmp0(signal_name, "LocationUpdated"))
		return;

	/* Obtain location path */
	g_variant_get_child(parameters, 1, "&o", &location_path);

	/* Obtain location */
	error = NULL;
	location = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(client), G_DBUS_PROXY_FLAGS_NONE,
	                                 NULL, "org.freedesktop.GeoClue2", location_path,
	                                 "org.freedesktop.GeoClue2.Location", NULL, &error);
	if (!location) {
		weprintf(_("Unable to obtain location: %s."), error->message);
		g_error_free(error);
		mark_error(state);
		return;
	}

	/* Read location properties */
	lat_v = g_dbus_proxy_get_cached_property(location, "Latitude");
	state->data.location.latitude = g_variant_get_double(lat_v);
	lon_v = g_dbus_proxy_get_cached_property(location, "Longitude");
	state->data.location.longitude = g_variant_get_double(lon_v);
	state->data.available = 1;

	send_data(state);
}


/* Callback when GeoClue name appears on the bus */
static void
on_name_appeared(GDBusConnection *conn, const gchar *name, const gchar *name_owner, gpointer user_data)
{
	struct location_state *state = user_data;
	const gchar *client_path;
	GDBusProxy *geoclue_client;
	GVariant *client_path_v;
	GDBusProxy *geoclue_manager;
	GError *error;
	GVariant *ret_v;
	gchar *dbus_error;

	(void) name;
	(void) name_owner;

	/* Obtain GeoClue Manager */
	error = NULL;
	geoclue_manager = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL,
	                                        "org.freedesktop.GeoClue2", "/org/freedesktop/GeoClue2/Manager",
	                                        "org.freedesktop.GeoClue2.Manager", NULL, &error);
	if (!geoclue_manager) {
		weprintf(_("Unable to obtain GeoClue Manager: %s."), error->message);
		g_error_free(error);
		mark_error(state);
		return;
	}

	/* Obtain GeoClue Client path */
	error = NULL;
	client_path_v = g_dbus_proxy_call_sync(geoclue_manager, "GetClient", NULL,
	                                       G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
	if (!client_path_v) {
		weprintf(_("Unable to obtain GeoClue client path: %s."), error->message);
		g_error_free(error);
		g_object_unref(geoclue_manager);
		mark_error(state);
		return;
	}

	g_variant_get(client_path_v, "(&o)", &client_path);

	/* Obtain GeoClue client */
	error = NULL;
	geoclue_client = g_dbus_proxy_new_sync(conn, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.GeoClue2",
	                                       client_path, "org.freedesktop.GeoClue2.Client", NULL, &error);
	if (!geoclue_client) {
		weprintf(_("Unable to obtain GeoClue Client: %s."), error->message);
		g_error_free(error);
		g_variant_unref(client_path_v);
		g_object_unref(geoclue_manager);
		mark_error(state);
		return;
	}

	g_variant_unref(client_path_v);

	/* Set desktop id (basename of the .desktop file) */
	error = NULL;
	ret_v = g_dbus_proxy_call_sync(geoclue_client, "org.freedesktop.DBus.Properties.Set",
	                               g_variant_new("(ssv)", "org.freedesktop.GeoClue2.Client",
	                                             "DesktopId", g_variant_new("s", "redshift")),
	                               G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
	if (!ret_v) {
		/* Ignore this error for now. The property is not available
		 * in early versions of GeoClue2. */
	} else {
		g_variant_unref(ret_v);
	}

	/* Set distance threshold */
	error = NULL;
	ret_v = g_dbus_proxy_call_sync(geoclue_client, "org.freedesktop.DBus.Properties.Set",
	                               g_variant_new("(ssv)", "org.freedesktop.GeoClue2.Client",
	                                             "DistanceThreshold", g_variant_new("u", 50000)),
	                               G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
	if (!ret_v) {
		weprintf(_("Unable to set distance threshold: %s."), error->message);
		g_error_free(error);
		g_object_unref(geoclue_client);
		g_object_unref(geoclue_manager);
		mark_error(state);
		return;
	}

	g_variant_unref(ret_v);

	/* Attach signal callback to client */
	g_signal_connect(geoclue_client, "g-signal", G_CALLBACK(geoclue_client_signal_cb), user_data);

	/* Start GeoClue client */
	error = NULL;
	ret_v = g_dbus_proxy_call_sync(geoclue_client, "Start", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
	if (!ret_v) {
		weprintf(_("Unable to start GeoClue client: %s."), error->message);
		if (g_dbus_error_is_remote_error(error)) {
			dbus_error = g_dbus_error_get_remote_error( error);
			if (!g_strcmp0(dbus_error, DBUS_ACCESS_ERROR))
				print_denial_message();
			g_free(dbus_error);
		}
		g_error_free(error);
		g_object_unref(geoclue_client);
		g_object_unref(geoclue_manager);
		mark_error(state);
		return;
	}

	g_variant_unref(ret_v);
}


/* Callback when GeoClue disappears from the bus */
static void
on_name_vanished(GDBusConnection *connection, const gchar *name, gpointer user_data)
{
	struct location_state *state = user_data;

	(void) connection;
	(void) name;

	state->data.available = 0;
	send_data(state);
}


/* Callback when the pipe to the main thread is closed */
static gboolean
on_pipe_closed(GIOChannel *channel, GIOCondition condition, gpointer user_data)
{
	struct location_state *state = user_data;
	g_main_loop_quit(state->loop);

	(void) channel;
	(void) condition;
	return FALSE;
}


/* Run loop for location provider thread */
static void *
run_geoclue2_loop(void *state_)
{
	struct location_state *state = state_;
	GMainContext *context;
	guint watcher_id;
	GIOChannel *pipe_channel;
	GSource *pipe_source;

	context = g_main_context_new();
	g_main_context_push_thread_default(context);
	state->loop = g_main_loop_new(context, FALSE);

	watcher_id = g_bus_watch_name(G_BUS_TYPE_SYSTEM, "org.freedesktop.GeoClue2",
	                              G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
	                              on_name_appeared, on_name_vanished, state, NULL);

	/* Listen for closure of pipe */
	pipe_channel = g_io_channel_unix_new(state->pipe_fd_write);
	pipe_source = g_io_create_watch(pipe_channel, G_IO_IN | G_IO_HUP | G_IO_ERR);
        g_source_set_callback(pipe_source, (GSourceFunc)on_pipe_closed, state, NULL);
        g_source_attach(pipe_source, context);

	g_main_loop_run(state->loop);

	g_source_unref(pipe_source);
	g_io_channel_unref(pipe_channel);
	close(state->pipe_fd_write);

	g_bus_unwatch_name(watcher_id);

	g_main_loop_unref(state->loop);
	g_main_context_unref(context);

	return NULL;
}


static int
geoclue2_create(struct location_state **state_out)
{
#if !GLIB_CHECK_VERSION(2, 35, 0)
	g_type_init();
#endif
	*state_out = emalloc(sizeof(**state_out));
	return 0;
}


static int
geoclue2_start(struct location_state *state)
{
	int pipefds[2];

	state->pipe_fd_read = -1;
	state->pipe_fd_write = -1;

	state->data.available = 0;
	state->data.error = 0;
	state->data.location.latitude = 0;
	state->data.location.longitude = 0;
	state->saved_data = state->data;

	pipe_rdnonblock(pipefds);
	state->pipe_fd_read = pipefds[0];
	state->pipe_fd_write = pipefds[1];

	send_data(state); /* TODO why? */

	state->thread = g_thread_new("geoclue2", run_geoclue2_loop, state);

	return 0;
}


static void
geoclue2_free(struct location_state *state)
{
	if (state->pipe_fd_read >= 0)
		close(state->pipe_fd_read);

	/* Closing the pipe should cause the thread to exit, but it may be blocked by I/O */
	install_forceful_exit_signal_handlers();
	g_thread_join(state->thread);
	state->thread = NULL;

	free(state);
}


static void
geoclue2_print_help(FILE *f)
{
	fputs(_("Use the location as discovered by a GeoClue2 provider.\n"), f);
	fputs("\n", f);
}


static int
geoclue2_set_option(struct location_state *state, const char *key, const char *value)
{
	(void) state;
	(void) value;
	weprintf(_("Unknown method parameter: `%s'."), key);
	return -1;
}


static int
geoclue2_get_fd(struct location_state *state)
{
	return state->pipe_fd_read;
}


static int
geoclue2_fetch(struct location_state *state, struct location *location_out, int *available_out)
{
	struct location_data data;
	ssize_t r;

	for (;;) {
		r = read(state->pipe_fd_read, &data, sizeof(data));
		if (r == (ssize_t)sizeof(data)) {
			state->saved_data = data;
		} else if (r > 0) {
			/* writes of 512 bytes or less are always atomic on pipes */
			weprintf("read <pipe>: %s", _("Unexpected message size"));
		} else if (!r || errno == EAGAIN) {
			break;
		} else if (errno != EINTR) {
			weprintf("read <pipe>:");
			state->saved_data.error = 1;
			break;
		}
	}

	*location_out = state->saved_data.location;
	*available_out = state->saved_data.available;
	return state->saved_data.error ? -1 : 0;
}


const struct location_provider geoclue2_location_provider = LOCATION_PROVIDER_INIT("geoclue2", geoclue2);