/*
 * Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
 *
 * 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 Lesser 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: Milan Crha <mcrha@redhat.com>
 */

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

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

#include "e-util/e-util.h"
#include "calendar/gui/comp-util.h"

#include "e-cal-base-shell-view.h"
#include "e-cal-base-shell-sidebar.h"

#define E_CAL_BASE_SHELL_SIDEBAR_GET_PRIVATE(obj) \
	(G_TYPE_INSTANCE_GET_PRIVATE \
	((obj), E_TYPE_CAL_BASE_SHELL_SIDEBAR, ECalBaseShellSidebarPrivate))

struct _ECalBaseShellSidebarPrivate {
	ECalendar *date_navigator; /* not referenced, is inside itself */
	GtkWidget *paned; /* not referenced, is inside itself */
	ESourceSelector *selector; /* not referenced, is inside itself */

	gulong date_navigator_scroll_event_handler_id;

	GHashTable *selected_uids; /* source UID -> cancellable */
};

enum {
	PROP_0,
	PROP_DATE_NAVIGATOR,
	PROP_SELECTOR
};

enum {
	CLIENT_OPENED,
	CLIENT_CLOSED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

G_DEFINE_DYNAMIC_TYPE (ECalBaseShellSidebar, e_cal_base_shell_sidebar, E_TYPE_SHELL_SIDEBAR)

static gboolean
cal_base_shell_sidebar_map_uid_to_source (GValue *value,
					  GVariant *variant,
					  gpointer user_data)
{
	ESourceRegistry *registry;
	ESource *source;
	const gchar *uid;

	registry = E_SOURCE_REGISTRY (user_data);
	uid = g_variant_get_string (variant, NULL);
	if (uid != NULL && *uid != '\0')
		source = e_source_registry_ref_source (registry, uid);
	else
		source = e_source_registry_ref_default_calendar (registry);
	g_value_take_object (value, source);

	return (source != NULL);
}

static GVariant *
cal_base_shell_sidebar_map_source_to_uid (const GValue *value,
					  const GVariantType *expected_type,
					  gpointer user_data)
{
	GVariant *variant = NULL;
	ESource *source;

	source = g_value_get_object (value);

	if (source != NULL) {
		const gchar *uid;

		uid = e_source_get_uid (source);
		variant = g_variant_new_string (uid);
	}

	return variant;
}

static void
cal_base_shell_sidebar_restore_state_cb (EShellWindow *shell_window,
					 EShellView *shell_view,
					 EShellSidebar *shell_sidebar)
{
	ECalBaseShellSidebarPrivate *priv;
	ESourceRegistry *registry;
	ESourceSelector *selector;
	GSettings *settings;
	const gchar *primary_source_key = NULL;

	priv = E_CAL_BASE_SHELL_SIDEBAR (shell_sidebar)->priv;

	g_signal_handlers_disconnect_by_func (
		shell_window,
		cal_base_shell_sidebar_restore_state_cb, shell_sidebar);

	switch (e_cal_base_shell_view_get_source_type (shell_view)) {
		case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
			primary_source_key = "primary-calendar";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
			primary_source_key = "primary-memos";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
			primary_source_key = "primary-tasks";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_LAST:
			g_warn_if_reached ();
			return;
	}

	selector = E_SOURCE_SELECTOR (priv->selector);
	registry = e_source_selector_get_registry (selector);

	/* Bind GObject properties to settings keys. */

	settings = e_util_ref_settings ("org.gnome.evolution.calendar");

	g_settings_bind_with_mapping (
		settings, primary_source_key,
		selector, "primary-selection",
		G_SETTINGS_BIND_DEFAULT,
		cal_base_shell_sidebar_map_uid_to_source,
		cal_base_shell_sidebar_map_source_to_uid,
		g_object_ref (registry),
		(GDestroyNotify) g_object_unref);

	if (priv->date_navigator) {
		if (e_shell_window_is_main_instance (shell_window)) {
			g_settings_bind (
				settings, "date-navigator-pane-position",
				priv->paned, "vposition",
				G_SETTINGS_BIND_DEFAULT);
		} else {
			g_settings_bind (
				settings, "date-navigator-pane-position-sub",
				priv->paned, "vposition",
				G_SETTINGS_BIND_DEFAULT |
				G_SETTINGS_BIND_GET_NO_CHANGES);
		}
	}

	g_object_unref (settings);
}

static guint32
cal_base_shell_sidebar_check_state (EShellSidebar *shell_sidebar)
{
	ECalBaseShellSidebar *cal_base_shell_sidebar;
	ESourceSelector *selector;
	ESourceRegistry *registry;
	ESource *source;
	gboolean is_writable = FALSE;
	gboolean is_removable = FALSE;
	gboolean is_remote_creatable = FALSE;
	gboolean is_remote_deletable = FALSE;
	gboolean in_collection = FALSE;
	gboolean refresh_supported = FALSE;
	gboolean has_primary_source = FALSE;
	guint32 state = 0;

	cal_base_shell_sidebar = E_CAL_BASE_SHELL_SIDEBAR (shell_sidebar);
	selector = e_cal_base_shell_sidebar_get_selector (cal_base_shell_sidebar);
	source = e_source_selector_ref_primary_selection (selector);
	registry = e_source_selector_get_registry (selector);

	if (source != NULL) {
		EClient *client;
		ESource *collection;

		has_primary_source = TRUE;
		is_writable = e_source_get_writable (source);
		is_removable = e_source_get_removable (source);
		is_remote_creatable = e_source_get_remote_creatable (source);
		is_remote_deletable = e_source_get_remote_deletable (source);

		collection = e_source_registry_find_extension (
			registry, source, E_SOURCE_EXTENSION_COLLECTION);
		if (collection != NULL) {
			in_collection = TRUE;
			g_object_unref (collection);
		}

		client = e_client_selector_ref_cached_client (
			E_CLIENT_SELECTOR (selector), source);

		if (client != NULL) {
			refresh_supported =
				e_client_check_refresh_supported (client);
			g_object_unref (client);
		}

		g_object_unref (source);
	}

	if (e_source_selector_count_total (selector) == e_source_selector_count_selected (selector))
		state |= E_CAL_BASE_SHELL_SIDEBAR_ALL_SOURCES_SELECTED;
	if (has_primary_source)
		state |= E_CAL_BASE_SHELL_SIDEBAR_HAS_PRIMARY_SOURCE;
	if (is_writable)
		state |= E_CAL_BASE_SHELL_SIDEBAR_PRIMARY_SOURCE_IS_WRITABLE;
	if (is_removable)
		state |= E_CAL_BASE_SHELL_SIDEBAR_PRIMARY_SOURCE_IS_REMOVABLE;
	if (is_remote_creatable)
		state |= E_CAL_BASE_SHELL_SIDEBAR_PRIMARY_SOURCE_IS_REMOTE_CREATABLE;
	if (is_remote_deletable)
		state |= E_CAL_BASE_SHELL_SIDEBAR_PRIMARY_SOURCE_IS_REMOTE_DELETABLE;
	if (in_collection)
		state |= E_CAL_BASE_SHELL_SIDEBAR_PRIMARY_SOURCE_IN_COLLECTION;
	if (refresh_supported)
		state |= E_CAL_BASE_SHELL_SIDEBAR_SOURCE_SUPPORTS_REFRESH;

	return state;
}

static gboolean
cal_base_shell_sidebar_date_navigator_scroll_event_cb (ECalBaseShellSidebar *cal_base_shell_sidebar,
						       GdkEventScroll *event,
						       ECalendar *date_navigator)
{
	ECalendarItem *calitem;
	gint year = -1, month = -1;
	GdkScrollDirection direction;

	calitem = date_navigator->calitem;
	e_calendar_item_get_first_month (calitem, &year, &month);
	if (year == -1 || month == -1)
		return FALSE;

	direction = event->direction;

	if (direction == GDK_SCROLL_SMOOTH) {
		static gdouble total_delta_y = 0.0;

		total_delta_y += event->delta_y;

		if (total_delta_y >= 1.0) {
			total_delta_y = 0.0;
			direction = GDK_SCROLL_DOWN;
		} else if (total_delta_y <= -1.0) {
			total_delta_y = 0.0;
			direction = GDK_SCROLL_UP;
		} else {
			return FALSE;
		}
	}

	switch (direction) {
		case GDK_SCROLL_UP:
			month--;
			if (month < 0) {
				year--;
				month += 12;
			}
			break;

		case GDK_SCROLL_DOWN:
			month++;
			if (month >= 12) {
				year++;
				month -= 12;
			}
			break;

		default:
			g_return_val_if_reached (FALSE);
	}

	e_calendar_item_set_first_month (calitem, year, month);

	return TRUE;
}

typedef struct _OpenClientData {
	const gchar *extension_name;
	ECalBaseShellSidebar *sidebar;
	ESource *source;
	EClient *client;
} OpenClientData;

static void
open_client_data_free (gpointer pdata)
{
	OpenClientData *data = pdata;

	if (data) {
		if (data->client) {
			g_signal_emit (data->sidebar, signals[CLIENT_OPENED], 0, data->client);
		} else {
			ESourceSelector *selector = e_cal_base_shell_sidebar_get_selector (data->sidebar);
			e_source_selector_unselect_source (selector, data->source);
		}

		g_clear_object (&data->sidebar);
		g_clear_object (&data->source);
		g_clear_object (&data->client);
		g_free (data);
	}
}

static void
e_cal_base_shell_sidebar_open_client_thread (EAlertSinkThreadJobData *job_data,
					     gpointer user_data,
					     GCancellable *cancellable,
					     GError **error)
{
	EClientSelector *selector;
	OpenClientData *data = user_data;
	GError *local_error = NULL;

	g_return_if_fail (data != NULL);

	selector = E_CLIENT_SELECTOR (e_cal_base_shell_sidebar_get_selector (data->sidebar));
	data->client = e_client_selector_get_client_sync (
		selector, data->source, TRUE, (guint32) -1, cancellable, &local_error);

	e_util_propagate_open_source_job_error (job_data, data->extension_name, local_error, error);
}

static void
e_cal_base_shell_sidebar_ensure_source_opened (ECalBaseShellSidebar *sidebar,
					       ESource *source)
{
	OpenClientData *data;
	EShellView *shell_view;
	EActivity *activity;
	gchar *description = NULL, *alert_ident = NULL, *alert_arg_0 = NULL, *display_name;
	const gchar *extension_name = NULL;

	g_return_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (sidebar));
	g_return_if_fail (E_IS_SOURCE (source));

