//Tested with BCEL-5.1
//http://jakarta.apache.org/builds/jakarta-bcel/release/v5.1/

package com.puppycrawl.tools.checkstyle.bcel;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.bcel.Repository;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Visitor;
import org.apache.bcel.util.ClassLoaderRepository;
import com.puppycrawl.tools.checkstyle.DefaultContext;
import com.puppycrawl.tools.checkstyle.ModuleFactory;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.Context;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessages;

/**
 * Checks a set of class files using BCEL
 * @author Rick Giles
 */
//TODO: Refactor with AbstractFileSetCheck and TreeWalker
public class ClassFileSetCheck
    extends AbstractFileSetCheck
    implements IObjectSetVisitor
{
    /** visitors for BCEL parse tree walk */
    private final Set mTreeVisitors = new HashSet();

    /** all the registered checks */
    private final Set mAllChecks = new HashSet();

    /** all visitors for IObjectSetVisitor visits */
    private final Set mObjectSetVisitors = new HashSet();

    /** class loader to resolve classes with. **/
    private ClassLoader mClassLoader;

    /** context of child components */
    private Context mChildContext;

    /** a factory for creating submodules (i.e. the Checks) */
    private ModuleFactory mModuleFactory;

    /** Error messages */
    HashMap mMessageMap = new HashMap();

    /**
     * Creates a new <code>ClassFileSetCheck</code> instance.
     * Initializes the acceptable file extensions.
     */
    public ClassFileSetCheck()
    {
        setFileExtensions(new String[]{"class", "jar", "zip"});
    }

    /**
     * Stores the class loader and makes it the Repository's class loader.
     * @param aClassLoader class loader to resolve classes with.
     */
    public void setClassLoader(ClassLoader aClassLoader)
    {
        Repository.setRepository(new ClassLoaderRepository(aClassLoader));
        mClassLoader = aClassLoader;
    }

    /**
     * Sets the module factory for creating child modules (Checks).
     * @param aModuleFactory the factory
     */
    public void setModuleFactory(ModuleFactory aModuleFactory)
    {
        mModuleFactory = aModuleFactory;
    }

    /**
     * Instantiates, configures and registers a Check that is specified
     * in the provided configuration.
     * @see com.puppycrawl.tools.checkstyle.api.AutomaticBean
     */
    public void setupChild(Configuration aChildConf)
        throws CheckstyleException
    {
        // TODO: improve the error handing
        final String name = aChildConf.getName();
        final Object module = mModuleFactory.createModule(name);
        if (!(module instanceof AbstractCheckVisitor)) {
            throw new CheckstyleException(
                "ClassFileSet is not allowed as a parent of " + name);
        }
        final AbstractCheckVisitor c = (AbstractCheckVisitor) module;
        c.contextualize(mChildContext);
        c.configure(aChildConf);
        c.init();

        registerCheck(c);
    }

    /** @see com.puppycrawl.tools.checkstyle.api.Configurable */
    public void finishLocalSetup()
    {
        DefaultContext checkContext = new DefaultContext();
        checkContext.add("classLoader", mClassLoader);
        checkContext.add("messageMap", mMessageMap);
        checkContext.add("severity", getSeverity());

        mChildContext = checkContext;
    }

    /**
     * Register a check.
     * @param aCheck the check to register
     */
    private void registerCheck(AbstractCheckVisitor aCheck)
    {
        mAllChecks.add(aCheck);
    }

    /**
     * @see com.puppycrawl.tools.checkstyle.api.FileSetCheck
     */
    public void process(File[] aFiles)
    {
        registerVisitors();

        // get all the JavaClasses in the files
        final Set javaClasses = extractJavaClasses(aFiles);

        visitSet(javaClasses);

        // walk each Java class parse tree
        final JavaClassWalker walker = new JavaClassWalker();
        walker.setVisitor(getTreeVisitor());
        final Iterator it = javaClasses.iterator();
        while (it.hasNext()) {
            final JavaClass clazz = (JavaClass) it.next();
            visitObject(clazz);
            walker.walk(clazz);
            leaveObject(clazz);
        }

        leaveSet(javaClasses);
        fireErrors();
    }

    /**
     * Gets the visitor for a parse tree walk.
     * @return the visitor for a parse tree walk.
     */
    private Visitor getTreeVisitor()
    {
        return new VisitorSet(mTreeVisitors);
    }

    /**
     * Registers all the visitors for IObjectSetVisitor visits, and for
     * tree walk visits.
     */
    private void registerVisitors()
    {
        mObjectSetVisitors.addAll(mAllChecks);
        final Iterator it = mAllChecks.iterator();
        while (it.hasNext()) {
            final AbstractCheckVisitor check = (AbstractCheckVisitor) it.next();
            final IDeepVisitor visitor = check.getVisitor();
            mObjectSetVisitors.add(visitor);
            mTreeVisitors.add(visitor);
        }
    }

    /**
     * Gets the set of all visitors for all the checks.
     * @return the set of all visitors for all the checks.
     */
    private Set getObjectSetVisitors()
    {
        return mObjectSetVisitors;
    }

    /**
     * Gets the set of all JavaClasses within a set of Files.
     * @param aFiles the set of files to extract from.
     * @return the set of all JavaClasses within aFiles.
     */
    private Set extractJavaClasses(File[] aFiles)
    {
        final Set result = new HashSet();
        final File[] classFiles = filter(aFiles);
        // get Java classes from each filtered file
        for (int i = 0; i < classFiles.length; i++) {
            try {
                final Set extracted = extractJavaClasses(classFiles[i]);
                result.addAll(extracted);
            }
            catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return result;
    }

    /** @see com.puppycrawl.tools.checkstyle.bcel.IObjectSetVisitor */
    public void visitSet(Set aSet)
    {
        // register the JavaClasses in the Repository
        Repository.clearCache();
        Iterator it = aSet.iterator();
        while (it.hasNext()) {
            final JavaClass javaClass = (JavaClass) it.next();
            Repository.addClass(javaClass);
        }

        // visit the visitors
        it = getObjectSetVisitors().iterator();
        while (it.hasNext()) {
            final IObjectSetVisitor visitor = (IObjectSetVisitor) it.next();
            visitor.visitSet(aSet);
        }
    }

    /** @see com.puppycrawl.tools.checkstyle.bcel.IObjectSetVisitor */
    public void visitObject(Object aObject)
    {
        final Iterator it = getObjectSetVisitors().iterator();
        while (it.hasNext()) {
            final IObjectSetVisitor visitor = (IObjectSetVisitor) it.next();
            visitor.visitObject(aObject);
        }
    }

    /** @see com.puppycrawl.tools.checkstyle.bcel.IObjectSetVisitor */
    public void leaveObject(Object aObject)
    {
        final Iterator it = getObjectSetVisitors().iterator();
        while (it.hasNext()) {
            final IObjectSetVisitor visitor = (IObjectSetVisitor) it.next();
            visitor.leaveObject(aObject);
        }
    }

    /** @see com.puppycrawl.tools.checkstyle.bcel.IObjectSetVisitor */
    public void leaveSet(Set aSet)
    {
        final Iterator it = getObjectSetVisitors().iterator();
        while (it.hasNext()) {
            final IObjectSetVisitor visitor = (IObjectSetVisitor) it.next();
            visitor.leaveSet(aSet);
        }
    }

    /**
     * Extracts the JavaClasses from .class, .zip, and .jar files.
     * @param aFile the file to extract from.
     * @return the set of JavaClasses from aFile.
     * @throws IOException if there is an error.
     */
    private Set extractJavaClasses(File aFile)
        throws IOException
    {
        final Set result = new HashSet();
        final String fileName = aFile.getPath();
        if (fileName.endsWith(".jar") || fileName.endsWith(".zip")) {
            final ZipFile zipFile = new ZipFile(fileName);
            final Enumeration entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                final ZipEntry entry = (ZipEntry) entries.nextElement();
                final String entryName = entry.getName();
                if (entryName.endsWith(".class")) {
                    final InputStream in = zipFile.getInputStream(entry);
                    final JavaClass javaClass =
                        new ClassParser(in, entryName).parse();
                    result.add(javaClass);
                }
            }
        }
        else {
            final JavaClass javaClass = new ClassParser(fileName).parse();
            result.add(javaClass);
        }
        return result;
    }

    /**
     * Notify all listeners about the errors in a file.
     * Calls <code>MessageDispatcher.fireErrors()</code> with
     * all logged errors and than clears errors' list.
     */
    private void fireErrors()
    {
        Set keys = mMessageMap.keySet();
        Iterator iter = keys.iterator();
        while (iter.hasNext()) {
            String key = (String) iter.next();
            getMessageDispatcher().fireFileStarted(key);
            LocalizedMessages localizedMessages = (LocalizedMessages) mMessageMap.get(key);
            final LocalizedMessage[] errors = localizedMessages.getMessages();
            localizedMessages.reset();
            getMessageDispatcher().fireErrors(key, errors);
            getMessageDispatcher().fireFileFinished(key);
        }
    }

}