/*******************************************************************************
 * Copyright (c) 2000, 2019 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
 *     Microsoft Corporation - copied to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.structure;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IResource;

import org.eclipse.ltk.core.refactoring.RefactoringStatus;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.Type;
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.SearchMatch;
import org.eclipse.jdt.core.search.SearchPattern;

import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringScopeFactory;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringSearchEngine;
import org.eclipse.jdt.internal.corext.refactoring.SearchResultGroup;
import org.eclipse.jdt.internal.corext.refactoring.util.JavaElementUtil;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.eclipse.jdt.internal.corext.util.JdtFlags;
import org.eclipse.jdt.internal.corext.util.SearchUtils;

/**
 * This class is used to find references to constructors.
 */
public class ConstructorReferenceFinder {
	private final IType fType;
	private final IMethod[] fConstructors;

	private ConstructorReferenceFinder(IType type) throws JavaModelException{
		fConstructors= JavaElementUtil.getAllConstructors(type);
		fType= type;
	}

	private ConstructorReferenceFinder(IMethod constructor){
		fConstructors= new IMethod[]{constructor};
		fType= constructor.getDeclaringType();
	}

	public static SearchResultGroup[] getConstructorReferences(IType type, IProgressMonitor pm, RefactoringStatus status) throws JavaModelException{
		return new ConstructorReferenceFinder(type).getConstructorReferences(pm, null, IJavaSearchConstants.REFERENCES, status);
	}

	public static SearchResultGroup[] getConstructorReferences(IType type, WorkingCopyOwner owner, IProgressMonitor pm, RefactoringStatus status) throws JavaModelException{
		return new ConstructorReferenceFinder(type).getConstructorReferences(pm, owner, IJavaSearchConstants.REFERENCES, status);
	}

	public static SearchResultGroup[] getConstructorOccurrences(IMethod constructor, IProgressMonitor pm, RefactoringStatus status) throws JavaModelException{
		Assert.isTrue(constructor.isConstructor());
		return new ConstructorReferenceFinder(constructor).getConstructorReferences(pm, null, IJavaSearchConstants.ALL_OCCURRENCES, status);
	}

	private SearchResultGroup[] getConstructorReferences(IProgressMonitor pm, WorkingCopyOwner owner, int limitTo, RefactoringStatus status) throws JavaModelException{
		IJavaSearchScope scope= createSearchScope();
		SearchPattern pattern= RefactoringSearchEngine.createOrPattern(fConstructors, limitTo);
		if (pattern == null){
			if (fConstructors.length != 0)
				return new SearchResultGroup[0];
			return getImplicitConstructorReferences(pm, owner, status);
		}
		return removeUnrealReferences(RefactoringSearchEngine.search(pattern, owner, scope, pm, status));
	}

