/*
 * Copyright (C) 2007 Novell, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"

#include <string.h>
#include <glib/gi18n.h>

#include "eggsmclient.h"
#include "eggsmclient-private.h"

static void egg_sm_client_debug_handler (const char *log_domain,
        GLogLevelFlags log_level,
        const char *message,
        gpointer user_data);

enum
{
    SAVE_STATE,
    QUIT_REQUESTED,
    QUIT_CANCELLED,
    QUIT,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

struct _EggSMClientPrivate
{
	GKeyFile *state_file;
};

#define EGG_SM_CLIENT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), EGG_TYPE_SM_CLIENT, EggSMClientPrivate))

G_DEFINE_TYPE (EggSMClient, egg_sm_client, G_TYPE_OBJECT)

static EggSMClient *global_client;
static EggSMClientMode global_client_mode = EGG_SM_CLIENT_MODE_NORMAL;

static void
egg_sm_client_init (EggSMClient *client)
{
	;
}

static void
egg_sm_client_class_init (EggSMClientClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	g_type_class_add_private (klass, sizeof (EggSMClientPrivate));

	/**
	 * EggSMClient::save_state:
	 * @client: the client
	 * @state_file: a #GKeyFile to save state information into
	 *
	 * Emitted when the session manager has requested that the
	 * application save information about its current state. The
	 * application should save its state into @state_file, and then the
	 * session manager may then restart the application in a future
	 * session and tell it to initialize itself from that state.
	 *
	 * You should not save any data into @state_file's "start group"
	 * (ie, the %NULL group). Instead, applications should save their
	 * data into groups with names that start with the application name,
	 * and libraries that connect to this signal should save their data
	 * into groups with names that start with the library name.
	 *
	 * Alternatively, rather than (or in addition to) using @state_file,
	 * the application can save its state by calling
	 * egg_sm_client_set_restart_command() during the processing of this
	 * signal (eg, to include a list of files to open).
	 **/
	signals[SAVE_STATE] =
	    g_signal_new ("save_state",
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_LAST,
	                  G_STRUCT_OFFSET (EggSMClientClass, save_state),
	                  NULL, NULL,
	                  g_cclosure_marshal_VOID__POINTER,
	                  G_TYPE_NONE,
	                  1, G_TYPE_POINTER);

	/**
	 * EggSMClient::quit_requested:
	 * @client: the client
	 *
	 * Emitted when the session manager requests that the application
	 * exit (generally because the user is logging out). The application
	 * should decide whether or not it is willing to quit (perhaps after
	 * asking the user what to do with documents that have unsaved
	 * changes) and then call egg_sm_client_will_quit(), passing %TRUE
	 * or %FALSE to give its answer to the session manager. (It does not
	 * need to give an answer before returning from the signal handler;
	 * it can interact with the user asynchronously and then give its
	 * answer later on.) If the application does not connect to this
	 * signal, then #EggSMClient will automatically return %TRUE on its
	 * behalf.
	 *
	 * The application should not save its session state as part of
	 * handling this signal; if the user has requested that the session
	 * be saved when logging out, then ::save_state will be emitted
	 * separately.
	 *
	 * If the application agrees to quit, it should then wait for either
	 * the ::quit_cancelled or ::quit signals to be emitted.
	 **/
	signals[QUIT_REQUESTED] =
	    g_signal_new ("quit_requested",
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_LAST,
	                  G_STRUCT_OFFSET (EggSMClientClass, quit_requested),
	                  NULL, NULL,
	                  g_cclosure_marshal_VOID__VOID,
	                  G_TYPE_NONE,
	                  0);

	/**
	 * EggSMClient::quit_cancelled:
	 * @client: the client
	 *
	 * Emitted when the session manager decides to cancel a logout after
	 * the application has already agreed to quit. After receiving this
	 * signal, the application can go back to what it was doing before
	 * receiving the ::quit_requested signal.
	 **/
	signals[QUIT_CANCELLED] =
	    g_signal_new ("quit_cancelled",
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_LAST,
	                  G_STRUCT_OFFSET (EggSMClientClass, quit_cancelled),
	                  NULL, NULL,
	                  g_cclosure_marshal_VOID__VOID,
	                  G_TYPE_NONE,
	                  0);

	/**
	 * EggSMClient::quit:
	 * @client: the client
	 *
	 * Emitted when the session manager wants the application to quit
	 * (generally because the user is logging out). The application
	 * should exit as soon as possible after receiving this signal; if
	 * it does not, the session manager may choose to forcibly kill it.
	 *
	 * Normally a GUI application would only be sent a ::quit if it
	 * agreed to quit in response to a ::quit_requested signal. However,
	 * this is not guaranteed; in some situations the session manager
	 * may decide to end the session without giving applications a
	 * chance to object.
	 **/
	signals[QUIT] =
	    g_signal_new ("quit",
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_LAST,
	                  G_STRUCT_OFFSET (EggSMClientClass, quit),
	                  NULL, NULL,
	                  g_cclosure_marshal_VOID__VOID,
	                  G_TYPE_NONE,
	                  0);
}