	shell_view = e_shell_sidebar_get_shell_view (E_SHELL_SIDEBAR (sidebar));

	switch (e_cal_base_shell_view_get_source_type (shell_view)) {
		case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
			extension_name = E_SOURCE_EXTENSION_CALENDAR;
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
			extension_name = E_SOURCE_EXTENSION_MEMO_LIST;
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
			extension_name = E_SOURCE_EXTENSION_TASK_LIST;
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_LAST:
			g_warn_if_reached ();
			return;
	}

	display_name = e_util_get_source_full_name (e_shell_get_registry (e_shell_backend_get_shell (e_shell_view_get_shell_backend (shell_view))), source);

	if (!e_util_get_open_source_job_info (extension_name, display_name,
		&description, &alert_ident, &alert_arg_0)) {
		g_free (display_name);
		g_warn_if_reached ();
		return;
	}

	g_free (display_name);

	data = g_new0 (OpenClientData, 1);
	data->extension_name = extension_name; /* no need to copy, it's a static string */
	data->sidebar = g_object_ref (sidebar);
	data->source = g_object_ref (source);

	activity = e_shell_view_submit_thread_job (
		shell_view, description, alert_ident, alert_arg_0,
		e_cal_base_shell_sidebar_open_client_thread, data,
		open_client_data_free);

