/*******************************************************************************
 * 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
 *     Red Hat Inc. - copied to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.structure;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
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.ImportDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NameQualifiedType;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.manipulation.ImportReferencesCollector;

import org.eclipse.jdt.internal.corext.dom.Bindings;

/**
 * Removes imports that are no longer required.
 * <p>
 * {@link #registerRemovedNode(ASTNode)} registers nodes that got removed from the AST.
 * Do not register nodes that are moved to a different place in the same AST.
 * <p>
 * If a node is removed but some parts of it are moved to a different place in the same AST,
 * then use {@link #registerRetainedNode(ASTNode)} to keep imports for the retained nodes.
 * <p>
 * Additional imports that will be added to the AST need to be registered with one of the
 * {@code register*Import*(..)} methods. Such imports have typically been created with
 * {@link ImportRewrite#addImport(ITypeBinding)} etc.
 */
public class ImportRemover {

	private static class StaticImportData {

		private boolean fField;

		private String fMember;

		private String fQualifier;

		private StaticImportData(String qualifier, String member, boolean field) {
			fQualifier= qualifier;
			fMember= member;
			fField= field;
		}
	}

	private final String PROPERTY_KEY= String.valueOf(System.currentTimeMillis());
	private final String REMOVED= "removed"; //$NON-NLS-1$
	private final String RETAINED= "retained"; //$NON-NLS-1$
	
	private Set<String> fAddedImports= new HashSet<>();

	private Set<StaticImportData> fAddedStaticImports= new HashSet<>();

	private final IJavaProject fProject;

	private boolean fHasRemovedNodes;

	private List<ImportDeclaration> fInlinedStaticImports= new ArrayList<>();

	private final CompilationUnit fRoot;

	public ImportRemover(IJavaProject project, CompilationUnit root) {
		fProject= project;
		fRoot= root;
	}

	private void divideTypeRefs(List<SimpleName> importNames, List<SimpleName> staticNames, List<SimpleName> removedRefs, List<SimpleName> unremovedRefs) {
		final List<int[]> removedStartsEnds= new ArrayList<>();
		fRoot.accept(new ASTVisitor(true) {
			int fRemovingStart= -1;
			@Override
			public void preVisit(ASTNode node) {
				Object property= node.getProperty(PROPERTY_KEY);
				if (property == REMOVED) {
					if (fRemovingStart == -1) {
						fRemovingStart= node.getStartPosition();
					} else {
						/*
						 * Bug in client code: REMOVED node should not be nested inside another REMOVED node without
						 * an intermediate RETAINED node.
						 * Drop REMOVED property to prevent problems later (premature end of REMOVED section).
						 */
						node.setProperty(PROPERTY_KEY, null);
					}
				} else if (property == RETAINED) {
					if (fRemovingStart != -1) {
						removedStartsEnds.add(new int[] { fRemovingStart, node.getStartPosition() });
						fRemovingStart= -1;
					} else {
						/*
						 * Bug in client code: RETAINED node should not be nested inside another RETAINED node without
						 * an intermediate REMOVED node and must have an enclosing REMOVED node.
						 * Drop RETAINED property to prevent problems later (premature restart of REMOVED section).
						 */
						node.setProperty(PROPERTY_KEY, null);
					}
				}
				super.preVisit(node);
			}
			@Override
			public void postVisit(ASTNode node) {
				Object property= node.getProperty(PROPERTY_KEY);
				if (property == RETAINED) {
					int end= node.getStartPosition() + node.getLength();
					fRemovingStart= end;
				} else if (property == REMOVED) {
					if (fRemovingStart != -1) {
						int end= node.getStartPosition() + node.getLength();
						removedStartsEnds.add(new int[] { fRemovingStart, end });
						fRemovingStart= -1;
					}
				}
				super.postVisit(node);
			}
		});
		
		for (Iterator<SimpleName> iterator= importNames.iterator(); iterator.hasNext();) {
			SimpleName name= iterator.next();
			if (isInRemoved(name, removedStartsEnds))
				removedRefs.add(name);
			else
				unremovedRefs.add(name);
		}
		for (Iterator<SimpleName> iterator= staticNames.iterator(); iterator.hasNext();) {
			SimpleName name= iterator.next();
			if (isInRemoved(name, removedStartsEnds))
				removedRefs.add(name);
			else
				unremovedRefs.add(name);
		}
		for (Iterator<ImportDeclaration> iterator= fInlinedStaticImports.iterator(); iterator.hasNext(); ) {
			ImportDeclaration importDecl= iterator.next();
			Name name= importDecl.getName();
			if (name instanceof QualifiedName)
				name= ((QualifiedName) name).getName();
			removedRefs.add((SimpleName) name);
		}
	}

	private boolean isInRemoved(SimpleName ref, List<int[]> removedStartsEnds) {
		int start= ref.getStartPosition();
		int end= start + ref.getLength();
		for (int[] removedStartsEnd : removedStartsEnds) {
			if (start >= removedStartsEnd[0] && end <= removedStartsEnd[1]) {
				return true;
			}
		}
		return false;
	}

