/*******************************************************************************
 * Copyright (c) 2000, 2023 IBM Corporation 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:
 *		IBM Corporation - initial API and implementation
 *		Stephan Herrmann - Contribution for Bug 378024 - Ordering of comments between imports not preserved
 *		John Glassmyer <jogl@google.com> - import group sorting is broken - https://bugs.eclipse.org/430303
 *******************************************************************************/
package org.eclipse.jdt.internal.core.dom.rewrite.imports;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.dom.rewrite.imports.ConflictIdentifier.Conflicts;
import org.eclipse.jdt.internal.core.util.Util;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.text.edits.TextEdit;

/**
 * Allows the caller to specify imports to be added to or removed from a compilation unit and
 * creates a TextEdit which, applied to the compilation unit, effects the specified additions and
 * removals.
 * <p>
 * Operates in either of two modes (selected via {@link ImportRewriteConfiguration.Builder}'s
 * static factory methods):
 * <ul>
 * <li>Discarding original imports and totally sorting all imports added thereafter. This mode is
 * used by the Organize Imports operation.</li>
 * <li>Preserving original imports and placing each added import adjacent to the one most closely
 * matching it. This mode is used e.g. when Content Assist adds an import for a completed name.</li>
 * </ul>
 */
public final class ImportRewriteAnalyzer {
	/**
	 * Encapsulates, for a computed import rewrite, a {@code TextEdit} that can be applied to effect
	 * the rewrite as well as the names of imports created by the rewrite.
	 */
	public static final class RewriteResult {
		private final TextEdit textEdit;
		private final Set<ImportName> createdImports;

		RewriteResult(TextEdit textEdit, Set<ImportName> createdImports) {
			this.textEdit = textEdit;
			this.createdImports = Collections.unmodifiableSet(createdImports);
		}

		/**
		 * Returns a {@link TextEdit} describing the changes necessary to perform the rewrite.
		 */
		public TextEdit getTextEdit() {
			return this.textEdit;
		}

		public String[] getCreatedImports() {
			return extractQualifiedNames(false, this.createdImports);
		}

		public String[] getCreatedStaticImports() {
			return extractQualifiedNames(true, this.createdImports);
		}

		private String[] extractQualifiedNames(boolean b, Collection<ImportName> imports) {
			List<String> names = new ArrayList<String>(imports.size());
			for (ImportName importName : imports) {
				if (importName.isStatic == b) {
					names.add(importName.qualifiedName);
				}
			}

			return names.toArray(new String[names.size()]);
		}
	}

	/**
	 * Returns the value of the formatter option specifying how many blank lines to insert between
	 * import groups.
	 */
	private static int getBlankLinesBetweenImportGroups(IJavaProject javaProject) {
		int num = -1;

		String blankLinesOptionValue =
				javaProject.getOption(DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_BETWEEN_IMPORT_GROUPS, true);
		try {
			num = Integer.parseInt(blankLinesOptionValue);
		} catch (NumberFormatException e) {
			String message = String.format(
					"Could not parse the value of %s as an integer: %s", //$NON-NLS-1$
					DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_BETWEEN_IMPORT_GROUPS,
					blankLinesOptionValue);
			Util.log(new Status(IStatus.WARNING, JavaCore.PLUGIN_ID, message, e));
		}

		return num >= 0 ? num : 1;
	}

	/**
	 * Returns the value of the formatter option specifying whether to insert a space between the
	 * imported name and the semicolon in an import declaration.
	 */
	private static boolean shouldInsertSpaceBeforeSemicolon(IJavaProject javaProject) {
		return JavaCore.INSERT.equals(
				javaProject.getOption(DefaultCodeFormatterConstants.FORMATTER_INSERT_SPACE_BEFORE_SEMICOLON, true));
	}

