#include "stdafx.h"
#include "ScreenSaver.h"

namespace gui {

#if defined(WINDOWS)

	ScreenSaver::ScreenSaver() {}

	void ScreenSaver::platformInit() {}

	void ScreenSaver::platformDestroy() {}

	void ScreenSaver::inhibit() {
		// ES_CONTINUOUS makes ES_DISPLAY_REQUIRED "permanent" until we say otherwise.
		SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
	}

	void ScreenSaver::resume() {
		// Just specifying ES_CONTINUOUS resets all other flags.
		SetThreadExecutionState(ES_CONTINUOUS);
	}

#elif defined(LINUX) && defined(GUI_GTK)

	ScreenSaver::ScreenSaver() : sessionBus(NULL) {}

	void ScreenSaver::platformInit() {
		// Note 'g_bus_get_sync' seems to reuse a global connection if possible. This is good for
		// us, since we don't have to look into getting the DBus connection some other way to avoid
		// duplicate connections! (This is only heavily implied by the thread-safety discussion in
		// the documentation, not stated explicitly)
		sessionBus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
	}

	void ScreenSaver::platformDestroy() {
		if (sessionBus)
			g_object_unref(sessionBus);
	}

	// Helper to print a GVariant.
	static void print(GVariant *variant) {
		GString *str = g_variant_print_string(variant, NULL, true);
		PLN(str->str);
		g_string_free(str, true);
	}

	void ScreenSaver::inhibit() {
		// Note: 'wantCookie' might already be true. We get called again from init for example.
		if (hasCookie)
			return;

		wantCookie = true;

		if (initConnection()) {
			// If this is the first call, we might not yet have established who to talk to. We have
			// set 'wantCookie', that is enough for the init code to call us again later.
			if (state == sInitializing)
				return;

			// Send a request!
			sendInhibit();
		} else {
			// Start the backup code (works only on X11).
			if (inhibitX11()) {
				const guint interval = 10;
				hasCookie = true;
				cookie = g_timeout_add_seconds(interval, &ScreenSaver::backupTimer, this);
			} else {
				std::cerr << "Failed to inhibit the screensaver. No *.ScreenSaver clients found on the session DBus, "
						  << "nor are we able to use the X11 fallback." << std::endl;
			}
		}
	}

	void ScreenSaver::resume() {
		wantCookie = false;

		// No cookie, no worries.
		if (!hasCookie)
			return;

		if (state == sInitialized) {
			// Return the cookie. Note: Since we have a cookie, we know that we have a name to connect to.
			GVariant *params = g_variant_new("(u)", cookie);
			const DBusName &name = names.back();
			g_dbus_connection_call(sessionBus,
								name.busName.c_str(),
								name.path.c_str(),
								name.interfaceName.c_str(),
								"UnInhibit",
								params,
								NULL, // No result.
								G_DBUS_CALL_FLAGS_NONE,
								-1, // Default timeout
								NULL, // No cancellation
								NULL, // No callback
								this);
		} else {
			// Backup solution: destroy the timer.
			GSource *s = g_main_context_find_source_by_id(NULL, cookie);
			if (s)
				g_source_destroy(s);
		}

		cookie = 0;
		hasCookie = false;
		wantCookie = false;
	}

	bool ScreenSaver::initConnection() {
		if (!sessionBus)
			return false;

		// Tried before and failed?
		if (state == sNoRemoteEndpoint)
			return false;

		// Have we already started initialization?
		if (state != sUnknown)
			return true;

		// We need to start initialization. Start by asking for all names on the bus, so that we can
		// find and try out the promising ones.
		g_dbus_connection_call(sessionBus,
							"org.freedesktop.DBus",
							"/org/freedesktop/DBus",
							"org.freedesktop.DBus",
							"ListNames",
							NULL,  // No parameters.
							G_VARIANT_TYPE("(as)"), // Result, list of strings
							G_DBUS_CALL_FLAGS_NONE,
							-1, // Default timeout
							NULL, // No cancellation
							&ScreenSaver::nameListReceived,
							this);

		state = sInitializing;
		return true;
	}

	void ScreenSaver::initDone() {
		if (names.empty()) {
			state = sNoRemoteEndpoint;
			// If someone attempted to inhibit the screen saver, call 'inhibit' again to trigger
			// the fallback or to request a cookie now.
			if (wantCookie)
				inhibit();
		} else {
			state = sInitialized;
		}
	}

	void ScreenSaver::nameListReceived(GObject *source, GAsyncResult *result, gpointer data) {
		ScreenSaver *me = (ScreenSaver *)data;
		GError *error = NULL;
		GVariant *value = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);