static gboolean sm_client_disable = FALSE;
static char *sm_client_state_file = NULL;
static char *sm_client_id = NULL;
static char *sm_config_prefix = NULL;

static gboolean
sm_client_post_parse_func (GOptionContext  *context,
                           GOptionGroup    *group,
                           gpointer         data,
                           GError         **error)
{
	EggSMClient *client = egg_sm_client_get ();

	if (sm_client_id == NULL)
	{
		const gchar *desktop_autostart_id;

		desktop_autostart_id = g_getenv ("DESKTOP_AUTOSTART_ID");

		if (desktop_autostart_id != NULL)
			sm_client_id = g_strdup (desktop_autostart_id);
	}

	/* Unset DESKTOP_AUTOSTART_ID in order to avoid child processes to
	 * use the same client id. */
	g_unsetenv ("DESKTOP_AUTOSTART_ID");

	if (EGG_SM_CLIENT_GET_CLASS (client)->startup)
		EGG_SM_CLIENT_GET_CLASS (client)->startup (client, sm_client_id);
	return TRUE;
}

/**
 * egg_sm_client_get_option_group:
 *
 * Creates a %GOptionGroup containing the session-management-related
 * options. You should add this group to the application's
 * %GOptionContext if you want to use #EggSMClient.
 *
 * Return value: the %GOptionGroup
 **/
GOptionGroup *
egg_sm_client_get_option_group (void)
{
	const GOptionEntry entries[] =
	{
		{
			"sm-client-disable", 0, 0,
			G_OPTION_ARG_NONE, &sm_client_disable,
			N_("Disable connection to session manager"), NULL
		},
		{
			"sm-client-state-file", 0, 0,
			G_OPTION_ARG_FILENAME, &sm_client_state_file,
			N_("Specify file containing saved configuration"), N_("FILE")
		},
		{
			"sm-client-id", 0, 0,
			G_OPTION_ARG_STRING, &sm_client_id,
			N_("Specify session management ID"), N_("ID")
		},
		/* MateClient compatibility option */
		{
			"sm-disable", 0, G_OPTION_FLAG_HIDDEN,
			G_OPTION_ARG_NONE, &sm_client_disable,
			NULL, NULL
		},
		/* MateClient compatibility option. This is a dummy option that only
		 * exists so that sessions saved by apps with MateClient can be restored
		 * later when they've switched to EggSMClient. See bug #575308.
		 */
		{
			"sm-config-prefix", 0, G_OPTION_FLAG_HIDDEN,
			G_OPTION_ARG_STRING, &sm_config_prefix,
			NULL, NULL
		},
		{ NULL }
	};
	GOptionGroup *group;

	/* Use our own debug handler for the "EggSMClient" domain. */
	g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG,
	                   egg_sm_client_debug_handler, NULL);

	group = g_option_group_new ("sm-client",
	                            _("Session management options:"),
	                            _("Show session management options"),
	                            NULL, NULL);
	g_option_group_add_entries (group, entries);
	g_option_group_set_parse_hooks (group, NULL, sm_client_post_parse_func);

	return group;
}

/**
 * egg_sm_client_set_mode:
 * @mode: an #EggSMClient mode
 *
 * Sets the "mode" of #EggSMClient as follows:
 *
 *    %EGG_SM_CLIENT_MODE_DISABLED: Session management is completely
 *    disabled. The application will not even connect to the session
 *    manager. (egg_sm_client_get() will still return an #EggSMClient,
 *    but it will just be a dummy object.)
 *
 *    %EGG_SM_CLIENT_MODE_NO_RESTART: The application will connect to
 *    the session manager (and thus will receive notification when the
 *    user is logging out, etc), but will request to not be
 *    automatically restarted with saved state in future sessions.
 *
 *    %EGG_SM_CLIENT_MODE_NORMAL: The default. #EggSMCLient will
 *    function normally.
 *
 * This must be called before the application's main loop begins.
 **/
void
egg_sm_client_set_mode (EggSMClientMode mode)
{
	global_client_mode = mode;
}

/**
 * egg_sm_client_get_mode:
 *
 * Gets the global #EggSMClientMode. See egg_sm_client_set_mode()
 * for details.
 *
 * Return value: the global #EggSMClientMode
 **/
EggSMClientMode
egg_sm_client_get_mode (void)
{
	return global_client_mode;
}

