/*******************************************************************************
 * Copyright (c) 2004, 2009 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.core.sync;

import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.internal.tasks.core.AbstractTask;
import org.eclipse.mylyn.internal.tasks.core.ITasksCoreConstants;
import org.eclipse.mylyn.internal.tasks.core.ITasksCoreConstants.ObjectSchedulingRule;
import org.eclipse.mylyn.internal.tasks.core.RepositoryQuery;
import org.eclipse.mylyn.internal.tasks.core.TaskList;
import org.eclipse.mylyn.internal.tasks.core.data.TaskDataManager;
import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryModel;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITask.SynchronizationState;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskDataCollector;
import org.eclipse.mylyn.tasks.core.data.TaskRelation;
import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession;
import org.eclipse.mylyn.tasks.core.sync.SynchronizationJob;
import org.eclipse.osgi.util.NLS;

/**
 * @author Mik Kersten
 * @author Rob Elves
 * @author Steffen Pingel
 */
public class SynchronizeQueriesJob extends SynchronizationJob {

	private class TaskCollector extends TaskDataCollector {

		private final Set<ITask> removedQueryResults;

		private final RepositoryQuery repositoryQuery;

		private int resultCount;

		private final SynchronizationSession session;

		public TaskCollector(RepositoryQuery repositoryQuery, SynchronizationSession session) {
			this.repositoryQuery = repositoryQuery;
			this.session = session;
			this.removedQueryResults = new HashSet<ITask>(repositoryQuery.getChildren());
		}