	if (activity) {
		GCancellable *cancellable;

		cancellable = e_activity_get_cancellable (activity);

		g_hash_table_insert (sidebar->priv->selected_uids,
			g_strdup (e_source_get_uid (source)),
			g_object_ref (cancellable));

		g_object_unref (activity);
	}

	g_free (description);
	g_free (alert_ident);
	g_free (alert_arg_0);
}

static void
e_cal_base_shell_sidebar_source_selected (ESourceSelector *selector,
					  ESource *source,
					  ECalBaseShellSidebar *sidebar)
{
	g_return_if_fail (E_IS_SOURCE_SELECTOR (selector));
	g_return_if_fail (E_IS_SOURCE (source));
	g_return_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (sidebar));

	if (!g_hash_table_contains (sidebar->priv->selected_uids, e_source_get_uid (source))) {
		e_cal_base_shell_sidebar_ensure_source_opened (sidebar, source);
	}
}

static void
e_cal_base_shell_sidebar_source_unselected (ESourceSelector *selector,
					    ESource *source,
					    ECalBaseShellSidebar *sidebar)
{
	g_return_if_fail (E_IS_SOURCE_SELECTOR (selector));
	g_return_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (sidebar));

	if (g_hash_table_remove (sidebar->priv->selected_uids, e_source_get_uid (source)))
		g_signal_emit (sidebar, signals[CLIENT_CLOSED], 0, source);
}

