/*******************************************************************************
 * Copyright (c) 2000, 2007 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.code;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;

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


/* package */ class SnippetFinder extends GenericVisitor {
	
	public static class Match {
		private List fNodes;
		private Map fLocalMappings;
		
		public Match() {
			fNodes= new ArrayList(10);
			fLocalMappings= new HashMap();
		}
		public void add(ASTNode node) {
			fNodes.add(node);
		}
		public boolean hasCorrectNesting(ASTNode node) {
			if (fNodes.size() == 0)
				return true;
			ASTNode parent= node.getParent();
			if(((ASTNode)fNodes.get(0)).getParent() != parent)
				return false;
			// Here we know that we have two elements. In this case the
			// parent must be a block or a switch statement. Otherwise a 
			// snippet like "if (true) foo(); else foo();" would match 
			// the pattern "foo(); foo();"
			int nodeType= parent.getNodeType();
			return nodeType == ASTNode.BLOCK || nodeType == ASTNode.SWITCH_STATEMENT;
		}
		public ASTNode[] getNodes() {
			return (ASTNode[])fNodes.toArray(new ASTNode[fNodes.size()]);
		}
		public void addLocal(IVariableBinding org, SimpleName local) {
			fLocalMappings.put(org, local);
		}
		public SimpleName getMappedName(IVariableBinding org) {
			return (SimpleName)fLocalMappings.get(org);
		}
		public IVariableBinding getMappedBinding(IVariableBinding org) {
			SimpleName name= (SimpleName) fLocalMappings.get(org);
			return ASTNodes.getVariableBinding(name);
		}
		public boolean isEmpty() {
			return fNodes.isEmpty() && fLocalMappings.isEmpty();
		}
		/**
		 * Tests if the whole duplicate is the full body of a method. If so
		 * don't replace it since we would replace a method body with a new
		 * method body which doesn't make to much sense.
		 * 
		 * @return whether the duplicte is the whole method body
		 */
		public boolean isMethodBody() {
			ASTNode first= (ASTNode)fNodes.get(0);
			if (first.getParent() == null)
				return false;
			ASTNode candidate= first.getParent().getParent();
			if (candidate == null || candidate.getNodeType() != ASTNode.METHOD_DECLARATION)
				return false;
			MethodDeclaration method= (MethodDeclaration)candidate;
			return method.getBody().statements().size() == fNodes.size();
		}
		public MethodDeclaration getEnclosingMethod() {
			ASTNode first= (ASTNode)fNodes.get(0);
			return (MethodDeclaration)ASTNodes.getParent(first, ASTNode.METHOD_DECLARATION);
		}
	}
	
	private class Matcher extends ASTMatcher {
		public boolean match(SimpleName candidate, Object s) {
			if (!(s instanceof SimpleName))
				return false;
				
			SimpleName snippet= (SimpleName)s;
			if (candidate.isDeclaration() != snippet.isDeclaration())
				return false;
			
			IBinding cb= candidate.resolveBinding();
			IBinding sb= snippet.resolveBinding();
			if (cb == null || sb == null)
				return false;
			IVariableBinding vcb= ASTNodes.getVariableBinding(candidate);
			IVariableBinding vsb= ASTNodes.getVariableBinding(snippet);
			if (vcb == null || vsb == null)
				return Bindings.equals(cb, sb);
			if (!vcb.isField() && !vsb.isField() && Bindings.equals(vcb.getType(), vsb.getType())) {
				SimpleName mapped= fMatch.getMappedName(vsb);
				if (mapped != null) {
					IVariableBinding mappedBinding= ASTNodes.getVariableBinding(mapped);
					if (!Bindings.equals(vcb, mappedBinding))
						return false;
				}
				fMatch.addLocal(vsb, candidate);
				return true;
			}
			return Bindings.equals(cb, sb);	
		}
	}

	private List fResult= new ArrayList(2);
	private Match fMatch;
	private ASTNode[] fSnippet;
	private int fIndex;
	private Matcher fMatcher;
	private int fTypes;
	
	private SnippetFinder(ASTNode[] snippet) {
		super(true);
		fSnippet= snippet;
		fMatcher= new Matcher();
		reset();
	}
	
	public static Match[] perform(ASTNode start, ASTNode[] snippet) {
		Assert.isTrue(start instanceof AbstractTypeDeclaration || start instanceof AnonymousClassDeclaration);
		SnippetFinder finder= new SnippetFinder(snippet);
		start.accept(finder);
		for (Iterator iter = finder.fResult.iterator(); iter.hasNext();) {
			Match match = (Match)iter.next();
			ASTNode[] nodes= match.getNodes();
			// doesn't match if the candidate is the left hand side of an 
			// assignment and the snippet consists of a single node.
			// Otherwise y= i; i= z; results in y= e(); e()= z;
			if (nodes.length == 1 && isLeftHandSideOfAssignment(nodes[0])) {
				iter.remove();
			}
		}
		return (Match[])finder.fResult.toArray(new Match[finder.fResult.size()]);
	}
	
	private static boolean isLeftHandSideOfAssignment(ASTNode node) {
		ASTNode parent= node.getParent();
		return parent != null && parent.getNodeType() == ASTNode.ASSIGNMENT && ((Assignment)parent).getLeftHandSide() == node;
	}
	
	public boolean visit(TypeDeclaration node) {
		if (++fTypes > 1)
			return false;
		return super.visit(node);
	}
	
	public void endVisit(TypeDeclaration node) {
		--fTypes;
		super.endVisit(node);
	}
	
	public boolean visit(EnumDeclaration node) {
		if (++fTypes > 1)
			return false;
		return super.visit(node);
	}
	
	public void endVisit(EnumDeclaration node) {
		--fTypes;
		super.endVisit(node);
	}
	
	public boolean visit(AnnotationTypeDeclaration node) {
		if (++fTypes > 1)
			return false;
		return super.visit(node);
	}
	
	public void endVisit(AnnotationTypeDeclaration node) {
		--fTypes;
		super.endVisit(node);
	}
	
	protected boolean visitNode(ASTNode node) {
		if (matches(node)) {
			return false;
		} else if (!isResetted()){
			reset();
			if (matches(node))
				return false;
		}
		return true;
	}
	
	private boolean matches(ASTNode node) {
		if (isSnippetNode(node))
			return false;
		if (node.subtreeMatch(fMatcher, fSnippet[fIndex]) && fMatch.hasCorrectNesting(node)) {
			fMatch.add(node);
			fIndex++;
			if (fIndex == fSnippet.length) {
				fResult.add(fMatch);
				reset();
			}
			return true;
		}
		return false;
	}

	private boolean isResetted() {
		return fIndex == 0 && fMatch.isEmpty();
	}

	private void reset() {
		fIndex= 0;
		fMatch= new Match();
	}
	
	private boolean isSnippetNode(ASTNode node) {
		for (int i= 0; i < fSnippet.length; i++) {
			if (node == fSnippet[i])
				return true;
		}
		return false;
	}
}