		@Override
		public void accept(TaskData taskData) {
			ITask task = taskList.getTask(taskData.getRepositoryUrl(), taskData.getTaskId());
			if (task == null) {
				task = tasksModel.createTask(repository, taskData.getTaskId());
				((AbstractTask) task).setSynchronizationState(SynchronizationState.INCOMING_NEW);
				if (taskData.isPartial() && connector.canSynchronizeTask(repository, task)) {
					session.markStale(task);
				}
			} else {
				removedQueryResults.remove(task);
			}
			taskList.addTask(task, repositoryQuery);
			try {
				session.putTaskData(task, taskData);
			} catch (CoreException e) {
				StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, "Failed to save task", e)); //$NON-NLS-1$
			}
			resultCount++;
		}

		public Set<ITask> getRemovedChildren() {
			return removedQueryResults;
		}

		public int getResultCount() {
			return resultCount;
		}

		@Override
		public void failed(String taskId, IStatus status) {
			ITask task = taskList.getTask(repositoryQuery.getRepositoryUrl(), taskId);
			if (task != null && status != null) {
				statuses.add(status);
				((AbstractTask) task).setStatus(status);
				if (!isUser()) {
					((AbstractTask) task).setSynchronizing(false);
				}
				taskList.notifyElementChanged(task);
			}
		}

	}

	public static final String MAX_HITS_REACHED = Messages.SynchronizeQueriesJob_Max_allowed_number_of_hits_returned_exceeded;

	private final AbstractRepositoryConnector connector;

	private final Set<RepositoryQuery> queries;

	private final TaskRepository repository;

	private final TaskDataManager taskDataManager;

	private final TaskList taskList;

	private final IRepositoryModel tasksModel;

	private final List<IStatus> statuses;

	public SynchronizeQueriesJob(TaskList taskList, TaskDataManager taskDataManager, IRepositoryModel tasksModel,
			AbstractRepositoryConnector connector, TaskRepository repository, Set<RepositoryQuery> queries) {
		super(Messages.SynchronizeQueriesJob_Synchronizing_Queries + " (" + repository.getRepositoryLabel() + ")"); //$NON-NLS-1$//$NON-NLS-2$
		this.taskList = taskList;
		this.taskDataManager = taskDataManager;
		this.tasksModel = tasksModel;
		this.connector = connector;
		this.repository = repository;
		this.queries = queries;
		this.statuses = new ArrayList<IStatus>();
	}

	@Override
	public IStatus run(IProgressMonitor jobMonitor) {
		try {
			monitor.setCanceled(false);
			monitor.attach(jobMonitor);
			try {
				monitor.beginTask(Messages.SynchronizeQueriesJob_Processing, 20 + queries.size() * 20 + 40 + 10);

				Set<ITask> allTasks;
				if (!isFullSynchronization()) {
					allTasks = new HashSet<ITask>();
					for (RepositoryQuery query : queries) {
						allTasks.addAll(query.getChildren());
					}
				} else {
					allTasks = taskList.getTasks(repository.getRepositoryUrl());
				}

				ObjectSchedulingRule rule = new ObjectSchedulingRule(repository);
				try {
					Job.getJobManager().beginRule(rule, monitor);

					final Map<String, TaskRelation[]> relationsByTaskId = new HashMap<String, TaskRelation[]>();
					SynchronizationSession session = new SynchronizationSession(taskDataManager) {
						@Override
						public void putTaskData(ITask task, TaskData taskData) throws CoreException {
							boolean changed = connector.hasTaskChanged(repository, task, taskData);
							taskDataManager.putUpdatedTaskData(task, taskData, isUser(), this, monitor);
							if (taskData.isPartial()) {
								if (changed && connector.canSynchronizeTask(repository, task)) {
									markStale(task);
								}
							} else {
								Collection<TaskRelation> relations = connector.getTaskRelations(taskData);
								if (relations != null) {
									relationsByTaskId.put(task.getTaskId(), relations.toArray(new TaskRelation[0]));
								}
							}
						}
					};
					session.setTaskRepository(repository);
					session.setFullSynchronization(isFullSynchronization());
					session.setTasks(Collections.unmodifiableSet(allTasks));
					session.setNeedsPerformQueries(true);
					session.setUser(isUser());

					updateQueryStatus(null);
					try {
						boolean success = preSynchronization(session, new SubProgressMonitor(monitor, 20));

						if ((success && session.needsPerformQueries()) || isUser()) {
							// synchronize queries, tasks changed within query are added to set of tasks to be synchronized
							synchronizeQueries(monitor, session);
						} else {
							monitor.worked(queries.size() * 20);
						}
					} finally {
						for (RepositoryQuery repositoryQuery : queries) {
							repositoryQuery.setSynchronizing(false);
						}
						taskList.notifySynchronizationStateChanged(queries);
					}

					Set<ITask> tasksToBeSynchronized = new HashSet<ITask>();
					for (ITask task : session.getStaleTasks()) {
						tasksToBeSynchronized.add(task);
						((AbstractTask) task).setSynchronizing(true);
					}

					// synchronize tasks that were marked by the connector
					SynchronizeTasksJob job = new SynchronizeTasksJob(taskList, taskDataManager, tasksModel, connector,
							repository, tasksToBeSynchronized);
					job.setUser(isUser());
					job.setSession(session);
					if (!tasksToBeSynchronized.isEmpty()) {
						Policy.checkCanceled(monitor);
						IStatus result = job.run(new SubProgressMonitor(monitor, 30));
						if (result == Status.CANCEL_STATUS) {
							throw new OperationCanceledException();
						}
						statuses.addAll(job.getStatuses());
					}
					monitor.subTask(Messages.SynchronizeQueriesJob_Receiving_related_tasks);
					job.synchronizedTaskRelations(monitor, relationsByTaskId);
					monitor.worked(10);

					session.setChangedTasks(tasksToBeSynchronized);
					if (statuses.size() > 0) {
						Status status = new MultiStatus(ITasksCoreConstants.ID_PLUGIN, 0,
								statuses.toArray(new IStatus[0]), "Query synchronization failed", null); //$NON-NLS-1$
						session.setStatus(status);
					}

					// hook into the connector for synchronization time stamp management
					postSynchronization(session, new SubProgressMonitor(monitor, 10));
				} finally {
					Job.getJobManager().endRule(rule);
				}
			} catch (OperationCanceledException e) {
				return Status.CANCEL_STATUS;
			} catch (Exception e) {
				StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, "Synchronization failed", e)); //$NON-NLS-1$
			} catch (LinkageError e) {
				StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, NLS.bind(
						"Synchronization for connector ''{0}'' failed", connector.getConnectorKind()), e)); //$NON-NLS-1$
			} finally {
				monitor.done();
			}
		} finally {
			monitor.detach(jobMonitor);
		}
		return Status.OK_STATUS;
	}

	private void synchronizeQueries(IProgressMonitor monitor, SynchronizationSession session) {
		for (RepositoryQuery repositoryQuery : queries) {
			Policy.checkCanceled(monitor);
			monitor.subTask(MessageFormat.format(Messages.SynchronizeQueriesJob_Synchronizing_query_X,
					repositoryQuery.getSummary()));
			synchronizeQuery(repositoryQuery, session, new SubProgressMonitor(monitor, 20));
		}
	}

	private boolean postSynchronization(SynchronizationSession event, IProgressMonitor monitor) {
		try {
			Policy.checkCanceled(monitor);
			monitor.subTask(Messages.SynchronizeQueriesJob_Updating_repository_state);
			if (!isUser()) {
				monitor = Policy.backgroundMonitorFor(monitor);
			}
			connector.postSynchronization(event, monitor);
			return true;
		} catch (CoreException e) {
			updateQueryStatus(e.getStatus());
			return false;
		}
	}

	private boolean preSynchronization(ISynchronizationSession event, IProgressMonitor monitor) {
		try {
			Policy.checkCanceled(monitor);
			monitor.subTask(Messages.SynchronizeQueriesJob_Querying_repository);
			if (!isUser()) {
				monitor = Policy.backgroundMonitorFor(monitor);
			}
			connector.preSynchronization(event, monitor);
			return true;
		} catch (CoreException e) {
			// synchronization is unlikely to succeed, inform user and exit
			updateQueryStatus(e.getStatus());
			statuses.add(e.getStatus());
			return false;
		}
	}

	private void synchronizeQuery(RepositoryQuery repositoryQuery, SynchronizationSession event,
			IProgressMonitor monitor) {
		TaskCollector collector = new TaskCollector(repositoryQuery, event);

		if (!isUser()) {
			monitor = Policy.backgroundMonitorFor(monitor);
		}
		IStatus result = connector.performQuery(repository, repositoryQuery, collector, event, monitor);
		if (result == null || result.isOK()) {
			if (collector.getResultCount() >= TaskDataCollector.MAX_HITS) {
				StatusHandler.log(new Status(IStatus.WARNING, ITasksCoreConstants.ID_PLUGIN, MAX_HITS_REACHED + "\n" //$NON-NLS-1$
						+ repositoryQuery.getSummary()));
			}

			Set<ITask> removedChildren = collector.getRemovedChildren();
			if (!removedChildren.isEmpty()) {
				taskList.removeFromContainer(repositoryQuery, removedChildren);
			}

			repositoryQuery.setLastSynchronizedStamp(new SimpleDateFormat("MMM d, H:mm:ss").format(new Date())); //$NON-NLS-1$
		} else if (result.getSeverity() == IStatus.CANCEL) {
			throw new OperationCanceledException();
		} else {
			repositoryQuery.setStatus(result);
			statuses.add(result);
		}
	}

	private void updateQueryStatus(final IStatus status) {
		for (RepositoryQuery repositoryQuery : queries) {
			repositoryQuery.setStatus(status);
		}
		taskList.notifySynchronizationStateChanged(queries);
	}

	public Collection<IStatus> getStatuses() {
		return Collections.unmodifiableCollection(statuses);
	}

}
