/*
 * e-shell-backend.c
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *   Jonathon Jongsma <jonathon.jongsma@collabora.co.uk>
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 * Copyright (C) 2009 Intel Corporation
 */

/**
 * SECTION: e-shell-backend
 * @short_description: dynamically loaded capabilities
 * @include: shell/e-shell-backend.h
 **/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "e-shell-backend.h"

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

#include "e-util/e-util.h"

#include "e-shell.h"
#include "e-shell-view.h"

#define E_SHELL_BACKEND_GET_PRIVATE(obj) \
	(G_TYPE_INSTANCE_GET_PRIVATE \
	((obj), E_TYPE_SHELL_BACKEND, EShellBackendPrivate))

struct _EShellBackendPrivate {

	/* We keep a reference to corresponding EShellView subclass
	 * since it keeps a reference back to us.  This ensures the
	 * subclass is not finalized before we are, otherwise it
	 * would leak its EShellBackend reference. */
	EShellViewClass *shell_view_class;

	/* This tracks what the backend is busy doing. */
	GQueue *activities;

	gchar *config_dir;
	gchar *data_dir;
	gchar *prefer_new_item;

	/* This is set to delay shutdown. */
	gulong notify_busy_handler_id;

	guint started : 1;
};

enum {
	PROP_0,
	PROP_BUSY,
	PROP_PREFER_NEW_ITEM
};

enum {
	ACTIVITY_ADDED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

G_DEFINE_ABSTRACT_TYPE (EShellBackend, e_shell_backend, E_TYPE_EXTENSION)

static void
shell_backend_debug_list_activities (EShellBackend *shell_backend)
{
	EShellBackendClass *class;
	GList *head, *link;
	guint n_activities;

	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);

	n_activities = g_queue_get_length (shell_backend->priv->activities);

	if (n_activities == 0)
		return;

	g_debug (
		"%u active '%s' %s:",
		n_activities, class->name,
		(n_activities == 1) ? "activity" : "activities");

	head = g_queue_peek_head_link (shell_backend->priv->activities);

	for (link = head; link != NULL; link = g_list_next (link)) {
		EActivity *activity = E_ACTIVITY (link->data);
		gchar *description;
		const gchar *was;

		description = e_activity_describe (activity);
		was = e_activity_get_last_known_text (activity);

		if (description != NULL)
			g_debug ("* %s", description);
		else if (was != NULL)
			g_debug ("* (was \"%s\")", was);
		else
			g_debug ("* (no description)");

		g_free (description);
	}
}

static void
shell_backend_activity_finalized_cb (EShellBackend *shell_backend,
                                     EActivity *finalized_activity)
{
	g_queue_remove (shell_backend->priv->activities, finalized_activity);

	/* Only emit "notify::busy" when switching from busy to idle. */
	if (g_queue_is_empty (shell_backend->priv->activities))
		g_object_notify (G_OBJECT (shell_backend), "busy");

	g_object_unref (shell_backend);
}

static void
shell_backend_notify_busy_cb (EShellBackend *shell_backend,
                              GParamSpec *pspec,
                              EActivity *activity)
{
	shell_backend_debug_list_activities (shell_backend);

	if (!e_shell_backend_is_busy (shell_backend)) {
		/* Disconnecting this signal handler will unreference the
		 * EActivity and allow the shell to proceed with shutdown. */
		g_signal_handler_disconnect (
			shell_backend,
			shell_backend->priv->notify_busy_handler_id);
		shell_backend->priv->notify_busy_handler_id = 0;
	}
}

static void
shell_backend_prepare_for_quit_cb (EShell *shell,
                                   EActivity *activity,
                                   EShellBackend *shell_backend)
{
	shell_backend_debug_list_activities (shell_backend);

	if (e_shell_backend_is_busy (shell_backend)) {
		gulong handler_id;

		/* Referencing the EActivity delays shutdown; the
		 * reference count acts like a counting semaphore. */
		handler_id = g_signal_connect_data (
			shell_backend, "notify::busy",
			G_CALLBACK (shell_backend_notify_busy_cb),
			g_object_ref (activity),
			(GClosureNotify) g_object_unref, 0);
		shell_backend->priv->notify_busy_handler_id = handler_id;
	}
}