	public IBinding[] getImportsToRemove() {
		ArrayList<SimpleName> importNames= new ArrayList<>();
		ArrayList<SimpleName> staticNames= new ArrayList<>();

		ImportReferencesCollector.collect(fRoot, fProject, null, importNames, staticNames);

		List<SimpleName> removedRefs= new ArrayList<>();
		List<SimpleName> unremovedRefs= new ArrayList<>();
		divideTypeRefs(importNames, staticNames, removedRefs, unremovedRefs);
		if (removedRefs.size() == 0)
			return new IBinding[0];

		HashMap<String, IBinding> potentialRemoves= getPotentialRemoves(removedRefs);
		for (Iterator<SimpleName> iterator= unremovedRefs.iterator(); iterator.hasNext();) {
			SimpleName name= iterator.next();
			potentialRemoves.remove(name.getIdentifier());
		}

		Collection<IBinding> importsToRemove= potentialRemoves.values();
		return importsToRemove.toArray(new IBinding[importsToRemove.size()]);
	}

	private HashMap<String, IBinding> getPotentialRemoves(List<SimpleName> removedRefs) {
		HashMap<String, IBinding>potentialRemoves= new HashMap<>();
		for (Iterator<SimpleName> iterator= removedRefs.iterator(); iterator.hasNext();) {
			SimpleName name= iterator.next();
			if (fAddedImports.contains(name.getIdentifier()) || hasAddedStaticImport(name))
				continue;
			IBinding binding= name.resolveBinding();
			if (binding != null)
				potentialRemoves.put(name.getIdentifier(), binding);
		}
		return potentialRemoves;
	}

	private boolean hasAddedStaticImport(SimpleName name) {
		IBinding binding= name.resolveBinding();
		if (binding instanceof IVariableBinding) {
			IVariableBinding variable= (IVariableBinding) binding;
			return hasAddedStaticImport(variable.getDeclaringClass().getQualifiedName(), variable.getName(), true);
		} else if (binding instanceof IMethodBinding) {
			IMethodBinding method= (IMethodBinding) binding;
			return hasAddedStaticImport(method.getDeclaringClass().getQualifiedName(), method.getName(), false);
		}
		return false;
	}

	private boolean hasAddedStaticImport(String qualifier, String member, boolean field) {
		StaticImportData data= null;
		for (final Iterator<StaticImportData> iterator= fAddedStaticImports.iterator(); iterator.hasNext();) {
			data= iterator.next();
			if (data.fQualifier.equals(qualifier) && data.fMember.equals(member) && data.fField == field)
				return true;
		}
		return false;
	}

	public boolean hasRemovedNodes() {
		return fHasRemovedNodes || fInlinedStaticImports.size() != 0;
	}

	public void registerAddedImport(String typeName) {
		int dot= typeName.lastIndexOf('.');
		if (dot == -1)
			fAddedImports.add(typeName);
		else
			fAddedImports.add(typeName.substring(dot + 1));
	}

	public void registerAddedImports(Type newTypeNode) {
		newTypeNode.accept(new ASTVisitor(true) {

			private void addName(SimpleName name) {
				fAddedImports.add(name.getIdentifier());
			}

			@Override
			public boolean visit(NameQualifiedType node) {
				addName(node.getName());
				return false;
			}
			
			@Override
			public boolean visit(QualifiedName node) {
				addName(node.getName());
				return false;
			}

			@Override
			public boolean visit(QualifiedType node) {
				addName(node.getName());
				return false;
			}

			@Override
			public boolean visit(SimpleName node) {
				addName(node);
				return false;
			}
		});
	}

	public void registerAddedStaticImport(String qualifier, String member, boolean field) {
		fAddedStaticImports.add(new StaticImportData(qualifier, member, field));
	}

	public void registerAddedStaticImport(IBinding binding) {
		if (binding instanceof IVariableBinding) {
			ITypeBinding declaringType= ((IVariableBinding) binding).getDeclaringClass();
			fAddedStaticImports.add(new StaticImportData(Bindings.getRawQualifiedName(declaringType), binding.getName(), true));

		} else if (binding instanceof IMethodBinding) {
			ITypeBinding declaringType= ((IMethodBinding) binding).getDeclaringClass();
			fAddedStaticImports.add(new StaticImportData(Bindings.getRawQualifiedName(declaringType), binding.getName(), false));

		} else {
			throw new IllegalArgumentException(binding.toString());
		}
	}

	public void registerRemovedNode(ASTNode removed) {
		fHasRemovedNodes= true;
		removed.setProperty(PROPERTY_KEY, REMOVED);
	}

	public void registerRetainedNode(ASTNode retained) {
		retained.setProperty(PROPERTY_KEY, RETAINED);
	}

	public void applyRemoves(ImportRewrite importRewrite) {
		IBinding[] bindings= getImportsToRemove();
		for (int i= 0; i < bindings.length; i++) {
			if (bindings[i] instanceof ITypeBinding) {
				ITypeBinding typeBinding= (ITypeBinding) bindings[i];
				importRewrite.removeImport(typeBinding.getTypeDeclaration().getQualifiedName());
			} else if (bindings[i] instanceof IMethodBinding) {
				IMethodBinding binding= (IMethodBinding) bindings[i];
				importRewrite.removeStaticImport(binding.getDeclaringClass().getQualifiedName() + '.' + binding.getName());
			} else if (bindings[i] instanceof IVariableBinding) {
				IVariableBinding binding= (IVariableBinding) bindings[i];
				importRewrite.removeStaticImport(binding.getDeclaringClass().getQualifiedName() + '.' + binding.getName());
			}
		}
	}

	public void registerInlinedStaticImport(ImportDeclaration importDecl) {
		fInlinedStaticImports.add(importDecl);
	}
}
