/*******************************************************************************
 * Copyright (c) 2005, 2015 BEA Systems, Inc.
 * 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:
 *    tyeung@bea.com - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.apt.core.internal.util;

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

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.TypeDeclaration;

/**
 * Home for ast visitors of various flavors.
 */
public class Visitors {
	
	/**
     * Traverse the ast looking for annotations at the declaration level.
     * This visitor only operates at the declaration level. Method body
     * and field initializers and static block will be ignored.
     */
    public static final class AnnotationVisitor extends ASTVisitor
    {
        private final List<Annotation> _annotations;
        /**
         * @param annotations to be populated by this visitor
         */
        public AnnotationVisitor(final List<Annotation> annotations)
        { _annotations = annotations; }

        public boolean visit(MarkerAnnotation annotation)
        {
            _annotations.add(annotation);
            return false;
        }

        public boolean visit(SingleMemberAnnotation annotation)
        {
            _annotations.add(annotation);
            return false;
        }

        public boolean visit(NormalAnnotation annotation)
        {
            _annotations.add(annotation);
            return false;
        }

        // make sure we don't hit Arguments other than formal parameters.
        public boolean visit(Block blk){ return false; }
        public boolean visit(DoStatement doStatement){ return false; }
        public boolean visit(ForStatement forStatement){ return false; }
        public boolean visit(IfStatement ifStatement){ return false; }
        public boolean visit(TryStatement tryStatement){ return false; }
        
        public void reset(){ _annotations.clear(); }
    }
    
    /**
     * Locate all the annotations and the declaration that they annotate.
     * This visitor only operates at the declaration level. Method body
     * and field initializers and static block will be ignored.
     */
    public static final class AnnotatedNodeVisitor extends ASTVisitor
    {
        private final Map<ASTNode, List<Annotation>> _result;
        
        /**
         * @param map to be populated by this visitor. 
         *        Key is the declaration ast node and the value is the list 
         *        of annotation ast nodes that annotate the declaration.        
         */
        public AnnotatedNodeVisitor(Map<ASTNode, List<Annotation>> map)
        {
            _result = map;
        }

		/**
		 * visit package declaration
		 */
		public boolean visit(org.eclipse.jdt.core.dom.PackageDeclaration node)
        {
			final List<Annotation> annotations = node.annotations();
			if( !annotations.isEmpty() )
				_result.put(node, annotations);

            return false;
        }

		/**
		 * visit class and interface declaration
		 */
        public boolean visit(org.eclipse.jdt.core.dom.TypeDeclaration node)
        {
            visitBodyDeclaration(node);
            return true;
        }

		/**
		 * visit annotation type declaration
		 */
        public boolean visit(org.eclipse.jdt.core.dom.AnnotationTypeDeclaration node)
        {
            visitBodyDeclaration(node);
            return true;
        }

		/**
		 * visit enum type declaration
		 */
        public boolean visit(org.eclipse.jdt.core.dom.EnumDeclaration node)
        {
            visitBodyDeclaration(node);
            return true;
        }

		/**
		 * visit field declaration
		 */
        public boolean visit(org.eclipse.jdt.core.dom.FieldDeclaration node)
        {
            visitBodyDeclaration(node);
            return true;
        }

		/**
		 * visit enum constant declaration
		 */
        public boolean visit(org.eclipse.jdt.core.dom.EnumConstantDeclaration node)
        {
            visitBodyDeclaration(node);
            return true;
        }

		/**
		 * visit method declaration
		 */
        public boolean visit(MethodDeclaration node)
        {
            visitBodyDeclaration(node);
            return true;
        }

		/**
		 * visit annotation type member
		 */
        public boolean visit(AnnotationTypeMemberDeclaration node)
        {
            visitBodyDeclaration(node);
            return true;
        }

        private void visitBodyDeclaration(final BodyDeclaration node)
        {
            final List<IExtendedModifier> extMods = node.modifiers();
			List<Annotation> annos = null;
            for( IExtendedModifier extMod : extMods ){
                if( extMod.isAnnotation() ){
					if( annos == null ){
                        annos = new ArrayList<>(2);
                        _result.put(node, annos);
					}
                    annos.add((Annotation)extMod);
                }
            }
        }

		/**
		 * visiting formal parameter declaration.
		 */
		public boolean visit(SingleVariableDeclaration node)
		{
			final List<IExtendedModifier> extMods = node.modifiers();
			List<Annotation> annos = null;
            for( IExtendedModifier extMod : extMods ){
                if( extMod.isAnnotation() ){
					if( annos == null ){
                        annos = new ArrayList<>(2);
                        _result.put(node, annos);
					}
                    annos.add((Annotation)extMod);
                }
            }
			return false;
		}

		/**
		 * @return false so we skip everything beyond declaration level.
		 */
        public boolean visit(Block node)
        {   // so we don't look into anything beyond declaration level.
            return false;
        }
        public boolean visit(MarkerAnnotation node){ return false; }
        public boolean visit(NormalAnnotation node){ return false; }
        public boolean visit(SingleMemberAnnotation node){ return false; }
    }
	
    /**
     * Given an annotation locate the declaration that its annotates. 
     * This visitor only operates at the declaration level. Method body
     * and field initializers and static block will be ignored.   
     *
     */
    public static final class DeclarationFinder extends ASTVisitor
    {
    	private final Annotation _anno;
    	// The declaration, could be a body declaration or a parameter
    	// could also remain null if the annotation doesn't actually
    	// annotates anything.
    	private ASTNode _result = null;
    	public DeclarationFinder(final Annotation annotation)
    	{
    		_anno = annotation;
    	}
    	