static GObject *
shell_backend_constructor (GType type,
                           guint n_construct_properties,
                           GObjectConstructParam *construct_properties)
{
	EShellBackend *shell_backend;
	EShellBackendClass *class;
	EShellViewClass *shell_view_class;
	EShell *shell;
	GObject *object;

	/* Chain up to parent's construct() method. */
	object = G_OBJECT_CLASS (e_shell_backend_parent_class)->constructor (
		type, n_construct_properties, construct_properties);

	shell_backend = E_SHELL_BACKEND (object);
	shell = e_shell_backend_get_shell (shell_backend);

	/* Install a reference to ourselves in the
	 * corresponding EShellViewClass structure. */
	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);
	shell_view_class = g_type_class_ref (class->shell_view_type);
	shell_view_class->shell_backend = g_object_ref (shell_backend);
	shell_backend->priv->shell_view_class = shell_view_class;

	g_signal_connect (
		shell, "prepare-for-quit",
		G_CALLBACK (shell_backend_prepare_for_quit_cb),
		shell_backend);

	return object;
}

static void
shell_backend_get_property (GObject *object,
                            guint property_id,
                            GValue *value,
                            GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_BUSY:
			g_value_set_boolean (
				value, e_shell_backend_is_busy (
				E_SHELL_BACKEND (object)));
			return;

		case PROP_PREFER_NEW_ITEM:
			g_value_set_string (
				value,
				e_shell_backend_get_prefer_new_item (
				E_SHELL_BACKEND (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
shell_backend_set_property (GObject *object,
                            guint property_id,
                            const GValue *value,
                            GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_PREFER_NEW_ITEM:
			e_shell_backend_set_prefer_new_item (
				E_SHELL_BACKEND (object),
				g_value_get_string (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
shell_backend_dispose (GObject *object)
{
	EShellBackendPrivate *priv;

	priv = E_SHELL_BACKEND_GET_PRIVATE (object);

	if (priv->shell_view_class != NULL) {
		g_type_class_unref (priv->shell_view_class);
		priv->shell_view_class = NULL;
	}

	if (priv->notify_busy_handler_id > 0) {
		g_signal_handler_disconnect (
			object, priv->notify_busy_handler_id);
		priv->notify_busy_handler_id = 0;
	}

	/* Chain up to parent's dispose() method. */
	G_OBJECT_CLASS (e_shell_backend_parent_class)->dispose (object);
}

static void
shell_backend_finalize (GObject *object)
{
	EShellBackendPrivate *priv;

	priv = E_SHELL_BACKEND_GET_PRIVATE (object);

	g_warn_if_fail (g_queue_is_empty (priv->activities));
	g_queue_free (priv->activities);

	g_free (priv->config_dir);
	g_free (priv->data_dir);
	g_free (priv->prefer_new_item);

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (e_shell_backend_parent_class)->finalize (object);
}

static const gchar *
shell_backend_get_config_dir (EShellBackend *shell_backend)
{
	EShellBackendClass *class;

	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);

	/* Determine the user config directory for this backend. */
	if (G_UNLIKELY (shell_backend->priv->config_dir == NULL)) {
		const gchar *user_config_dir;

		user_config_dir = e_get_user_config_dir ();
		shell_backend->priv->config_dir =
			g_build_filename (user_config_dir, class->name, NULL);
		g_mkdir_with_parents (shell_backend->priv->config_dir, 0700);
	}

	return shell_backend->priv->config_dir;
}

static const gchar *
shell_backend_get_data_dir (EShellBackend *shell_backend)
{
	EShellBackendClass *class;

	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);

	/* Determine the user data directory for this backend. */
	if (G_UNLIKELY (shell_backend->priv->data_dir == NULL)) {
		const gchar *user_data_dir;

		user_data_dir = e_get_user_data_dir ();
		shell_backend->priv->data_dir =
			g_build_filename (user_data_dir, class->name, NULL);
		g_mkdir_with_parents (shell_backend->priv->data_dir, 0700);
	}

	return shell_backend->priv->data_dir;
}

static void
e_shell_backend_class_init (EShellBackendClass *class)
{
	GObjectClass *object_class;
	EExtensionClass *extension_class;

	g_type_class_add_private (class, sizeof (EShellBackendPrivate));

	object_class = G_OBJECT_CLASS (class);
	object_class->constructor = shell_backend_constructor;
	object_class->get_property = shell_backend_get_property;
	object_class->set_property = shell_backend_set_property;
	object_class->dispose = shell_backend_dispose;
	object_class->finalize = shell_backend_finalize;

	extension_class = E_EXTENSION_CLASS (class);
	extension_class->extensible_type = E_TYPE_SHELL;

	class->get_config_dir = shell_backend_get_config_dir;
	class->get_data_dir = shell_backend_get_data_dir;

	/**
	 * EShellBackend:busy
	 *
	 * Whether any activities are still in progress.
	 **/
	g_object_class_install_property (
		object_class,
		PROP_BUSY,
		g_param_spec_boolean (
			"busy",
			"Busy",
			"Whether any activities are still in progress",
			FALSE,
			G_PARAM_READABLE |
			G_PARAM_STATIC_STRINGS));

	/**
	 * EShellBackend:prefer-new-item
	 *
	 * Name of an item to prefer in New toolbar button; can be NULL
	 **/
	g_object_class_install_property (
		object_class,
		PROP_PREFER_NEW_ITEM,
		g_param_spec_string (
			"prefer-new-item",
			"Prefer New Item",
			"Name of an item to prefer in New toolbar button",
			NULL,
			G_PARAM_READWRITE |
			G_PARAM_STATIC_STRINGS));

	/**
	 * EShellBackend::activity-added
	 * @shell_backend: the #EShellBackend that emitted the signal
	 * @activity: an #EActivity
	 *
	 * Broadcasts a newly added activity.
	 **/
	signals[ACTIVITY_ADDED] = g_signal_new (
		"activity-added",
		G_OBJECT_CLASS_TYPE (object_class),
		G_SIGNAL_RUN_LAST,
		0, NULL, NULL,
		g_cclosure_marshal_VOID__OBJECT,
		G_TYPE_NONE, 1,
		E_TYPE_ACTIVITY);
}

static void
e_shell_backend_init (EShellBackend *shell_backend)
{
	shell_backend->priv = E_SHELL_BACKEND_GET_PRIVATE (shell_backend);
	shell_backend->priv->activities = g_queue_new ();
}

/**
 * e_shell_backend_compare:
 * @shell_backend_a: an #EShellBackend
 * @shell_backend_b: an #EShellBackend
 *
 * Using the <structfield>sort_order</structfield> field from both backends'
 * #EShellBackendClass, compares @shell_backend_a with @shell_mobule_b and
 * returns -1, 0 or +1 if @shell_backend_a is found to be less than, equal
 * to or greater than @shell_backend_b, respectively.
 *
 * Returns: -1, 0 or +1, for a less than, equal to or greater than result
 **/
gint
e_shell_backend_compare (EShellBackend *shell_backend_a,
                         EShellBackend *shell_backend_b)
{
	gint a = E_SHELL_BACKEND_GET_CLASS (shell_backend_a)->sort_order;
	gint b = E_SHELL_BACKEND_GET_CLASS (shell_backend_b)->sort_order;

	return (a < b) ? -1 : (a > b);
}

/**
 * e_shell_backend_get_config_dir:
 * @shell_backend: an #EShellBackend
 *
 * Returns the absolute path to the configuration directory for
 * @shell_backend.  The string is owned by @shell_backend and should
 * not be modified or freed.
 *
 * Returns: the backend's configuration directory
 **/
const gchar *
e_shell_backend_get_config_dir (EShellBackend *shell_backend)
{
	EShellBackendClass *class;

	g_return_val_if_fail (E_IS_SHELL_BACKEND (shell_backend), NULL);

	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);
	g_return_val_if_fail (class->get_config_dir != NULL, NULL);

	return class->get_config_dir (shell_backend);
}

/**
 * e_shell_backend_get_data_dir:
 * @shell_backend: an #EShellBackend
 *
 * Returns the absolute path to the data directory for @shell_backend.
 * The string is owned by @shell_backend and should not be modified or
 * freed.
 *
 * Returns: the backend's data directory
 **/
const gchar *
e_shell_backend_get_data_dir (EShellBackend *shell_backend)
{
	EShellBackendClass *class;

	g_return_val_if_fail (E_IS_SHELL_BACKEND (shell_backend), NULL);

	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);
	g_return_val_if_fail (class->get_data_dir != NULL, NULL);

	return class->get_data_dir (shell_backend);
}

/**
 * e_shell_backend_get_shell:
 * @shell_backend: an #EShellBackend
 *
 * Returns the #EShell singleton.
 *
 * Returns: the #EShell
 **/
EShell *
e_shell_backend_get_shell (EShellBackend *shell_backend)
{
	EExtensible *extensible;

	g_return_val_if_fail (E_IS_SHELL_BACKEND (shell_backend), NULL);

	extensible = e_extension_get_extensible (E_EXTENSION (shell_backend));

	return E_SHELL (extensible);
}

/**
 * e_shell_backend_add_activity:
 * @shell_backend: an #EShellBackend
 * @activity: an #EActivity
 *
 * Emits an #EShellBackend::activity-added signal and tracks the @activity
 * until it is finalized.
 **/
void
e_shell_backend_add_activity (EShellBackend *shell_backend,
                              EActivity *activity)
{
	EActivityState state;

	g_return_if_fail (E_IS_SHELL_BACKEND (shell_backend));
	g_return_if_fail (E_IS_ACTIVITY (activity));

	state = e_activity_get_state (activity);

	/* Disregard cancelled or completed activities. */

	if (state == E_ACTIVITY_CANCELLED)
		return;

	if (state == E_ACTIVITY_COMPLETED)
		return;

	g_queue_push_tail (shell_backend->priv->activities, activity);

	/* Emit the "activity-added" signal before adding a weak reference
	 * to the EActivity because EShellTaskbar's signal handler also adds
	 * a weak reference to the EActivity, and we want its GWeakNotify
	 * to run before ours, since ours may destroy the EShellTaskbar
	 * during application shutdown. */
	g_signal_emit (shell_backend, signals[ACTIVITY_ADDED], 0, activity);

	/* We reference the backend on every activity to
	 * guarantee the backend outlives the activity. */
	g_object_weak_ref (
		G_OBJECT (activity), (GWeakNotify)
		shell_backend_activity_finalized_cb,
		g_object_ref (shell_backend));

	/* Only emit "notify::busy" when switching from idle to busy. */
	if (g_queue_get_length (shell_backend->priv->activities) == 1)
		g_object_notify (G_OBJECT (shell_backend), "busy");
}

/**
 * e_shell_backend_is_busy:
 * @shell_backend: an #EShellBackend
 *
 * Returns %TRUE if any activities passed to e_shell_backend_add_activity()
 * are still in progress, %FALSE if the @shell_backend is currently idle.
 *
 * Returns: %TRUE if activities are still in progress
 **/
gboolean
e_shell_backend_is_busy (EShellBackend *shell_backend)
{
	g_return_val_if_fail (E_IS_SHELL_BACKEND (shell_backend), FALSE);

	return !g_queue_is_empty (shell_backend->priv->activities);
}

/**
 * e_shell_backend_get_prefer_new_item:
 * @shell_backend: an #EShellBackend
 *
 * Returns: Name of a preferred item in New toolbar button, %NULL or
 * an empty string for no preference.
 *
 * Since: 3.4
 **/
const gchar *
e_shell_backend_get_prefer_new_item (EShellBackend *shell_backend)
{
	g_return_val_if_fail (shell_backend != NULL, NULL);
	g_return_val_if_fail (E_IS_SHELL_BACKEND (shell_backend), NULL);

	return shell_backend->priv->prefer_new_item;
}

/**
 * e_shell_backend_set_prefer_new_item:
 * @shell_backend: an #EShellBackend
 * @prefer_new_item: name of an item
 *
 * Sets name of a preferred item in New toolbar button. Use %NULL or
 * an empty string for no preference.
 *
 * Since: 3.4
 **/
void
e_shell_backend_set_prefer_new_item (EShellBackend *shell_backend,
                                     const gchar *prefer_new_item)
{
	g_return_if_fail (shell_backend != NULL);
	g_return_if_fail (E_IS_SHELL_BACKEND (shell_backend));

	if (g_strcmp0 (shell_backend->priv->prefer_new_item, prefer_new_item) == 0)
		return;

	g_free (shell_backend->priv->prefer_new_item);
	shell_backend->priv->prefer_new_item = g_strdup (prefer_new_item);

	g_object_notify (G_OBJECT (shell_backend), "prefer-new-item");
}

/**
 * e_shell_backend_cancel_all:
 * @shell_backend: an #EShellBackend
 *
 * Cancels all activities passed to e_shell_backend_add_activity() that
 * have not already been finalized.  Note that an #EActivity can only be
 * cancelled if it was given a #GCancellable object.
 *
 * Also, assuming all activities are cancellable, there may still be a
 * delay before e_shell_backend_is_busy() returns %FALSE, because some
 * activities may not be able to respond to the cancellation request
 * immediately.  Connect to the "notify::busy" signal if you need
 * notification of @shell_backend becoming idle.
 **/
void
e_shell_backend_cancel_all (EShellBackend *shell_backend)
{
	GList *list, *iter;

	g_return_if_fail (E_IS_SHELL_BACKEND (shell_backend));

	list = g_queue_peek_head_link (shell_backend->priv->activities);

	for (iter = list; iter != NULL; iter = g_list_next (iter))
		e_activity_cancel (E_ACTIVITY (iter->data));
}

/**
 * e_shell_backend_start:
 * @shell_backend: an #EShellBackend
 *
 * Tells the @shell_backend to begin loading data or running background
 * tasks which may consume significant resources.  This gets called in
 * reponse to the user switching to the corresponding #EShellView for
 * the first time.  The function is idempotent for each @shell_backend.
 **/
void
e_shell_backend_start (EShellBackend *shell_backend)
{
	EShellBackendClass *class;

	g_return_if_fail (E_IS_SHELL_BACKEND (shell_backend));

	if (shell_backend->priv->started)
		return;

	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);

	if (class->start != NULL)
		class->start (shell_backend);

	shell_backend->priv->started = TRUE;
}

/**
 * e_shell_backend_is_started:
 * @shell_backend: an #EShellBackend
 *
 * Returns whether e_shell_backend_start() was called for @shell_backend.
 *
 * Returns: whether @shell_backend is started
 **/
gboolean
e_shell_backend_is_started (EShellBackend *shell_backend)
{
	g_return_val_if_fail (E_IS_SHELL_BACKEND (shell_backend), FALSE);

	return shell_backend->priv->started;
}

/**
 * e_shell_backend_migrate:
 * @shell_backend: an #EShellBackend
 * @major: major part of version to migrate from
 * @minor: minor part of version to migrate from
 * @micro: micro part of version to migrate from
 * @error: return location for a #GError, or %NULL
 *
 * Attempts to migrate data and settings from version %major.%minor.%micro.
 * Returns %TRUE if the migration was successful or if no action was
 * necessary.  Returns %FALSE and sets %error if the migration failed.
 *
 * Returns: %TRUE if successful, %FALSE otherwise
 **/
gboolean
e_shell_backend_migrate (EShellBackend *shell_backend,
                        gint major,
                        gint minor,
                        gint micro,
                        GError **error)
{
	EShellBackendClass *class;

	g_return_val_if_fail (E_IS_SHELL_BACKEND (shell_backend), TRUE);

	class = E_SHELL_BACKEND_GET_CLASS (shell_backend);

	if (class->migrate == NULL)
		return TRUE;

	return class->migrate (shell_backend, major, minor, micro, error);
}