	/**
	 * Reads the positions of each existing import declaration along with any associated comments,
	 * and returns these in a list whose iteration order reflects the existing order of the imports
	 * in the compilation unit.
	 */
	private static List<OriginalImportEntry> readOriginalImports(CompilationUnit compilationUnit) {
		List<ImportDeclaration> importDeclarations = compilationUnit.imports();

		if (importDeclarations.isEmpty()) {
			return Collections.emptyList();
		}

		List<Comment> comments = compilationUnit.getCommentList();

		int currentCommentIndex = 0;

		// Skip over package and file header comments (see https://bugs.eclipse.org/121428).
		ImportDeclaration firstImport = importDeclarations.get(0);
		PackageDeclaration packageDeclaration = compilationUnit.getPackage();
		int firstImportStartPosition = packageDeclaration == null
				? firstImport.getStartPosition()
				: compilationUnit.getExtendedStartPosition(packageDeclaration)
						+ compilationUnit.getExtendedLength(packageDeclaration);
		while (currentCommentIndex < comments.size()
				&& comments.get(currentCommentIndex).getStartPosition() < firstImportStartPosition) {
			currentCommentIndex++;
		}

		List<OriginalImportEntry> imports = new ArrayList<OriginalImportEntry>(importDeclarations.size());
		int previousExtendedEndPosition = -1;
		for (ImportDeclaration currentImport : importDeclarations) {
			int extendedEndPosition = compilationUnit.getExtendedStartPosition(currentImport)
					+ compilationUnit.getExtendedLength(currentImport);

			int commentAfterImportIndex = currentCommentIndex;
			while (commentAfterImportIndex < comments.size()
					&& comments.get(commentAfterImportIndex).getStartPosition() < extendedEndPosition) {
				commentAfterImportIndex++;
			}

			List<ImportComment> importComments;
			if (commentAfterImportIndex == currentCommentIndex) {
				importComments = Collections.emptyList();
			} else {
				importComments = selectImportComments(
						compilationUnit,
						comments,
						currentImport.getStartPosition(),
						currentCommentIndex,
						commentAfterImportIndex);
			}

			int importAndCommentsStartPosition = importComments.isEmpty()
					? currentImport.getStartPosition()
							: Math.min(currentImport.getStartPosition(), importComments.get(0).region.getOffset());

			IRegion leadingWhitespaceRegion;
			int precedingLineDelimiters;
			if (previousExtendedEndPosition == -1) {
				leadingWhitespaceRegion = new Region(importAndCommentsStartPosition, 0);
				precedingLineDelimiters = 0;
			} else {
				leadingWhitespaceRegion = new Region(
						previousExtendedEndPosition, importAndCommentsStartPosition - previousExtendedEndPosition);
				int importAndCommentsFirstLine = compilationUnit.getLineNumber(importAndCommentsStartPosition);
				int lastLineOfPrevious = compilationUnit.getLineNumber(previousExtendedEndPosition - 1);
				precedingLineDelimiters = importAndCommentsFirstLine - lastLineOfPrevious;
			}
			IRegion importAndCommentsRegion =
					new Region(importAndCommentsStartPosition, extendedEndPosition - importAndCommentsStartPosition);

			imports.add(new OriginalImportEntry(
					ImportName.createFor(currentImport),
					importComments,
					precedingLineDelimiters,
					leadingWhitespaceRegion,
					importAndCommentsRegion));

			currentCommentIndex = commentAfterImportIndex;
			previousExtendedEndPosition = extendedEndPosition;
		}

		return imports;
	}

	private static List<ImportComment> selectImportComments(
			CompilationUnit compilationUnit,
			List<Comment> comments,
			int importDeclarationStartPosition,
			int commentStartIndex,
			int commentEndIndex) {
		List<ImportComment> importComments = new ArrayList<ImportComment>(comments.size());

		Iterator<Comment> commentIterator = comments.subList(commentStartIndex, commentEndIndex).iterator();
		Comment currentComment = commentIterator.hasNext() ? commentIterator.next() : null;
		while (currentComment != null) {
			int currentCommentStartPosition = currentComment.getStartPosition();
			int currentCommentLength = currentComment.getLength();

			Comment nextComment = commentIterator.hasNext() ? commentIterator.next() : null;

			int succeedingLineDelims;
			int nextCommentStartPosition = nextComment == null ? Integer.MAX_VALUE : nextComment.getStartPosition();
			int nextStartPosition = Math.min(importDeclarationStartPosition, nextCommentStartPosition);
			if (nextStartPosition == Integer.MAX_VALUE) {
				// This trailing comment is located at the end of the import's extended range
				// and we don't care how many line delimiters follow it.
				succeedingLineDelims = 0;
			} else {
				int currentCommentEndLine =
						compilationUnit.getLineNumber(currentCommentStartPosition + currentCommentLength);
				int nextStartLine = compilationUnit.getLineNumber(nextStartPosition);
				succeedingLineDelims = nextStartLine - currentCommentEndLine;
			}

			importComments.add(new ImportComment(
					new Region(currentCommentStartPosition, currentCommentLength), succeedingLineDelims));

			currentComment = nextComment;
		}

		return importComments;
	}

