/*******************************************************************************
 * Copyright (c) 2000, 2018 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
 *     Nikolay Metchev <nikolaymetchev@gmail.com> - Import static (Ctrl+Shift+M) creates imports for private methods - https://bugs.eclipse.org/409594
 *     Fabian Pfaff <fabian.pfaff@vogella.com> - Import static doesn't work when static method is referenced from subtype - https://github.com/eclipse-jdt/eclipse.jdt.ui/issues/276
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.codemanipulation;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;

import org.eclipse.core.resources.IWorkspaceRunnable;

import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NameQualifiedType;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;
import org.eclipse.jdt.core.manipulation.TypeKinds;
import org.eclipse.jdt.core.manipulation.TypeNameMatchCollector;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.TypeNameMatch;

import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.core.manipulation.dom.ASTResolving;
import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.util.JavaConventionsUtil;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.internal.ui.JavaUIStatus;


/**
 * Add imports to a compilation unit.
 * The input is an array of full qualified type names. No elimination of unnecessary
 * imports is done (use ImportStructure for this). Duplicates are not added.
 * If the compilation unit is open in an editor, be sure to pass over its working copy.
 */
public class AddImportsOperation implements IWorkspaceRunnable {

	public interface IChooseImportQuery {
		/**
		 * Selects an import from a list of choices.
		 * @param openChoices Array of found types
		 * @param containerName Name type to be imported
		 * @return Returns <code>null</code> to cancel the operation, or the
		 *         selected imports.
		 */
		TypeNameMatch chooseImport(TypeNameMatch[] openChoices, String containerName);
	}

	private ICompilationUnit fCompilationUnit;
	private final int fSelectionOffset;
	private final int fSelectionLength;
	private final IChooseImportQuery fQuery;
	private IStatus fStatus;
	private boolean fDoSave;
	private final boolean fApply;
	private MultiTextEdit fResultingEdit;

	/**
	 * Generate import statements for the passed java elements
	 * Elements must be of type IType (-> single import) or IPackageFragment
	 * (on-demand-import). Other JavaElements are ignored
	 * @param cu The compilation unit
	 * @param selectionOffset Start of the current text selection
	 * 	@param selectionLength End of the current text selection
	 * @param query Query element to be used for UI interaction or <code>null</code> to not select anything
	 * when multiple possibilities are available
	 * @param save If set, the result will be saved
	 */
	public AddImportsOperation(ICompilationUnit cu, int selectionOffset, int selectionLength, IChooseImportQuery query, boolean save) {
		this(cu, selectionOffset, selectionLength, query, save, true);
	}

	/**
	 * Generate import statements for the passed java elements
	 * Elements must be of type IType (-> single import) or IPackageFragment
	 * (on-demand-import). Other JavaElements are ignored
	 * @param cu The compilation unit
	 * @param selectionOffset Start of the current text selection
	 * 	@param selectionLength End of the current text selection
	 * @param query Query element to be used for UI interaction or <code>null</code> to not select anything
	 * when multiple possibilities are available
	 * @param save If set, the result will be saved
	 * @param apply If set, the resulting edit will be applied
	 */
	public AddImportsOperation(ICompilationUnit cu, int selectionOffset, int selectionLength, IChooseImportQuery query, boolean save, boolean apply) {
		super();
		Assert.isNotNull(cu);

		fCompilationUnit= cu;
		fSelectionOffset= selectionOffset;
		fSelectionLength= selectionLength;
		fQuery= query;
		fStatus= Status.OK_STATUS;
		fDoSave= save;
		fApply= apply;
	}

	/**
	 * @return Returns the status.
	 */
	public IStatus getStatus() {
		return fStatus;
	}

	/**
	 * Runs the operation.
	 *
	 * @param monitor The progress monitor
	 * @throws CoreException if accessing the CU or rewritting the imports fails
	 * @throws OperationCanceledException Runtime error thrown when operation is canceled.
	 */
	@Override
	public void run(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
		SubMonitor subMonitor= SubMonitor.convert(monitor, CodeGenerationMessages.AddImportsOperation_description, 4);

		CompilationUnit astRoot= SharedASTProviderCore.getAST(fCompilationUnit, SharedASTProviderCore.WAIT_YES, subMonitor.split(1));
		if (astRoot == null)
			throw new OperationCanceledException();

		ImportRewrite importRewrite= StubUtility.createImportRewrite(astRoot, true);

		MultiTextEdit res= new MultiTextEdit();

		TextEdit edit= evaluateEdits(astRoot, importRewrite, fSelectionOffset, fSelectionLength, subMonitor.split(1));
		if (edit == null) {
			return;
		}
		res.addChild(edit);

		TextEdit importsEdit= importRewrite.rewriteImports(subMonitor.split(1));
		res.addChild(importsEdit);

		fResultingEdit= res;

		if (fApply) {
			JavaModelUtil.applyEdit(fCompilationUnit, res, fDoSave, subMonitor.split(1));
		}
		subMonitor.setWorkRemaining(0);
	}

