/*******************************************************************************
 * Copyright (c) 2011 SAP AG.
 * 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:
 *    Mathias Kinzler (SAP AG) - initial implementation
 *******************************************************************************/
package org.eclipse.egit.ui.internal.pull;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.IJobChangeListener;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.egit.core.internal.job.RuleUtil;
import org.eclipse.egit.core.op.PullOperation;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.branch.CleanupUncomittedChangesDialog;
import org.eclipse.egit.ui.internal.credentials.EGitCredentialsProvider;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.api.RebaseResult;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;

/**
 * UI wrapper for {@link PullOperation}
 */
public class PullOperationUI extends JobChangeAdapter {
	private static final IStatus NOT_TRIED_STATUS = new Status(IStatus.ERROR,
			Activator.getPluginId(), UIText.PullOperationUI_NotTriedMessage);

	private final Repository[] repositories;

	/**
	 * Holds the number of PullOperationUIs that need to complete before the
	 * pull results can be shown. The number includes this instance of
	 * PullOperationUI and all subtasks spawned by it.
	 */
	private final AtomicInteger tasksToWaitFor = new AtomicInteger(1);

	/** pull results per repository */
	protected final Map<Repository, Object> results = Collections
			.synchronizedMap(new LinkedHashMap<Repository, Object>());

	private final PullOperation pullOperation;

	/**
	 * @param repositories
	 */
	public PullOperationUI(Set<Repository> repositories) {
		this.repositories = repositories.toArray(new Repository[repositories
				.size()]);
		int timeout = Activator.getDefault().getPreferenceStore().getInt(
				UIPreferences.REMOTE_CONNECTION_TIMEOUT);
		pullOperation = new PullOperation(repositories, timeout);
		pullOperation.setCredentialsProvider(new EGitCredentialsProvider());
		for (Repository repository : repositories)
			results.put(repository, NOT_TRIED_STATUS);
	}

	/**
	 * Starts this operation asynchronously
	 */
	public void start() {
		start(this);
	}

	private void start(IJobChangeListener jobChangeListener) {
		// figure out a job name
		String jobName;
		if (this.repositories.length == 1) {
			String repoName = Activator.getDefault().getRepositoryUtil()
					.getRepositoryName(repositories[0]);
			String shortBranchName;
			try {
				shortBranchName = repositories[0].getBranch();
			} catch (IOException e) {
				// ignore here
				shortBranchName = ""; //$NON-NLS-1$
			}
			jobName = NLS.bind(UIText.PullOperationUI_PullingTaskName,
					shortBranchName, repoName);
		} else
			jobName = UIText.PullOperationUI_PullingMultipleTaskName;
		Job job = new WorkspaceJob(jobName) {

			@Override
			public IStatus runInWorkspace(IProgressMonitor monitor) {
				execute(monitor);
				// we always return OK and handle display of errors on our own
				return Status.OK_STATUS;
			}

			@Override
			public boolean belongsTo(Object family) {
				if (JobFamilies.PULL.equals(family))
					return true;
				return super.belongsTo(family);
			}
		};
		job.setRule(RuleUtil.getRuleForRepositories(Arrays.asList(repositories)));
		job.setUser(true);
		job.addJobChangeListener(jobChangeListener);
		job.schedule();
	}

	/**
	 * Starts this operation synchronously.
	 *
	 * @param monitor
	 */
	public void execute(IProgressMonitor monitor) {
		try {
			pullOperation.execute(monitor);
			results.putAll(pullOperation.getResults());
		} catch (CoreException e) {
			if (e.getStatus().getSeverity() == IStatus.CANCEL)
				results.putAll(pullOperation.getResults());
			else
				Activator.handleError(e.getMessage(), e, true);
		}
	}