	private static RewriteSite determineRewriteSite(
			CompilationUnit compilationUnit, List<OriginalImportEntry> originalImports) throws JavaModelException {
		IRegion importsRegion = determineImportsRegion(originalImports);

		IRegion surroundingRegion = determineSurroundingRegion(compilationUnit, importsRegion);

		boolean hasPrecedingElements = surroundingRegion.getOffset() != 0;

		boolean hasSucceedingElements =
				surroundingRegion.getOffset() + surroundingRegion.getLength() != compilationUnit.getLength();

		return new RewriteSite(
				surroundingRegion,
				importsRegion,
				hasPrecedingElements,
				hasSucceedingElements);
	}

	/**
	 * Determines the region originally occupied by imports and their associated comments.
	 * <p>
	 * Returns null if originalImports is null or empty.
	 */
	private static IRegion determineImportsRegion(List<OriginalImportEntry> originalImports) {
		if (originalImports == null || originalImports.isEmpty()) {
			return null;
		}

		OriginalImportEntry firstImport = originalImports.get(0);
		int start = firstImport.declarationAndComments.getOffset();

		OriginalImportEntry lastImport = originalImports.get(originalImports.size() - 1);
		int end = lastImport.declarationAndComments.getOffset()
				+ lastImport.declarationAndComments.getLength();

		return new Region(start, end - start);
	}

	/**
	 * Determines the region to be occupied by imports, their associated comments, and surrounding
	 * whitespace.
	 */
	private static IRegion determineSurroundingRegion(CompilationUnit compilationUnit, IRegion importsRegion) throws JavaModelException {
		NavigableMap<Integer, ASTNode> nodesTreeMap = mapTopLevelNodes(compilationUnit);

		int surroundingStart;
		int positionAfterImports;
		if (importsRegion == null) {
			PackageDeclaration packageDeclaration = compilationUnit.getPackage();
			if (packageDeclaration != null) {
				surroundingStart = compilationUnit.getExtendedStartPosition(packageDeclaration)
						+ compilationUnit.getExtendedLength(packageDeclaration);
			}
			else {
				surroundingStart = 0;
				if (!nodesTreeMap.isEmpty()) {
					ASTNode topNode = nodesTreeMap.firstEntry().getValue();
					if (topNode instanceof Comment) {
						for (ASTNode node : nodesTreeMap.values()) {
							if (!(node instanceof Comment)) {
								break;
							} else {
								surroundingStart = node.getStartPosition() + node.getLength();
							}
						}
					}
				}
			}

			positionAfterImports = surroundingStart;
		} else {
			Entry<Integer, ASTNode> lowerEntry = nodesTreeMap.lowerEntry(importsRegion.getOffset());
			if (lowerEntry != null) {
				ASTNode precedingNode = lowerEntry.getValue();
				surroundingStart = precedingNode.getStartPosition() + precedingNode.getLength();
			} else {
				surroundingStart = 0;
			}

			positionAfterImports = importsRegion.getOffset() + importsRegion.getLength();
		}

		int surroundingEnd = positionAfterImports;
		IBuffer buffer = compilationUnit.getTypeRoot().getBuffer();
		int length = buffer.getLength();
		while (surroundingEnd < length && Character.isWhitespace(buffer.getChar(surroundingEnd))) {
			surroundingEnd++;
		}

		return new Region(surroundingStart, surroundingEnd - surroundingStart);
	}

