/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, 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/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * ASMClosureCompilerImpl.java
 *
 * Created on August 13, 2004, 3:34 PM
 */

package com.sun.enterprise.tools.verifier.apiscan.classfile;

import java.io.IOException;
import java.util.*;
import java.util.logging.Level;

/**
 * An implementation of {@link ClosureCompilerImplBase} based on
 * ASM.
 *
 * It is not a public class because I expect users to use this class via
 * the bridge setup from {@link ClosureCompilerImpl}.
 * It extends the common implementation from ClosureCompilerImplBase.
 * @author Sanjeeb.Sahoo@Sun.COM
 */
class ASMClosureCompilerImpl extends ClosureCompilerImplBase {

    private Collection<MethodRef> methodReferences = new HashSet<MethodRef>();

    private HashSet<MethodRef> visitedMethods = new HashSet<MethodRef>();

    private Stack<MethodRef> callStack = new Stack<MethodRef>();

    // holds result of current buildClosure() method
    private boolean result;

    private HashSet<String> closure = new HashSet<String>();

    //map of refencing path to list of not found classes.
    private Map<String, List<String>> failed = new HashMap<String, List<String>>();

    private static final String myClassName = "ASMClosureCompilerImpl"; // NOI18N

    /**
     * @param loader the ClassFileLoader that is used to load the referenced
     *               classes.
     */
    public ASMClosureCompilerImpl(ClassFileLoader loader) {
        super(loader);
    }

    private void resetResult() {
        logger.entering("ASMClosureCompilerImpl", "resetResult", // NOI18N
                new Object[]{});
        result = true;
    }

    private void setResult(boolean newresult){
        result = result && newresult;
    }

    private boolean getResult() {
        return result;
    }

    /**
     * {@inheritDoc}
     */
    public boolean buildClosure(String className) {
        logger.entering(myClassName, "buildClosure", className); // NOI18N
        resetResult(); // at the entry of this public method, we reset result.
        if (needToBuildClosure(className)) {
            visitedClasses.add(className); // yes, we add before visting
            ClassFile cf = load(className);
            if(cf==null){
                return false;
            }
            // top level methods whose closure needs to be calculated.
            // TODO: We must get precise list of methods.
            Collection<? extends Method> methods = cf.getMethods();
            for(Method next:methods){
                methodReferences.add(next.getSelfReference());
                buildClosure(next);
            }
        }
        return getResult();
    }

    private void buildClosure(Method m) {
        MethodRef methodReference = m.getSelfReference();
        logger.entering("ASMClosureCompilerImpl", "buildClosure", // NOI18N
                new Object[]{methodReference});
        callStack.push(methodReference);
        if (needToBuildClosure(methodReference)) {
            visitedMethods.add(methodReference);
            Collection<MethodRef> methodRefs = m.getReferencedMethods();
            methodReferences.addAll(methodRefs);
            for(MethodRef nextMethodRef : methodRefs) {
                if (!needToBuildClosure(nextMethodRef)) continue;
                ClassFile cf = load(Util.convertToExternalClassName
                        (nextMethodRef.getOwningClassName()));
                if (cf == null) {
                    continue; // no need to call handleFailure() as it is done by this.load()
                }
                Method nextMethod = cf.getMethod(nextMethodRef);
                if(nextMethod==null){
                    handleFailure(nextMethodRef);
                    continue;
                }
                buildClosure(nextMethod);//recurssive call
            } // for
            // now go thru' all the classes that will be loaded when this
            // method is called.
            for(String className : m.getReferencedClasses()){
                String externalClassName = Util.convertToExternalClassName(className);
                if(!needToBuildClosure(externalClassName)) continue;
                load(externalClassName);
            }
        }
        callStack.pop();
    }

    /**
     * load class with given name. It also recurssively loads all classes in the
     * super class chain and interfaces implemented by this class and
     * its super classes, until it reaches a class which is found in the
     * excluded list. It also builds closure of each of the class's clinit method.
     *
     * @param className name of class in external format.
     * @return ClassFile representing this class name,
     * It returns null if class has been already loaded or need not be loaded
     * because it is an excluded class or could not be loaded. If class could
     * could not be loaded, it calls appropriate handleFailure().
     * @see #needToLoad(String)
     */
    protected ClassFile load(String className) {
        logger.entering("ASMClosureCompilerImpl", "load", // NOI18N
                new Object[]{className});
        ClassFile cf = null;
        try{
            cf = loader.load(className);
        }catch(IOException e){
            handleFailure(className);
        }
        if((cf!=null) && needToLoad(className)) {
            closure.add(className); // yes we add before actually loading.
            MethodRef clinitMethodRef =
                    new MethodRef(Util.convertToInternalClassName(className),
                            MethodRef.CLINIT_NAME,
                            MethodRef.CLINIT_DESC);
            Method clinitMethod  = cf.getMethod(clinitMethodRef);
            try{
                // See we push clinitMethodRef here so that we can display
                // useful error to user even during class loading.
                callStack.push(clinitMethodRef);
                String superClassName = cf.getNameOfSuperClass();
                if(superClassName!=null && needToBuildClosure(superClassName)) { // super class of java.lang.Object is null
                    load(superClassName); //recurssive call
                }
                for(String interfaceClassName : cf.getNamesOfInterfaces()){
                    if(needToBuildClosure(interfaceClassName)) {
                        load(interfaceClassName); // recurssive call
                    }
                }
            }finally{
                // pop callStack before buildClosure is called for clinit
                callStack.pop();
            }
            if(clinitMethod!=null) {
                methodReferences.add(clinitMethodRef);
                buildClosure(clinitMethod); // recurssive call
            }
        }
        logger.exiting("ASMClosureCompilerImpl", "load", cf==null?"null":cf.getName()); // NOI18N
        return cf;
    }

