/*******************************************************************************
 * Copyright (c) 2014-2017 Red Hat Inc., and others
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Mickael Istria (Red Hat Inc.) - initial API and implementation
 ******************************************************************************/
package org.eclipse.ui.internal.wizards.datatransfer;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceDescription;
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.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobGroup;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.wizards.datatransfer.ProjectConfigurator;

/**
 * The {@link SmartImportJob} is a Job to import a given folder or archive. It
 * detects the nested projects in the given source and configure projects
 * according to the metadata it could find. The behavior is extensible, and
 * contributors can add a {@link ProjectConfigurator} strategy via extension
 * point to add support for more project kinds.
 *
 * @since 3.12
 *
 */
public class SmartImportJob extends Job {

	/*
	 * Input parameters
	 */
	private File rootDirectory;
	private Set<File> directoriesToImport;
	private Set<File> excludedDirectories;
	private boolean discardRootProject;
	private boolean deepChildrenDetection;
	private boolean configureProjects;
	private boolean closeProjectsAfterImport;
	private boolean reconfigureEclipseProjects;
	private IWorkingSet[] workingSets;

	/*
	 * working fields
	 */
	private IProject rootProject;
	private IWorkspaceRoot workspaceRoot;
	private ProjectConfiguratorExtensionManager configurationManager;
	private RecursiveImportListener listener;

	protected Map<File, List<ProjectConfigurator>> importProposals;
	private Map<IProject, List<ProjectConfigurator>> report;
	private Map<IPath, Exception> errors;

	private JobGroup crawlerJobGroup;

	/**
	 * Builds a new instance of the job
	 *
	 * @param rootDirectory
	 *            the root directory to import and analyze
	 * @param workingSets
	 *            working sets to assign to imported projects
	 * @param configureProjects
	 *            whether we want to configure projects (natures etc...)
	 *            according to their metadata
	 * @param recuriveChildrenDetection
	 *            whether to recurse for detection of nested projects
	 */
	public SmartImportJob(File rootDirectory, Set<IWorkingSet> workingSets, boolean configureProjects, boolean recuriveChildrenDetection) {
		super(rootDirectory.getAbsolutePath());
		this.workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
		this.rootDirectory = rootDirectory;
		setWorkingSets(workingSets);
		this.configureProjects = configureProjects;
		this.deepChildrenDetection = recuriveChildrenDetection;
		this.report = Collections.synchronizedMap(new HashMap<>());
		this.errors = Collections.synchronizedMap(new HashMap<>());
		this.crawlerJobGroup = new JobGroup(DataTransferMessages.SmartImportJob_detectAndConfigureProjects, 0, 1);
	}

	/**
	 * Sets the working sets to assign to newly imported projects.
	 *
	 * @param workingSets
	 *            to assign to newly imported projects.
	 * @since 3.13
	 */
	public void setWorkingSets(Set<IWorkingSet> workingSets) {
		if (workingSets != null) {
			this.workingSets = workingSets.toArray(new IWorkingSet[workingSets.size()]);
		} else {
			this.workingSets = new IWorkingSet[0];
		}
	}

	/**
	 * @return The root directory for the import operation
	 */
	public File getRoot() {
		return this.rootDirectory;
	}

	/**
	 * Sets the directories that have been detected by preliminary detection and that
	 * user has selected to import. Those will be imported and configured in any case.
	 * This does not impact output of {@link #getImportProposals(IProgressMonitor)}
	 * @param directories
	 */
	public void setDirectoriesToImport(Set<File> directories) {
		this.directoriesToImport = directories;
	}

	/**
	 * Set directories that users specifically configured as to NOT import.
	 * Projects UNDER those directories may be imported, but never project directly
	 * in one of those directories.
	 * This does not impact output of {@link #getImportProposals(IProgressMonitor)}
	 * @param directories
	 */
	public void setExcludedDirectories(Set<File> directories) {
		this.excludedDirectories = directories;
	}

