/*******************************************************************************
 * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br>
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
 * Copyright (C) 2010, Benjamin Muskalla <bmuskalla@eclipsesource.com>
 * Copyright (C) 2012, Stefan Lay <stefan.lay@sap.com>
 * 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
 *******************************************************************************/

package org.eclipse.egit.ui.internal.clone;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

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.Job;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.core.internal.util.ProjectUtil;
import org.eclipse.egit.core.op.CloneOperation;
import org.eclipse.egit.core.op.CloneOperation.PostCloneTask;
import org.eclipse.egit.core.op.ConfigureFetchAfterCloneTask;
import org.eclipse.egit.core.op.ConfigureGerritAfterCloneTask;
import org.eclipse.egit.core.op.ConfigurePushAfterCloneTask;
import org.eclipse.egit.core.op.SetRepositoryConfigPropertyTask;
import org.eclipse.egit.core.securestorage.UserPasswordCredentials;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.internal.SecureStoreUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.clone.GitCloneSourceProviderExtension.CloneSourceProvider;
import org.eclipse.egit.ui.internal.components.RepositorySelection;
import org.eclipse.egit.ui.internal.credentials.EGitCredentialsProvider;
import org.eclipse.egit.ui.internal.provisional.wizards.GitRepositoryInfo;
import org.eclipse.egit.ui.internal.provisional.wizards.GitRepositoryInfo.PushInfo;
import org.eclipse.egit.ui.internal.provisional.wizards.GitRepositoryInfo.RepositoryConfigProperty;
import org.eclipse.egit.ui.internal.provisional.wizards.IRepositorySearchResult;
import org.eclipse.egit.ui.internal.provisional.wizards.NoRepositoryInfoException;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.wizard.IWizardContainer;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.IWorkingSet;

/**
 * Implements the basic functionality of a clone wizard
 */
public abstract class AbstractGitCloneWizard extends Wizard {

	/**
	 * a page for branch selection
	 */
	protected SourceBranchPage validSource;

	/**
	 * a page for selection of the clone destination
	 */
	protected CloneDestinationPage cloneDestination;

	/**
	 * the path where a clone has been created in
	 */
	protected String alreadyClonedInto;

	/**
	 * whether the clone operation is done later on by the caller of the wizard
	 */
	protected boolean callerRunsCloneOperation;

	/**
	 * the result which was found when the last search was done
	 */
	protected IRepositorySearchResult currentSearchResult;

	private CloneOperation cloneOperation;

	/**
	 * Construct the clone wizard based on given repository search result. If
	 * the search result is an instance of org.eclipse.jface.wizard.WizardPage,
	 * then the page is shown in the wizard before the repository info is read.
	 * The repository location page that allows the repository info to be
	 * provided by different search providers is not shown.
	 *
	 * @param searchResult
	 *            the search result to initialize the clone wizard with.
	 */
	public AbstractGitCloneWizard(IRepositorySearchResult searchResult) {
		this();
		this.currentSearchResult = searchResult;
	}

	/**
	 * Construct the clone wizard with a repository location page that allows
	 * the repository info to be provided by different search providers.
	 */
	public AbstractGitCloneWizard() {
		setNeedsProgressMonitor(true);
		validSource = new SourceBranchPage() {

			@Override
			public void setVisible(boolean visible) {
				if (visible) {
					setSelection(getRepositorySelection());
					setCredentials(getCredentials());
				}
				super.setVisible(visible);
			}
		};
		cloneDestination = new CloneDestinationPage() {
			@Override
			public void setVisible(boolean visible) {
				if (visible)
					setSelection(getRepositorySelection(),
							validSource.getAvailableBranches(),
							validSource.getSelectedBranches(),
							validSource.getHEAD());
				super.setVisible(visible);
			}
		};
	}

	/**
	 * subclasses may add pages to the Wizard which will be shown before the clone step
	 */
	protected abstract void addPreClonePages();

	/**
	 * subclasses may add pages to the Wizard which will be shown after the clone step
	 */
	protected abstract void addPostClonePages();

	@Override
	final public void addPages() {
		if (hasSearchResult())
			addRepositorySearchPage();
		else
			addRepositoryLocationPage();
		addPreClonePages();
		addPage(validSource);
		addPage(cloneDestination);
		addPostClonePages();
	}

	/**
	 * @return if the search result is set
	 */
	protected boolean hasSearchResult() {
		return currentSearchResult != null;
	}

	private void addRepositorySearchPage() {
		if (currentSearchResult instanceof WizardPage)
			addPage((WizardPage)currentSearchResult);
	}

	private void addRepositoryLocationPage() {
		List<CloneSourceProvider> cloneSourceProviders = getCloneSourceProviders();
		if (hasSingleCloneSourceProviderWithFixedLocation(cloneSourceProviders))
			try {
				addPage(cloneSourceProviders.get(0).getRepositorySearchPage());
			} catch (CoreException e) {
				Activator.logError(e.getLocalizedMessage(), e);
			}
		else
			addPage(new RepositoryLocationPage(cloneSourceProviders));
	}