    private void handleFailure(MethodRef mr){
        logger.logp(Level.WARNING, "ASMClosureCompilerImpl", "handleFailure", 
                getClass().getName() + ".exception1", new Object[]{mr.toString()});
//        setResult(false);
        // TODO: We should look for base class methods
    }

    /**
     * @param referencedClass is the name of the class in external format.
     */
    private void handleFailure(String referencedClass) {
        logger.entering("ASMClosureCompilerImpl", "handleFailure", // NOI18N
                new Object[]{referencedClass});
        setResult(false);
        String referencingPath = "";
        try {
            StringBuilder referencingPathBuffer = new StringBuilder();
            for (MethodRef m : callStack) {
                if (referencingPathBuffer.length() != 0)
                    referencingPathBuffer.append("->"); // NOI18N
                referencingPathBuffer.append(m);
            }
            referencingPath = referencingPathBuffer.toString();
        } catch (EmptyStackException e) {
        }
        List<String> failedList = failed.get(referencingPath);
        if (failedList == null) {
            failedList = new ArrayList<String>();
            failed.put(referencingPath, failedList);
        }
        failedList.add(referencedClass);
    }

    /**
     * {@inheritDoc}
     */
    public Collection getClosure() {
        return Collections.unmodifiableCollection(methodReferences);
    }

    /**
     * {@inheritDoc}
     */
    public Map getFailed() {
        return Collections.unmodifiableMap(failed);
    }
    
    /**
     * Reset the closure for next closure computation.
     * Clear the internal cache. It includes the result it has collected since
     * last reset(). But it does not clear the excludedd list. If you want to
     * reset the excluded list, create a new ClosureCompiler.
     */
    public void reset() {
        methodReferences.clear();
        visitedClasses.clear();
        failed.clear();
        closure.clear();
    }

    /**
     * @param className name of class in external format.
     * @return
     */
    protected boolean needToLoad(String className) {
        return !closure.contains(className);
    }

    protected boolean needToBuildClosure(MethodRef methodRef) {
        boolean result = true;
        final String owningClassName = methodRef.getOwningClassName();
        if (visitedMethods.contains(methodRef))
            result = false;
        else if (excludedClasses.contains(owningClassName)) {
            result = false;
        } else if (excludedPackages.contains(getPackageName(owningClassName))) {
            result = false;
        } else {
            for (Iterator i = excludedPatterns.iterator(); i.hasNext();) {
                String pattern = (String) i.next();
                if (owningClassName.startsWith(pattern)) {
                    result = false;
                    break;
                }
            }
        }
        logger.logp(Level.FINEST, myClassName, "needToBuildClosure", // NOI18N
                methodRef + " " + result); // NOI18N
        return result;
    }

    // used for pretty printing
    public String toString() {
        StringBuilder sb=new StringBuilder();
        if(logger.isLoggable(Level.FINER)){
            sb.append("\n<Closure>"); // NOI18N

            sb.append("\n\t<ExcludedClasses>"); // NOI18N
            for(Iterator i=excludedClasses.iterator(); i.hasNext();) {
                sb.append("\n\t\t"); // NOI18N
                sb.append((String)i.next());
            }
            sb.append("\n\t</ExcludedClasses>"); // NOI18N

            sb.append("\n\t<ExcludedPackages>"); // NOI18N
            for(Iterator i=excludedPackages.iterator(); i.hasNext();){
                sb.append("\n\t\t"); // NOI18N
                sb.append((String)i.next());
            }
            sb.append("\n\t</ExcludedPackages>"); // NOI18N
            
            sb.append("\n\t<ExcludedPatterns>"); // NOI18N
            for(Iterator i=excludedPatterns.iterator(); i.hasNext();){
                sb.append("\n\t\t"); // NOI18N
                sb.append((String)i.next());
            }
            sb.append("\n\t</ExcludedPatterns>"); // NOI18N
            
            sb.append("\n\t<Methods>"); // NOI18N
            for(MethodRef m : methodReferences){
                sb.append("\n\t\t"); // NOI18N
                sb.append(m);
            }
            sb.append("\n\t</Methods>"); // NOI18N

            sb.append("\n\t<Closure>"); // NOI18N
            for(String c : closure){
                sb.append("\n\t\t"); // NOI18N
                sb.append(c);
            }
            sb.append("\n\t</Closure>"); // NOI18N
        }
        sb.append("\n\t<Failed>"); // NOI18N
        for(Iterator i=failed.entrySet().iterator(); i.hasNext();) {
            Map.Entry referencingPathToFailedList=(Map.Entry)i.next();
            sb.append("\n\t\t"); // NOI18N
            sb.append("<ReferencingPath>"); // NOI18N
            sb.append("\n\t\t\t"); // NOI18N
            sb.append(referencingPathToFailedList.getKey());
            sb.append("\n\t\t"); // NOI18N
            sb.append("</ReferencingPath>"); // NOI18N
            sb.append("\n\t\t"); // NOI18N
            sb.append("<Classes>"); // NOI18N
            for(Iterator iii=((List)referencingPathToFailedList.getValue()).iterator(); iii.hasNext();){
                sb.append("\n\t\t\t"); // NOI18N
                sb.append((String)iii.next());
            }
            sb.append("\n\t\t"); // NOI18N
            sb.append("</Classes>"); // NOI18N
        }
        sb.append("\n\t</Failed>"); // NOI18N
        if(logger.isLoggable(Level.FINER)){
            sb.append("\n</Closure>"); // NOI18N
        }
        return sb.toString();        
    }

}