	/**
	 * Adds a listener to be notified of progress (detection/configuration of
	 * sub-projects)
	 *
	 * @param listener
	 */
	public void setListener(RecursiveImportListener listener) {
		this.listener = listener;
	}

	@Override
	public IStatus run(IProgressMonitor monitor) {
		try {
			IWorkspace workspace = ResourcesPlugin.getWorkspace();
			IWorkspaceDescription description = workspace.getDescription();
			boolean isAutoBuilding = workspace.isAutoBuilding();
			if (isAutoBuilding) {
				description.setAutoBuilding(false);
				workspace.setDescription(description);
			}

			if (directoriesToImport != null) {
				this.deepChildrenDetection = false;
				SubMonitor loopMonitor = SubMonitor.convert(monitor,
						DataTransferMessages.SmartImportJob_configuringSelectedDirectories,
						directoriesToImport.size() * (configureProjects ? 3 : 2) + 1);
				Comparator<File> rootToLeafComparator = (arg0, arg1) -> {
					int lengthDiff = arg0.getAbsolutePath().length() - arg1.getAbsolutePath().length();
					if (lengthDiff != 0) {
						return lengthDiff;
					}
					return arg0.compareTo(arg1);
				};
				SortedSet<File> directories = new TreeSet<>(rootToLeafComparator);
				directories.addAll(this.directoriesToImport);
				SortedMap<File, IProject> leafToRootProjects = new TreeMap<>(Collections.reverseOrder(rootToLeafComparator));
				final Set<IProject> alreadyConfiguredProjects = new HashSet<>();
				loopMonitor.worked(1);
				for (final File directoryToImport : directories) {
					final boolean alreadyAnEclipseProject = new File(directoryToImport, IProjectDescription.DESCRIPTION_FILE_NAME).isFile();
					try {
						IProject newProject = toExistingOrNewProject(directoryToImport, loopMonitor.split(1),
								IResource.BACKGROUND_REFRESH);
						if (alreadyAnEclipseProject) {
							alreadyConfiguredProjects.add(newProject);
						}
						leafToRootProjects.put(directoryToImport, newProject);
						loopMonitor.worked(1);
					} catch (CouldNotImportProjectException ex) {
						IPath path = IPath.fromOSString(directoryToImport.getAbsolutePath());
						if (listener != null) {
							listener.errorHappened(path, ex);
						}
						this.errors.put(path, ex);
					}
				}
				if (configureProjects) {
					JobGroup multiDirectoriesJobGroup = new JobGroup(
							DataTransferMessages.SmartImportJob_configuringSelectedDirectories, 20, 1);
					for (final IProject newProject : leafToRootProjects.values()) {
						Job directoryJob = new Job(
								NLS.bind(DataTransferMessages.SmartImportJob_configuring, newProject.getName())) {
							@Override
							protected IStatus run(IProgressMonitor aMonitor) {
								try {
									importProjectAndChildrenRecursively(newProject,
											!alreadyConfiguredProjects.contains(newProject), monitor);
									return Status.OK_STATUS;
								} catch (Exception ex) {
									return new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, ex.getMessage(),
											ex);
								}
							}
						};
						// Job1 on path1 and Job2 on path2 can be run in parallel IFF path1 isn't a prefix of path2 and vice-versa
						directoryJob.setRule(new SubdirectoryOrSameNameSchedulingRule(newProject));
						directoryJob.setUser(true);
						directoryJob.setJobGroup(multiDirectoriesJobGroup);
						directoryJob.schedule();
					}
					multiDirectoriesJobGroup.join(0, loopMonitor.split(leafToRootProjects.size()));
				}


			} else { // no specific projects included, consider only root
				SubMonitor subMonitor = SubMonitor.convert(monitor, 3);
				File rootProjectFile = new File(this.rootDirectory, IProjectDescription.DESCRIPTION_FILE_NAME);
				boolean isRootANewProject = !rootProjectFile.isFile();
				this.rootProject = toExistingOrNewProject(this.rootDirectory, subMonitor, IResource.NONE);

				if (this.configureProjects) {
					importProjectAndChildrenRecursively(this.rootProject, isRootANewProject, subMonitor);

					if (isRootANewProject && rootProjectWorthBeingRemoved()) {
						Display.getDefault()
								.syncExec(() -> discardRootProject = MessageDialog.openQuestion(
										PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
										DataTransferMessages.SmartImportJob_discardRootProject_title,
										DataTransferMessages.SmartImportJob_discardRootProject_description));
						if (this.discardRootProject) {
							this.rootProject.delete(false, true, subMonitor);
							if (isRootANewProject) {
								rootProjectFile.delete();
							}
							this.report.remove(this.rootProject);
						}
					}
				}
			}
			if (closeProjectsAfterImport) {
				this.report.keySet().forEach(project -> {
					try {
						project.close(monitor);
					} catch (CoreException e) {
						listener.errorHappened(project.getLocation(), e);
					}
				});
			}

			if (isAutoBuilding) {
				description.setAutoBuilding(true);
				workspace.setDescription(description);
			}
		} catch (Exception ex) {
			return new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, ex.getMessage(), ex);
		}
		return Status.OK_STATUS;
	}

	protected boolean rootProjectWorthBeingRemoved() {
		if (this.report.size() == 1) {
			return false;
		}
		List<ProjectConfigurator> rootProjectConfigurators = this.report.get(this.rootProject);
		if (rootProjectConfigurators.isEmpty()) {
			return true;
		}
		boolean areOnlyDummyConfigurators = true;
		for (ProjectConfigurator configurator : rootProjectConfigurators) {
			// TODO: semantics whether configurator is "strong enough" for a root project should be put inside configurator
			areOnlyDummyConfigurators &= (configurator instanceof EclipseProjectConfigurator || configurator instanceof EclipseWorkspaceConfigurator);
		}
		return areOnlyDummyConfigurators;
	}


	private final class CrawlFolderJob extends Job {
		private final IFolder childFolder;
		private final Set<IProject> res;

		private CrawlFolderJob(String name, IFolder childFolder, Set<IProject> res) {
			super(name);
			this.childFolder = childFolder;
			this.res = res;
		}

		@Override
		public IStatus run(IProgressMonitor progressMonitor) {
			SubMonitor subMonitor = null;
			if (progressMonitor instanceof SubMonitor) {
				subMonitor = (SubMonitor) progressMonitor;
			} else {
				subMonitor = SubMonitor.convert(progressMonitor);
			}
			try {
				Set<IProject> projectFromCurrentContainer = importProjectAndChildrenRecursively(childFolder, false,
						subMonitor);
				res.addAll(projectFromCurrentContainer);
				return Status.OK_STATUS;
			} catch (Exception ex) {
				return new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, ex.getMessage(), ex);
			}
		}
	}

	private Set<IProject> searchAndImportChildrenProjectsRecursively(final IContainer parentContainer,
			Set<IPath> directoriesToExclude, final IProgressMonitor progressMonitor) throws Exception {
		SubMonitor subMonitor = SubMonitor.convert(progressMonitor, parentContainer.members().length);
		IPath parentLocation = parentContainer.getLocation();
		if (parentLocation == null) {
			return Collections.emptySet();
		}
		for (IProject processedProjects : Collections.synchronizedSet(this.report.keySet())) {
			if (parentLocation.equals(processedProjects.getLocation())) {
				return Collections.emptySet();
			}
		}
		parentContainer.refreshLocal(IResource.DEPTH_ONE, progressMonitor); // make sure we know all children
		Set<IFolder> childrenToProcess = new HashSet<>();
		final Set<IProject> res = Collections.synchronizedSet(new HashSet<>());
		for (IResource childResource : parentContainer.members()) {
			if (childResource.getType() == IResource.FOLDER && !childResource.isDerived()) {
				IPath location = childResource.getLocation();
				if (location == null) {
					continue;
				}
				boolean excluded = false;
				if (directoriesToExclude != null) {
					for (IPath excludedPath : directoriesToExclude) {
						if (!excludedPath.isPrefixOf(parentLocation) && excludedPath.isPrefixOf(location)) {
							excluded = true;
						}
					}
				}
				if (!excluded) {
					childrenToProcess.add((IFolder)childResource);
				}
			}
		}

		Set<CrawlFolderJob> jobs = new HashSet<>();
		for (final IFolder childFolder : childrenToProcess) {
			CrawlFolderJob crawlerJob = new CrawlFolderJob(
					NLS.bind(DataTransferMessages.SmartImportJob_crawling,
							SmartImportWizard.toAbsolutePath(childFolder)),
					childFolder, res);
			if (crawlerJobGroup.getMaxThreads() == 0 || crawlerJobGroup.getActiveJobs().size() < crawlerJobGroup.getMaxThreads()) {
				crawlerJob.setJobGroup(crawlerJobGroup);
				jobs.add(crawlerJob);
				crawlerJob.schedule();
			} else {
				crawlerJob.run(subMonitor);
				subMonitor.worked(1);
			}
		}
		for (CrawlFolderJob job : jobs) {
			job.join(0, subMonitor.split(1));
		}
		subMonitor.done();
		return res;
	}

	private Set<IProject> importProjectAndChildrenRecursively(final IContainer container, boolean forceFullProjectCheck,
			IProgressMonitor progressMonitor) throws Exception {
		Set<IProject> projectFromCurrentContainer = new HashSet<>();
		final IPath containerLocation = container.getLocation();
		if (containerLocation == null) {
			return projectFromCurrentContainer;
		}
		int allWork = 30 + ProjectConfiguratorExtensionManager.getAllExtensionLabels().size() * 5;
		SubMonitor subMonitor = SubMonitor.convert(progressMonitor,
				NLS.bind(DataTransferMessages.SmartImportJob_inspecting,
						SmartImportWizard.toAbsolutePath(container)),
				allWork);
		boolean isAlreadyAnEclipseProject = false;
		Set<ProjectConfigurator> mainProjectConfigurators = new HashSet<>();
		Set<IPath> excludedPaths = new HashSet<>();
		if (this.excludedDirectories != null) {
			for (File excludedDirectory : this.excludedDirectories) {
				excludedPaths.add(IPath.fromOSString(excludedDirectory.getAbsolutePath()));
			}
		}
		container.refreshLocal(IResource.DEPTH_INFINITE, progressMonitor);
		if (!forceFullProjectCheck) {
			EclipseProjectConfigurator eclipseProjectConfigurator = new EclipseProjectConfigurator();
			if (eclipseProjectConfigurator.shouldBeAnEclipseProject(container, subMonitor.split(1))) {
				isAlreadyAnEclipseProject = true;
			}
		}

		if (this.configurationManager == null) {
			this.configurationManager = new ProjectConfiguratorExtensionManager();
		}
		Collection<ProjectConfigurator> activeConfigurators = this.configurationManager.getAllActiveProjectConfigurators(container);
		Set<ProjectConfigurator> potentialSecondaryConfigurators = new HashSet<>();
		IProject project = null;
		for (ProjectConfigurator configurator : activeConfigurators) {
			// exclude Eclipse project configurator for root project if is new
			if (configurator instanceof EclipseProjectConfigurator && forceFullProjectCheck) {
				continue;
			}
			if (configurator.shouldBeAnEclipseProject(container, subMonitor.split(1))) {
				mainProjectConfigurators.add(configurator);
				if (project == null) {
					// Create project
					try {
						project = toExistingOrNewProject(containerLocation.toFile(), subMonitor.split(1),
								IResource.BACKGROUND_REFRESH);
					} catch (CouldNotImportProjectException ex) {
						this.errors.put(containerLocation, ex);
						if (this.listener != null) {
							this.listener.errorHappened(containerLocation, ex);
						}
						return projectFromCurrentContainer;
					}
					projectFromCurrentContainer.add(project);
				}
			} else {
				potentialSecondaryConfigurators.add(configurator);
			}
		}

		if (!mainProjectConfigurators.isEmpty()) {
			project.refreshLocal(IResource.DEPTH_INFINITE, subMonitor.split(1));
		}
		for (ProjectConfigurator configurator : mainProjectConfigurators) {
			IProgressMonitor childMonitor = subMonitor.split(1);
			if (configurator instanceof EclipseProjectConfigurator || !isAlreadyAnEclipseProject || this.reconfigureEclipseProjects) {
				configurator.configure(project, excludedPaths, childMonitor);
				this.report.get(project).add(configurator);
				if (this.listener != null) {
					listener.projectConfigured(project, configurator);
				}
			}
			excludedPaths.addAll(toPathSet(configurator.getFoldersToIgnore(project, subMonitor.split(20))));
		}

		if (deepChildrenDetection) {
			Set<IProject> allNestedProjects = searchAndImportChildrenProjectsRecursively(container, excludedPaths,
					progressMonitor);
			projectFromCurrentContainer.addAll(allNestedProjects);
		}
		// exclude all known children projects
		for (IProject other : container.getWorkspace().getRoot().getProjects()) {
			IPath otherLocation = other.getLocation();
			if (otherLocation != null && !containerLocation.equals(otherLocation)
					&& containerLocation.isPrefixOf(otherLocation)) {
				excludedPaths.add(otherLocation);
			}
		}

		if (mainProjectConfigurators.isEmpty() && (!isAlreadyAnEclipseProject || forceFullProjectCheck)) {
			// Apply secondary configurators
			if (project == null) {
				// Create project
				try {
					project = toExistingOrNewProject(containerLocation.toFile(), subMonitor.split(1),
							IResource.BACKGROUND_REFRESH);
				} catch (CouldNotImportProjectException ex) {
					this.errors.put(containerLocation, ex);
					if (this.listener != null) {
						this.listener.errorHappened(containerLocation, ex);
					}
					return projectFromCurrentContainer;
				}
				projectFromCurrentContainer.add(project);
			}
			project.refreshLocal(IResource.DEPTH_ONE, subMonitor.split(1));
			// At least depth one, maybe INFINITE is necessary
			progressMonitor.setTaskName(
					NLS.bind(DataTransferMessages.SmartImportJob_continuingConfiguration, project.getName()));
			for (ProjectConfigurator additionalConfigurator : potentialSecondaryConfigurators) {
				if (additionalConfigurator.canConfigure(project, excludedPaths, subMonitor.split(1))) {
					additionalConfigurator.configure(project, excludedPaths, subMonitor.split(1));
					this.report.get(project).add(additionalConfigurator);
					if (this.listener != null) {
						listener.projectConfigured(project, additionalConfigurator);
					}
					excludedPaths
							.addAll(toPathSet(additionalConfigurator.getFoldersToIgnore(project, subMonitor.split(1))));
				}
			}
		}
		if (project != null) {
			// make sure this folder isn't going to be processed again
			excludedPaths.add(project.getLocation());
		}
		subMonitor.done();
		return projectFromCurrentContainer;
	}

	private Set<IPath> toPathSet(Set<? extends IContainer> resources) {
		if (resources == null || resources.isEmpty()) {
			return Collections.emptySet();
		}
		Set<IPath> res = new HashSet<>();
		for (IContainer container : resources) {
			IPath location = container.getLocation();
			if (location != null) {
				res.add(location);
			}
		}
		return res;
	}

	/**
	 * @param directory
	 * @param workingSets
	 * @param refreshMode One {@link IResource#BACKGROUND_REFRESH} for background refresh, or {@link IResource#NONE} for immediate refresh
	 * @return
	 * @throws Exception
	 */
	private IProject toExistingOrNewProject(File directory, IProgressMonitor progressMonitor, int refreshMode) throws CouldNotImportProjectException {
		try {
			SubMonitor subMonitor = SubMonitor.convert(progressMonitor, NLS.bind(
					DataTransferMessages.SmartImportJob_importingProjectIntoWorkspace, directory.getAbsolutePath()), 2);
			IProject project = projectAlreadyExistsInWorkspace(directory);
			if (project == null) {
				project = createOrImportProject(directory, subMonitor.split(1));
			}
			subMonitor.setWorkRemaining(1);

			project.open(refreshMode, subMonitor.split(1));
			if (!this.report.containsKey(project)) {
				this.report.put(project, new ArrayList<>());
			}
			if (this.listener != null) {
				this.listener.projectCreated(project);
			}
			return project;
		} catch (Exception ex) {
			throw new CouldNotImportProjectException(directory, ex);
		}
	}


	private IProject projectAlreadyExistsInWorkspace(File directory) {
		for (IProject project : workspaceRoot.getProjects()) {
			File file = SmartImportWizard.toFile(project);
			if (file == null) {
				continue;
			}
			if (file.getAbsoluteFile().equals(directory.getAbsoluteFile())) {
				return project;
			}
		}
		return null;
	}

	private IProject createOrImportProject(File directory, IProgressMonitor progressMonitor) throws Exception {
		IProjectDescription desc = null;
		File expectedProjectDescriptionFile = new File(directory, IProjectDescription.DESCRIPTION_FILE_NAME);
		if (expectedProjectDescriptionFile.exists()) {
			desc = ResourcesPlugin.getWorkspace().loadProjectDescription(IPath.fromOSString(expectedProjectDescriptionFile.getAbsolutePath()));
			String expectedName = desc.getName();
			IProject projectWithSameName = this.workspaceRoot.getProject(expectedName);
			if (projectWithSameName.exists()) {
				if (directory.equals(SmartImportWizard.toFile(projectWithSameName))) {
					// project seems already there
					return projectWithSameName;
				}
				throw new CouldNotImportProjectException(directory,
						NLS.bind(DataTransferMessages.SmartImportProposals_anotherProjectWithSameNameExists_description, expectedName));
			}
		} else {
			String projectName = generateNewProjectName(directory, this.workspaceRoot);
			desc = ResourcesPlugin.getWorkspace().newProjectDescription(projectName);
		}
		desc.setLocation(IPath.fromOSString(directory.getAbsolutePath()));
		IProject res = this.workspaceRoot.getProject(desc.getName());
		res.create(desc, progressMonitor);
		PlatformUI.getWorkbench().getWorkingSetManager().addToWorkingSets(res, this.workingSets);
		return res;
	}

	private static String generateNewProjectName(File directory, IWorkspaceRoot root) {
		String projectName = directory.getName();
		if (root.getProject(projectName).exists()) {
			StringBuilder projectNameBuilder = new StringBuilder(projectName);
			File currentDirectory = directory;
			while (currentDirectory.canRead() && directory.getParentFile() != null
					&& root.getProject(projectNameBuilder.toString()).exists()) {
				projectNameBuilder.insert(0, "_"); //$NON-NLS-1$
				projectNameBuilder.insert(0, currentDirectory.getParentFile().getName());
				currentDirectory = currentDirectory.getParentFile();
			}
			if (!root.getProject(projectNameBuilder.toString()).exists()) {
				projectName = projectNameBuilder.toString();
			} else {
				int i = 1;
				do {
					projectName = directory.getName() + '(' + i + ')';
					i++;
				} while (root.getProject(projectName).exists());
			}
		}
		return projectName;
	}

	/**
	 *
	 * @return the project found/created for the root folder
	 */
	public IProject getRootProject() {
		return this.rootProject;
	}

	/**
	 *
	 * @return The list of projects found/imported and the strategy that were
	 *         used in order to configure them.
	 */
	public Map<IProject, List<ProjectConfigurator>> getConfiguredProjects() {
		return this.report;
	}

	/**
	 * @return the import errors that happened.
	 */
	public Map<IPath, Exception> getErrors() {
		return this.errors;
	}

	/**
	 *
	 * @param monitor
	 * @return the proposals for the import operation.
	 */
	public Map<File, List<ProjectConfigurator>> getImportProposals(IProgressMonitor monitor) {
		if (!this.deepChildrenDetection) {
			Map<File, List<ProjectConfigurator>> res = new HashMap<>();
			res.put(rootDirectory, Collections.emptyList());
			return res;
		}
		if (this.importProposals == null) {
			Map<File, List<ProjectConfigurator>> res = new HashMap<>();
			if (this.configurationManager == null) {
				this.configurationManager = new ProjectConfiguratorExtensionManager();
			}
			List<ProjectConfigurator> activeConfigurators = configurationManager
					.getAllActiveProjectConfigurators(this.rootDirectory);
			SubMonitor loopMonitor = SubMonitor.convert(monitor, activeConfigurators.size());
			for (ProjectConfigurator configurator : activeConfigurators) {
				Set<File> supportedDirectories = configurator.findConfigurableLocations(
						SmartImportJob.this.rootDirectory,
						loopMonitor.split(1));
				if (supportedDirectories != null) {
					for (File supportedDirectory : supportedDirectories) {
						if (supportedDirectory.isDirectory()) {
							if (!res.containsKey(supportedDirectory)) {
								res.put(supportedDirectory, new ArrayList<>());
							}
							res.get(supportedDirectory).add(configurator);
						} else {
							IDEWorkbenchPlugin.log("Project detection must return only directories.\n" //$NON-NLS-1$
									+ supportedDirectory + " is not a directory.\nContributed by " //$NON-NLS-1$
									+ configurator.getClass().getName());
						}
					}
				}
			}
			for (ProjectConfigurator configurator : activeConfigurators) {
				configurator.removeDirtyDirectories(res);
			}
			this.importProposals = res;
		}
		return this.importProposals;
	}

	/**
	 * @return whether the job is set to configure projects (set natures and
	 *         other).
	 */
	public boolean isConfigureProjects() {
		return this.configureProjects;
	}

	/**
	 *
	 * @return whether the job will look for nested projects in case no
	 *         directory is passed to {@link #setDirectoriesToImport(Set)}
	 */
	public boolean isDetectNestedProjects() {
		return this.deepChildrenDetection;
	}

	/**
	 * Sets whether the job should look for nested projects. This value is
	 * ignored if consumer specifies directories to import via
	 * {@link #setDirectoriesToImport(Set)}.
	 *
	 * @param detectNestedProjects
	 */
	public void setDetectNestedProjects(boolean detectNestedProjects) {
		this.deepChildrenDetection = detectNestedProjects;
	}

	/**
	 * @param closeProjectsAfterImport
	 *            if true, imported projects are closed in the workspace
	 */
	void setCloseProjectsAfterImport(boolean closeProjectsAfterImport) {
		this.closeProjectsAfterImport = closeProjectsAfterImport;
	}

	/**
	 * Forget the initial import proposals.
	 */
	public void resetProposals() {
		this.importProposals = null;
	}

	/**
	 *
	 * @return The directories that will be crawled for import
	 */
	public Set<File> getDirectoriesToImport() {
		return this.directoriesToImport;
	}

	@Override
	public boolean belongsTo(Object family) {
		return family == SmartImportJob.class;
	}

}