	private boolean hasSingleCloneSourceProviderWithFixedLocation(
			List<CloneSourceProvider> cloneSourceProviders) {
		return cloneSourceProviders.size() == 1 && cloneSourceProviders.get(0).hasFixLocation();
	}

	/**
	 * @return a list of CloneSourceProviders, may be extended by a subclass
	 */
	protected List<CloneSourceProvider> getCloneSourceProviders() {
		return GitCloneSourceProviderExtension.getCloneSourceProvider();
	}

	/**
	 * Do the clone using data which were collected on the pages
	 * {@code validSource} and {@code cloneDestination}
	 *
	 * @param gitRepositoryInfo
	 * @return if clone was successful
	 * @throws URISyntaxException
	 */
	protected boolean performClone(GitRepositoryInfo gitRepositoryInfo) throws URISyntaxException {
		URIish uri = new URIish(gitRepositoryInfo.getCloneUri());
		UserPasswordCredentials credentials = gitRepositoryInfo.getCredentials();
		setWindowTitle(NLS.bind(UIText.GitCloneWizard_jobName, uri.toString()));
		final boolean allSelected;
		final Collection<Ref> selectedBranches;
		if (validSource.isSourceRepoEmpty()) {
			// fetch all branches of empty repo
			allSelected = true;
			selectedBranches = Collections.emptyList();
		} else {
			allSelected = validSource.isAllSelected();
			selectedBranches = validSource.getSelectedBranches();
		}
		final File workdir = cloneDestination.getDestinationFile();
		final Ref ref = cloneDestination.getInitialBranch();
		final String remoteName = cloneDestination.getRemote();

		boolean created = workdir.exists();
		if (!created)
			created = workdir.mkdirs();

		if (!created || !workdir.isDirectory()) {
			final String errorMessage = NLS.bind(
					UIText.GitCloneWizard_errorCannotCreate, workdir.getPath());
			ErrorDialog.openError(getShell(), getWindowTitle(),
					UIText.GitCloneWizard_failed, new Status(IStatus.ERROR,
							Activator.getPluginId(), 0, errorMessage, null));
			// let's give user a chance to fix this minor problem
			return false;
		}

		int timeout = Activator.getDefault().getPreferenceStore()
				.getInt(UIPreferences.REMOTE_CONNECTION_TIMEOUT);
		final CloneOperation op = new CloneOperation(uri, allSelected,
				selectedBranches, workdir, ref != null ? ref.getName() : null,
				remoteName, timeout);
		if (credentials != null)
			op.setCredentialsProvider(new EGitCredentialsProvider(
					credentials.getUser(), credentials.getPassword()));
		else
			op.setCredentialsProvider(new EGitCredentialsProvider());
		op.setCloneSubmodules(cloneDestination.isCloneSubmodules());

		configureFetchSpec(op, gitRepositoryInfo, remoteName);
		configurePush(op, gitRepositoryInfo, remoteName);
		configureRepositoryConfig(op, gitRepositoryInfo);
		configureGerrit(op, gitRepositoryInfo, remoteName, timeout);

		if (cloneDestination.isImportProjects()) {
			final IWorkingSet[] sets = cloneDestination.getWorkingSets();
			op.addPostCloneTask(new PostCloneTask() {
				public void execute(Repository repository,
						IProgressMonitor monitor) throws CoreException {
					importProjects(repository, sets);
				}
			});
		}

		alreadyClonedInto = workdir.getPath();

		if (!callerRunsCloneOperation)
			runAsJob(uri, op, gitRepositoryInfo);
		else
			cloneOperation = op;
		return true;
	}

	@Override
	public IWizardPage getNextPage(IWizardPage page) {
		if (page instanceof IRepositorySearchResult) {
			currentSearchResult = (IRepositorySearchResult)page;
			return validSource;
		}
		return super.getNextPage(page);
	}

	/**
	 * @return the repository selected by the user
	 */
	protected RepositorySelection getRepositorySelection() {
		try {
			return (new RepositorySelection(new URIish(currentSearchResult.getGitRepositoryInfo().getCloneUri()), null));
		} catch (URISyntaxException e) {
			Activator.error(UIText.GitImportWizard_errorParsingURI, e);
			return null;
		} catch (NoRepositoryInfoException e) {
			Activator.error(UIText.GitImportWizard_noRepositoryInfo, e);
			return null;
		} catch (Exception e) {
			Activator.error(e.getMessage(), e);
			return null;
		}
	}

	/**
	 * @return the credentials
	 */
	protected UserPasswordCredentials getCredentials() {
		try {
			return currentSearchResult.getGitRepositoryInfo().getCredentials();
		} catch (NoRepositoryInfoException e) {
			Activator.error(UIText.GitImportWizard_noRepositoryInfo, e);
			return null;
		} catch (Exception e) {
			Activator.error(e.getMessage(), e);
			return null;
		}
	}

	private void configureFetchSpec(CloneOperation op,
			GitRepositoryInfo gitRepositoryInfo, String remoteName) {
		for (String fetchRefSpec : gitRepositoryInfo.getFetchRefSpecs())
			op.addPostCloneTask(new ConfigureFetchAfterCloneTask(remoteName, fetchRefSpec));
	}