static void
e_cal_base_shell_sidebar_selector_realize_cb (ESourceSelector *selector,
					      ECalBaseShellSidebar *sidebar)
{
	g_return_if_fail (E_IS_SOURCE_SELECTOR (selector));
	g_return_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (sidebar));

	e_cal_base_shell_sidebar_ensure_sources_open (sidebar);
}

typedef struct {
	ESource *source;
	ESource *destination;
	gboolean do_copy;
	icalcomponent *icalcomp;
	EClientSelector *selector;
} TransferItemToData;

static void
transfer_item_to_data_free (gpointer ptr)
{
	TransferItemToData *titd = ptr;

	if (titd) {
		g_clear_object (&titd->source);
		g_clear_object (&titd->destination);
		g_clear_object (&titd->selector);

		if (titd->icalcomp)
			icalcomponent_free (titd->icalcomp);

		g_free (titd);
	}
}

static void
cal_base_shell_sidebar_transfer_thread (EAlertSinkThreadJobData *job_data,
					gpointer user_data,
					GCancellable *cancellable,
					GError **error)
{
	TransferItemToData *titd = user_data;
	EClient *source_client, *destination_client;

	g_return_if_fail (titd != NULL);
	g_return_if_fail (E_IS_SOURCE (titd->source));
	g_return_if_fail (E_IS_SOURCE (titd->destination));
	g_return_if_fail (E_IS_CLIENT_SELECTOR (titd->selector));
	g_return_if_fail (titd->icalcomp != NULL);

	source_client = e_client_selector_get_client_sync (
		titd->selector, titd->source, FALSE, 30, cancellable, error);
	if (!source_client)
		return;

	destination_client = e_client_selector_get_client_sync (
		titd->selector, titd->destination, FALSE, 30, cancellable, error);
	if (!destination_client) {
		g_object_unref (source_client);
		return;
	}

	cal_comp_transfer_item_to_sync (E_CAL_CLIENT (source_client), E_CAL_CLIENT (destination_client),
		titd->icalcomp, titd->do_copy, cancellable, error);

	g_clear_object (&source_client);
	g_clear_object (&destination_client);
}