	public void done(IJobChangeEvent event) {
		PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {

			public void run() {
				Map<Repository, Object> res = new LinkedHashMap<Repository, Object>(
						PullOperationUI.this.results);
				Shell shell = PlatformUI.getWorkbench()
						.getActiveWorkbenchWindow().getShell();
				handlePullResults(res, shell);
			}
		});
	}

	/**
	 * Post-process the pull results, allowing the user to deal with uncommitted
	 * changes and re-pull if the initial pull failed because of these changes
	 *
	 * @param resultsMap
	 *            a copy of the initial pull results
	 * @param shell
	 */
	private void handlePullResults(final Map<Repository, Object> resultsMap,
			Shell shell) {
		for (Entry<Repository, Object> entry : resultsMap.entrySet()) {
			Object result = entry.getValue();
			if (result instanceof PullResult) {
				PullResult pullResult = (PullResult) result;
				if (pullResult.getRebaseResult() != null
						&& RebaseResult.Status.UNCOMMITTED_CHANGES == pullResult
								.getRebaseResult().getStatus()) {
					handleUncommittedChanges(entry.getKey(), pullResult
							.getRebaseResult().getUncommittedChanges(), shell);
				}
			}
		}

		if (tasksToWaitFor.decrementAndGet() == 0 && !results.isEmpty())
			showResults(shell);
	}

	/**
	 * @param repository
	 * @param files
	 *            uncommitted files that were in the way for the rebase
	 * @param shell
	 */
	private void handleUncommittedChanges(final Repository repository,
			final List<String> files, Shell shell) {
		String repoName = Activator.getDefault().getRepositoryUtil()
				.getRepositoryName(repository);
		CleanupUncomittedChangesDialog cleanupUncomittedChangesDialog = new CleanupUncomittedChangesDialog(
				shell,
				MessageFormat
						.format(UIText.AbstractRebaseCommandHandler_cleanupDialog_title,
								repoName),
				UIText.AbstractRebaseCommandHandler_cleanupDialog_text,
				repository, files);
		cleanupUncomittedChangesDialog.open();
		if (cleanupUncomittedChangesDialog.shouldContinue()) {
			final PullOperationUI parentOperation = this;
			final PullOperationUI pullOperationUI = new PullOperationUI(
					Collections.singleton(repository));
			tasksToWaitFor.incrementAndGet();
			pullOperationUI.start(new JobChangeAdapter() {

				@Override
				public void done(IJobChangeEvent event) {
					// put the result from this subtask into the result map of
					// the outer PullOperationUI and display the results if all
					// subtasks have reported back
					parentOperation.results.putAll(pullOperationUI.results);
					int missing = parentOperation.tasksToWaitFor.decrementAndGet();
					if (missing == 0)
						parentOperation.showResults();
				}
			});
		}
	}

	private void showResults() {
		PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
			public void run() {
				Shell shell = PlatformUI.getWorkbench()
						.getActiveWorkbenchWindow().getShell();
				showResults(shell);
			}
		});
	}

	private void showResults(final Shell shell) {
		if (this.results.isEmpty())
			// shouldn't really happen, but just in case...
			return;
		else if (this.results.size() == 1) {
			Entry<Repository, Object> entry = this.results.entrySet()
					.iterator().next();
			if (entry.getValue() instanceof PullResult)
				new PullResultDialog(shell, entry.getKey(), (PullResult) entry
						.getValue()).open();
			else {
				IStatus status = (IStatus) entry.getValue();
				if (status == NOT_TRIED_STATUS) {
					MessageDialog
							.openInformation(
									shell,
									UIText.PullOperationUI_PullCanceledWindowTitle,
									UIText.PullOperationUI_PullOperationCanceledMessage);
				} else if (status.getException() instanceof TransportException) {
					ErrorDialog.openError(shell,
							UIText.PullOperationUI_PullFailed,
							UIText.PullOperationUI_ConnectionProblem,
							status);
				} else
					Activator.handleError(status.getMessage(), status
							.getException(), true);
			}
		} else
			new MultiPullResultDialog(shell, results).open();
	}
}
