/*******************************************************************************
 * 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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
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.AbstractTaskContainer;
import org.eclipse.mylyn.internal.tasks.core.ITasksCoreConstants;
import org.eclipse.mylyn.internal.tasks.core.ITasksCoreConstants.MutexSchedulingRule;
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.IRepositoryManager;
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.ITaskContainer;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler;
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.data.TaskRelation.Direction;
import org.eclipse.mylyn.tasks.core.data.TaskRelation.Kind;
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 SynchronizeTasksJob extends SynchronizationJob {

	private final AbstractRepositoryConnector connector;

	private final TaskDataManager taskDataManager;

	private final TaskList taskList;

	private final Set<ITask> allTasks;

	private final IRepositoryManager repositoryManager;

	private TaskRepository taskRepository;

	private Map<String, TaskRelation[]> relationsByTaskId;

	private boolean updateRelations;

	private final IRepositoryModel tasksModel;

	private SynchronizationSession session;

	private final List<IStatus> statuses;

	public SynchronizeTasksJob(TaskList taskList, TaskDataManager synchronizationManager, IRepositoryModel tasksModel,
			AbstractRepositoryConnector connector, TaskRepository taskRepository, Set<ITask> tasks) {
		this(taskList, synchronizationManager, tasksModel, connector, (IRepositoryManager) null, tasks);
		this.taskRepository = taskRepository;
	}

	public SynchronizeTasksJob(TaskList taskList, TaskDataManager synchronizationManager, IRepositoryModel tasksModel,
			AbstractRepositoryConnector connector, IRepositoryManager repositoryManager, Set<ITask> tasks) {
		super("Synchronizing Tasks (" + tasks.size() + " tasks)"); //$NON-NLS-1$ //$NON-NLS-2$
		this.taskList = taskList;
		this.taskDataManager = synchronizationManager;
		this.tasksModel = tasksModel;
		this.connector = connector;
		this.repositoryManager = repositoryManager;
		this.allTasks = tasks;
		this.statuses = new ArrayList<IStatus>();
		setRule(new MutexSchedulingRule());
	}

	@Override
	public IStatus run(IProgressMonitor jobMonitor) {
		try {
			monitor.setCanceled(false);
			monitor.attach(jobMonitor);
			try {
				if (taskRepository == null) {
					try {
						monitor.beginTask(Messages.SynchronizeTasksJob_Processing, allTasks.size() * 100);
						// group tasks by repository
						Map<TaskRepository, Set<ITask>> tasksByRepository = new HashMap<TaskRepository, Set<ITask>>();
						for (ITask task : allTasks) {
							TaskRepository repository = repositoryManager.getRepository(task.getConnectorKind(),
									task.getRepositoryUrl());
							Set<ITask> tasks = tasksByRepository.get(repository);
							if (tasks == null) {
								tasks = new HashSet<ITask>();
								tasksByRepository.put(repository, tasks);
							}
							tasks.add(task);
						}
						// synchronize tasks for each repositories
						for (TaskRepository taskRepository : tasksByRepository.keySet()) {
							setName(MessageFormat.format(Messages.SynchronizeTasksJob_Synchronizing_Tasks__X_,
									taskRepository.getRepositoryLabel()));
							this.taskRepository = taskRepository;
							Set<ITask> repositoryTasks = tasksByRepository.get(taskRepository);
							run(repositoryTasks, new SubProgressMonitor(monitor, repositoryTasks.size() * 100));
						}
					} finally {
						monitor.done();
					}
				} else {
					run(allTasks, monitor);
				}
			} catch (OperationCanceledException e) {
				for (ITask task : allTasks) {
					((AbstractTask) task).setSynchronizing(false);
					taskList.notifyElementChanged(task);
				}
				return Status.CANCEL_STATUS;
			}
		} finally {
			monitor.detach(jobMonitor);
		}
		return Status.OK_STATUS;
	}

	private void run(Set<ITask> tasks, IProgressMonitor monitor) {
		relationsByTaskId = new HashMap<String, TaskRelation[]>();
		updateRelations = true;
		runInternal(tasks, monitor);
		synchronizedTaskRelations(monitor, relationsByTaskId);
	}

	public void synchronizedTaskRelations(IProgressMonitor monitor, Map<String, TaskRelation[]> relationsByTaskId) {
		updateRelations = false;
		for (String taskId : relationsByTaskId.keySet()) {
			ITask parentTask = taskList.getTask(taskRepository.getRepositoryUrl(), taskId);
			if (parentTask instanceof ITaskContainer) {
				Set<ITask> removedChildTasks = new HashSet<ITask>(((ITaskContainer) parentTask).getChildren());

				TaskRelation[] relations = relationsByTaskId.get(taskId);
				for (TaskRelation relation : relations) {
					if (relation.getDirection() == Direction.OUTWARD && relation.getKind() == Kind.CONTAINMENT) {
						ITask task = taskList.getTask(taskRepository.getRepositoryUrl(), relation.getTaskId());
						if (task == null) {
							try {
								task = synchronizeTask(monitor, relation.getTaskId());
							} catch (CoreException e) {
								String taskKey = (task != null && task.getTaskKey() != null)
										? task.getTaskKey()
										: relation.getTaskId();
								StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, NLS.bind(
										Messages.SynchronizeTasksJob_Synchronization_of_task_ID_REPOSITORY_failed,
										taskKey, taskRepository.getRepositoryLabel()), e));
							}
						} else {
							removedChildTasks.remove(task);
						}

						if (task != null) {
							taskList.addTask(task, (AbstractTaskContainer) parentTask);
						}
					}
				}

				for (ITask task : removedChildTasks) {
					taskList.removeFromContainer((AbstractTaskContainer) parentTask, task);
				}
			}
		}
	}

	private void runInternal(Set<ITask> tasks, IProgressMonitor monitor) {
		if (!isUser()) {
			monitor = Policy.backgroundMonitorFor(monitor);
		}
		try {
			monitor.beginTask(Messages.SynchronizeTasksJob_Processing, tasks.size() * 100);
			if (canGetMultiTaskData(taskRepository)) {
				try {
					for (ITask task : tasks) {
						resetStatus(task);
					}
					synchronizeTasks(new SubProgressMonitor(monitor, tasks.size() * 100), taskRepository, tasks);
				} catch (CoreException e) {
					for (ITask task : tasks) {
						updateStatus(taskRepository, task, e.getStatus());
					}
				}
			} else {
				for (ITask task : tasks) {
					Policy.checkCanceled(monitor);
					resetStatus(task);
					try {
						synchronizeTask(new SubProgressMonitor(monitor, 100), task);
					} catch (CoreException e) {
						updateStatus(taskRepository, task, e.getStatus());
					}
				}
			}
		} catch (OperationCanceledException e) {
			throw e;
		} catch (Exception e) {
			StatusHandler.log(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN, "Synchronization failed", e)); //$NON-NLS-1$
		} finally {
			monitor.done();
		}
	}

	private boolean canGetMultiTaskData(TaskRepository taskRepository) {
		AbstractTaskDataHandler taskDataHandler = connector.getTaskDataHandler();
		return taskDataHandler != null && taskDataHandler.canGetMultiTaskData(taskRepository);
	}

	private void synchronizeTask(IProgressMonitor monitor, ITask task) throws CoreException {
		monitor.subTask(MessageFormat.format(Messages.SynchronizeTasksJob_Receiving_task_X, task.getSummary()));
		resetStatus(task);
		if (!isUser()) {
			monitor = Policy.backgroundMonitorFor(monitor);
		}
		String taskId = task.getTaskId();
		TaskData taskData = connector.getTaskData(taskRepository, taskId, monitor);
		if (taskData != null) {
			updateFromTaskData(taskRepository, task, taskData, monitor);
			return;
		}
		throw new CoreException(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN,
				"Connector failed to return task data for task \"" + task + "\"")); //$NON-NLS-1$ //$NON-NLS-2$
	}

	private ITask synchronizeTask(IProgressMonitor monitor, String taskId) throws CoreException {
		monitor.subTask(MessageFormat.format(Messages.SynchronizeTasksJob_Receiving_task_X, taskId));
		if (!isUser()) {
			monitor = Policy.backgroundMonitorFor(monitor);
		}

		TaskData taskData = connector.getTaskData(taskRepository, taskId, monitor);
		if (taskData != null) {
			return createFromTaskData(taskRepository, taskId, taskData, monitor);
		}

		throw new CoreException(new Status(IStatus.ERROR, ITasksCoreConstants.ID_PLUGIN,
				"Connector failed to return task data for task \"" + taskId + "\"")); //$NON-NLS-1$ //$NON-NLS-2$
	}

	private void resetStatus(ITask task) {
		if (((AbstractTask) task).getStatus() != null) {
			((AbstractTask) task).setStatus(null);
			taskList.notifySynchronizationStateChanged(task);
		}
	}

	private void synchronizeTasks(final IProgressMonitor monitor, final TaskRepository repository, Set<ITask> tasks)
			throws CoreException {
		monitor.subTask(MessageFormat.format(Messages.SynchronizeTasksJob_Receiving_X_tasks_from_X, tasks.size(),
				repository.getRepositoryLabel()));

		final Map<String, ITask> idToTask = new HashMap<String, ITask>();
		for (ITask task : tasks) {
			idToTask.put(task.getTaskId(), task);
		}

		TaskDataCollector collector = new TaskDataCollector() {
			@Override
			public void accept(TaskData taskData) {
				ITask task = idToTask.remove(taskData.getTaskId());
				if (task != null) {
					updateFromTaskData(repository, task, taskData, monitor);
				}
			}

			@Override
			public void failed(String taskId, IStatus status) {
				ITask task = idToTask.get(taskId);
				if (task != null) {
					updateStatus(taskRepository, task, status);
				}
			}
		};

		Set<String> taskIds = Collections.unmodifiableSet(new HashSet<String>(idToTask.keySet()));
		connector.getTaskDataHandler().getMultiTaskData(repository, taskIds, collector, monitor);
	}

	private void updateFromTaskData(TaskRepository taskRepository, ITask task, TaskData taskData,
			IProgressMonitor monitor) {
		try {

			taskDataManager.putUpdatedTaskData(task, taskData, isUser(), getSession(), monitor);
			if (updateRelations) {
				Collection<TaskRelation> relations = connector.getTaskRelations(taskData);
				if (relations != null) {
					relationsByTaskId.put(task.getTaskId(), relations.toArray(new TaskRelation[0]));
				}
			}
		} catch (CoreException e) {
			updateStatus(taskRepository, task, e.getStatus());
		}
	}

	private ITask createFromTaskData(TaskRepository taskRepository, String taskId, TaskData taskData,
			IProgressMonitor monitor) throws CoreException {
		ITask task = tasksModel.createTask(taskRepository, taskData.getTaskId());
		((AbstractTask) task).setSynchronizationState(SynchronizationState.INCOMING_NEW);
		taskDataManager.putUpdatedTaskData(task, taskData, isUser(), getSession(), monitor);
		return task;
	}

	private void updateStatus(TaskRepository repository, ITask task, IStatus status) {
		statuses.add(status);
		((AbstractTask) task).setStatus(status);
		if (!isUser()) {
			((AbstractTask) task).setSynchronizing(false);
		}
		taskList.notifyElementChanged(task);
	}

	public SynchronizationSession getSession() {
		return session;
	}

	public void setSession(SynchronizationSession session) {
		this.session = session;
	}

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

}
