/*******************************************************************************
 * Copyright (C) 2011, 2012 Robin Stocker <robin@nibor.org>
 *
 * 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.core.internal;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.ProjectReference;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.core.op.CloneOperation;
import org.eclipse.egit.core.op.ConnectProviderOperation;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.TeamException;

/**
 * Processes project references, clones and imports them.
 */
public class ProjectReferenceImporter {

	private final String[] referenceStrings;

	/**
	 * @param referenceStrings the reference strings to import
	 */
	public ProjectReferenceImporter(String[] referenceStrings) {
		this.referenceStrings = referenceStrings;
	}

	/**
	 * Imports the projects as described in the reference strings.
	 *
	 * @param monitor progress monitor
	 * @return the imported projects
	 * @throws TeamException
	 */
	public List<IProject> run(IProgressMonitor monitor) throws TeamException {

		final Map<URIish, Map<String, Set<ProjectReference>>> repositories = parseReferenceStrings();

		final List<IProject> importedProjects = new ArrayList<IProject>();

		for (final Map.Entry<URIish, Map<String, Set<ProjectReference>>> entry : repositories
				.entrySet()) {
			final URIish gitUrl = entry.getKey();
			final Map<String, Set<ProjectReference>> refs = entry
					.getValue();

			for (final Map.Entry<String, Set<ProjectReference>> refEntry : refs
					.entrySet()) {
				final String refName = refEntry.getKey();
				final Set<ProjectReference> projects = refEntry.getValue();

				final Set<String> allRefs = refs.keySet();

				File repositoryPath = null;
				if (allRefs.size() == 1)
					repositoryPath = findConfiguredRepository(gitUrl);

				if (repositoryPath == null) {
					try {
						IPath workDir = getWorkingDir(gitUrl, refName, refs.keySet());
						repositoryPath = cloneIfNecessary(gitUrl, refName, workDir, projects, monitor);
					} catch (final InterruptedException e) {
						// was canceled by user
						return Collections.emptyList();
					}
				}

				getRepositoryUtil().addConfiguredRepository(repositoryPath);

				IPath newWorkDir = new Path(repositoryPath.getAbsolutePath())
						.removeLastSegments(1);
				List<IProject> p = importProjects(projects, newWorkDir,
						repositoryPath, monitor);
				importedProjects.addAll(p);
			}
		}
		return importedProjects;
	}

	private static File cloneIfNecessary(final URIish gitUrl, final String refToCheckout, final IPath workDir,
			final Set<ProjectReference> projects, IProgressMonitor monitor) throws TeamException, InterruptedException {

		final File repositoryPath = workDir.append(Constants.DOT_GIT_EXT).toFile();

		if (workDir.toFile().exists()) {
			if (repositoryAlreadyExistsForUrl(repositoryPath, gitUrl))
				return repositoryPath;
			else {
				final Collection<String> projectNames = new LinkedList<String>();
				for (final ProjectReference projectReference : projects)
					projectNames.add(projectReference.getProjectDir());
				throw new TeamException(
						NLS.bind(CoreText.GitProjectSetCapability_CloneToExistingDirectory,
								new Object[] { workDir, projectNames, gitUrl }));
			}
		} else {
			try {
				int timeout = 60;
				final CloneOperation cloneOperation = new CloneOperation(
						gitUrl, true, null, workDir.toFile(), refToCheckout,
						Constants.DEFAULT_REMOTE_NAME, timeout);
				cloneOperation.run(monitor);

				return repositoryPath;
			} catch (final InvocationTargetException e) {
				throw getTeamException(e);
			}
		}
	}

	private Map<URIish, Map<String, Set<ProjectReference>>> parseReferenceStrings()
			throws TeamException {
		final Map<URIish, Map<String, Set<ProjectReference>>> repositories = new LinkedHashMap<URIish, Map<String, Set<ProjectReference>>>();

		for (final String reference : referenceStrings) {
			try {
				final ProjectReference projectReference = new ProjectReference(
						reference);
				Map<String, Set<ProjectReference>> repositoryBranches = repositories
						.get(projectReference.getRepository());
				if (repositoryBranches == null) {
					repositoryBranches = new HashMap<String, Set<ProjectReference>>();
					repositories.put(projectReference.getRepository(),
							repositoryBranches);
				}
				Set<ProjectReference> projectReferences = repositoryBranches
						.get(projectReference.getBranch());
				if (projectReferences == null) {
					projectReferences = new LinkedHashSet<ProjectReference>();
					repositoryBranches.put(projectReference.getBranch(),
							projectReferences);
				}

				projectReferences.add(projectReference);
			} catch (final IllegalArgumentException e) {
				throw new TeamException(reference, e);
			} catch (final URISyntaxException e) {
				throw new TeamException(reference, e);
			}
		}

		return repositories;
	}

