/*******************************************************************************
 * Copyright (c) 2004, 2013 Tasktop Technologies and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.tasks.ui;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.core.net.proxy.IProxyChangeEvent;
import org.eclipse.core.net.proxy.IProxyChangeListener;
import org.eclipse.core.net.proxy.IProxyService;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.identity.core.IIdentityService;
import org.eclipse.mylyn.commons.net.WebUtil;
import org.eclipse.mylyn.commons.notifications.core.NotificationEnvironment;
import org.eclipse.mylyn.commons.notifications.feed.ServiceMessageManager;
import org.eclipse.mylyn.commons.notifications.ui.AbstractUiNotification;
import org.eclipse.mylyn.commons.ui.compatibility.CommonColors;
import org.eclipse.mylyn.commons.ui.compatibility.CommonFonts;
import org.eclipse.mylyn.commons.workbench.TaskBarManager;
import org.eclipse.mylyn.internal.commons.notifications.feed.ServiceMessage;
import org.eclipse.mylyn.internal.discovery.ui.DiscoveryUi;
import org.eclipse.mylyn.internal.monitor.ui.MonitorUiPlugin;
import org.eclipse.mylyn.internal.tasks.core.AbstractSearchHandler;
import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
import org.eclipse.mylyn.internal.tasks.core.IRepositoryModelListener;
import org.eclipse.mylyn.internal.tasks.core.ITasksCoreConstants;
import org.eclipse.mylyn.internal.tasks.core.LocalRepositoryConnector;
import org.eclipse.mylyn.internal.tasks.core.RepositoryExternalizationParticipant;
import org.eclipse.mylyn.internal.tasks.core.RepositoryModel;
import org.eclipse.mylyn.internal.tasks.core.RepositoryTemplateManager;
import org.eclipse.mylyn.internal.tasks.core.TaskActivityManager;
import org.eclipse.mylyn.internal.tasks.core.TaskActivityUtil;
import org.eclipse.mylyn.internal.tasks.core.TaskList;
import org.eclipse.mylyn.internal.tasks.core.TaskRepositoryDelta;
import org.eclipse.mylyn.internal.tasks.core.TaskRepositoryDelta.Type;
import org.eclipse.mylyn.internal.tasks.core.TaskRepositoryManager;
import org.eclipse.mylyn.internal.tasks.core.data.SynchronizationManger;
import org.eclipse.mylyn.internal.tasks.core.data.TaskDataManager;
import org.eclipse.mylyn.internal.tasks.core.data.TaskDataStore;
import org.eclipse.mylyn.internal.tasks.core.externalization.ExternalizationManager;
import org.eclipse.mylyn.internal.tasks.core.externalization.IExternalizationParticipant;
import org.eclipse.mylyn.internal.tasks.core.externalization.TaskActivationExternalizationParticipant;
import org.eclipse.mylyn.internal.tasks.core.externalization.TaskListExternalizationParticipant;
import org.eclipse.mylyn.internal.tasks.core.externalization.TaskListExternalizer;
import org.eclipse.mylyn.internal.tasks.core.util.RepositoryConnectorLoader;
import org.eclipse.mylyn.internal.tasks.core.util.TaskRepositoryKeyringMigrator;
import org.eclipse.mylyn.internal.tasks.core.util.TaskRepositorySecureStoreMigrator;
import org.eclipse.mylyn.internal.tasks.core.util.TasksCoreExtensionReader;
import org.eclipse.mylyn.internal.tasks.ui.actions.ActivateTaskDialogAction;
import org.eclipse.mylyn.internal.tasks.ui.actions.NewTaskAction;
import org.eclipse.mylyn.internal.tasks.ui.notifications.TaskListNotificationReminder;
import org.eclipse.mylyn.internal.tasks.ui.notifications.TaskListNotifier;
import org.eclipse.mylyn.internal.tasks.ui.util.TasksUiExtensionReader;
import org.eclipse.mylyn.internal.tasks.ui.views.TaskListView;
import org.eclipse.mylyn.internal.tasks.ui.views.TaskRepositoriesView;
import org.eclipse.mylyn.tasks.core.AbstractDuplicateDetector;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITask.PriorityLevel;
import org.eclipse.mylyn.tasks.core.ITaskContainer;
import org.eclipse.mylyn.tasks.core.RepositoryTemplate;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.activity.AbstractTaskActivityMonitor;
import org.eclipse.mylyn.tasks.core.context.AbstractTaskContextStore;
import org.eclipse.mylyn.tasks.ui.AbstractRepositoryConnectorUi;
import org.eclipse.mylyn.tasks.ui.AbstractTaskRepositoryLinkProvider;
import org.eclipse.mylyn.tasks.ui.TasksUi;
import org.eclipse.mylyn.tasks.ui.TasksUiImages;
import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorPageFactory;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IStartup;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.FormColors;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.progress.IProgressService;
import org.eclipse.ui.progress.UIJob;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;

/**
 * Main entry point for the Tasks UI.
 *
 * @author Mik Kersten
 * @since 3.0
 */
public class TasksUiPlugin extends AbstractUIPlugin {

	private static final int DELAY_QUERY_REFRESH_ON_STARTUP = 20 * 1000;

	private static final int DEFAULT_LINK_PROVIDER_TIMEOUT = 5 * 1000;

	public static final String ID_PLUGIN = "org.eclipse.mylyn.tasks.ui"; //$NON-NLS-1$

	private static final String DIRECTORY_METADATA = ".metadata"; //$NON-NLS-1$

	private static final String NAME_DATA_DIR = ".mylyn"; //$NON-NLS-1$

	private static final char DEFAULT_PATH_SEPARATOR = '/';

	private static final int NOTIFICATION_DELAY = 5000;

	private static final String PREF_MIGRATED_TASK_REPOSITORIES_FROM_SECURE_STORE = "migrated.task.repositories.secure.store"; //$NON-NLS-1$

	private static final String PREF_MIGRATED_TASK_REPOSITORIES_FROM_KEYRING = "migrated.task.repositories.keyring"; //$NON-NLS-1$

	private static final String PROP_FORCE_CREDENTIALS_MIGRATION = "org.eclipse.mylyn.tasks.force.credentials.migration"; //$NON-NLS-1$

	private static TasksUiPlugin INSTANCE;

	private static ExternalizationManager externalizationManager;

	private static TaskActivityManager taskActivityManager;

	private static TaskRepositoryManager repositoryManager;

	private static TaskListSynchronizationScheduler synchronizationScheduler;

	private static TaskDataManager taskDataManager;

	private static Map<String, AbstractRepositoryConnectorUi> repositoryConnectorUiMap = new HashMap<String, AbstractRepositoryConnectorUi>();

	private TaskListNotificationManager taskListNotificationManager;

	private TaskListBackupManager taskListBackupManager;

	private RepositoryTemplateManager repositoryTemplateManager;

	private ServiceMessageManager serviceMessageManager;

	private final Set<AbstractTaskEditorPageFactory> taskEditorPageFactories = new HashSet<AbstractTaskEditorPageFactory>();

	private final TreeSet<AbstractTaskRepositoryLinkProvider> repositoryLinkProviders = new TreeSet<AbstractTaskRepositoryLinkProvider>(
			new OrderComparator());