	/**
	 * Builds a NavigableMap containing all of the given compilation unit's top-level nodes
	 * (package declaration, import declarations, type declarations, and non-doc comments),
	 * keyed by start position.
	 */
	private static NavigableMap<Integer, ASTNode> mapTopLevelNodes(CompilationUnit compilationUnit) {
		NavigableMap<Integer, ASTNode> map = new TreeMap<Integer, ASTNode>();

		Collection<ASTNode> nodes = new ArrayList<ASTNode>();
		if (compilationUnit.getPackage() != null) {
			nodes.add(compilationUnit.getPackage());
		}
		nodes.addAll(compilationUnit.imports());
		nodes.addAll(compilationUnit.types());
		for (Comment comment : ((List<Comment>) compilationUnit.getCommentList())) {
			// Include only top-level (non-doc) comments;
			// doc comments are contained within their parent nodes' ranges.
			if (comment.getParent() == null) {
				nodes.add(comment);
			}
		}

		for (ASTNode node : nodes) {
			map.put(node.getStartPosition(), node);
		}

		return map;
	}

	/**
	 * Builds an {@code IdentityHashMap} having the elements of {@code imports} as values and each
	 * element's {@code importName} as corresponding key. This map can be used to recall the {@code
	 * ImportEntry} corresponding to a given {@code ImportName} instance even when there are
	 * duplicate import declarations (where multiple {@code ImportEntry}s have equal, but not
	 * identical, {@code ImportName}s).
	 */
	private static Map<ImportName, OriginalImportEntry> mapImportsByNameIdentity(List<OriginalImportEntry> imports) {
		Map<ImportName, OriginalImportEntry> importsByName = new IdentityHashMap<ImportName, OriginalImportEntry>();

		for (OriginalImportEntry currentImport : imports) {
			importsByName.put(currentImport.importName, currentImport);
		}

		return Collections.unmodifiableMap(importsByName);
	}

	/**
	 * Returns a new {@code List} containing those elements of {@code imports} (in their existing
	 * order) not contained in {@code importsToSubtract}.
	 */
	private static List<ImportName> subtractImports(
			Collection<ImportName> existingImports, Set<ImportName> importsToSubtract) {
		List<ImportName> remainingImports = new ArrayList<ImportName>(existingImports.size());
		for (ImportName existingImport : existingImports) {
			if (!importsToSubtract.contains(existingImport)) {
				remainingImports.add(existingImport);
			}
		}
		return remainingImports;
	}

	private final List<OriginalImportEntry> originalImportEntries;
	private final List<ImportName> originalImportsList;
	private final Set<ImportName> originalImportsSet;

	private final ImportDeclarationWriter importDeclarationWriter;

	private final ImportAdder importAdder;

	private final Set<ImportName> importsToAdd;
	private final Set<ImportName> importsToRemove;

	private final boolean reportAllResultantImportsAsCreated;

	private final Set<String> typeExplicitSimpleNames;
	private final Set<String> staticExplicitSimpleNames;

	private final Set<String> implicitImportContainerNames;

	private final ConflictIdentifier conflictIdentifier;

	private final OnDemandComputer onDemandComputer;

	private final Map<ImportName, OriginalImportEntry> importsByNameIdentity;

	private final String lineDelimiter;

	private final ImportEditor importEditor;

