/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */
package com.sun.enterprise.deployment.annotation.impl;

import java.util.EmptyStackException;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
import java.util.Stack;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import java.util.logging.Logger;

import com.sun.enterprise.deployment.annotation.ProcessingContext;
import com.sun.enterprise.deployment.annotation.AnnotationProcessor;
import com.sun.enterprise.deployment.annotation.AnnotationInfo;
import com.sun.enterprise.deployment.annotation.AnnotationProcessorException;
import com.sun.enterprise.deployment.annotation.AnnotationHandler;
import com.sun.enterprise.deployment.annotation.AnnotatedElementHandler;
import com.sun.enterprise.deployment.annotation.ComponentInfo;
import com.sun.enterprise.deployment.annotation.ResultType;
import com.sun.enterprise.deployment.annotation.HandlerProcessingResult;
import com.sun.enterprise.deployment.annotation.ProcessingResult;
import com.sun.enterprise.deployment.annotation.Scanner;
import java.util.logging.Level;


/**
 *
 * @author dochez
 */
public class AnnotationProcessorImpl implements AnnotationProcessor {
    
    AnnotationProcessorImpl delegate;
    Map<Class<? extends Annotation>, List<AnnotationHandler>> handlers = 
            new HashMap<Class<? extends Annotation>, List<AnnotationHandler>>();
    
    int errorCount;
    Logger logger;
    Stack<StackElement> annotatedElements = new Stack<StackElement>();
    Set<Package> visitedPackages = new HashSet<Package>();
    
    /** Creates a new instance of AnnotationProcessorImpl */
    public AnnotationProcessorImpl() {
        logger = AnnotationUtils.getLogger();
    }
    
    public void setDelegate(AnnotationProcessorImpl delegate) {
        this.delegate = delegate;
    }    
    public ProcessingContext createContext() {
        ProcessingContext ctx = new ProcessingContextImpl(this);
        ctx.setErrorHandler(new DefaultErrorHandler());
        return ctx;
    }
    
    /**
     * Log a message on the default logger
     */
    public void log(Level level, AnnotationInfo locator, String localizedMessage){
        if (logger!=null && logger.isLoggable(level)){
            if (locator!=null){
                logger.log(level, AnnotationUtils.getLocalString(
                    "enterprise.deployment.annotation.error",
                    "{2}\n symbol: {0}\n location: {1}",
                    new Object[] { locator.getAnnotation().annotationType().getName(), locator.getAnnotatedElement(), localizedMessage}));
            } else{
                logger.log(level, localizedMessage);
            }
        }
    }
    
    /**
     * Starts the annotation processing tool passing the processing context which 
     * encapuslate all information necessary for the configuration of the tool. 
     * @param ctx is the initialized processing context
     * @return the result of the annoations processing
     */    
    public ProcessingResult process(ProcessingContext ctx)
        throws AnnotationProcessorException
    {
        
        Scanner scanner = ctx.getProcessingInput();
        ProcessingResultImpl result = new ProcessingResultImpl();
        errorCount=0;
        
        for (Class c : scanner.getElements()) {
            
            result.add(process(ctx, c));          
        }
        return result;
    }
    
    /**
     * Process a set of classes from the parameter list rather than from the 
     * processing context. This allow the annotation handlers to call be the
     * annotation processing tool when classes need to be processed in a 
     * particular context rather than when they are picked up by the scanner.
     * 
     * @param the processing context 
     * @param the list of classes to process
     * @return the processing result for such classes
     * @throws AnnotationProcessorException if handlers fail to process 
     * an annotation
     */    
    public ProcessingResult process(ProcessingContext ctx, Class[] classes)
        throws AnnotationProcessorException {
        
        ProcessingResultImpl result = new ProcessingResultImpl();
        for (Class c : classes) {
            result.add(process(ctx, c));
        }
        return result;
    }
    