	//XXX this method is a workaround for jdt core bug 27236
	private SearchResultGroup[] removeUnrealReferences(SearchResultGroup[] groups) {
		List<SearchResultGroup> result= new ArrayList<>(groups.length);
		for (SearchResultGroup group : groups) {
			ICompilationUnit cu= group.getCompilationUnit();
			if (cu == null)
				continue;
			CompilationUnit cuNode= new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL).parse(cu, false);
			SearchMatch[] allSearchResults= group.getSearchResults();
			List<SearchMatch> realConstructorReferences= new ArrayList<>(Arrays.asList(allSearchResults));
			for (SearchMatch searchResult : allSearchResults) {
				if (! isRealConstructorReferenceNode(ASTNodeSearchUtil.getAstNode(searchResult, cuNode)))
					realConstructorReferences.remove(searchResult);
			}
			if (! realConstructorReferences.isEmpty())
				result.add(new SearchResultGroup(group.getResource(), realConstructorReferences.toArray(new SearchMatch[realConstructorReferences.size()])));
		}
		return result.toArray(new SearchResultGroup[result.size()]);
	}

	//XXX this method is a workaround for jdt core bug 27236
	private boolean isRealConstructorReferenceNode(ASTNode node){
		String typeName= fConstructors[0].getDeclaringType().getElementName();
		if (node.getParent() instanceof AbstractTypeDeclaration
				&& ((AbstractTypeDeclaration) node.getParent()).getNameProperty().equals(node.getLocationInParent())) {
			//Example:
			//	class A{
			//	    A(){}
			//	}
			//	class B extends A {}
			//==> "B" is found as reference to A()
			return false;
		}
		if (node.getParent() instanceof MethodDeclaration
				&& MethodDeclaration.NAME_PROPERTY.equals(node.getLocationInParent())) {
			MethodDeclaration md= (MethodDeclaration)node.getParent();
			if (md.isConstructor() && ! md.getName().getIdentifier().equals(typeName)) {
				//Example:
				//	class A{
				//	    A(){}
				//	}
				//	class B extends A{
				//	    B(){}
				//	}
				//==> "B" in "B(){}" is found as reference to A()
				return false;
			}
		}
		return true;
	}

	private IJavaSearchScope createSearchScope() throws JavaModelException{
		if (fConstructors.length == 0)
			return RefactoringScopeFactory.create(fType);
		return RefactoringScopeFactory.create(getMostVisibleConstructor());
	}

	private IMethod getMostVisibleConstructor() throws JavaModelException {
		Assert.isTrue(fConstructors.length > 0);
		IMethod candidate= fConstructors[0];
		int visibility= JdtFlags.getVisibilityCode(fConstructors[0]);
		for (int i= 1; i < fConstructors.length; i++) {
			IMethod constructor= fConstructors[i];
			if (JdtFlags.isHigherVisibility(JdtFlags.getVisibilityCode(constructor), visibility))
				candidate= constructor;
		}
		return candidate;
	}

	private SearchResultGroup[] getImplicitConstructorReferences(IProgressMonitor pm, WorkingCopyOwner owner, RefactoringStatus status) throws JavaModelException {
		pm.beginTask("", 2); //$NON-NLS-1$
		List<SearchMatch> searchMatches= new ArrayList<>(getImplicitConstructorReferencesFromHierarchy(owner, new SubProgressMonitor(pm, 1)));
		searchMatches.addAll(getImplicitConstructorReferencesInClassCreations(owner, new SubProgressMonitor(pm, 1), status));
		pm.done();
		return RefactoringSearchEngine.groupByCu(searchMatches.toArray(new SearchMatch[searchMatches.size()]), status);
	}

	//List of SearchResults
	private List<SearchMatch> getImplicitConstructorReferencesInClassCreations(WorkingCopyOwner owner, IProgressMonitor pm, RefactoringStatus status) throws JavaModelException {
		//XXX workaround for jdt core bug 23112
		SearchPattern pattern= SearchPattern.createPattern(fType, IJavaSearchConstants.REFERENCES, SearchUtils.GENERICS_AGNOSTIC_MATCH_RULE);
		if (pattern == null) {
			return List.of();
		}
		IJavaSearchScope scope= RefactoringScopeFactory.create(fType);
		SearchResultGroup[] refs= RefactoringSearchEngine.search(pattern, owner, scope, pm, status);
		List<SearchMatch> result= new ArrayList<>();
		for (SearchResultGroup group : refs) {
			ICompilationUnit cu= group.getCompilationUnit();
			if (cu == null)
				continue;
			CompilationUnit cuNode= new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL).parse(cu, false);
			for (SearchMatch searchResult : group.getSearchResults()) {
				ASTNode node= ASTNodeSearchUtil.getAstNode(searchResult, cuNode);
				if (isImplicitConstructorReferenceNodeInClassCreations(node))
					result.add(searchResult);
			}
		}
		return result;
	}

	public static boolean isImplicitConstructorReferenceNodeInClassCreations(ASTNode node) {
		if (node instanceof Type) {
			final ASTNode parent= node.getParent();
			if (parent instanceof ClassInstanceCreation) {
				return (node.equals(((ClassInstanceCreation) parent).getType()));
			} else if (parent instanceof ParameterizedType) {
				final ASTNode grandParent= parent.getParent();
				if (grandParent instanceof ClassInstanceCreation) {
					final ParameterizedType type= (ParameterizedType) ((ClassInstanceCreation) grandParent).getType();
					return (node.equals(type.getType()));
				}
			}
		}
		return false;
	}

	//List of SearchResults
	private List<SearchMatch> getImplicitConstructorReferencesFromHierarchy(WorkingCopyOwner owner, IProgressMonitor pm) throws JavaModelException{
		IType[] subTypes= getNonBinarySubtypes(owner, fType, pm);
		List<SearchMatch> result= new ArrayList<>(subTypes.length);
		for (IType subType : subTypes) {
			result.addAll(getAllSuperConstructorInvocations(subType));
		}
		return result;
	}

	private static IType[] getNonBinarySubtypes(WorkingCopyOwner owner, IType type, IProgressMonitor monitor) throws JavaModelException{
		ITypeHierarchy hierarchy= null;
		if (owner == null)
			hierarchy= type.newTypeHierarchy(monitor);
		else
			hierarchy= type.newSupertypeHierarchy(owner, monitor);
		IType[] subTypes= hierarchy.getAllSubtypes(type);
		List<IType> result= new ArrayList<>(subTypes.length);
		for (IType subType : subTypes) {
			if (! subType.isBinary()) {
				result.add(subType);
			}
		}
		return result.toArray(new IType[result.size()]);
	}

	//Collection of SearchResults
	private static Collection<SearchMatch> getAllSuperConstructorInvocations(IType type) throws JavaModelException {
		IMethod[] constructors= JavaElementUtil.getAllConstructors(type);
		CompilationUnit cuNode= new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL).parse(type.getCompilationUnit(), false);
		List<SearchMatch> result= new ArrayList<>(constructors.length);
		for (IMethod constructor : constructors) {
			ASTNode superCall= getSuperConstructorCallNode(constructor, cuNode);
			if (superCall != null)
				result.add(createSearchResult(superCall, constructor));
		}
		return result;
	}

	private static SearchMatch createSearchResult(ASTNode superCall, IMethod constructor) {
		int start= superCall.getStartPosition();
		int end= ASTNodes.getInclusiveEnd(superCall); //TODO: why inclusive?
		IResource resource= constructor.getResource();
		return new SearchMatch(constructor, SearchMatch.A_ACCURATE, start, end - start,
				SearchEngine.getDefaultSearchParticipant(), resource);
	}

	private static SuperConstructorInvocation getSuperConstructorCallNode(IMethod constructor, CompilationUnit cuNode) throws JavaModelException {
		Assert.isTrue(constructor.isConstructor());
		MethodDeclaration constructorNode= ASTNodeSearchUtil.getMethodDeclarationNode(constructor, cuNode);
		Assert.isTrue(constructorNode.isConstructor());
		Block body= constructorNode.getBody();
		Assert.isNotNull(body);
		List<Statement> statements= body.statements();
		if (! statements.isEmpty() && statements.get(0) instanceof SuperConstructorInvocation)
			return (SuperConstructorInvocation)statements.get(0);
		return null;
	}
}