static gboolean
e_cal_base_shell_sidebar_selector_data_dropped (ESourceSelector *selector,
						GtkSelectionData *selection_data,
						ESource *destination,
						GdkDragAction action,
						guint info,
						ECalBaseShellSidebar *sidebar)
{
	icalcomponent *icalcomp = NULL;
	EActivity *activity;
	EShellView *shell_view;
	ESource *source = NULL;
	ESourceRegistry *registry;
	gchar **segments;
	gchar *source_uid = NULL;
	gchar *message = NULL;
	gchar *display_name = NULL;
	const gchar *alert_ident = NULL;
	const guchar *data;
	gboolean do_copy;
	TransferItemToData *titd;

	g_return_val_if_fail (E_IS_SOURCE_SELECTOR (selector), FALSE);
	g_return_val_if_fail (E_IS_SOURCE (destination), FALSE);
	g_return_val_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (sidebar), FALSE);

	data = gtk_selection_data_get_data (selection_data);
	g_return_val_if_fail (data != NULL, FALSE);

	segments = g_strsplit ((const gchar *) data, "\n", 2);
	if (g_strv_length (segments) != 2)
		goto exit;

	source_uid = g_strdup (segments[0]);
	icalcomp = icalparser_parse_string (segments[1]);

	if (!icalcomp)
		goto exit;

	registry = e_source_selector_get_registry (selector);
	source = e_source_registry_ref_source (registry, source_uid);
	if (!source)
		goto exit;

	display_name = e_util_get_source_full_name (registry, destination);
	do_copy = action == GDK_ACTION_COPY ? TRUE : FALSE;
	shell_view = e_shell_sidebar_get_shell_view (E_SHELL_SIDEBAR (sidebar));

	switch (e_cal_base_shell_view_get_source_type (shell_view)) {
		case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
			message = do_copy ?
				g_strdup_printf (_("Copying an event into the calendar '%s'"), display_name) :
				g_strdup_printf (_("Moving an event into the calendar '%s'"), display_name);
			alert_ident = do_copy ? "calendar:failed-copy-event" : "calendar:failed-move-event";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
			message = do_copy ?
				g_strdup_printf (_("Copying a memo into the memo list '%s'"), display_name) :
				g_strdup_printf (_("Moving a memo into the memo list '%s'"), display_name);
			alert_ident = do_copy ? "calendar:failed-copy-memo" : "calendar:failed-move-memo";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
			message = do_copy ?
				g_strdup_printf (_("Copying a task into the task list '%s'"), display_name) :
				g_strdup_printf (_("Moving a task into the task list '%s'"), display_name);
			alert_ident = do_copy ? "calendar:failed-copy-task" : "calendar:failed-move-task";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_LAST:
			g_warn_if_reached ();
			goto exit;
	}

	titd = g_new0 (TransferItemToData, 1);
	titd->source = g_object_ref (source);
	titd->destination = g_object_ref (destination);
	titd->do_copy = do_copy;
	titd->icalcomp = icalcomp;
	titd->selector = g_object_ref (selector);

	icalcomp = NULL;

	activity = e_shell_view_submit_thread_job (shell_view, message,
		alert_ident, display_name, cal_base_shell_sidebar_transfer_thread,
		titd, transfer_item_to_data_free);

	g_clear_object (&activity);

 exit:
	if (icalcomp)
		icalcomponent_free (icalcomp);

	g_clear_object (&source);
	g_free (message);
	g_free (source_uid);
	g_free (display_name);
	g_strfreev (segments);

	return TRUE;
}

static void
cancel_and_unref (gpointer data)
{
	GCancellable *cancellable = data;

	if (cancellable) {
		g_cancellable_cancel (cancellable);
		g_object_unref (cancellable);
	}
}