    	/**
    	 *  @return back the result of the search.
    	 */
    	public ASTNode getAnnotatedNode(){return _result;}
    	
    	/**
    	 * We only visit nodes that can have annotations on them
    	 */
    	public boolean visit(AnnotationTypeDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	public boolean visit(AnnotationTypeMemberDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	public boolean visit(EnumDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	public boolean visit(EnumConstantDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	public boolean visit(FieldDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	public boolean visit(MethodDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	public boolean visit(TypeDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	public boolean visit(SingleVariableDeclaration node) {
    		return internalVisit(node);
    	}
    	
    	private boolean internalVisit(ASTNode node) {
    		// terminate the search.
    		if( _result != null ) return false;
    		int nodeStart = node.getStartPosition();
    		int nodeEnd = nodeStart + node.getLength();
    		int annoStart = _anno.getStartPosition();
    		int annoEnd = annoStart + _anno.getLength();
    		
    		if (nodeStart > annoEnd) {
    			// We've passed our position. No need to search any further
    			return false;
    		}
    		if (nodeEnd > annoStart) { // nodeStart <= annoEnd && nodeEnd > annoStart
    			// This annotation declaration surrounds the offset
    			List<IExtendedModifier> extendedModifiers;
    			if (node.getNodeType() == ASTNode.SINGLE_VARIABLE_DECLARATION) {
    				SingleVariableDeclaration declaration = (SingleVariableDeclaration)node;
    				extendedModifiers = declaration.modifiers();
    			}
    			else {
    				BodyDeclaration declaration = (BodyDeclaration)node;
    				extendedModifiers = declaration.modifiers();
    			}    			
    			for (IExtendedModifier modifier : extendedModifiers) {
    				// found what we came to look for.
    				if( modifier == _anno ){
    					_result = node;
    					return false;
    				}
    			}   			
    		}
    		
    		// Keep searching
    		return true;
    	}
    	
    	/**
		 * @return false so we skip everything beyond declaration level.
		 */
        public boolean visit(Block node)
        {   // so we don't look into anything beyond declaration level.
            return false;
        }
        public boolean visit(MarkerAnnotation node){ return false; }
        public boolean visit(NormalAnnotation node){ return false; }
        public boolean visit(SingleMemberAnnotation node){ return false; }
    }
	/**
	 * Responsible for finding the ending offset of the tighest ast node match that starts 
	 * at a given offset. This ast visitor can operator on an array of offsets in one pass.   
     * @author tyeung     
     */
    public static class EndingOffsetFinder extends ASTVisitor 
    {
    	private final int[] _sortedStartingOffset;
    	/** 
    	 * parallel to <code>_sortedOffsets</code> and contains 
    	 * the ending offset of the ast node that has the tightest match for the 
    	 * corresponding starting offset.
    	 */
    	private final int[] _endingOffsets;
    	
    	/**
    	 * @param offsets the array of offsets which will be sorted.
    	 * @throws IllegalArgumentException if <code>offsets</code> is <code>null</code>.
    	 */
    	public EndingOffsetFinder(int[] offsets)
    	{
    		if(offsets == null)
    			throw new IllegalArgumentException("argument cannot be null."); //$NON-NLS-1$
    		// sort the array first
    		Arrays.sort(offsets);
    	
    		// look for duplicates.		
    		int count = 0;	
    		for( int i=1, len=offsets.length; i<len; i++){
    			if( offsets[i-1] == offsets[i] )
    				continue;			
    			count ++;
    		}	
    	
    		if( count != offsets.length ){
    			_sortedStartingOffset = new int[count];
    	
    			int index = 0;
    			for( int i=0, len=offsets.length; i<len; i++){
    				if( i != 0 && offsets[i-1] == offsets[i] )
    					continue;
    				_sortedStartingOffset[index++] = offsets[i];
    			}		
    		}
    		else{
    			_sortedStartingOffset = offsets;
    		}
    		
    		_endingOffsets = new int[count];
    		for( int i=0; i<count; i++ )
    			_endingOffsets[i] = 0;
    	}
    	
    	public void preVisit(ASTNode node) 
    	{
    		final int startingOffset = node.getStartPosition();
    		final int endingOffset = startingOffset + node.getLength();
    		// starting offset is inclusive
    		int startIndex = Arrays.binarySearch(_sortedStartingOffset, startingOffset);
    		// ending offset is exclusive
    		int endIndex = Arrays.binarySearch(_sortedStartingOffset, endingOffset);
    		if( startIndex < 0 )
    			startIndex = - startIndex - 1;		
    		if( endIndex < 0 )
    			endIndex = - endIndex - 1;
    		else 
    			// endIndex needs to be exclusive and we want to 
    			// include the 'endIndex'th entry in our computation.
    			endIndex ++; 
    		if( startIndex >= _sortedStartingOffset.length )
    			return;
    		
    		for( int i=startIndex; i<endIndex; i++ ){    			
    			if( _endingOffsets[i] == 0 )
    				_endingOffsets[i] = endingOffset;
    			else if( endingOffset < _endingOffsets[i] )
    				_endingOffsets[i] = endingOffset;
    		}
    	}
    	
    	
    	public int getEndingOffset(final int startingOffset)
    	{
    		int index = Arrays.binarySearch(_sortedStartingOffset, startingOffset);
    		if( index == -1 ) return 0;
    		return _endingOffsets[index];
    	}
    }
}
