/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2015 Oracle and/or its affiliates. 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_1_1.html
 * or packager/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 packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [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.
 */

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

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

/**
 * An implementation of {@link ClosureCompilerImplBase} based on
 * BCEL.
 *
 * @author Sanjeeb.Sahoo@Sun.COM
 */
public class BCELClosureCompilerImpl extends ClosureCompilerImplBase {

    /*
     * Earlier this class used to be called as ClosureCompilerImpl.
     * So to get the history of this file, use above name in CVS
     */

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

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

    private HashSet<String> nativeMethods = 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 = "BCELClosureCompilerImpl"; // NOI18N

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

    //See corresponding method of ClosureCompiler for javadocs
    public boolean buildClosure(String className) {
        logger.entering(myClassName, "buildClosure", className); // NOI18N
        ClassFile cf;
        if (!needToBuildClosure(className))
            return true;
        try {
            cf = loader.load(className);
        } catch (IOException e) {
            handleFailure(className);
            return false;
        }
        return buildClosure(cf);
    }

    /**
     * @param cf class file whose closure needs to be computed. The behavior is
     *           same as the other buildClosure() method.
     */
    private boolean buildClosure(ClassFile cf) {
        boolean result = true;
        callStack.push(cf);
        if (needToBuildClosure(cf.getName())) {
            visitedClasses.add(cf.getName());
            Collection<String> names = cf.getAllReferencedClassNames();
            closure.addAll(names);
            // TODO: We should not be doing this here. Its just a quick &
            // dirty solution.
            for(Method m : cf.getMethods()) {
                if(m.isNative()) {
                    final String methodDesc =
                            m.getOwningClass().getName()+ "." + m.getName(); // NOI18N
                    nativeMethods.add(methodDesc);
                }
            }
            for (Iterator i = names.iterator(); i.hasNext();) {
                String nextExternalName = (String) i.next();
                if (!needToBuildClosure(nextExternalName)) continue;
                ClassFile next;
                try {
                    next = loader.load(nextExternalName);
                } catch (IOException e) {
                    result = false;
                    handleFailure(nextExternalName);
                    continue;
                }
                boolean newresult = buildClosure(next);//recurssive call
                result = newresult && result;
            }
        }
        callStack.pop();
        return result;
    }

    private void handleFailure(String referencedClass) {
        String referencingPath = "";
        try {
            StringBuilder referencingPathBuffer = new StringBuilder();
            for (Iterator i = callStack.iterator(); i.hasNext();) {
                if (referencingPathBuffer.length() != 0)
                    referencingPathBuffer.append(File.separator);
                referencingPathBuffer.append(((ClassFile) i.next()).getName());
            }
            referencingPath = referencingPathBuffer.toString();
        } catch (EmptyStackException e) {
        }
        logger.finer(
                "Could not locate " + referencingPath + File.separator + // NOI18N
                referencedClass);
        List<String> failedList = failed.get(referencingPath);
        if (failedList == null) {
            failedList = new ArrayList<String>();
            failed.put(referencingPath, failedList);
        }
        failedList.add(referencedClass);
    }

    //See corresponding method of ClosureCompiler for javadocs
    public Collection getClosure() {
        return Collections.unmodifiableCollection(closure);
    }

    //See corresponding method of ClosureCompiler for javadocs
    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() {
        closure.clear();
        visitedClasses.clear();
        failed.clear();
        nativeMethods.clear();
    }

    public Collection<String> getNativeMethods() {
        return Collections.unmodifiableCollection(nativeMethods);
    }

    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<Classes>"); // NOI18N
            for(Iterator i=closure.iterator(); i.hasNext();){
                sb.append("\n\t\t"); // NOI18N
                sb.append((String)i.next());
            }
            sb.append("\n\t</Classes>"); // 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

        sb.append("\n\t<NativeMethods>"); // NOI18N
        for(String s : nativeMethods) {
            sb.append("\n\t\t"); // NOI18N
            sb.append(s);
        }
        sb.append("\n\t</NativeMethods>"); // NOI18N

        if(logger.isLoggable(Level.FINER)){
            sb.append("\n</Closure>"); // NOI18N
        }
        return sb.toString();        
    }

}