	/**
	 * @param gitUrl
	 * @param branch
	 *            the branch to check out
	 * @param allBranches
	 *            all branches which should be checked out for this gitUrl
	 * @return the directory where the project should be checked out
	 */
	private static IPath getWorkingDir(URIish gitUrl, String branch,
			Set<String> allBranches) {
		final IPath workspaceLocation = ResourcesPlugin.getWorkspace()
				.getRoot().getRawLocation();
		final String humanishName = gitUrl.getHumanishName();
		String extendedName;
		if (allBranches.size() == 1 || branch.equals(Constants.MASTER))
			extendedName = humanishName;
		else
			extendedName = humanishName + "_" + branch; //$NON-NLS-1$
		final IPath workDir = workspaceLocation.append(extendedName);
		return workDir;
	}

	private static File findConfiguredRepository(URIish gitUrl) {
		for (String repoDir : getRepositoryUtil().getConfiguredRepositories()) {
			File repoDirFile = new File(repoDir);
			if (repositoryAlreadyExistsForUrl(repoDirFile, gitUrl))
				return repoDirFile;
		}
		return null;
	}

	private static boolean repositoryAlreadyExistsForUrl(File repositoryPath,
			URIish gitUrl) {
		if (repositoryPath.exists()) {
			Repository existingRepository;
			try {
				existingRepository = FileRepositoryBuilder
						.create(repositoryPath);
			} catch (IOException e) {
				return false;
			}
			try {
				boolean exists = containsRemoteForUrl(
						existingRepository.getConfig(), gitUrl);
				return exists;
			} catch (URISyntaxException e) {
				return false;
			} finally {
				existingRepository.close();
			}
		}
		return false;
	}

	private static boolean containsRemoteForUrl(Config config, URIish url)
			throws URISyntaxException {
		Set<String> remotes = config.getSubsections(ConfigConstants.CONFIG_REMOTE_SECTION);
		for (String remote : remotes) {
			String remoteUrl = config.getString(
					ConfigConstants.CONFIG_REMOTE_SECTION,
					remote,
					ConfigConstants.CONFIG_KEY_URL);
			URIish existingUrl = new URIish(remoteUrl);
			if (existingUrl.equals(url))
				return true;

			// there may be slight differences in the URLs...
			URIish canonExistingUrl = canonicalizeURL(existingUrl);
			URIish canonUrl = canonicalizeURL(url);
			if (canonExistingUrl.equals(canonUrl))
				return true;
		}
		return false;
	}

	private static URIish canonicalizeURL(URIish existingUrl) {
		// try URLs without user name, since often project sets contain
		// anonymous URLs, and remote URL might be anonymous as well
		URIish newURL = existingUrl.setUser(null);

		// some URLs end with .git, some don't
		String path = existingUrl.getPath();
		if (path.endsWith(".git")) { //$NON-NLS-1$
			newURL = existingUrl.setPath(path.substring(0,
					path.lastIndexOf(".git"))); //$NON-NLS-1$
		}

		return newURL;
	}

	private List<IProject> importProjects(final Set<ProjectReference> projects,
			final IPath workDir, final File repositoryPath,
			final IProgressMonitor monitor) throws TeamException {
		try {

			List<IProject> importedProjects = new ArrayList<IProject>();

			// import projects from the current repository to workspace
			final IWorkspace workspace = ResourcesPlugin.getWorkspace();
			final IWorkspaceRoot root = workspace.getRoot();
			for (final ProjectReference projectToImport : projects) {
				final IPath projectDir = workDir.append(projectToImport
						.getProjectDir());
				final IProjectDescription projectDescription = workspace
						.loadProjectDescription(projectDir
								.append(IProjectDescription.DESCRIPTION_FILE_NAME));
				final IProject project = root.getProject(projectDescription
						.getName());
				if (!project.exists()) {
					project.create(projectDescription, monitor);
					importedProjects.add(project);
				}

				project.open(monitor);
				final ConnectProviderOperation connectProviderOperation = new ConnectProviderOperation(
						project, repositoryPath);
				connectProviderOperation.execute(monitor);
			}

			return importedProjects;

		} catch (final CoreException e) {
			throw TeamException.asTeamException(e);
		}
	}

	private static TeamException getTeamException(final Throwable throwable) {
		Throwable current = throwable;
		while (current.getCause() != null)
			current = current.getCause();
		return new TeamException(current.getMessage(), current);
	}

	private static RepositoryUtil getRepositoryUtil() {
		return Activator.getDefault().getRepositoryUtil();
	}
}