/**
 * egg_sm_client_get:
 *
 * Returns the master #EggSMClient for the application.
 *
 * On platforms that support saved sessions (ie, POSIX/X11), the
 * application will only request to be restarted by the session
 * manager if you call egg_set_desktop_file() to set an application
 * desktop file. In particular, if the desktop file contains the key
 * "X
 *
 * Return value: the master #EggSMClient.
 **/
EggSMClient *
egg_sm_client_get (void)
{
	if (!global_client)
	{
		if (global_client_mode != EGG_SM_CLIENT_MODE_DISABLED &&
		        !sm_client_disable)
		{
#if defined (GDK_WINDOWING_WIN32)
			global_client = egg_sm_client_win32_new ();
#elif defined (GDK_WINDOWING_QUARTZ)
			global_client = egg_sm_client_osx_new ();
#else
			/* If both D-Bus and XSMP are compiled in, try XSMP first
			 * (since it supports state saving) and fall back to D-Bus
			 * if XSMP isn't available.
			 */
# ifdef EGG_SM_CLIENT_BACKEND_XSMP
			global_client = egg_sm_client_xsmp_new ();
# endif
# ifdef EGG_SM_CLIENT_BACKEND_DBUS
			if (!global_client)
				global_client = egg_sm_client_dbus_new ();
# endif
#endif
		}

		/* Fallback: create a dummy client, so that callers don't have
		 * to worry about a %NULL return value.
		 */
		if (!global_client)
			global_client = g_object_new (EGG_TYPE_SM_CLIENT, NULL);
	}

	return global_client;
}

/**
 * egg_sm_client_is_resumed:
 * @client: the client
 *
 * Checks whether or not the current session has been resumed from
 * a previous saved session. If so, the application should call
 * egg_sm_client_get_state_file() and restore its state from the
 * returned #GKeyFile.
 *
 * Return value: %TRUE if the session has been resumed
 **/
gboolean
egg_sm_client_is_resumed (EggSMClient *client)
{
	g_return_val_if_fail (client == global_client, FALSE);

	return sm_client_state_file != NULL;
}

/**
 * egg_sm_client_get_state_file:
 * @client: the client
 *
 * If the application was resumed by the session manager, this will
 * return the #GKeyFile containing its state from the previous
 * session.
 *
 * Note that other libraries and #EggSMClient itself may also store
 * state in the key file, so if you call egg_sm_client_get_groups(),
 * on it, the return value will likely include groups that you did not
 * put there yourself. (It is also not guaranteed that the first
 * group created by the application will still be the "start group"
 * when it is resumed.)
 *
 * Return value: the #GKeyFile containing the application's earlier
 * state, or %NULL on error. You should not free this key file; it
 * is owned by @client.
 **/
GKeyFile *
egg_sm_client_get_state_file (EggSMClient *client)
{
	EggSMClientPrivate *priv = EGG_SM_CLIENT_GET_PRIVATE (client);
	char *state_file_path;
	GError *err = NULL;

	g_return_val_if_fail (client == global_client, NULL);

	if (!sm_client_state_file)
		return NULL;
	if (priv->state_file)
		return priv->state_file;

	if (!strncmp (sm_client_state_file, "file://", 7))
		state_file_path = g_filename_from_uri (sm_client_state_file, NULL, NULL);
	else
		state_file_path = g_strdup (sm_client_state_file);

	priv->state_file = g_key_file_new ();
	if (!g_key_file_load_from_file (priv->state_file, state_file_path, 0, &err))
	{
		g_warning ("Could not load SM state file '%s': %s",
		           sm_client_state_file, err->message);
		g_clear_error (&err);
		g_key_file_free (priv->state_file);
		priv->state_file = NULL;
	}

	g_free (state_file_path);
	return priv->state_file;
}

/**
 * egg_sm_client_set_restart_command:
 * @client: the client
 * @argc: the length of @argv
 * @argv: argument vector
 *
 * Sets the command used to restart @client if it does not have a
 * .desktop file that can be used to find its restart command.
 *
 * This can also be used when handling the ::save_state signal, to
 * save the current state via an updated command line. (Eg, providing
 * a list of filenames to open when the application is resumed.)
 **/
void
egg_sm_client_set_restart_command (EggSMClient  *client,
                                   int           argc,
                                   const char  **argv)
{
	g_return_if_fail (EGG_IS_SM_CLIENT (client));

	if (EGG_SM_CLIENT_GET_CLASS (client)->set_restart_command)
		EGG_SM_CLIENT_GET_CLASS (client)->set_restart_command (client, argc, argv);
}

/**
 * egg_sm_client_set_discard_command:
 * @client: the client
 * @argc: the length of @argv
 * @argv: argument vector
 *
 * Sets the command used to discard a custom state file if using
 * egg_sm_client_set_restart_command(), which must be called before
 * using this function.
 **/