    private ProcessingResult process(ProcessingContext ctx, Class c) 
        throws AnnotationProcessorException {
        
        Scanner scanner = ctx.getProcessingInput();
        ProcessingResultImpl result = new ProcessingResultImpl();
        
        // let's see first if this package is new to us and annotated.
        Package classPackage = c.getPackage();
        if (classPackage != null && visitedPackages.add(classPackage)) {
            // new package
            result.add(classPackage, 
                    processAnnotations(ctx, ElementType.PACKAGE, classPackage));
        }

        ComponentInfo info = null;
        try {
            info = scanner.getComponentInfo(c);
        } catch (NoClassDefFoundError err) {
            // issue 456: allow verifier to report this issue
            AnnotationProcessorException ape = 
                    new AnnotationProcessorException(
                            AnnotationUtils.getLocalString(
                                    "enterprise.deployment.annotation.classnotfounderror",
                                    "Class [ {0} ] not found. Error while loading [ {1} ]",
                                    new Object[]{err.getMessage(), c}));
            ctx.getErrorHandler().error(ape);
            throw err;
        }

        // process the class itself.
        AnnotatedElementHandler handler= ctx.getHandler();
        logStart(handler, ElementType.TYPE,c);
        result.add(c, processAnnotations(ctx, c));
        
        // now dive into the fields.
        for (Field field : info.getFields()) {
            result.add(field,processAnnotations(ctx, ElementType.FIELD, field));
        }
        
        // constructors...
        for (Constructor constructor : info.getConstructors()) {
            logStart(ctx.getHandler(), ElementType.CONSTRUCTOR, constructor);
            result.add(constructor, processAnnotations(ctx, constructor));
            
            // parameters
            processParameters(ctx, constructor.getParameterAnnotations());
            
            logEnd(ctx.getHandler(), ElementType.CONSTRUCTOR, constructor);
            
        }
        
        // methods...
        for (Method method : info.getMethods()) {
            logStart(ctx.getHandler(), ElementType.METHOD, method);
            result.add(method, processAnnotations(ctx, method));
            
            // parameters
            processParameters(ctx, method.getParameterAnnotations());
            
            logEnd(ctx.getHandler(), ElementType.METHOD, method);
        }
        
        // Because of annotation inheritance, we need to to travel to
        // the superclasses to ensure that annotations defined at the
        // TYPE level are processed at this component level.
        // Note : so far, I am ignoring the implemented interfaces
        Class currentClass = c.getSuperclass();
        while (currentClass!=null && !currentClass.equals(Object.class)) {
            // the trick is to add the results for this class, not
            // for the ones they are defined in...
            result.add(c, processAnnotations(ctx, currentClass));
            currentClass = currentClass.getSuperclass();
        }
        
        // end of class processing, we need to get the top handler
        // since it may have changed during the annotation processing
        logEnd(ctx.getHandler(), ElementType.TYPE, c);  
        
        return result;
    }
    
    private HandlerProcessingResult processParameters(ProcessingContext ctx, Annotation[][] parametersAnnotations)
    throws AnnotationProcessorException
    {

        HandlerProcessingResultImpl result = new HandlerProcessingResultImpl();
        
        // process the method parameters...
        for (Annotation[] parameterAnnotations : parametersAnnotations) {
            logStart(ctx.getHandler(), ElementType.PARAMETER, null);
            if (parameterAnnotations!=null) {
                for (Annotation annotation : parameterAnnotations) {
                    AnnotationInfo info = new AnnotationInfo(ctx, null, annotation, ElementType.PARAMETER);
                    process(ctx, info, result);
                    dumpProcessingResult(result);
                }
            }
            logEnd(ctx.getHandler(), ElementType.PARAMETER, null);
        }
        return result;        
    }
    
    private HandlerProcessingResult processAnnotations(ProcessingContext ctx, ElementType type, AnnotatedElement element) 
        throws AnnotationProcessorException
    {
        
        AnnotatedElementHandler handler = ctx.getHandler();
        logStart(handler, type, element);
        HandlerProcessingResult result = processAnnotations(ctx, element);
        logEnd(handler, type, element);

        dumpProcessingResult(result);                    
        
        return result;
    }
    
    private HandlerProcessingResult processAnnotations(ProcessingContext ctx, AnnotatedElement element)
        throws AnnotationProcessorException
    {
    
        HandlerProcessingResultImpl result= new HandlerProcessingResultImpl();
        
        for (Annotation annotation : element.getAnnotations()) {
            // initialize the result...
            AnnotationInfo subElement = new AnnotationInfo(ctx, element, annotation, getTopElementType());
            if (!result.processedAnnotations().containsKey(annotation.annotationType())) {
                process(ctx, subElement, result);
            } else {
                if (AnnotationUtils.shouldLog("annotation")) { 
                    logger.finer("Annotation " + annotation.annotationType() + " already processed");
                }
            }       
        }
        return result;
    }
    
