/*******************************************************************************
 * Copyright (c) 2015 Google Inc and others.
 * 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
 *
 * Contributors:
 *     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.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.JavaProject;

/**
 * Used as a constructor parameter to ImportRewriteAnalyzer to configure its behavior.
 * <p>
 * The starting points are the two static factory methods of {@link Builder}.
 */
public final class ImportRewriteConfiguration {
	public enum OriginalImportHandling {
		/**
		 * Specifies to discard original imports and totally sort all new imports, as in the case of
		 * the "Organize Imports" operation.
		 */
		DISCARD {
			@Override
			boolean shouldRemoveOriginalImports() {
				return true;
			}

			@Override
			boolean shouldFixAllLineDelimiters() {
				return true;
			}

			@Override
			ImportAdder createImportAdder(Comparator<ImportName> importComparator) {
				return new ReorderingImportAdder(importComparator);
			}
		},
		/**
		 * Specifies to keep original imports in their original order, placing each newly added
		 * import adjacent to the original import that it most closely matches.
		 */
		PRESERVE_IN_ORDER {
			@Override
			boolean shouldRemoveOriginalImports() {
				return false;
			}

			@Override
			boolean shouldFixAllLineDelimiters() {
				return false;
			}

			@Override
			ImportAdder createImportAdder(Comparator<ImportName> importComparator) {
				return new OrderPreservingImportAdder(importComparator);
			}
		},
		;

		/**
		 * If true, ImportRewriteAnalyzer will, during its initialization, mark all original imports
		 * for removal.
		 */
		abstract boolean shouldRemoveOriginalImports();

		/**
		 * If true, line delimiters will be standardized between every pair of adjacent imports.
		 * Otherwise, line delimiters will be corrected only between pairs of adjacent imports that
		 * were not adjacent originally.
		 */
		abstract boolean shouldFixAllLineDelimiters();

		/**
		 * Creates the {@link ImportAdder} which will combine and order new and existing imports
		 * together.
		 */
		abstract ImportAdder createImportAdder(Comparator<ImportName> importComparator);
	}

	/**
	 * Specifies how to sort import declarations by their packages and/or containing types.
	 */
	public enum ImportContainerSorting {
		/**
		 * Sorts imports by each import's package and any containing types, in lexicographic order.
		 * For example (assuming that all of the imports belong to the same import group):
		 * <pre>
		 * import java.net.Socket;
		 * import java.util.Map;
		 * import java.util.Set;
		 * import java.util.Map.Entry;
		 * </pre>
		 */
		BY_PACKAGE_AND_CONTAINING_TYPE {
			@Override
			Comparator<ImportName> createContainerComparator(JavaProject javaProject) {
				return new PackageAndContainingTypeImportComparator();
			}
		},

		/**
		 * Sorts imports by each import's package, in lexicographic order. For example (assuming all
		 * of the imports belong to the same import group):
		 * <pre>
		 * import java.net.Socket;
		 * import java.util.Map;
		 * import java.util.Map.Entry;
		 * import java.util.Set;
		 * </pre>
		 */
		BY_PACKAGE {
			@Override
			Comparator<ImportName> createContainerComparator(JavaProject javaProject) {
				return new PackageImportComparator(javaProject);
			}
		},
		;

		abstract Comparator<ImportName> createContainerComparator(JavaProject javaProject);
	}