	public ImportRewriteAnalyzer(
			ICompilationUnit cu,
			CompilationUnit astRoot,
			ImportRewriteConfiguration configuration) throws JavaModelException {
		this.originalImportEntries = Collections.unmodifiableList(readOriginalImports(astRoot));

		List<ImportName> importsList = new ArrayList<ImportName>(this.originalImportEntries.size());
		Set<ImportName> importsSet = new HashSet<ImportName>();
		for (ImportEntry originalImportEntry : this.originalImportEntries) {
			ImportName importName = originalImportEntry.importName;
			importsList.add(importName);
			importsSet.add(importName);
		}
		this.originalImportsList = Collections.unmodifiableList(importsList);
		this.originalImportsSet = Collections.unmodifiableSet(importsSet);

		this.importsToAdd = new LinkedHashSet<ImportName>();

		this.importsToRemove = new LinkedHashSet<ImportName>();
		if (configuration.originalImportHandling.shouldRemoveOriginalImports()) {
			this.importsToRemove.addAll(importsSet);
			this.reportAllResultantImportsAsCreated = true;
		} else {
			this.reportAllResultantImportsAsCreated = false;
		}

		this.typeExplicitSimpleNames = new HashSet<String>();
		this.staticExplicitSimpleNames = new HashSet<String>();

		ImportGroupComparator importGroupComparator = new ImportGroupComparator(configuration.importOrder);

		JavaProject javaProject = (JavaProject) cu.getJavaProject();

		this.importAdder = configuration.originalImportHandling.createImportAdder(new ImportComparator(
				importGroupComparator,
				configuration.typeContainerSorting.createContainerComparator(javaProject),
				configuration.staticContainerSorting.createContainerComparator(javaProject)));

		this.implicitImportContainerNames =
				configuration.implicitImportIdentification.determineImplicitImportContainers(cu);

		this.onDemandComputer = new OnDemandComputer(
				configuration.typeOnDemandThreshold,
				configuration.staticOnDemandThreshold);

		this.conflictIdentifier = new ConflictIdentifier(
				this.onDemandComputer,
				new TypeConflictingSimpleNameFinder(javaProject, new SearchEngine()),
				new StaticConflictingSimpleNameFinder(javaProject),
				this.implicitImportContainerNames);

		this.importsByNameIdentity = mapImportsByNameIdentity(this.originalImportEntries);

		this.importDeclarationWriter = new ImportDeclarationWriter(shouldInsertSpaceBeforeSemicolon(javaProject));

		this.lineDelimiter = cu.findRecommendedLineSeparator();

		this.importEditor = new ImportEditor(
				this.lineDelimiter,
				configuration.originalImportHandling.shouldFixAllLineDelimiters(),
				getBlankLinesBetweenImportGroups(javaProject) + 1,
				importGroupComparator,
				this.originalImportEntries,
				determineRewriteSite(astRoot, this.originalImportEntries),
				this.importDeclarationWriter);
	}

	/**
	 * Specifies that applying the rewrite should result in the compilation unit containing the
	 * specified import.
	 * <p>
	 * Has no effect if the compilation unit otherwise would contain the given import.
	 * <p>
	 * Overrides any previous corresponding call to {@link #removeImport}.
	 */
	public void addImport(boolean isStatic, String qualifiedName) {
		ImportName importToAdd = ImportName.createFor(isStatic, qualifiedName);
		this.importsToAdd.add(importToAdd);
		this.importsToRemove.remove(importToAdd);
	}

	/**
	 * Specifies that applying the rewrite should result in the compilation unit not containing the
	 * specified import.
	 * <p>
	 * Has no effect if the compilation unit otherwise would not contain the given import.
	 * <p>
	 * Overrides any previous corresponding call to {@link #addImport}.
	 */
	public void removeImport(boolean isStatic, String qualifiedName) {
		ImportName importToRemove = ImportName.createFor(isStatic, qualifiedName);
		this.importsToAdd.remove(importToRemove);
		this.importsToRemove.add(importToRemove);
	}

	/**
	 * Specifies that any import of the given simple name must be explicit - that it may neither be
	 * reduced into an on-demand (".*") import nor be filtered as an implicit (e.g. "java.lang.*")
	 * import.
	 */
	public void requireExplicitImport(boolean isStatic, String simpleName) {
		if (isStatic) {
			this.staticExplicitSimpleNames.add(simpleName);
		} else {
			this.typeExplicitSimpleNames.add(simpleName);
		}
	}

	/**
	 * Computes and returns the result of performing the rewrite, incorporating all changes
	 * specified by calls to {@link #addImport}, {@link #removeImport}, and
	 * {@link #requireExplicitImport}.
	 * <p>
	 * This method has no side-effects.
	 */
	public RewriteResult analyzeRewrite(IProgressMonitor monitor) throws JavaModelException {
		List<ImportName> computedImportOrder = computeImportOrder(monitor);

		List<ImportEntry> resultingImportEntries = matchExistingOrCreateNew(computedImportOrder);

		TextEdit edit = this.importEditor.createTextEdit(resultingImportEntries);

		Set<ImportName> createdImports = new HashSet<ImportName>(computedImportOrder);
		if (!this.reportAllResultantImportsAsCreated) {
			createdImports.removeAll(this.originalImportsSet);
		}

		return new RewriteResult(edit, createdImports);
	}