void
egg_sm_client_set_discard_command (EggSMClient  *client,
                                   int           argc,
                                   const char  **argv)
{
	g_return_if_fail (EGG_IS_SM_CLIENT (client));

	if (EGG_SM_CLIENT_GET_CLASS (client)->set_discard_command)
		EGG_SM_CLIENT_GET_CLASS (client)->set_discard_command (client, argc, argv);
}

/**
 * egg_sm_client_will_quit:
 * @client: the client
 * @will_quit: whether or not the application is willing to quit
 *
 * This MUST be called in response to the ::quit_requested signal, to
 * indicate whether or not the application is willing to quit. The
 * application may call it either directly from the signal handler, or
 * at some later point (eg, after asynchronously interacting with the
 * user).
 *
 * If the application does not connect to ::quit_requested,
 * #EggSMClient will call this method on its behalf (passing %TRUE
 * for @will_quit).
 *
 * After calling this method, the application should wait to receive
 * either ::quit_cancelled or ::quit.
 **/
void
egg_sm_client_will_quit (EggSMClient *client,
                         gboolean     will_quit)
{
	g_return_if_fail (EGG_IS_SM_CLIENT (client));

	if (EGG_SM_CLIENT_GET_CLASS (client)->will_quit)
		EGG_SM_CLIENT_GET_CLASS (client)->will_quit (client, will_quit);
}

/**
 * egg_sm_client_end_session:
 * @style: a hint at how to end the session
 * @request_confirmation: whether or not the user should get a chance
 * to confirm the action
 *
 * Requests that the session manager end the current session. @style
 * indicates how the session should be ended, and
 * @request_confirmation indicates whether or not the user should be
 * given a chance to confirm the logout/reboot/shutdown. Both of these
 * flags are merely hints though; the session manager may choose to
 * ignore them.
 *
 * Return value: %TRUE if the request was sent; %FALSE if it could not
 * be (eg, because it could not connect to the session manager).
 **/
gboolean
egg_sm_client_end_session (EggSMClientEndStyle  style,
                           gboolean             request_confirmation)
{
	EggSMClient *client = egg_sm_client_get ();

	g_return_val_if_fail (EGG_IS_SM_CLIENT (client), FALSE);

	if (EGG_SM_CLIENT_GET_CLASS (client)->end_session)
	{
		return EGG_SM_CLIENT_GET_CLASS (client)->end_session (client, style,
		        request_confirmation);
	}
	else
		return FALSE;
}

/* Signal-emitting callbacks from platform-specific code */

GKeyFile *
egg_sm_client_save_state (EggSMClient *client)
{
	GKeyFile *state_file;
	char *group;

	g_return_val_if_fail (client == global_client, NULL);

	state_file = g_key_file_new ();

	g_debug ("Emitting save_state");
	g_signal_emit (client, signals[SAVE_STATE], 0, state_file);
	g_debug ("Done emitting save_state");

	group = g_key_file_get_start_group (state_file);
	if (group)
	{
		g_free (group);
		return state_file;
	}
	else
	{
		g_key_file_free (state_file);
		return NULL;
	}
}

void
egg_sm_client_quit_requested (EggSMClient *client)
{
	g_return_if_fail (client == global_client);

	if (!g_signal_has_handler_pending (client, signals[QUIT_REQUESTED], 0, FALSE))
	{
		g_debug ("Not emitting quit_requested because no one is listening");
		egg_sm_client_will_quit (client, TRUE);
		return;
	}

	g_debug ("Emitting quit_requested");
	g_signal_emit (client, signals[QUIT_REQUESTED], 0);
	g_debug ("Done emitting quit_requested");
}

void
egg_sm_client_quit_cancelled (EggSMClient *client)
{
	g_return_if_fail (client == global_client);

	g_debug ("Emitting quit_cancelled");
	g_signal_emit (client, signals[QUIT_CANCELLED], 0);
	g_debug ("Done emitting quit_cancelled");
}

void
egg_sm_client_quit (EggSMClient *client)
{
	g_return_if_fail (client == global_client);

	g_debug ("Emitting quit");
	g_signal_emit (client, signals[QUIT], 0);
	g_debug ("Done emitting quit");

	/* FIXME: should we just call gtk_main_quit() here? */
}

static void
egg_sm_client_debug_handler (const char *log_domain,
                             GLogLevelFlags log_level,
                             const char *message,
                             gpointer user_data)
{
	static int debug = -1;

	if (debug < 0)
		debug = (g_getenv ("EGG_SM_CLIENT_DEBUG") != NULL);

	if (debug)
		g_log_default_handler (log_domain, log_level, message, NULL);
}