static void
cal_base_shell_sidebar_get_property (GObject *object,
				     guint property_id,
				     GValue *value,
				     GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_DATE_NAVIGATOR:
			g_value_set_object (
				value,
				e_cal_base_shell_sidebar_get_date_navigator (
				E_CAL_BASE_SHELL_SIDEBAR (object)));
			return;

		case PROP_SELECTOR:
			g_value_set_object (
				value,
				e_cal_base_shell_sidebar_get_selector (
				E_CAL_BASE_SHELL_SIDEBAR (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
e_cal_base_shell_sidebar_update_calendar_margin_cb (GObject *object,
													GParamSpec *pspec,
													gpointer *user_data)
{
	EShellWindow *shell_window;
	GtkWidget *calendar;
	gboolean switcher_visible;

	shell_window = E_SHELL_WINDOW (object);
	calendar = GTK_WIDGET (user_data);
	switcher_visible = e_shell_window_get_switcher_visible (shell_window);

	if (switcher_visible)
		gtk_widget_set_margin_bottom (calendar, 0);
	else
		gtk_widget_set_margin_bottom (calendar, 6);
}

static void
cal_base_shell_sidebar_constructed (GObject *object)
{
	EShellWindow *shell_window;
	EShellView *shell_view;
	EShellBackend *shell_backend;
	EShell *shell;
	EClientCache *client_cache;
	const gchar *source_extension = NULL, *selector_name = NULL, *restore_state_signal = NULL;
	ECalBaseShellSidebar *cal_base_shell_sidebar;
	GtkWidget *container, *widget;
	AtkObject *a11y;
	gboolean add_navigator = FALSE;

	/* Chain up to parent's constructed() method. */
	G_OBJECT_CLASS (e_cal_base_shell_sidebar_parent_class)->constructed (object);

	cal_base_shell_sidebar = E_CAL_BASE_SHELL_SIDEBAR (object);
	shell_view = e_shell_sidebar_get_shell_view (E_SHELL_SIDEBAR (object));
	shell_backend = e_shell_view_get_shell_backend (shell_view);
	shell_window = e_shell_view_get_shell_window (shell_view);
	shell = e_shell_backend_get_shell (shell_backend);

	switch (e_cal_base_shell_view_get_source_type (shell_view)) {
		case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
			source_extension = E_SOURCE_EXTENSION_CALENDAR;
			selector_name = _("Calendar Selector");
			restore_state_signal = "shell-view-created::calendar";
			add_navigator = TRUE;
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
			source_extension = E_SOURCE_EXTENSION_MEMO_LIST;
			selector_name = _("Memo List Selector");
			restore_state_signal = "shell-view-created::memos";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
			source_extension = E_SOURCE_EXTENSION_TASK_LIST;
			selector_name = _("Task List Selector");
			restore_state_signal = "shell-view-created::tasks";
			break;
		case E_CAL_CLIENT_SOURCE_TYPE_LAST:
			g_warn_if_reached ();
			return;
	}

	client_cache = e_shell_get_client_cache (shell);

	container = GTK_WIDGET (object);

	widget = e_paned_new (GTK_ORIENTATION_VERTICAL);
	gtk_container_add (GTK_CONTAINER (container), widget);
	cal_base_shell_sidebar->priv->paned = widget;

	container = widget;

	widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
	gtk_paned_pack1 (GTK_PANED (container), widget, TRUE, TRUE);

	container = widget;

	widget = gtk_scrolled_window_new (NULL, NULL);
	gtk_scrolled_window_set_policy (
		GTK_SCROLLED_WINDOW (widget),
		GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
	gtk_scrolled_window_set_shadow_type (
		GTK_SCROLLED_WINDOW (widget),
		GTK_SHADOW_IN);
	gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0);

	container = widget;

	widget = e_client_selector_new (client_cache, source_extension);
	a11y = gtk_widget_get_accessible (widget);
	atk_object_set_name (a11y, selector_name);
	cal_base_shell_sidebar->priv->selector = E_SOURCE_SELECTOR (widget);
	gtk_container_add (GTK_CONTAINER (container), widget);

	e_source_selector_load_groups_setup (cal_base_shell_sidebar->priv->selector,
		e_shell_view_get_state_key_file (shell_view));

	if (add_navigator) {
		ECalendarItem *calitem;

		container = cal_base_shell_sidebar->priv->paned;

		widget = e_calendar_new ();
		gtk_widget_set_margin_top (widget, 6);
		#if GTK_CHECK_VERSION(3,12,0)
		gtk_widget_set_margin_start (widget, 6);
		gtk_widget_set_margin_end (widget, 6);
		#endif
		calitem = E_CALENDAR (widget)->calitem;
		e_calendar_item_set_days_start_week_sel (calitem, 9);
		e_calendar_item_set_max_days_sel (calitem, 42);
		gtk_paned_pack2 (GTK_PANED (container), widget, FALSE, FALSE);
		cal_base_shell_sidebar->priv->date_navigator = E_CALENDAR (widget);
		gtk_widget_show (widget);

		gnome_canvas_item_set (
			GNOME_CANVAS_ITEM (cal_base_shell_sidebar->priv->date_navigator->calitem),
			"move-selection-when-moving", FALSE,
			NULL);

		cal_base_shell_sidebar->priv->date_navigator_scroll_event_handler_id = g_signal_connect_swapped (
			cal_base_shell_sidebar->priv->date_navigator, "scroll-event",
			G_CALLBACK (cal_base_shell_sidebar_date_navigator_scroll_event_cb), cal_base_shell_sidebar);
	}

	gtk_widget_show_all (GTK_WIDGET (object));

	gtk_drag_dest_set (
		GTK_WIDGET (cal_base_shell_sidebar->priv->selector), GTK_DEST_DEFAULT_ALL,
		NULL, 0, GDK_ACTION_COPY | GDK_ACTION_MOVE);

	e_drag_dest_add_calendar_targets (GTK_WIDGET (cal_base_shell_sidebar->priv->selector));

	g_signal_connect (shell_window,
		"notify::switcher-visible", G_CALLBACK (e_cal_base_shell_sidebar_update_calendar_margin_cb),
		widget);

	g_signal_connect (cal_base_shell_sidebar->priv->selector,
		"realize", G_CALLBACK (e_cal_base_shell_sidebar_selector_realize_cb),
		cal_base_shell_sidebar);

	g_signal_connect (cal_base_shell_sidebar->priv->selector,
		"data-dropped", G_CALLBACK (e_cal_base_shell_sidebar_selector_data_dropped),
		cal_base_shell_sidebar);

	g_signal_connect (cal_base_shell_sidebar->priv->selector,
		"source-selected", G_CALLBACK (e_cal_base_shell_sidebar_source_selected),
		cal_base_shell_sidebar);

	g_signal_connect (cal_base_shell_sidebar->priv->selector,
		"source-unselected", G_CALLBACK (e_cal_base_shell_sidebar_source_unselected),
		cal_base_shell_sidebar);

	/* Restore widget state from the last session once
	 * the shell view is fully initialized and visible. */
	g_signal_connect (
		shell_window, restore_state_signal,
		G_CALLBACK (cal_base_shell_sidebar_restore_state_cb),
		cal_base_shell_sidebar);
}

static void
cal_base_shell_sidebar_dispose (GObject *object)
{
	ECalBaseShellSidebar *cal_base_shell_sidebar;

	cal_base_shell_sidebar = E_CAL_BASE_SHELL_SIDEBAR (object);

	if (cal_base_shell_sidebar->priv->date_navigator_scroll_event_handler_id > 0 &&
	    cal_base_shell_sidebar->priv->date_navigator) {
		g_signal_handler_disconnect (cal_base_shell_sidebar->priv->date_navigator,
			cal_base_shell_sidebar->priv->date_navigator_scroll_event_handler_id);
		cal_base_shell_sidebar->priv->date_navigator_scroll_event_handler_id = 0;
	}

	cal_base_shell_sidebar->priv->date_navigator = NULL;
	cal_base_shell_sidebar->priv->selector = NULL;
	cal_base_shell_sidebar->priv->paned = NULL;

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

static void
cal_base_shell_sidebar_finalize (GObject *object)
{
	ECalBaseShellSidebar *cal_base_shell_sidebar;

	cal_base_shell_sidebar = E_CAL_BASE_SHELL_SIDEBAR (object);

	g_hash_table_destroy (cal_base_shell_sidebar->priv->selected_uids);
	cal_base_shell_sidebar->priv->selected_uids = NULL;

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

static void
e_cal_base_shell_sidebar_class_init (ECalBaseShellSidebarClass *class)
{
	GObjectClass *object_class;
	EShellSidebarClass *shell_sidebar_class;

	g_type_class_add_private (class, sizeof (ECalBaseShellSidebarPrivate));

	object_class = G_OBJECT_CLASS (class);
	object_class->get_property = cal_base_shell_sidebar_get_property;
	object_class->constructed = cal_base_shell_sidebar_constructed;
	object_class->dispose = cal_base_shell_sidebar_dispose;
	object_class->finalize = cal_base_shell_sidebar_finalize;

	shell_sidebar_class = E_SHELL_SIDEBAR_CLASS (class);
	shell_sidebar_class->check_state = cal_base_shell_sidebar_check_state;

	g_object_class_install_property (
		object_class,
		PROP_SELECTOR,
		g_param_spec_object (
			"selector",
			"Source Selector Widget",
			"This widget displays groups of calendars",
			E_TYPE_SOURCE_SELECTOR,
			G_PARAM_READABLE));

	g_object_class_install_property (
		object_class,
		PROP_DATE_NAVIGATOR,
		g_param_spec_object (
			"date-navigator",
			"Date Navigator Widget",
			"This widget displays a miniature calendar",
			E_TYPE_CALENDAR,
			G_PARAM_READABLE));

	signals[CLIENT_OPENED] = g_signal_new (
		"client-opened",
		G_OBJECT_CLASS_TYPE (object_class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET (ECalBaseShellSidebarClass, client_opened),
		NULL, NULL,
		g_cclosure_marshal_VOID__OBJECT,
		G_TYPE_NONE, 1,
		E_TYPE_CAL_CLIENT);

	signals[CLIENT_CLOSED] = g_signal_new (
		"client-closed",
		G_OBJECT_CLASS_TYPE (object_class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET (ECalBaseShellSidebarClass, client_closed),
		NULL, NULL,
		g_cclosure_marshal_VOID__OBJECT,
		G_TYPE_NONE, 1,
		E_TYPE_SOURCE);
}

static void
e_cal_base_shell_sidebar_class_finalize (ECalBaseShellSidebarClass *class)
{
}

static void
e_cal_base_shell_sidebar_init (ECalBaseShellSidebar *cal_base_shell_sidebar)
{
	cal_base_shell_sidebar->priv = E_CAL_BASE_SHELL_SIDEBAR_GET_PRIVATE (cal_base_shell_sidebar);
	cal_base_shell_sidebar->priv->selected_uids =
		g_hash_table_new_full (g_str_hash, g_str_equal, g_free, cancel_and_unref);
}

void
e_cal_base_shell_sidebar_type_register (GTypeModule *type_module)
{
	/* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
	 *     function, so we have to wrap it with a public function in
	 *     order to register types from a separate compilation unit. */
	e_cal_base_shell_sidebar_register_type (type_module);
}

GtkWidget *
e_cal_base_shell_sidebar_new (EShellView *shell_view)
{
	g_return_val_if_fail (E_IS_SHELL_VIEW (shell_view), NULL);

	return g_object_new (
		E_TYPE_CAL_BASE_SHELL_SIDEBAR,
		"shell-view", shell_view, NULL);
}

ECalendar *
e_cal_base_shell_sidebar_get_date_navigator (ECalBaseShellSidebar *cal_base_shell_sidebar)
{
	g_return_val_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (cal_base_shell_sidebar), NULL);

	return cal_base_shell_sidebar->priv->date_navigator;
}

ESourceSelector *
e_cal_base_shell_sidebar_get_selector (ECalBaseShellSidebar *cal_base_shell_sidebar)
{
	g_return_val_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (cal_base_shell_sidebar), NULL);

	return cal_base_shell_sidebar->priv->selector;
}

void
e_cal_base_shell_sidebar_ensure_sources_open (ECalBaseShellSidebar *cal_base_shell_sidebar)
{
	GList *selected, *link;
	ESourceSelector *selector;

	g_return_if_fail (E_IS_CAL_BASE_SHELL_SIDEBAR (cal_base_shell_sidebar));

	selector = cal_base_shell_sidebar->priv->selector;
	g_return_if_fail (E_IS_SOURCE_SELECTOR (selector));

	selected = e_source_selector_get_selection (selector);
	for (link = selected; link; link = g_list_next (link)) {
		ESource *source = link->data;

		e_cal_base_shell_sidebar_ensure_source_opened (cal_base_shell_sidebar, source);
	}

	g_list_free_full (selected, g_object_unref);
}