	private List<ImportName> computeImportOrder(IProgressMonitor progressMonitor) throws JavaModelException {
		Set<ImportName> importsWithAdditionsAndRemovals = new HashSet<ImportName>(this.originalImportsSet);
		importsWithAdditionsAndRemovals.addAll(this.importsToAdd);
		importsWithAdditionsAndRemovals.removeAll(this.importsToRemove);

		Set<ImportName> touchedContainers = determineTouchedContainers();

		Conflicts conflicts = this.conflictIdentifier.identifyConflicts(
				importsWithAdditionsAndRemovals,
				touchedContainers,
				this.typeExplicitSimpleNames,
				this.staticExplicitSimpleNames,
				progressMonitor);

		Set<String> allTypeExplicitSimpleNames = new HashSet<String>(this.typeExplicitSimpleNames);
		allTypeExplicitSimpleNames.addAll(conflicts.typeConflicts);

		Set<String> allStaticExplicitSimpleNames = new HashSet<String>(this.staticExplicitSimpleNames);
		allStaticExplicitSimpleNames.addAll(conflicts.staticConflicts);

		Set<ImportName> implicitImports = identifyImplicitImports(this.importsToAdd, allTypeExplicitSimpleNames);
		List<ImportName> importsWithoutImplicits =
				subtractImports(importsWithAdditionsAndRemovals, implicitImports);

		Collection<OnDemandReduction> onDemandReductions = this.onDemandComputer.identifyPossibleReductions(
				new HashSet<ImportName>(importsWithoutImplicits),
				touchedContainers,
				allTypeExplicitSimpleNames,
				allStaticExplicitSimpleNames);

		ImportsDelta delta = computeDelta(implicitImports, onDemandReductions);

		List<ImportName> importsWithRemovals = subtractImports(this.originalImportsList, delta.importsToRemove);

		List<ImportName> importsWithAdditions = this.importAdder.addImports(importsWithRemovals, delta.importsToAdd);

		return importsWithAdditions;
	}

	private Set<ImportName> determineTouchedContainers() {
		Collection<ImportName> touchedContainers = new ArrayList<ImportName>(
				this.importsToAdd.size() + this.importsToRemove.size());

		for (ImportName addedImport : this.importsToAdd) {
			touchedContainers.add(addedImport.getContainerOnDemand());
		}
		for (ImportName removedImport : this.importsToRemove) {
			touchedContainers.add(removedImport.getContainerOnDemand());
		}

		return Collections.unmodifiableSet(new HashSet<ImportName>(touchedContainers));
	}

	private Set<ImportName> identifyImplicitImports(
			Collection<ImportName> addedImports, Set<String> allTypeExplicitSimpleNames) {
		if (this.implicitImportContainerNames.isEmpty()) {
			return Collections.emptySet();
		}

		Collection<ImportName> implicits = new ArrayList<ImportName>(addedImports.size());
		for (ImportName addedImport : addedImports) {
			boolean isImplicit = this.implicitImportContainerNames.contains(addedImport.containerName)
					&& !allTypeExplicitSimpleNames.contains(addedImport.simpleName);
			if (isImplicit) {
				implicits.add(addedImport);
			}
		}

		if (implicits.isEmpty()) {
			return Collections.emptySet();
		}

		return new HashSet<ImportName>(implicits);
	}

	private List<ImportEntry> matchExistingOrCreateNew(Collection<ImportName> importNames) {
		List<ImportEntry> importEntries = new ArrayList<ImportEntry>(importNames.size());
		for (ImportName importName : importNames) {
			ImportEntry importEntry = this.importsByNameIdentity.get(importName);

			if (importEntry == null) {
				importEntry = new NewImportEntry(importName);
			}

			importEntries.add(importEntry);
		}
		return importEntries;
	}

	private ImportsDelta computeDelta(
			Collection<ImportName> implicitImports, Collection<OnDemandReduction> onDemandReductions) {
		Collection<ImportName> additions = new ArrayList<ImportName>(this.originalImportsList.size());
		additions.addAll(this.importsToAdd);

		Collection<ImportName> removals = new ArrayList<ImportName>(this.originalImportsList.size());
		removals.addAll(this.importsToRemove);
		removals.addAll(implicitImports);

		additions.removeAll(removals);

		for (OnDemandReduction onDemandReduction : onDemandReductions) {
			additions.removeAll(onDemandReduction.reducibleImports);
			removals.addAll(onDemandReduction.reducibleImports);

			additions.add(onDemandReduction.containerOnDemand);
			removals.remove(onDemandReduction.containerOnDemand);
		}

		return new ImportsDelta(additions, removals);
	}
}