	private void configurePush(CloneOperation op,
			GitRepositoryInfo gitRepositoryInfo, String remoteName) {
		for (PushInfo pushInfo : gitRepositoryInfo.getPushInfos())
			try {
				URIish uri = pushInfo.getPushUri() != null ? new URIish(
						pushInfo.getPushUri()) : null;
				ConfigurePushAfterCloneTask task = new ConfigurePushAfterCloneTask(
						remoteName, pushInfo.getPushRefSpec(), uri);
				op.addPostCloneTask(task);
			} catch (URISyntaxException e) {
				Activator.handleError(UIText.GitCloneWizard_failed, e, true);
			}
	}

	private void configureRepositoryConfig(CloneOperation op, GitRepositoryInfo gitRepositoryInfo) {
		for (RepositoryConfigProperty p : gitRepositoryInfo.getRepositoryConfigProperties()) {
			SetRepositoryConfigPropertyTask task = new SetRepositoryConfigPropertyTask(
					p.getSection(), p.getSubsection(), p.getName(),
					p.getValue());
			op.addPostCloneTask(task);
		}
	}

	private void configureGerrit(CloneOperation op,
			GitRepositoryInfo gitRepositoryInfo, String remoteName, int timeout) {
		ConfigureGerritAfterCloneTask task = new ConfigureGerritAfterCloneTask(
				gitRepositoryInfo.getCloneUri(), remoteName, timeout);
		op.addPostCloneTask(task);
	}

	private void importProjects(final Repository repository,
			final IWorkingSet[] sets) {
		String repoName = Activator.getDefault().getRepositoryUtil()
				.getRepositoryName(repository);
		Job importJob = new WorkspaceJob(MessageFormat.format(
				UIText.GitCloneWizard_jobImportProjects, repoName)) {

			public IStatus runInWorkspace(IProgressMonitor monitor) {
				List<File> files = new ArrayList<File>();
				ProjectUtil.findProjectFiles(files, repository.getWorkTree(),
						true, monitor);
				if (files.isEmpty())
					return Status.OK_STATUS;

				Set<ProjectRecord> records = new LinkedHashSet<ProjectRecord>();
				for (File file : files)
					records.add(new ProjectRecord(file));
				try {
					ProjectUtils.createProjects(records, sets, monitor);
				} catch (InvocationTargetException e) {
					Activator.logError(e.getLocalizedMessage(), e);
				} catch (InterruptedException e) {
					Activator.logError(e.getLocalizedMessage(), e);
				}
				return Status.OK_STATUS;
			}
		};
		importJob.schedule();
	}

	/**
	 * @param container
	 * @param repositoryInfo
	 */
	public void runCloneOperation(IWizardContainer container, final GitRepositoryInfo repositoryInfo) {
		try {
			container.run(true, true, new IRunnableWithProgress() {
				public void run(IProgressMonitor monitor)
						throws InvocationTargetException, InterruptedException {
					executeCloneOperation(cloneOperation, repositoryInfo, monitor);
				}
			});
		} catch (InvocationTargetException e) {
			Activator.handleError(UIText.GitCloneWizard_failed, e.getCause(),
					true);
		} catch (InterruptedException e) {
			// nothing to do
		}
	}

	private void runAsJob(final URIish uri, final CloneOperation op,
			final GitRepositoryInfo repositoryInfo) {
		final Job job = new Job(NLS.bind(UIText.GitCloneWizard_jobName,
				uri.toString())) {
			@Override
			protected IStatus run(final IProgressMonitor monitor) {
				try {
					return executeCloneOperation(op, repositoryInfo, monitor);
				} catch (InterruptedException e) {
					return Status.CANCEL_STATUS;
				} catch (InvocationTargetException e) {
					Throwable thr = e.getCause();
					return new Status(IStatus.ERROR, Activator.getPluginId(),
							0, thr.getMessage(), thr);
				}
			}

			@Override
			public boolean belongsTo(Object family) {
				if (JobFamilies.CLONE.equals(family))
					return true;
				return super.belongsTo(family);
			}
		};
		job.setUser(true);
		job.schedule();
	}

	private IStatus executeCloneOperation(final CloneOperation op,
			final GitRepositoryInfo repositoryInfo,
			final IProgressMonitor monitor) throws InvocationTargetException,
			InterruptedException {

		final RepositoryUtil util = Activator.getDefault().getRepositoryUtil();

		op.run(monitor);
		util.addConfiguredRepository(op.getGitDir());
		try {
			if (repositoryInfo.shouldSaveCredentialsInSecureStore())
				SecureStoreUtils.storeCredentials(repositoryInfo.getCredentials(),
						new URIish(repositoryInfo.getCloneUri()));
		} catch (Exception e) {
			Activator.error(e.getMessage(), e);
		}
		return Status.OK_STATUS;
	}

	/**
	 * @param newValue
	 *            if true the clone wizard just creates a clone operation. The
	 *            caller has to run this operation using runCloneOperation. If
	 *            false the clone operation is performed using a job.
	 */
	public void setCallerRunsCloneOperation(boolean newValue) {
		callerRunsCloneOperation = newValue;
	}

}