	private TaskListExternalizer taskListExternalizer;

	private final Map<String, Image> brandingIcons = new HashMap<String, Image>();

	private final Map<String, ImageDescriptor> overlayIcons = new HashMap<String, ImageDescriptor>();

	private final Set<AbstractDuplicateDetector> duplicateDetectors = new HashSet<AbstractDuplicateDetector>();

	private ISaveParticipant saveParticipant;

	private TaskJobFactory taskJobFactory;

	// shared colors for all forms
	private FormColors formColors;

	private static AbstractTaskContextStore contextStore;

	private final List<AbstractSearchHandler> searchHandlers = new ArrayList<AbstractSearchHandler>();

	private static final boolean DEBUG_HTTPCLIENT = "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.mylyn.tasks.ui/debug/httpclient")); //$NON-NLS-1$ //$NON-NLS-2$

	// XXX reconsider if this is necessary
	public static class TasksUiStartup implements IStartup {

		public void earlyStartup() {
			// ignore
		}
	}

	private static final class OrderComparator implements Comparator<AbstractTaskRepositoryLinkProvider> {
		public int compare(AbstractTaskRepositoryLinkProvider p1, AbstractTaskRepositoryLinkProvider p2) {
			return p1.getOrder() - p2.getOrder();
		}
	}

	public enum TaskListSaveMode {
		ONE_HOUR, THREE_HOURS, DAY;
		@Override
		public String toString() {
			switch (this) {
			case ONE_HOUR:
				return "1 hour"; //$NON-NLS-1$
			case THREE_HOURS:
				return "3 hours"; //$NON-NLS-1$
			case DAY:
				return "1 day"; //$NON-NLS-1$
			default:
				return "3 hours"; //$NON-NLS-1$
			}
		}

		public static TaskListSaveMode fromString(String string) {
			if (string == null) {
				return null;
			}
			if (string.equals("1 hour")) { //$NON-NLS-1$
				return ONE_HOUR;
			}
			if (string.equals("3 hours")) { //$NON-NLS-1$
				return THREE_HOURS;
			}
			if (string.equals("1 day")) { //$NON-NLS-1$
				return DAY;
			}
			return null;
		}

		public static long fromStringToLong(String string) {
			long hour = 3600 * 1000;
			switch (fromString(string)) {
			case ONE_HOUR:
				return hour;
			case THREE_HOURS:
				return hour * 3;
			case DAY:
				return hour * 24;
			default:
				return hour * 3;
			}
		}
	}

	public enum ReportOpenMode {
		EDITOR, INTERNAL_BROWSER, EXTERNAL_BROWSER;
	}

	private static ITaskListNotificationProvider REMINDER_NOTIFICATION_PROVIDER = new ITaskListNotificationProvider() {

		public Set<AbstractUiNotification> getNotifications() {
			Collection<AbstractTask> allTasks = TasksUiPlugin.getTaskList().getAllTasks();
			Set<AbstractUiNotification> reminders = new HashSet<AbstractUiNotification>();
			for (AbstractTask task : allTasks) {
				if (TasksUiPlugin.getTaskActivityManager().isPastReminder(task) && !task.isReminded()) {
					reminders.add(new TaskListNotificationReminder(task));
					task.setReminded(true);
				}
			}
			return reminders;
		}
	};

	private final org.eclipse.jface.util.IPropertyChangeListener PROPERTY_LISTENER = new org.eclipse.jface.util.IPropertyChangeListener() {

		public void propertyChange(org.eclipse.jface.util.PropertyChangeEvent event) {
			if (event.getProperty().equals(ITasksUiPreferenceConstants.PLANNING_ENDHOUR)
					|| event.getProperty().equals(ITasksUiPreferenceConstants.WEEK_START_DAY)) {
				updateTaskActivityManager();
			}

			if (event.getProperty().equals(ITasksUiPreferenceConstants.REPOSITORY_SYNCH_SCHEDULE_ENABLED)
					|| event.getProperty().equals(ITasksUiPreferenceConstants.REPOSITORY_SYNCH_SCHEDULE_MILISECONDS)) {
				updateSynchronizationScheduler(false);
			}

			if (event.getProperty().equals(ITasksUiPreferenceConstants.SERVICE_MESSAGES_ENABLED)) {
				if (getPreferenceStore().getBoolean(ITasksUiPreferenceConstants.SERVICE_MESSAGES_ENABLED)) {
					serviceMessageManager.start();
				} else {
					serviceMessageManager.stop();
				}
			}
		}
	};

	private AbstractTaskActivityMonitor taskActivityMonitor;

	@SuppressWarnings("rawtypes")
	private ServiceReference proxyServiceReference;

	private IProxyChangeListener proxyChangeListener;

	private static TaskListExternalizationParticipant taskListExternalizationParticipant;

	private final Set<IRepositoryModelListener> listeners = new HashSet<IRepositoryModelListener>();

	private File activationHistoryFile;

	private static TaskList taskList;

	private static RepositoryModel repositoryModel;

	private static TasksUiFactory uiFactory;

	@Deprecated
	public static String LABEL_VIEW_REPOSITORIES = Messages.TasksUiPlugin_Task_Repositories;

	private final AtomicInteger initializationCount = new AtomicInteger();

	private SynchronizationManger synchronizationManger;

	private RepositoryConnectorLoader connectorLoader;

	private class TasksUiInitializationJob extends UIJob {

		public TasksUiInitializationJob() {
			super(Messages.TasksUiPlugin_Initializing_Task_List);
			setSystem(true);
		}

		@SuppressWarnings("restriction")
		@Override
		public IStatus runInUIThread(IProgressMonitor monitor) {
			// NOTE: failure in one part of the initialization should
			// not prevent others
			monitor.beginTask("Initializing Task List", 5); //$NON-NLS-1$
			try {
				// Needs to run after workbench is loaded because it
				// relies on images.
				TasksUiExtensionReader.initWorkbenchUiExtensions(connectorLoader.getBlackList());

				if (externalizationManager.getLoadStatus() != null) {
					// XXX: recovery from task list load failure  (Rendered in task list)
				}

				List<String> commandLineArgs = Arrays.asList(Platform.getCommandLineArgs());
				boolean activateTask = !commandLineArgs.contains(ITasksCoreConstants.COMMAND_LINE_NO_ACTIVATE_TASK);
				if (activateTask) {
					try {
						Field field = org.eclipse.core.internal.resources.Workspace.class.getDeclaredField("crashed"); //$NON-NLS-1$
						field.setAccessible(true);
						Object value = field.get(ResourcesPlugin.getWorkspace());
						if (value instanceof Boolean) {
							activateTask = !(Boolean) value;
						}
					} catch (Throwable t) {
						t.printStackTrace();
						// ignore
					}
				}
				// Needs to happen asynchronously to avoid bug 159706
				for (AbstractTask task : taskList.getAllTasks()) {
					if (task.isActive()) {
						// the externalizer might set multiple tasks active
						task.setActive(false);
						if (activateTask) {
							// make sure only one task is activated
							taskActivityManager.activateTask(task);
							activateTask = false;
						}
					}
				}

				//taskActivityMonitor.reloadActivityTime();
			} catch (Throwable t) {
				StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
						"Could not initialize task activity", t)); //$NON-NLS-1$
			}
			monitor.worked(1);

			initializeNotificationsAndSynchronization();

			addSystemTaskBarActions();

			try {
				getPreferenceStore().addPropertyChangeListener(PROPERTY_LISTENER);

				// TODO: get rid of this, hack to make decorators show
				// up on startup
				TaskRepositoriesView repositoriesView = TaskRepositoriesView.getFromActivePerspective();
				if (repositoriesView != null) {
					repositoriesView.getViewer().refresh();
				}
			} catch (Throwable t) {
				StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
						"Could not finish Tasks UI initialization", t)); //$NON-NLS-1$
			} finally {
				monitor.done();
			}
			hideNonMatchingSubtasks();
			return new Status(IStatus.OK, TasksUiPlugin.ID_PLUGIN, IStatus.OK, "", null); //$NON-NLS-1$
		}

		/**
		 * hide non-matching subtasks, or display a message about it if this is not a fresh installation
		 */
		protected void hideNonMatchingSubtasks() {
			final String HIDE_SUBTASKS = "hide-subtasks"; //$NON-NLS-1$
			if (!getPreferenceStore().getBoolean(ITasksUiPreferenceConstants.FILTER_NON_MATCHING)
					&& !getPreferenceStore().getBoolean(ITasksUiPreferenceConstants.ENCOURAGED_FILTER_NON_MATCHING)) {
				if (taskList.getQueries().isEmpty()) {
					getPreferenceStore().setValue(ITasksUiPreferenceConstants.FILTER_NON_MATCHING, true);
				} else {
					TaskListView view = TaskListView.getFromActivePerspective();
					if (view != null && view.getServiceMessageControl() != null) {
						ServiceMessage message = new ServiceMessage("") { //$NON-NLS-1$
							@Override
							public boolean openLink(String link) {
								if (HIDE_SUBTASKS.equals(link)) {
									getPreferenceStore().setValue(ITasksUiPreferenceConstants.FILTER_NON_MATCHING, true);
									savePluginPreferences();
									return true;
								}
								return false;
							}
						};
						message.setId("hide.nonmatching.subtasks"); //$NON-NLS-1$
						message.setImage(Dialog.DLG_IMG_MESSAGE_INFO);
						message.setTitle(Messages.TasksUiPlugin_Hide_Irrelevant_Subtasks);
						message.setDescription(NLS.bind(Messages.TasksUiPlugin_Hide_Irrelevant_Subtasks_Message,
								HIDE_SUBTASKS));
						view.getServiceMessageControl().setMessage(message);
					}
				}
				// never do this again
				getPreferenceStore().setValue(ITasksUiPreferenceConstants.ENCOURAGED_FILTER_NON_MATCHING, true);
				savePluginPreferences();
			}
		}
	}

	public TasksUiPlugin() {
		super();
		INSTANCE = this;
	}

	private void addSystemTaskBarActions() {
		MenuManager taskBarMenuManager = TaskBarManager.getTaskBarMenuManager();
		if (taskBarMenuManager != null) {
			NewTaskAction newTaskAction = new NewTaskAction(Messages.TasksUiPlugin_New_Task, true);
			taskBarMenuManager.add(newTaskAction);

			Action activateTaskAction = new Action() {
				@Override
				public void run() {
					ActivateTaskDialogAction activateTaskDialogAction = new ActivateTaskDialogAction();
					IWorkbenchWindow window = getWorkbench().getActiveWorkbenchWindow();
					if (window == null && getWorkbench().getWorkbenchWindows().length > 0) {
						window = getWorkbench().getWorkbenchWindows()[0];
					}
					activateTaskDialogAction.init(window);
					activateTaskDialogAction.run(null);
				}
			};
			activateTaskAction.setImageDescriptor(TasksUiImages.CONTEXT_ACTIVE_CENTERED);
			activateTaskAction.setText(Messages.TasksUiPlugin_Activate_Task);
			taskBarMenuManager.add(activateTaskAction);
			taskBarMenuManager.update(true);
		}
	}

	private void updateSynchronizationScheduler(boolean initial) {
		if (synchronizationScheduler == null) {
			return;
		}

		boolean enabled = TasksUiPlugin.getDefault()
				.getPreferenceStore()
				.getBoolean(ITasksUiPreferenceConstants.REPOSITORY_SYNCH_SCHEDULE_ENABLED);
		if (enabled) {
			long interval = TasksUiPlugin.getDefault()
					.getPreferenceStore()
					.getLong(ITasksUiPreferenceConstants.REPOSITORY_SYNCH_SCHEDULE_MILISECONDS);
			if (initial) {
				synchronizationScheduler.setInterval(DELAY_QUERY_REFRESH_ON_STARTUP, interval);
			} else {
				synchronizationScheduler.setInterval(interval);
			}
		} else {
			synchronizationScheduler.setInterval(0);
		}
	}

	@SuppressWarnings("restriction")
	@Override
	public void start(BundleContext context) throws Exception {
		super.start(context);
		// NOTE: startup order is very sensitive
		try {
			// initialize framework and settings
			if (DEBUG_HTTPCLIENT) {
				// do this before anything else, once commons logging is initialized and an instance
				// of Log has been created it's too late
				initHttpLogging();
			}
			WebUtil.init();
			initializePreferences(getPreferenceStore());

			// initialize CommonFonts from UI thread: bug 240076
			if (CommonFonts.BOLD == null) {
				// ignore
			}

			File dataDir = new File(getDataDirectory());
			dataDir.mkdirs();

			// create data model
			externalizationManager = new ExternalizationManager(getDataDirectory());

			repositoryManager = new TaskRepositoryManager();
			IExternalizationParticipant repositoryParticipant = new RepositoryExternalizationParticipant(
					externalizationManager, repositoryManager);
			externalizationManager.addParticipant(repositoryParticipant);

			taskList = new TaskList();
			repositoryModel = new RepositoryModel(taskList, repositoryManager);
			taskListExternalizer = new TaskListExternalizer(repositoryModel, repositoryManager);

			taskListExternalizationParticipant = new TaskListExternalizationParticipant(repositoryModel, taskList,
					taskListExternalizer, externalizationManager, repositoryManager);
			//externalizationManager.load(taskListSaveParticipant);
			externalizationManager.addParticipant(taskListExternalizationParticipant);
			taskList.addChangeListener(taskListExternalizationParticipant);

			taskActivityManager = new TaskActivityManager(repositoryManager, taskList);

			taskActivityManager.addActivationListener(taskListExternalizationParticipant);

			// initialize
			updateTaskActivityManager();

			proxyServiceReference = context.getServiceReference(IProxyService.class.getName());
			if (proxyServiceReference != null) {
				@SuppressWarnings("unchecked")
				IProxyService proxyService = (IProxyService) context.getService(proxyServiceReference);
				if (proxyService != null) {
					proxyChangeListener = new IProxyChangeListener() {
						public void proxyInfoChanged(IProxyChangeEvent event) {
							List<TaskRepository> repositories = repositoryManager.getAllRepositories();
							for (TaskRepository repository : repositories) {
								if (repository.isDefaultProxyEnabled()) {
									repositoryManager.notifyRepositorySettingsChanged(repository,
											new TaskRepositoryDelta(Type.PROYX));
								}
							}
						}
					};
					proxyService.addProxyChangeListener(proxyChangeListener);
				}
			}

			repositoryTemplateManager = new RepositoryTemplateManager();

			// NOTE: initializing extensions in start(..) has caused race
			// conditions previously
			connectorLoader = new RepositoryConnectorLoader();
			connectorLoader.registerConnectors(repositoryManager, taskListExternalizer);
			connectorLoader.registerTemplates(repositoryManager, repositoryTemplateManager);
			TasksUiExtensionReader.initStartupExtensions(connectorLoader.getBlackList());

			// instantiate taskDataManager
			TaskDataStore taskDataStore = new TaskDataStore(repositoryManager);
			synchronizationManger = new SynchronizationManger(repositoryModel);
			taskDataManager = new TaskDataManager(taskDataStore, repositoryManager, taskList, taskActivityManager,
					synchronizationManger);

			taskJobFactory = new TaskJobFactory(taskList, taskDataManager, repositoryManager, repositoryModel);

			taskActivityMonitor = TasksCoreExtensionReader.loadTaskActivityMonitor();
			taskActivityMonitor.start(taskActivityManager);

			saveParticipant = new ISaveParticipant() {

				public void doneSaving(ISaveContext context) {
				}

				public void prepareToSave(ISaveContext context) throws CoreException {
				}

				public void rollback(ISaveContext context) {
				}

				public void saving(ISaveContext context) throws CoreException {
					if (context.getKind() == ISaveContext.FULL_SAVE) {
						externalizationManager.stop();
					}
				}
			};
			ResourcesPlugin.getWorkspace().addSaveParticipant(this, saveParticipant);

			// initialize externalization for task activation history
			IPath stateLocation = Platform.getStateLocation(getBundle());
			activationHistoryFile = stateLocation.append("TaskActivationHistory.xml").toFile(); //$NON-NLS-1$
			TaskActivationExternalizationParticipant taskActivationExternalizationParticipant = new TaskActivationExternalizationParticipant(
					externalizationManager, taskList, taskActivityManager.getTaskActivationHistory(),
					activationHistoryFile);
			taskActivityManager.addActivationListener(taskActivationExternalizationParticipant);
			externalizationManager.addParticipant(taskActivationExternalizationParticipant);

			// initialize managers
			initializeDataSources();

			migrateCredentials(repositoryManager.getAllRepositories());

			// make this available early for clients that are not initialized through tasks ui but need access
			taskListNotificationManager = new TaskListNotificationManager();

			String lastMod = getPreferenceStore().getString(
					ITasksUiPreferenceConstants.LAST_SERVICE_MESSAGE_LAST_MODIFIED);
			String etag = getPreferenceStore().getString(ITasksUiPreferenceConstants.LAST_SERVICE_MESSAGE_ETAG);
			String serviceMessageUrl = getPreferenceStore().getString(ITasksUiPreferenceConstants.SERVICE_MESSAGE_URL);

			Long checktime = getPreferenceStore().getLong(ITasksUiPreferenceConstants.LAST_SERVICE_MESSAGE_CHECKTIME);

			serviceMessageManager = new ServiceMessageManager(serviceMessageUrl, lastMod, etag, checktime,
					new NotificationEnvironment() {
				private Set<String> installedFeatures;

				@Override
				public Set<String> getInstalledFeatures(IProgressMonitor monitor) {
					if (installedFeatures == null) {
						installedFeatures = DiscoveryUi.createInstallJob().getInstalledFeatures(monitor);
					}
					return installedFeatures;
				}
			});

			// Disabled for initial 3.4 release as per bug#263528
			if (getPreferenceStore().getBoolean(ITasksUiPreferenceConstants.SERVICE_MESSAGES_ENABLED)) {
				serviceMessageManager.start();
			}

			// trigger lazy initialization
			new TasksUiInitializationJob().schedule();
		} catch (Exception e) {
			StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, "Task list initialization failed", e)); //$NON-NLS-1$
		}
	}

	/**
	 * Migrate credentials from the old secure store location, and from the deprecated keyring if the compatibility.auth
	 * bundle is present.
	 */
	@SuppressWarnings("deprecation")
	private void migrateCredentials(final List<TaskRepository> repositories) {
		final boolean force = Boolean.parseBoolean(System.getProperty(PROP_FORCE_CREDENTIALS_MIGRATION));
		final boolean migrateFromSecureStore = force
				|| !getPluginPreferences().getBoolean(PREF_MIGRATED_TASK_REPOSITORIES_FROM_SECURE_STORE);
		final boolean migrateFromKeyring = (force || !getPluginPreferences().getBoolean(
				PREF_MIGRATED_TASK_REPOSITORIES_FROM_KEYRING))
				&& isKeyringInstalled();
		if (!migrateFromSecureStore && !migrateFromKeyring) {
			return;
		}
		// Use a UI job to ensure the UI has loaded
		new UIJob("Credential Migration UI Job") { //$NON-NLS-1$
			@Override
			public IStatus runInUIThread(IProgressMonitor monitor) {
				// use a Job to ensure we do not access the secure store on the UI thread
				new Job("Credential Migration") { //$NON-NLS-1$
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						if (force) {
							StatusHandler.log(new Status(IStatus.INFO, ITasksCoreConstants.ID_PLUGIN, NLS.bind(
									"Forcing task repository credential migration because system property {0} is set.", //$NON-NLS-1$
									PROP_FORCE_CREDENTIALS_MIGRATION)));
						}
						if (migrateFromSecureStore) {
							new TaskRepositorySecureStoreMigrator().migrateCredentials(repositories);
						}
						if (migrateFromKeyring) {
							new TaskRepositoryKeyringMigrator("", "Basic").migrateCredentials(repositories); //$NON-NLS-1$ //$NON-NLS-2$
						}
						return Status.OK_STATUS;
					}

				}.schedule();
				return Status.OK_STATUS;
			}
		}.schedule();
	}

	private boolean isKeyringInstalled() {
		return Platform.getBundle("org.eclipse.core.runtime.compatibility.auth") != null; //$NON-NLS-1$
	}

	private void initHttpLogging() {
		System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); //$NON-NLS-1$ //$NON-NLS-2$
		System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true"); //$NON-NLS-1$ //$NON-NLS-2$
		System.setProperty("org.apache.commons.logging.simplelog.log.httpclient.wire", "debug"); //$NON-NLS-1$ //$NON-NLS-2$
		System.setProperty("org.apache.commons.logging.simplelog.log.httpclient.wire.header", "debug"); //$NON-NLS-1$ //$NON-NLS-2$
		System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.commons.httpclient", "debug"); //$NON-NLS-1$ //$NON-NLS-2$
		System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.commons.httpclient.HttpConnection", //$NON-NLS-1$
				"trace"); //$NON-NLS-1$
		System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.axis.message", "debug"); //$NON-NLS-1$ //$NON-NLS-2$
	}

	private void updateTaskActivityManager() {
		int endHour = getPreferenceStore().getInt(ITasksUiPreferenceConstants.PLANNING_ENDHOUR);
//		if (taskActivityManager.getEndHour() != endHour) {
//			taskActivityManager.setEndHour(endHour);
		TaskActivityUtil.setEndHour(endHour);
//		}

		int newWeekStartDay = getPreferenceStore().getInt(ITasksUiPreferenceConstants.WEEK_START_DAY);
		int oldWeekStartDay = taskActivityManager.getWeekStartDay();
		if (oldWeekStartDay != newWeekStartDay) {
			taskActivityManager.setWeekStartDay(newWeekStartDay);
//			taskActivityManager.setStartTime(new Date());
		}

		// event.getProperty().equals(TaskListPreferenceConstants.PLANNING_STARTDAY)
		// scheduledStartHour =
		// TasksUiPlugin.getDefault().getPreferenceStore().getInt(
		// TaskListPreferenceConstants.PLANNING_STARTHOUR);
	}

	private void loadTemplateRepositories() {
		// Add standard local task repository
		TaskRepository local = getLocalTaskRepository();
		repositoryManager.applyMigrators(local);

		// Add the automatically created templates
		for (AbstractRepositoryConnector connector : repositoryManager.getRepositoryConnectors()) {
			for (RepositoryTemplate template : repositoryTemplateManager.getTemplates(connector.getConnectorKind())) {
				if (template.addAutomatically && !TaskRepositoryUtil.isAddAutomaticallyDisabled(template.repositoryUrl)) {
					try {
						String repositoryUrl = TaskRepositoryManager.stripSlashes(template.repositoryUrl);
						TaskRepository taskRepository = repositoryManager.getRepository(connector.getConnectorKind(),
								repositoryUrl);
						if (taskRepository == null) {
							taskRepository = new TaskRepository(connector.getConnectorKind(), repositoryUrl);
							taskRepository.setVersion(template.version);
							taskRepository.setRepositoryLabel(template.label);
							taskRepository.setCharacterEncoding(template.characterEncoding);
							if (template.anonymous) {
								taskRepository.setProperty(
										"org.eclipse.mylyn.tasklist.repositories.enabled", String.valueOf(false)); //$NON-NLS-1$
								// bug 332747: avoid reseting password in shared keystore
								//taskRepository.setCredentials(AuthenticationType.REPOSITORY, null, true);
							}
							taskRepository.setCreatedFromTemplate(true);
							repositoryManager.addRepository(taskRepository);
							repositoryManager.applyMigrators(taskRepository);
						}
					} catch (Throwable t) {
						StatusHandler.log(new Status(IStatus.WARNING, TasksUiPlugin.ID_PLUGIN, NLS.bind(
								"Could not load repository template for repository {0}", template.repositoryUrl), t)); //$NON-NLS-1$
					}
				}
			}
		}
	}

	/**
	 * Returns the local task repository. If the repository does not exist it is created and added to the task
	 * repository manager.
	 *
	 * @return the local task repository; never <code>null</code>
	 * @since 3.0
	 */
	public TaskRepository getLocalTaskRepository() {
		TaskRepository localRepository = repositoryManager.getRepository(LocalRepositoryConnector.CONNECTOR_KIND,
				LocalRepositoryConnector.REPOSITORY_URL);
		if (localRepository == null) {
			localRepository = new TaskRepository(LocalRepositoryConnector.CONNECTOR_KIND,
					LocalRepositoryConnector.REPOSITORY_URL);
			localRepository.setVersion(LocalRepositoryConnector.REPOSITORY_VERSION);
			localRepository.setRepositoryLabel(LocalRepositoryConnector.REPOSITORY_LABEL);
			repositoryManager.addRepository(localRepository);
		}
		return localRepository;
	}

	@Override
	public void stop(BundleContext context) throws Exception {
		try {
			Job.getJobManager().cancel(ITasksCoreConstants.JOB_FAMILY_SYNCHRONIZATION);
			if (formColors != null) {
				formColors.dispose();
				formColors = null;
			}
			if (taskActivityMonitor != null) {
				taskActivityMonitor.stop();
			}

			if (ResourcesPlugin.getWorkspace() != null) {
				ResourcesPlugin.getWorkspace().removeSaveParticipant(this);
			}

			if (proxyServiceReference != null) {
				@SuppressWarnings("unchecked")
				IProxyService proxyService = (IProxyService) context.getService(proxyServiceReference);
				if (proxyService != null) {
					proxyService.removeProxyChangeListener(proxyChangeListener);
				}
				context.ungetService(proxyServiceReference);
			}
			if (identityServiceTracker != null) {
				identityServiceTracker.close();
				identityServiceTracker = null;
			}

			// wait until stop() to set these to reduce chance of crash after setting them but before creds are persisted
			getPluginPreferences().setValue(PREF_MIGRATED_TASK_REPOSITORIES_FROM_SECURE_STORE, Boolean.toString(true));
			if (isKeyringInstalled()) {
				getPluginPreferences().setValue(PREF_MIGRATED_TASK_REPOSITORIES_FROM_KEYRING, Boolean.toString(true));
			}

			if (PlatformUI.isWorkbenchRunning()) {
				if (taskListNotificationManager != null) {
					getPreferenceStore().removePropertyChangeListener(taskListNotificationManager);
				}
				if (taskListBackupManager != null) {
					getPreferenceStore().removePropertyChangeListener(taskListBackupManager);
				}
				getPreferenceStore().removePropertyChangeListener(PROPERTY_LISTENER);
				//taskListManager.getTaskList().removeChangeListener(taskListSaveManager);
				CommonColors.dispose();
//				if (ContextCorePlugin.getDefault() != null) {
//					ContextCorePlugin.getDefault().getPluginPreferences().removePropertyChangeListener(
//							PREFERENCE_LISTENER);
//				}
				serviceMessageManager.stop();
				INSTANCE = null;
			}
		} catch (Exception e) {
			StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
					"Task list stop terminated abnormally", e)); //$NON-NLS-1$
		} finally {
			super.stop(context);
		}
	}

	public String getDefaultDataDirectory() {
		return ResourcesPlugin.getWorkspace().getRoot().getLocation().toString() + '/' + DIRECTORY_METADATA + '/'
				+ NAME_DATA_DIR;
	}

	public String getDataDirectory() {
		return getPreferenceStore().getString(ITasksUiPreferenceConstants.PREF_DATA_DIR);
	}

	/**
	 * Persist <code>path</code> as data directory and loads data from <code>path</code>. This method may block if other
	 * jobs are running that modify tasks data. This method will only execute after all conflicting jobs have been
	 * completed.
	 *
	 * @throws CoreException
	 *             in case setting of the data directory did not complete normally
	 * @throws OperationCanceledException
	 *             if the operation is cancelled by the user
	 */
	public void setDataDirectory(final String path) throws CoreException {
		Assert.isNotNull(path);
		IRunnableWithProgress runner = new IRunnableWithProgress() {
			public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
				try {
					monitor.beginTask(Messages.TasksUiPlugin_Load_Data_Directory, IProgressMonitor.UNKNOWN);
					if (monitor.isCanceled()) {
						throw new InterruptedException();
					}

					TasksUi.getTaskActivityManager().deactivateActiveTask();

					// set new preference in case of a change
					if (!path.equals(getDataDirectory())) {
						getPreferenceStore().setValue(ITasksUiPreferenceConstants.PREF_DATA_DIR, path);
					}

					// reload data from new directory
					initializeDataSources();
				} finally {
					// FIXME roll back preferences change in case of an error?
					monitor.done();
				}
			}
		};

		IProgressService service = PlatformUI.getWorkbench().getProgressService();
		try {
			service.runInUI(service, runner, ITasksCoreConstants.ROOT_SCHEDULING_RULE);
		} catch (InvocationTargetException e) {
			throw new CoreException(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN, "Failed to set data directory", //$NON-NLS-1$
					e.getCause()));
		} catch (InterruptedException e) {
			throw new OperationCanceledException();
		}
	}

	public void reloadDataDirectory() throws CoreException {
		// no save just load what is there
		setDataDirectory(getDataDirectory());
	}

	/**
	 * Invoked on startup and when data is loaded from disk or when the data directory changes.
	 * <p>
	 * Public for testing.
	 */
	@SuppressWarnings("restriction")
	public void initializeDataSources() {
		// ensure that context directory exists
		File storeFile = new File(getDataDirectory(), ITasksCoreConstants.CONTEXTS_DIRECTORY);
		if (!storeFile.exists()) {
			storeFile.mkdirs();
		}

		taskDataManager.setDataPath(getDataDirectory());
		externalizationManager.setRootFolderPath(getDataDirectory());
		getContextStore().setDirectory(new File(getDataDirectory(), "tasks")); //$NON-NLS-1$

		externalizationManager.load();
		// TODO: Move management of template repositories to TaskRepositoryManager
		loadTemplateRepositories();

		taskActivityManager.clear();
		taskActivityMonitor.loadActivityTime();
		taskActivityManager.reloadPlanningData();

		if (!activationHistoryFile.exists() && taskActivityManager.getTaskActivationHistory().getSize() == 0) {
			// fall back to activity history
			List<ITask> tasks = taskActivityMonitor.getActivationHistory();
			for (ITask task : tasks) {
				taskActivityManager.getTaskActivationHistory().addTask((AbstractTask) task);
			}
		}

		if (!MonitorUiPlugin.getDefault()
				.getPreferenceStore()
				.getBoolean(MonitorUiPlugin.ACTIVITY_TRACKING_ENABLED + ".checked")) { //$NON-NLS-1$
			if (!taskActivityMonitor.getActivationHistory().isEmpty()) {
				// tasks have been active before so fore preference enabled
				MonitorUiPlugin.getDefault()
				.getPreferenceStore()
				.setValue(MonitorUiPlugin.ACTIVITY_TRACKING_ENABLED, true);
			}
			MonitorUiPlugin.getDefault()
			.getPreferenceStore()
			.setValue(MonitorUiPlugin.ACTIVITY_TRACKING_ENABLED + ".checked", true); //$NON-NLS-1$
			MonitorUiPlugin.getDefault().savePluginPreferences();
		}

		// inform listeners that initialization is complete
		for (final IRepositoryModelListener listener : listeners) {
			SafeRunner.run(new ISafeRunnable() {

				public void handleException(Throwable exception) {
					StatusHandler.log(new Status(IStatus.WARNING, TasksUiPlugin.ID_PLUGIN, "Listener failed: " //$NON-NLS-1$
							+ listener.getClass(), exception));
				}

				public void run() throws Exception {
					listener.loaded();
				}
			});
		}
	}

	@SuppressWarnings("deprecation")
	private void initializePreferences(IPreferenceStore store) {
		store.setDefault(ITasksUiPreferenceConstants.PREF_DATA_DIR, getDefaultDataDirectory());
		store.setDefault(ITasksUiPreferenceConstants.GROUP_SUBTASKS, true);
		store.setDefault(ITasksUiPreferenceConstants.NOTIFICATIONS_ENABLED, true);
		store.setDefault(ITasksUiPreferenceConstants.SERVICE_MESSAGES_ENABLED, false);
		store.setDefault(ITasksUiPreferenceConstants.FILTER_PRIORITY, PriorityLevel.P5.toString());
		store.setDefault(ITasksUiPreferenceConstants.EDITOR_TASKS_RICH, true);
		store.setDefault(ITasksUiPreferenceConstants.EDITOR_CURRENT_LINE_HIGHLIGHT, false);
		store.setDefault(ITasksUiPreferenceConstants.ACTIVATE_WHEN_OPENED, false);
		store.setDefault(ITasksUiPreferenceConstants.SHOW_TRIM, false);
		// remove preference
		store.setToDefault(ITasksUiPreferenceConstants.LOCAL_SUB_TASKS_ENABLED);
		store.setDefault(ITasksUiPreferenceConstants.USE_STRIKETHROUGH_FOR_COMPLETED, true);

		store.setDefault(ITasksUiPreferenceConstants.REPOSITORY_SYNCH_SCHEDULE_ENABLED, true);
		store.setDefault(ITasksUiPreferenceConstants.REPOSITORY_SYNCH_SCHEDULE_MILISECONDS, "" + (20 * 60 * 1000)); //$NON-NLS-1$

		//store.setDefault(TasksUiPreferenceConstants.BACKUP_SCHEDULE, 1);
		store.setDefault(ITasksUiPreferenceConstants.BACKUP_MAXFILES, 20);
		store.setDefault(ITasksUiPreferenceConstants.BACKUP_LAST, 0f);

		store.setDefault(ITasksUiPreferenceConstants.FILTER_HIDDEN, true);
		store.setDefault(ITasksUiPreferenceConstants.FILTER_ARCHIVE_MODE, true);
		store.setDefault(ITasksUiPreferenceConstants.ACTIVATE_MULTIPLE, false);
		store.setValue(ITasksUiPreferenceConstants.ACTIVATE_MULTIPLE, false);

		store.setDefault(ITasksUiPreferenceConstants.WEEK_START_DAY, Calendar.getInstance().getFirstDayOfWeek());
		//store.setDefault(TasksUiPreferenceConstants.PLANNING_STARTHOUR, 9);
		store.setDefault(ITasksUiPreferenceConstants.PLANNING_ENDHOUR, 18);

		store.setDefault(ITasksUiPreferenceConstants.AUTO_EXPAND_TASK_LIST, true);
		store.setDefault(ITasksUiPreferenceConstants.TASK_LIST_TOOL_TIPS_ENABLED, true);

		store.setDefault(ITasksUiPreferenceConstants.SERVICE_MESSAGE_URL, "http://eclipse.org/mylyn/updates.xml"); //$NON-NLS-1$
	}

	public static TaskActivityManager getTaskActivityManager() {
		return taskActivityManager;
	}

	public static TaskListNotificationManager getTaskListNotificationManager() {
		return INSTANCE.taskListNotificationManager;
	}

	/**
	 * Returns the shared instance.
	 */
	public static TasksUiPlugin getDefault() {
		return INSTANCE;
	}

	public boolean groupSubtasks(ITaskContainer element) {
		boolean groupSubtasks = TasksUiPlugin.getDefault()
				.getPreferenceStore()
				.getBoolean(ITasksUiPreferenceConstants.GROUP_SUBTASKS);

		if (element instanceof ITask) {
			AbstractRepositoryConnectorUi connectorUi = TasksUiPlugin.getConnectorUi(((ITask) element).getConnectorKind());
			if (connectorUi != null) {
				if (connectorUi.hasStrictSubtaskHierarchy()) {
					groupSubtasks = true;
				}
			}
		}

		if (element instanceof IRepositoryQuery) {
			AbstractRepositoryConnectorUi connectorUi = TasksUiPlugin.getConnectorUi(((IRepositoryQuery) element).getConnectorKind());
			if (connectorUi != null) {
				if (connectorUi.hasStrictSubtaskHierarchy()) {
					groupSubtasks = true;
				}
			}
		}

		return groupSubtasks;
	}

	private final Map<String, List<IDynamicSubMenuContributor>> menuContributors = new HashMap<String, List<IDynamicSubMenuContributor>>();

	@SuppressWarnings("rawtypes")
	private ServiceTracker identityServiceTracker;

	public Map<String, List<IDynamicSubMenuContributor>> getDynamicMenuMap() {
		return menuContributors;
	}

	public void addDynamicPopupContributor(String menuPath, IDynamicSubMenuContributor contributor) {
		List<IDynamicSubMenuContributor> contributors = menuContributors.get(menuPath);
		if (contributors == null) {
			contributors = new ArrayList<IDynamicSubMenuContributor>();
			menuContributors.put(menuPath, contributors);
		}
		contributors.add(contributor);
	}

	public String[] getSaveOptions() {
		String[] options = { TaskListSaveMode.ONE_HOUR.toString(), TaskListSaveMode.THREE_HOURS.toString(),
				TaskListSaveMode.DAY.toString() };
		return options;
	}

	public String getBackupFolderPath() {
		return getDataDirectory() + DEFAULT_PATH_SEPARATOR + ITasksCoreConstants.DEFAULT_BACKUP_FOLDER_NAME;
	}

	/**
	 * @since 3.0
	 */
	public AbstractTaskEditorPageFactory[] getTaskEditorPageFactories() {
		return taskEditorPageFactories.toArray(new AbstractTaskEditorPageFactory[0]);
	}

	/**
	 * @since 3.0
	 */
	public void addTaskEditorPageFactory(AbstractTaskEditorPageFactory factory) {
		Assert.isNotNull(factory);
		taskEditorPageFactories.add(factory);
	}

	/**
	 * @since 3.0
	 */
	public void removeTaskEditorPageFactory(AbstractTaskEditorPageFactory factory) {
		Assert.isNotNull(factory);
		taskEditorPageFactories.remove(factory);
	}

	public static TaskRepositoryManager getRepositoryManager() {
		return repositoryManager;
	}

	/**
	 * @since 3.0
	 */
	public static RepositoryTemplateManager getRepositoryTemplateManager() {
		return INSTANCE.repositoryTemplateManager;
	}

	public void addBrandingIcon(String repositoryType, Image icon) {
		brandingIcons.put(repositoryType, icon);
	}

	public Image getBrandingIcon(String repositoryType) {
		return brandingIcons.get(repositoryType);
	}

	public void addOverlayIcon(String repositoryType, ImageDescriptor icon) {
		overlayIcons.put(repositoryType, icon);
	}

	public ImageDescriptor getOverlayIcon(String repositoryType) {
		return overlayIcons.get(repositoryType);
	}

	public void addRepositoryLinkProvider(AbstractTaskRepositoryLinkProvider repositoryLinkProvider) {
		if (repositoryLinkProvider != null) {
			this.repositoryLinkProviders.add(repositoryLinkProvider);
		}
	}

	public static synchronized TaskListBackupManager getBackupManager() {
		if (INSTANCE.taskListBackupManager == null) {
			INSTANCE.taskListBackupManager = new TaskListBackupManager(INSTANCE.getBackupFolderPath());
			INSTANCE.getPreferenceStore().addPropertyChangeListener(INSTANCE.taskListBackupManager);
		}
		return INSTANCE.taskListBackupManager;
	}

	public void addRepositoryConnectorUi(AbstractRepositoryConnectorUi repositoryConnectorUi) {
		if (!repositoryConnectorUiMap.values().contains(repositoryConnectorUi)) {
			repositoryConnectorUiMap.put(repositoryConnectorUi.getConnectorKind(), repositoryConnectorUi);
		}
	}

	/**
	 * @since 3.0
	 */
	public static AbstractRepositoryConnector getConnector(String kind) {
		return getRepositoryManager().getRepositoryConnector(kind);
	}

	public static AbstractRepositoryConnectorUi getConnectorUi(String kind) {
		return repositoryConnectorUiMap.get(kind);
	}

	@Deprecated
	public static TaskListSynchronizationScheduler getSynchronizationScheduler() {
		return synchronizationScheduler;
	}

	/**
	 * @since 3.0
	 */
	public static TaskDataManager getTaskDataManager() {
		return taskDataManager;
	}

	/**
	 * @since 3.0
	 */
	public static TaskJobFactory getTaskJobFactory() {
		return INSTANCE.taskJobFactory;
	}

	public void addDuplicateDetector(AbstractDuplicateDetector duplicateDetector) {
		Assert.isNotNull(duplicateDetector);
		duplicateDetectors.add(duplicateDetector);
	}

	public Set<AbstractDuplicateDetector> getDuplicateSearchCollectorsList() {
		return duplicateDetectors;
	}

	public String getRepositoriesFilePath() {
		return getDataDirectory() + File.separator + TaskRepositoryManager.DEFAULT_REPOSITORIES_FILE;
	}

	public void addModelListener(IRepositoryModelListener listener) {
		listeners.add(listener);
	}

	public void removeModelListener(IRepositoryModelListener listener) {
		listeners.remove(listener);
	}

	public boolean canSetRepositoryForResource(final IResource resource) {
		if (resource == null) {
			return false;
		}

		// if a repository has already been linked only that provider should be queried to ensure that it is the same
		// provider that is used by getRepositoryForResource()
		final boolean result[] = new boolean[1];
		final boolean found[] = new boolean[1];
		for (final AbstractTaskRepositoryLinkProvider linkProvider : repositoryLinkProviders) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
							"Task repository link provider failed: \"" + linkProvider.getId() + "\"", e)); //$NON-NLS-1$ //$NON-NLS-2$
				}

				public void run() throws Exception {
					if (linkProvider.getTaskRepository(resource, getRepositoryManager()) != null) {
						found[0] = true;
						result[0] = linkProvider.canSetTaskRepository(resource);
					}
				}
			});
			if (found[0]) {
				return result[0];
			}
		}

		// find a provider that can set new repository
		for (final AbstractTaskRepositoryLinkProvider linkProvider : repositoryLinkProviders) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
							"Task repository link provider failed: \"" + linkProvider.getId() + "\"", e)); //$NON-NLS-1$ //$NON-NLS-2$
				}

				public void run() throws Exception {
					if (linkProvider.canSetTaskRepository(resource)) {
						result[0] = true;
					}
				}
			});
			if (result[0]) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Associate a Task Repository with a workbench project
	 *
	 * @param resource
	 *            project or resource belonging to a project
	 * @param repository
	 *            task repository to associate with given project
	 * @throws CoreException
	 */
	public void setRepositoryForResource(final IResource resource, final TaskRepository repository) {
		Assert.isNotNull(resource);
		for (final AbstractTaskRepositoryLinkProvider linkProvider : repositoryLinkProviders) {
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
							"Task repository link provider failed: \"" + linkProvider.getId() + "\"", e)); //$NON-NLS-1$ //$NON-NLS-2$
				}

				public void run() throws Exception {
					boolean canSetRepository = linkProvider.canSetTaskRepository(resource);
					if (canSetRepository) {
						linkProvider.setTaskRepository(resource, repository);
					}
				}
			});
		}
	}

	/**
	 * Retrieve the task repository that has been associated with the given project (or resource belonging to a project)
	 * NOTE: if call does not return in LINK_PROVIDER_TIMEOUT_SECONDS, the provide will be disabled until the next time
	 * that the Workbench starts.
	 */
	public TaskRepository getRepositoryForResource(final IResource resource) {
		Assert.isNotNull(resource);
		long timeout;
		try {
			timeout = Long.parseLong(System.getProperty(ITasksCoreConstants.PROPERTY_LINK_PROVIDER_TIMEOUT,
					DEFAULT_LINK_PROVIDER_TIMEOUT + "")); //$NON-NLS-1$
		} catch (NumberFormatException e) {
			timeout = DEFAULT_LINK_PROVIDER_TIMEOUT;
		}
		Set<AbstractTaskRepositoryLinkProvider> defectiveLinkProviders = new HashSet<AbstractTaskRepositoryLinkProvider>();
		for (final AbstractTaskRepositoryLinkProvider linkProvider : repositoryLinkProviders) {
			long startTime = System.currentTimeMillis();
			final TaskRepository[] repository = new TaskRepository[1];
			SafeRunnable.run(new ISafeRunnable() {
				public void handleException(Throwable e) {
					StatusHandler.log(new Status(IStatus.ERROR, ID_PLUGIN, "Task repository link provider failed: \"" //$NON-NLS-1$
							+ linkProvider.getId() + "\"", e)); //$NON-NLS-1$
				}

				public void run() throws Exception {
					repository[0] = linkProvider.getTaskRepository(resource, getRepositoryManager());
				}
			});
			long elapsed = System.currentTimeMillis() - startTime;
			if (timeout >= 0 && elapsed > timeout) {
				defectiveLinkProviders.add(linkProvider);
			}
			if (repository[0] != null) {
				return repository[0];
			}
		}
		if (!defectiveLinkProviders.isEmpty()) {
			repositoryLinkProviders.removeAll(defectiveLinkProviders);
			StatusHandler.log(new Status(IStatus.WARNING, ID_PLUGIN, "Repository link provider took over " + timeout //$NON-NLS-1$
					+ " ms to execute and was timed out: \"" + defectiveLinkProviders + "\"")); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return null;
	}

	public static ExternalizationManager getExternalizationManager() {
		return externalizationManager;
	}

	public static AbstractTaskActivityMonitor getTaskActivityMonitor() {
		return INSTANCE.taskActivityMonitor;
	}

	public static TaskList getTaskList() {
		return taskList;
	}

	public static RepositoryModel getRepositoryModel() {
		return repositoryModel;
	}

	/**
	 * Note: This is provisional API that is used by connectors.
	 * <p>
	 * DO NOT CHANGE.
	 */
	public void addSearchHandler(AbstractSearchHandler searchHandler) {
		searchHandlers.add(searchHandler);
	}

	/**
	 * Note: This is provisional API that is used by connectors.
	 * <p>
	 * DO NOT CHANGE.
	 */
	public void removeSearchHandler(AbstractSearchHandler searchHandler) {
		searchHandlers.remove(searchHandler);
	}

	public AbstractSearchHandler getSearchHandler(String connectorKind) {
		Assert.isNotNull(connectorKind);
		for (AbstractSearchHandler searchHandler : searchHandlers) {
			if (searchHandler.getConnectorKind().equals(connectorKind)) {
				return searchHandler;
			}
		}
		return null;
	}

	public FormColors getFormColors(Display display) {
		if (formColors == null) {
			formColors = new FormColors(display);
			formColors.markShared();
		}
		return formColors;
	}

	public void removeRepositoryLinkProvider(AbstractTaskRepositoryLinkProvider provider) {
		repositoryLinkProviders.remove(provider);
	}

	public TaskListExternalizer createTaskListExternalizer() {
		return new TaskListExternalizer(repositoryModel, repositoryManager);
	}

	public static TaskListExternalizationParticipant getTaskListExternalizationParticipant() {
		return taskListExternalizationParticipant;
	}

	public static TasksUiFactory getUiFactory() {
		if (uiFactory == null) {
			uiFactory = new TasksUiFactory();
		}
		return uiFactory;
	}

	public void initializeNotificationsAndSynchronization() {
		// this method is invoked by the tasks ui initialization job and the task list view
		// only proceed if both calls have been made
		if (initializationCount.incrementAndGet() != 2) {
			return;
		}

		try {
			taskListNotificationManager.addNotificationProvider(REMINDER_NOTIFICATION_PROVIDER);
//				taskListNotificationManager.addNotificationProvider(INCOMING_NOTIFICATION_PROVIDER);
			TaskListNotifier taskListNotifier = new TaskListNotifier(getTaskDataManager(), getSynchronizationManger());
			getTaskDataManager().addListener(taskListNotifier);
			taskListNotificationManager.addNotificationProvider(taskListNotifier);
			taskListNotificationManager.startNotification(NOTIFICATION_DELAY);
			getPreferenceStore().addPropertyChangeListener(taskListNotificationManager);
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
					"Could not initialize notifications", t)); //$NON-NLS-1$
		}

		try {
			// trigger backup scheduler
			getBackupManager();

			synchronizationScheduler = new TaskListSynchronizationScheduler(taskJobFactory);
			MonitorUiPlugin.getDefault().getActivityContextManager().addListener(synchronizationScheduler);
			updateSynchronizationScheduler(true);
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, TasksUiPlugin.ID_PLUGIN,
					"Could not initialize task list backup and synchronization", t)); //$NON-NLS-1$
		}
	}

	@SuppressWarnings("restriction")
	public ServiceMessageManager getServiceMessageManager() {
		return serviceMessageManager;
	}

	public SynchronizationManger getSynchronizationManger() {
		return synchronizationManger;
	}

	@SuppressWarnings({ "rawtypes", "unchecked", "restriction" })
	public IIdentityService getIdentityService() {
		if (identityServiceTracker == null) {
			identityServiceTracker = new ServiceTracker(getBundle().getBundleContext(),
					IIdentityService.class.getName(), null);
			identityServiceTracker.open();
		}
		return (IIdentityService) identityServiceTracker.getService();
	}

	public static synchronized AbstractTaskContextStore getContextStore() {
		if (contextStore == null) {
			contextStore = TasksCoreExtensionReader.loadTaskContextStore();
		}
		return contextStore;
	}

}