		if (error) {
			std::wcerr << L"Failed to look up the screen saver service on the session bus: " << error->message << std::endl;
			g_error_free(error);
		}

		if (value) {
			me->extractEndpoints(value);
			g_variant_unref(value);
		}

		// Note: Handles failures properly if the list of names is empty.
		me->sendInhibit();
	}

	void ScreenSaver::sendInhibit() {
		// During initialization?
		if (names.empty()) {
			initDone();
			return;
		}

		// Ask the screen saver name by calling 'Inhibit' - we always want a cookie when we are
		// initialized anyway.
		GVariant *params = g_variant_new("(ss)", "Storm GUI framework", "Frame:inhibitScreenSaver");
		const DBusName &name = names.back();
		g_dbus_connection_call(sessionBus,
							name.busName.c_str(),
							name.path.c_str(),
							name.interfaceName.c_str(),
							"Inhibit",
							params,
							G_VARIANT_TYPE("(u)"),
							G_DBUS_CALL_FLAGS_NONE,
							-1, // Default timeout
							NULL, // No cancellation
							&ScreenSaver::cookieReceived,
							this);
	}

	void ScreenSaver::cookieReceived(GObject *source, GAsyncResult *result, gpointer data) {
		ScreenSaver *me = (ScreenSaver *)data;
		GError *error = NULL;
		GVariant *value = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);

		bool initializing = me->state == sInitializing;

		if (value) {
			if (g_variant_is_container(value) && g_variant_n_children(value) >= 1) {
				GVariant *child = g_variant_get_child_value(value, 0);
				me->cookie = g_variant_get_uint32(child);
				me->hasCookie = true;
				g_variant_unref(child);
			}

			g_variant_unref(value);

			if (initializing) {
				// Keep only the current name in the list.
				me->names.erase(me->names.begin(), me->names.end() - 1);

				// And we are done!
				me->initDone();
			}

			if (me->hasCookie && !me->wantCookie) {
				// Someone asked for the cookie to be cancelled already!
				me->resume();
			}
		}

		if (error) {
			if (initializing) {
				if (g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) {
					// OK to skip the error.
				} else if (initializing) {
					std::wcerr << L"Failed to communicate with " << me->names.back().busName.c_str()
							   << L": " << error->message << L". Will try other names." << std::endl;
				}

				g_error_free(error);

				// More to try out...
				me->names.pop_back();
				me->sendInhibit();
			} else {
				std::wcerr << L"Error requesting screen saver inhibit cookie: " << error->message << std::endl;
				g_error_free(error);
			}
		}
	}

	static bool isScreenSaverName(const std::string &name) {
		if (name.size() > 12 && name.substr(name.size() - 12) == ".ScreenSaver")
			return true;
		else
			return false;
	}

	static ScreenSaver::DBusName dbusInfo(const std::string &name) {
		std::string path = "/" + name;
		for (size_t i = 0; i < path.size(); i++)
			if (path[i] == '.')
				path[i] = '/';

		ScreenSaver::DBusName out = {
			name,
			path,
			name,
		};
		return out;
	}

	void ScreenSaver::extractEndpoints(GVariant *tuple) {
		if (!g_variant_is_container(tuple) || g_variant_n_children(tuple) < 1)
			return;

		// First candidate. We place it last (that's where we look first).
		const std::string bestName = "org.freedesktop.ScreenSaver";
		bool hasBestName = false;

		GVariant *array = g_variant_get_child_value(tuple, 0);
		size_t children = g_variant_n_children(array);
		for (size_t i = 0; i < children; i++) {
			GVariant *str = g_variant_get_child_value(array, i);
			std::string name(g_variant_get_string(str, NULL));
			g_variant_unref(str);

			if (name == bestName)
				hasBestName = true;
			else if (isScreenSaverName(name))
				names.push_back(dbusInfo(name));
		}

		g_variant_unref(array);

		if (hasBestName)
			names.push_back(dbusInfo(bestName));
	}

	bool ScreenSaver::inhibitX11() {
		GdkDisplay *gdkDisplay = gdk_display_get_default();
		if (gdkDisplay && GDK_IS_X11_DISPLAY(gdkDisplay)) {
			Display *xDisplay = GDK_DISPLAY_XDISPLAY(gdkDisplay);
			XResetScreenSaver(xDisplay);
			return true;
		} else {
			return false;
		}
	}

	gboolean ScreenSaver::backupTimer(gpointer data) {
		ScreenSaver *me = (ScreenSaver *)data;
		me->inhibitX11();
		return G_SOURCE_CONTINUE;
	}

#else

#error "Screen saver interaction is not defined for your platform."

#endif

}