    private void process(ProcessingContext ctx, AnnotationInfo element, HandlerProcessingResultImpl result) 
        throws AnnotationProcessorException 
    {
        
        
        Annotation annotation = element.getAnnotation();
        if (AnnotationUtils.shouldLog("annotation")) {
            logger.finer("Annotation : " + annotation.annotationType().getName() + " delegate = " + delegate);
        }
        result.addResult(annotation.annotationType(), ResultType.UNPROCESSED);
        
        // we ignore all java.* annotations
        Package annPackage = annotation.annotationType().getPackage();
        if (annPackage != null && annPackage.getName().startsWith("java.lang"))
            return;
        
        List<AnnotationHandler> annotationHandlers = handlers.get(annotation.annotationType());
        if (annotationHandlers!=null) {
            for (AnnotationHandler handler : annotationHandlers) {
                
                // here we need to be careful, we are ready to invoke a handler
                // to process a particular annotation type. However, this handler 
                // may have defined a list of annotations that should be processed 
                // (if present on the annotated element) before itself. 
                // do this check and process those annotations first.
                Class<? extends Annotation>[] dependencies = handler.getTypeDependencies();
                if (dependencies!=null) {
                    AnnotatedElement ae = element.getAnnotatedElement();
                    for (Class<? extends Annotation> annotationType : dependencies) {
                        Annotation depAnnotation = ae.getAnnotation(annotationType);
                        if (depAnnotation!=null) {                        
                            ResultType resultType = result.processedAnnotations().get(annotationType);
                            if (resultType==null || resultType==ResultType.UNPROCESSED){
                                // annotation is present, process it.
                                AnnotationInfo info = new AnnotationInfo(ctx, ae, depAnnotation, getTopElementType());
                                process(ctx, info, result);
                            }
                        }
                    }
                }
                
                // at this point, all annotation that I declared depending on
                // are processed
                HandlerProcessingResult processingResult = null;
                try {
                    processingResult = handler.processAnnotation(element);
                } catch(AnnotationProcessorException ape) {
                    // I am logging this exception
                    log(Level.SEVERE, ape.getLocator(), ape.getMessage());
                    
                    // I am not throwing the exception unless it is fatal so annotation
                    // processing can continue and we have a chance to report all 
                    // errors. 
                    if (ape.isFatal()) {
                        throw ape;
                    } 
                    
                    if (++errorCount>100){
                        throw new AnnotationProcessorException(
                                AnnotationUtils.getLocalString(
                                    "enterprise.deployment.annotation.toomanyerror",
                                    "Too many errors, annotation processing abandoned."));
                    }
                    
                    processingResult =
                        HandlerProcessingResultImpl.getDefaultResult(
                        annotation.annotationType(), ResultType.FAILED);
                } catch(Throwable e){
                    AnnotationProcessorException ape = new AnnotationProcessorException(e.getMessage(), element);
                    ape.initCause(e);
                    throw ape;
                }
                result.addAll(processingResult);
            }
        } else {
            if (delegate!=null) {
                delegate.process(ctx, element, result);
            } else {           
                ctx.getErrorHandler().fine(
                        new AnnotationProcessorException("No handler defined for " 
                            + annotation.annotationType()));
            }
        }
    }
    
    private void dumpProcessingResult(HandlerProcessingResult result) {

        if (result==null || !AnnotationUtils.shouldLog("annotation")) {
            return;
        }
   
        Map<Class<? extends Annotation>, ResultType> annotationResults = 
                result.processedAnnotations();
        for (Class annotationType : annotationResults.keySet()) {            
            logger.finer("Annotation " + annotationType + " : " +
                    annotationResults.get(annotationType));
        }
    }
    public void pushAnnotationHandler(AnnotationHandler handler) {
        
        Class<? extends Annotation> type = handler.getAnnotationType();
        List<AnnotationHandler> currentHandlers = handlers.get(type);
        if (currentHandlers==null) {
            currentHandlers = new ArrayList<AnnotationHandler>();
            handlers.put(type, currentHandlers);
        }
        currentHandlers.add(handler);
    }
    
    public void popAnnotationHandler(Class<? extends Annotation> type) {
        List<AnnotationHandler> currentHandlers = handlers.get(type);
        if (currentHandlers!=null) {
            currentHandlers.remove(currentHandlers.size());
        }        
    }    

    public AnnotationHandler getAnnotationHandler(Class<? extends Annotation> type) {
        List<AnnotationHandler> currentHandlers = handlers.get(type);
        if (currentHandlers!=null && currentHandlers.size()>0) {
            return currentHandlers.get(0);
        }        
        return null;
    }      
    
    /**
     * @return the last element pushed on the stack which ElementType was
     * the one passed or null if no stack element is of the given type.
     */
    public AnnotatedElement getLastAnnotatedElement(ElementType type) {
        for (int i=annotatedElements.size();i!=0;i--) {
            StackElement e = annotatedElements.get(i - 1);
            if (e.getElementType().equals(type)) 
                return e.getAnnotatedElement();
        }
        return null;
    }    
    
    public Stack<StackElement> getStack() {
        return annotatedElements;
    }
    
    private void logStart(AnnotatedElementHandler handler, ElementType type, AnnotatedElement c) throws AnnotationProcessorException {
        
        if (AnnotationUtils.shouldLog("types")) {
            AnnotationUtils.getLogger().finer(type + " START : " + c);
        }
        
        // push it to our annotated element stack
        annotatedElements.push(new StackElement(type, c));
        if(delegate!=null) {
            delegate.getStack().push(new StackElement(type, c));
        }
        
        if (handler!=null) {
            handler.startElement(type, c);
        }   
    }
    
    private void logEnd(AnnotatedElementHandler handler, ElementType type, AnnotatedElement c) throws AnnotationProcessorException {
        
        if (AnnotationUtils.shouldLog("types")) {
            AnnotationUtils.getLogger().finer(type + " END : " + c);
        }
        
        // pop it from our annotated element stack
        annotatedElements.pop();
        if(delegate!=null) {
            delegate.getStack().pop();
        }
        
        if (handler!=null) {
            handler.endElement(type, c);
        }   
    }   
    
    /** 
     * @return the top annotated elements stack element type 
     */
    private ElementType getTopElementType() {
        try {
            StackElement top = annotatedElements.peek();
            return top.getElementType();
        } catch(EmptyStackException ex) {
            return null;
        }
    }
}
