package org.kie.eclipse.navigator.view.actions.repository;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
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.op.PullOperation;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.internal.pull.PullResultDialog;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.kie.eclipse.IKieConstants;
import org.kie.eclipse.navigator.Activator;
import org.kie.eclipse.navigator.view.content.RepositoryNode;
import org.kie.eclipse.server.IKieRepositoryHandler;
import org.kie.eclipse.server.IKieServerHandler;
import org.kie.eclipse.server.IKieServiceDelegate;
import org.kie.eclipse.utils.PreferencesUtils;

/**
 * UI wrapper for {@link PullOperation}
 */
public class PullOperationUI extends JobChangeAdapter implements IKieConstants {
	private static final IStatus NOT_TRIED_STATUS = new Status(IStatus.ERROR, Activator.getDefault().PLUGIN_ID, "Not tried");

	private final Repository repository;
	private final Shell shell;
	private final String repoName;
	private final RepositoryNode repoNode;
	/**
	 * 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(RepositoryNode container) {
		this.repoNode = container;
		final IKieRepositoryHandler handler = (IKieRepositoryHandler) container.getHandler();
		final IKieServerHandler serverHandler = (IKieServerHandler) handler.getRoot();
		final IKieServiceDelegate delegate = handler.getDelegate();
		this.repository = handler.getRepository();
		this.shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
		this.repoName = handler.getName();


		String username = delegate.getUsername();
		String password = delegate.getPassword();
		final CredentialsProvider credentialsProvider = new KieCredentialsProvider(serverHandler, username, password);

		int timeout = serverHandler.getPreference(PREF_REMOTE_TIMEOUT, 60);
		Set<Repository> repos = new HashSet<Repository>();
		repos.add(repository);
		pullOperation = new PullOperation(repos, timeout);
		pullOperation.setCredentialsProvider(credentialsProvider);
		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;
		String shortBranchName;
		try {
			shortBranchName = repository.getBranch();
		}
		catch (IOException e) {
			// ignore here
			shortBranchName = ""; //$NON-NLS-1$
		}
		jobName = NLS.bind("Pulling Branch {0} of Repository {1}", shortBranchName, repoName);

		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(null);
		job.setUser(true);
		job.addJobChangeListener(jobChangeListener);
		job.schedule();
	}

	/**
	 * Starts this operation synchronously.
	 *
	 * @param monitor
	 */
	@SuppressWarnings("restriction")
	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
				repoNode.handleException((Throwable) e);
		}
	}

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

			@Override
			public void run() {
				Map<Repository, Object> res = new LinkedHashMap<Repository, Object>(PullOperationUI.this.results);
				handlePullResults(res);
			}
		});
	}

	/**
	 * 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) {
		if (tasksToWaitFor.decrementAndGet() == 0 && !results.isEmpty())
			showResults();
	}

	private void showResults() {
		PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
			@Override
			public void run() {
				showResults(shell);
			}
		});
	}

	private void showResults(final Shell shell) {
		if (this.results.isEmpty())
			// shouldn't really happen, but just in case...
			return;
		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, "Pull Canceled", "The pull operation was canceled.");
			}
			else if (status.getException() instanceof TransportException) {
				ErrorDialog.openError(shell, "Pull Failed", "Git connection problem.\n"
						+ "\n\nMaybe you are offline or behind a proxy.\n"
						+ "Check your network connection and proxy configuration.", status);
			}
			else
				repoNode.handleException(status.getException());
		}
	}
}