	/**
	 * Specifies which types are considered to be implicitly imported.
	 * <p>
	 * An import declaration of such a type will not be added to the compilation unit unless it is
	 * needed to resolve a conflict with an on-demand imports, or the type's simple name has been
	 * specified with {@link ImportRewriteAnalyzer#requireExplicitImport}.
	 * <p>
	 * Also, implicitly imported types will be considered for conflicts when deciding which types
	 * from other packages can be reduced into on-demand imports. E.g. if java.lang.Integer were
	 * considered to be implicitly imported, that would prevent an import of com.example.Integer
	 * from being reduced into an on-demand import of com.example.*.
	 */
	public enum ImplicitImportIdentification {
		/**
		 * Specifies that types from the following packages are considered to be implicitly
		 * imported:
		 * <ul>
		 * <li>java.lang</li>
		 * <li>the package of the compilation unit being rewritten</li>
		 * </ul>
		 */
		JAVA_LANG_AND_CU_PACKAGE {
			@Override
			Set<String> determineImplicitImportContainers(ICompilationUnit compilationUnit) {
				Set<String> implicitImportContainerNames = new HashSet<String>();

				implicitImportContainerNames.add("java.lang"); //$NON-NLS-1$

				IJavaElement packageFragment = compilationUnit.getParent();
				String compilationUnitPackageName = packageFragment.getElementName();
				if (compilationUnitPackageName.isEmpty() && !packageFragment.exists() && compilationUnit.exists()) {
					/*
					 * For a file outside of the build path, JavaCore#create(IFile) creates an
					 * ICompilationUnit with the file's parent folder as package fragment root, and a default package.
					 * That "wrong" package is problematic for the ImportRewrite, since it doesn't get filtered
					 * and eventually leads to unused import statements.
					 */
					try {
						IPackageDeclaration[] packageDeclarations = compilationUnit.getPackageDeclarations();
						if (packageDeclarations.length > 0) {
							implicitImportContainerNames.add(packageDeclarations[0].getElementName());
							return implicitImportContainerNames;
						}
					} catch (JavaModelException e) {
						// continue
					}
				}
				implicitImportContainerNames.add(compilationUnitPackageName);

				return implicitImportContainerNames;
			}
		},
		/**
		 * Specifies that no types are considered to be implicitly imported.
		 */
		NONE {
			@Override
			Set<String> determineImplicitImportContainers(ICompilationUnit compilationUnit) {
				return Collections.emptySet();
			}
		},
		;

		abstract Set<String> determineImplicitImportContainers(ICompilationUnit compilationUnit);
	}

	public static class Builder {
		public static Builder discardingOriginalImports() {
			return new Builder(OriginalImportHandling.DISCARD);
		}

		public static Builder preservingOriginalImports() {
			return new Builder(OriginalImportHandling.PRESERVE_IN_ORDER);
		}

		final OriginalImportHandling originalImportHandling;
		ImportContainerSorting typeContainerSorting;
		ImportContainerSorting staticContainerSorting;
		ImplicitImportIdentification implicitImportIdentification;
		List<String> importOrder;
		Integer typeOnDemandThreshold;
		Integer staticOnDemandThreshold;

		private Builder(OriginalImportHandling originalImportHandling) {
			this.originalImportHandling = originalImportHandling;
			this.typeContainerSorting = ImportContainerSorting.BY_PACKAGE;
			this.staticContainerSorting = ImportContainerSorting.BY_PACKAGE_AND_CONTAINING_TYPE;
			this.implicitImportIdentification = ImplicitImportIdentification.JAVA_LANG_AND_CU_PACKAGE;
			this.importOrder = Collections.emptyList();
			this.typeOnDemandThreshold = null;
			this.staticOnDemandThreshold = null;
		}

		public Builder setTypeContainerSorting(ImportContainerSorting typeContainerSorting) {
			this.typeContainerSorting = typeContainerSorting;
			return this;
		}

		public Builder setStaticContainerSorting(ImportContainerSorting staticContainerSorting) {
			this.staticContainerSorting = staticContainerSorting;
			return this;
		}

		public Builder setImplicitImportIdentification(ImplicitImportIdentification implicitImportIdentification) {
			this.implicitImportIdentification = implicitImportIdentification;
			return this;
		}

		public Builder setImportOrder(List<String> importOrder) {
			this.importOrder = Collections.unmodifiableList(new ArrayList<String>(importOrder));
			return this;
		}

		public Builder setTypeOnDemandThreshold(int typeOnDemandThreshold) {
			this.typeOnDemandThreshold = typeOnDemandThreshold;
			return this;
		}

		public Builder setStaticOnDemandThreshold(int staticOnDemandThreshold) {
			this.staticOnDemandThreshold = staticOnDemandThreshold;
			return this;
		}

		public ImportRewriteConfiguration build() {
			return new ImportRewriteConfiguration(this);
		}
	}

	final OriginalImportHandling originalImportHandling;
	final ImportContainerSorting typeContainerSorting;
	final ImportContainerSorting staticContainerSorting;
	final ImplicitImportIdentification implicitImportIdentification;
	final List<String> importOrder;
	final int typeOnDemandThreshold;
	final int staticOnDemandThreshold;

	ImportRewriteConfiguration(Builder builder) {
		this.originalImportHandling = builder.originalImportHandling;
		this.typeContainerSorting = builder.typeContainerSorting;
		this.staticContainerSorting = builder.staticContainerSorting;
		this.implicitImportIdentification = builder.implicitImportIdentification;
		this.importOrder = builder.importOrder;
		this.typeOnDemandThreshold = builder.typeOnDemandThreshold;
		this.staticOnDemandThreshold = builder.staticOnDemandThreshold;
	}
}