	/**
	 * Returns the resulting edit
	 *
	 * @return the resulting edit
	 */
	public MultiTextEdit getResultingEdit() {
		return fResultingEdit;
	}

	private TextEdit evaluateEdits(CompilationUnit root, ImportRewrite importRewrite, int offset, int length, IProgressMonitor monitor) throws JavaModelException {
		SubMonitor subMonitor= SubMonitor.convert(monitor, 1);
		SimpleName nameNode= null;
		if (root != null) { // got an AST
			ASTNode node= NodeFinder.perform(root, offset, length);
			if (node instanceof MarkerAnnotation) {
				node= ((Annotation) node).getTypeName();
			}
			if (node instanceof QualifiedName) {
				nameNode= ((QualifiedName) node).getName();
			} else if (node instanceof SimpleName) {
				nameNode= (SimpleName) node;
			}
		}

		String name, simpleName, containerName;
		int qualifierStart;
		int simpleNameStart;

		if (nameNode != null) {
			simpleName= nameNode.getIdentifier();
			simpleNameStart= nameNode.getStartPosition();
			if (nameNode.getLocationInParent() == QualifiedName.NAME_PROPERTY) {
				Name qualifier= ((QualifiedName) nameNode.getParent()).getQualifier();
				containerName= qualifier.getFullyQualifiedName();
				name= JavaModelUtil.concatenateName(containerName, simpleName);
				qualifierStart= qualifier.getStartPosition();
			} else if (nameNode.getLocationInParent() == NameQualifiedType.NAME_PROPERTY) {
				NameQualifiedType nameQualifiedType= (NameQualifiedType) nameNode.getParent();
				Name qualifier= nameQualifiedType.getQualifier();
				containerName= qualifier.getFullyQualifiedName();
				name= JavaModelUtil.concatenateName(containerName, simpleName);
				qualifierStart= qualifier.getStartPosition();
				List<Annotation> annotations= nameQualifiedType.annotations();
				if (!annotations.isEmpty()) { // don't remove annotations
					simpleNameStart= annotations.get(0).getStartPosition();
				}
			} else if (nameNode.getLocationInParent() == MethodInvocation.NAME_PROPERTY) {
				ASTNode qualifier= ((MethodInvocation) nameNode.getParent()).getExpression();
				if (qualifier instanceof Name) {
					containerName= ASTNodes.asString(qualifier);
					name= JavaModelUtil.concatenateName(containerName, simpleName);
					qualifierStart= qualifier.getStartPosition();
				} else {
					return null;
				}
			} else {
				containerName= ""; //$NON-NLS-1$
				name= simpleName;
				qualifierStart= simpleNameStart;
			}

			IBinding binding= nameNode.resolveBinding();
			if (binding != null && !binding.isRecovered()) {
				if (binding instanceof ITypeBinding) {
					ITypeBinding typeBinding= ((ITypeBinding) binding).getTypeDeclaration();
					String qualifiedBindingName= typeBinding.getQualifiedName();
					if (containerName.length() > 0 && !qualifiedBindingName.equals(name)) {
						return null;
					}

					ImportRewriteContext context= new ContextSensitiveImportRewriteContext(root, qualifierStart, importRewrite);
					String res= importRewrite.addImport(typeBinding, context);
					if (containerName.length() > 0 && !res.equals(simpleName)) {
						// adding import failed
						fStatus= JavaUIStatus.createError(IStatus.ERROR, CodeGenerationMessages.AddImportsOperation_error_importclash, null);
						return null;
					}
					if (containerName.length() == 0 && res.equals(simpleName)) {
						// no change necessary
						return null;
					}
					return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, ""); //$NON-NLS-1$
				} else if (JavaModelUtil.is50OrHigher(fCompilationUnit.getJavaProject()) && (binding instanceof IVariableBinding || binding instanceof IMethodBinding)) {
					boolean isField= binding instanceof IVariableBinding;
					ITypeBinding declaringClass= isField ? ((IVariableBinding) binding).getDeclaringClass() : ((IMethodBinding) binding).getDeclaringClass();
					if (declaringClass == null) {
						return null; // variableBinding.getDeclaringClass() is null for array.length
					}
					if (Modifier.isStatic(binding.getModifiers())) {
						if (containerName.length() > 0) {
							ASTNode node= nameNode.getParent();
							boolean isDirectlyAccessible= false;
							while (node != null) {
								if (isTypeDeclarationSubTypeCompatible(node, declaringClass)) {
									isDirectlyAccessible= true;
									break;
								}
								node= node.getParent();
							}
							if (!isDirectlyAccessible) {
								if (Modifier.isPrivate(declaringClass.getModifiers())) {
									fStatus= JavaUIStatus.createError(IStatus.ERROR,
											Messages.format(CodeGenerationMessages.AddImportsOperation_error_not_visible_class, BasicElementLabels.getJavaElementName(declaringClass.getName())),
											null);
									return null;
								}
								String res= importRewrite.addStaticImport(declaringClass.getQualifiedName(), binding.getName(), isField);
								if (!res.equals(simpleName)) {
									// adding import failed
									return null;
								}
							}
							return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, ""); //$NON-NLS-1$
						}
					}
					return null; // no static imports for packages
				} else {
					return null;
				}
			}
			if (binding != null && binding.getKind() != IBinding.TYPE) {
				// recovered binding
				return null;
			}

		} else {
			IBuffer buffer= fCompilationUnit.getBuffer();

			qualifierStart= getNameStart(buffer, offset);
			int nameEnd= getNameEnd(buffer, offset + length);
			int len= nameEnd - qualifierStart;
			name= buffer.getText(qualifierStart, len).trim();
			if (name.length() == 0) {
				return null;
			}

			simpleName= Signature.getSimpleName(name);
			containerName= Signature.getQualifier(name);

			IJavaProject javaProject= fCompilationUnit.getJavaProject();
			if (simpleName.length() == 0 || JavaConventionsUtil.validateJavaTypeName(simpleName, javaProject).matches(IStatus.ERROR)
					|| (containerName.length() > 0 && JavaConventionsUtil.validateJavaTypeName(containerName, javaProject).matches(IStatus.ERROR))) {
				fStatus= JavaUIStatus.createError(IStatus.ERROR, CodeGenerationMessages.AddImportsOperation_error_invalid_selection, null);
				return null;
			}

			simpleNameStart= getSimpleNameStart(buffer, qualifierStart, containerName);

			int res= importRewrite.getDefaultImportRewriteContext().findInContext(containerName, simpleName, ImportRewriteContext.KIND_TYPE);
			if (res == ImportRewriteContext.RES_NAME_CONFLICT) {
				fStatus= JavaUIStatus.createError(IStatus.ERROR, CodeGenerationMessages.AddImportsOperation_error_importclash, null);
				return null;
			} else if (res == ImportRewriteContext.RES_NAME_FOUND) {
				return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, ""); //$NON-NLS-1$
			}
		}
		IJavaSearchScope searchScope= SearchEngine.createJavaSearchScope(new IJavaElement[] { fCompilationUnit.getJavaProject() });

		TypeNameMatch[] types= findAllTypes(simpleName, searchScope, nameNode, subMonitor.split(1));
		if (types.length == 0) {
			fStatus= JavaUIStatus.createError(IStatus.ERROR, Messages.format(CodeGenerationMessages.AddImportsOperation_error_notresolved_message, BasicElementLabels.getJavaElementName(simpleName)), null);
			return null;
		}

		TypeNameMatch chosen;
		if (types.length > 1 && fQuery != null) {
			chosen= fQuery.chooseImport(types, containerName);
			if (chosen == null) {
				throw new OperationCanceledException();
			}
		} else {
			chosen= types[0];
		}
		ImportRewriteContext context= root == null ? null : new ContextSensitiveImportRewriteContext(root, simpleNameStart, importRewrite);
		importRewrite.addImport(chosen.getFullyQualifiedName(), context);
		return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, ""); //$NON-NLS-1$
	}


	private boolean isTypeDeclarationSubTypeCompatible(ASTNode typeDeclaration, ITypeBinding supertype) {
		if (typeDeclaration instanceof AbstractTypeDeclaration) {
			ITypeBinding binding= ((AbstractTypeDeclaration) typeDeclaration).resolveBinding();
			return binding != null && binding.isSubTypeCompatible(supertype);
		} else if (typeDeclaration instanceof AnonymousClassDeclaration) {
			ITypeBinding binding= ((AnonymousClassDeclaration) typeDeclaration).resolveBinding();
			return binding != null && binding.isSubTypeCompatible(supertype);
		} else {
			return false;
		}
	}

	private int getNameStart(IBuffer buffer, int pos) {
		while (pos > 0) {
			char ch= buffer.getChar(pos - 1);
			if (!Character.isJavaIdentifierPart(ch) && ch != '.') {
				return pos;
			}
			pos--;
		}
		return pos;
	}

	private int getNameEnd(IBuffer doc, int pos) {
		if (pos > 0) {
			if (Character.isWhitespace(doc.getChar(pos - 1))) {
				return pos;
			}
		}
		int len= doc.getLength();
		while (pos < len) {
			char ch= doc.getChar(pos);
			if (!Character.isJavaIdentifierPart(ch)) {
				return pos;
			}
			pos++;
		}
		return pos;
	}

	private int getSimpleNameStart(IBuffer buffer, int nameStart, String containerName) {
		int containerLen= containerName.length();
		int docLen= buffer.getLength();
		if (containerLen > 0 && nameStart + containerLen + 1 < docLen) {
			for (int k= 0; k < containerLen; k++) {
				if (buffer.getChar(nameStart + k) != containerName.charAt(k)) {
					return nameStart;
				}
			}
			if (buffer.getChar(nameStart + containerLen) == '.') {
				return nameStart + containerLen + 1;
			}
		}
		return nameStart;
	}

	private int getSearchForConstant(int typeKinds) {
		final int CLASSES= TypeKinds.CLASSES;
		final int INTERFACES= TypeKinds.INTERFACES;
		final int ENUMS= TypeKinds.ENUMS;
		final int ANNOTATIONS= TypeKinds.ANNOTATIONS;

		switch (typeKinds & (CLASSES | INTERFACES | ENUMS | ANNOTATIONS)) {
			case CLASSES: return IJavaSearchConstants.CLASS;
			case INTERFACES: return IJavaSearchConstants.INTERFACE;
			case ENUMS: return IJavaSearchConstants.ENUM;
			case ANNOTATIONS: return IJavaSearchConstants.ANNOTATION_TYPE;
			case CLASSES | INTERFACES: return IJavaSearchConstants.CLASS_AND_INTERFACE;
			case CLASSES | ENUMS: return IJavaSearchConstants.CLASS_AND_ENUM;
			default: return IJavaSearchConstants.TYPE;
		}
	}


	/*
	 * Finds a type by the simple name.
	 */
	private TypeNameMatch[] findAllTypes(String simpleTypeName, IJavaSearchScope searchScope, SimpleName nameNode, IProgressMonitor monitor) throws JavaModelException {
		boolean is50OrHigher= JavaModelUtil.is50OrHigher(fCompilationUnit.getJavaProject());

		int typeKinds= TypeKinds.ALL_TYPES;
		if (nameNode != null) {
			typeKinds= ASTResolving.getPossibleTypeKinds(nameNode, is50OrHigher);
		}

		ArrayList<TypeNameMatch> typeInfos= new ArrayList<>();
		TypeNameMatchCollector requestor= new TypeNameMatchCollector(typeInfos);
		int matchMode= SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE;
		new SearchEngine().searchAllTypeNames(null, matchMode, simpleTypeName.toCharArray(), matchMode, getSearchForConstant(typeKinds), searchScope, requestor, IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, monitor);

		ArrayList<TypeNameMatch> typeRefsFound= new ArrayList<>(typeInfos.size());
		for (int i= 0, len= typeInfos.size(); i < len; i++) {
			TypeNameMatch curr= typeInfos.get(i);
			if (curr.getPackageName().length() > 0) { // do not suggest imports from the default package
				if (isOfKind(curr, typeKinds, is50OrHigher) && isVisible(curr)) {
					typeRefsFound.add(curr);
				}
			}
		}
		return typeRefsFound.toArray(new TypeNameMatch[typeRefsFound.size()]);
	}

	private boolean isOfKind(TypeNameMatch curr, int typeKinds, boolean is50OrHigher) {
		int flags= curr.getModifiers();
		if (Flags.isAnnotation(flags)) {
			return is50OrHigher && (typeKinds & TypeKinds.ANNOTATIONS) != 0;
		}
		if (Flags.isEnum(flags)) {
			return is50OrHigher && (typeKinds & TypeKinds.ENUMS) != 0;
		}
		if (Flags.isInterface(flags)) {
			return (typeKinds & TypeKinds.INTERFACES) != 0;
		}
		return (typeKinds & TypeKinds.CLASSES) != 0;
	}


	private boolean isVisible(TypeNameMatch curr) {
		int flags= curr.getModifiers();
		if (Flags.isPrivate(flags)) {
			return false;
		}
		if (Flags.isPublic(flags) || Flags.isProtected(flags)) {
			return true;
		}
		return curr.getPackageName().equals(fCompilationUnit.getParent().getElementName());
	}


	/**
	 * @return Returns the scheduling rule for this operation
	 */
	public ISchedulingRule getScheduleRule() {
		return fCompilationUnit.getJavaProject().getResource();
	}

}
