/*
 * Copyright (c) 2004-2005 Ant-Contrib project.  All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.sf.antcontrib.design;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Vector;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.DescendingVisitor;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Utility;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;
import org.apache.tools.ant.util.JAXPUtils;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;

/**
 * 
 * 
 * 
 * @author dhiller
 * 
 */

public class VerifyDesignDelegate implements Log {

    private File designFile;
    private Vector paths = new Vector();
    private boolean isCircularDesign = false;
    private boolean deleteFiles = false;
    private boolean fillInBuildException = false;
    private boolean needDeclarationsDefault = true;
    private boolean needDependsDefault = true;
    
    private Task task;
    private Design design;
    private HashSet primitives = new HashSet();
    private Vector designErrors = new Vector();
    private boolean verifiedAtLeastOne = false;

    public VerifyDesignDelegate(Task task) {
        this.task = task;
        primitives.add("B");
        primitives.add("C");
        primitives.add("D");
        primitives.add("F");
        primitives.add("I");
        primitives.add("J");
        primitives.add("S");
        primitives.add("Z");
    }

    public void addConfiguredPath(Path path) {
//      Path newPath = new Path(task.getProject());
//      path.
        
        
        paths.add(path);
    }

    public void setJar(File f) {
        Path p = (Path)task.getProject().createDataType("path");
        p.createPathElement().setLocation(f.getAbsoluteFile());
        addConfiguredPath(p);
    }

    public void setDesign(File f) {
        this.designFile = f;          
    }

    public void setCircularDesign(boolean isCircularDesign) {
        this.isCircularDesign = isCircularDesign;
    }

    public void setDeleteFiles(boolean deleteFiles) {
        this.deleteFiles = deleteFiles;
    }

    public void setFillInBuildException(boolean b) {
        fillInBuildException = b;
    }
    
    public void setNeedDeclarationsDefault(boolean b) {
        needDeclarationsDefault = b;
    }
    
    public void setNeedDependsDefault(boolean b) {
        needDependsDefault = b;
    }

    public void execute() {
        if(!designFile.exists() || designFile.isDirectory())
            throw new BuildException("design attribute in verifydesign element specified an invalid file="+designFile);
        
        verifyJarFilesExist();

        try {
            XMLReader reader = JAXPUtils.getXMLReader();
            DesignFileHandler ch = new DesignFileHandler(this, designFile, isCircularDesign, task.getLocation());
            ch.setNeedDeclarationsDefault(needDeclarationsDefault);
            ch.setNeedDependsDefault(needDependsDefault);
            reader.setContentHandler(ch);
            //reader.setEntityResolver(ch);
            //reader.setErrorHandler(ch);
            //reader.setDTDHandler(ch);

            log("about to start parsing file='"+designFile+"'", Project.MSG_INFO);
            FileInputStream fileInput = new FileInputStream(designFile);
            InputSource src = new InputSource(fileInput);
            reader.parse(src);
            design = ch.getDesign();

            Enumeration pathsEnum = paths.elements();
            Path p = null;
            while (pathsEnum.hasMoreElements()) {
                p = (Path)pathsEnum.nextElement();
                verifyPathAdheresToDesign(design, p);
            }

            //only put unused errors if there are no other errors
            //this is because you end up with false unused errors if you don't do this.
            if(designErrors.isEmpty())
                design.fillInUnusedPackages(designErrors);
            
            if (! designErrors.isEmpty()) {
                log(designErrors.size()+"Errors.", Project.MSG_WARN);
                if(!fillInBuildException)
                    throw new BuildException("Design check failed due to previous errors");
                throwAllErrors();
            }

        } catch (SAXException e) {
            maybeDeleteFiles();
            if (e.getException() != null
                    && e.getException() instanceof RuntimeException)
                throw (RuntimeException) e.getException();
            else if (e instanceof SAXParseException) {
                SAXParseException pe = (SAXParseException) e;
                throw new BuildException("\nProblem parsing design file='"
                        + designFile + "'.  \nline=" + pe.getLineNumber()
                        + " column=" + pe.getColumnNumber() + " Reason:\n"
                        + e.getMessage() + "\n", e);
            }
            throw new BuildException("\nProblem parsing design file='"
                    + designFile + "'. Reason:\n" + e, e);
        } catch (IOException e) {
            maybeDeleteFiles();
            throw new RuntimeException("See attached exception", e);
            // throw new BuildException("IOException on design file='"
            // + designFile + "'. attached:", e);
        } catch(RuntimeException e) {
            maybeDeleteFiles();
            throw e;
        } finally {

        }
        
        if(!verifiedAtLeastOne) 
            throw new BuildException("Did not find any class or jar files to verify");
    }
    //some auto builds like cruisecontrol can only report all the
    //standard ant task errors and the build exceptions so here
    //we need to fill in the buildexception so the errors are reported
    //correctly through those tools....though, you think ant has a hook
    //in that cruisecontrol is not using like LogListeners or something
    private void throwAllErrors() {
        String result = "Design check failed due to following errors";
        Enumeration exceptions = designErrors.elements();
        while(exceptions.hasMoreElements()) {
            BuildException be = (BuildException)exceptions.nextElement();
            String message = be.getMessage();
            result += "\n" + message;
        }
        throw new BuildException(result);
    }

    private void verifyJarFilesExist() {
        Enumeration pathsEnum = paths.elements();
        Path p = null;
        while (pathsEnum.hasMoreElements()) {
            p = (Path)pathsEnum.nextElement();
            String files[] = p.list();
            for (int i=0;i<files.length;i++) {
                File file = new File(files[i]);

                if (!file.exists())
                    throw new BuildException(VisitorImpl.getNoFileMsg(file));
            }            
        }
    }
    
    private void maybeDeleteFiles() {
        if (deleteFiles) {
            log("Deleting all class and jar files so you do not get tempted to\n" +
                    "use a jar that doesn't abide by the design(This option can\n" +
                    "be turned off if you really want)", Project.MSG_INFO);
            
            Enumeration pathsEnum = paths.elements();
            Path p = null;
            while (pathsEnum.hasMoreElements()) {
                p = (Path)pathsEnum.nextElement();
                deleteFilesInPath(p);
            }           
        }
    }
    
    private void deleteFilesInPath(Path p) {
        String files[] = p.list();
        for (int i=0;i<files.length;i++) {
            File file = new File(files[i]);

            boolean deleted = file.delete();
            if (! deleted) {
                file.deleteOnExit();
            }
        }       
    }
    
    private void verifyPathAdheresToDesign(Design d, Path p) throws ClassFormatException, IOException {
        String files[] = p.list();
        for (int i=0;i<files.length;i++) {
            File file = new File(files[i]);
            if(file.isDirectory()) {
                FileSet set = new FileSet();
                set.setDir(file);
                set.setProject(task.getProject());
                PatternSet.NameEntry entry1 = set.createInclude();
                PatternSet.NameEntry entry2 = set.createInclude();
                PatternSet.NameEntry entry3 = set.createInclude();
                entry1.setName("**/*.class");
                entry2.setName("**/*.jar");
                entry3.setName("**/*.war");
                DirectoryScanner scanner = set.getDirectoryScanner(task.getProject());
                scanner.setBasedir(file);
                String[] scannerFiles = scanner.getIncludedFiles();
                for(int j = 0; j < scannerFiles.length; j++) {
                    verifyPartOfPath(scannerFiles[j], new File(file, scannerFiles[j]), d);
                }
            } else
                verifyPartOfPath(files[i], file, d);
        }
    }
    
    private void verifyPartOfPath(String fileName, File file, Design d) throws IOException {
        if (fileName.endsWith(".jar") || fileName.endsWith(".war")) {
            JarFile jarFile = new JarFile(file);
            verifyJarAdheresToDesign(d, jarFile, file);
        } else if (fileName.endsWith(".class")) {
            verifyClassAdheresToDesign(d, file);
        } else
            throw new BuildException("Only directories, jars, wars, and class files can be supplied to verify design, not file="+file.getAbsolutePath());
    }

    private void verifyClassAdheresToDesign(Design d, File classFile)
            throws ClassFormatException, IOException {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(classFile);
            verifyClassAdheresToDesign(d, fis, classFile.getAbsolutePath(), classFile);
        }
        finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            }
            catch (IOException e) {
                ; //doh!!
            }
        }

    }
    
    private void verifyJarAdheresToDesign(Design d, JarFile jarFile, File original)
            throws ClassFormatException, IOException {

        try {
        Enumeration en = jarFile.entries();
        while(en.hasMoreElements()) {
            ZipEntry entry = (ZipEntry)en.nextElement();
            InputStream in = null;
            if(entry.getName().endsWith(".class")) {
                in = jarFile.getInputStream(entry);
                try {
                    in = jarFile.getInputStream(entry);
                    verifyClassAdheresToDesign(d, in, entry.getName(), original);
                }
                finally {
                    try {
                        if (in != null) {
                            in.close();
                        }
                    }
                    catch (IOException e) {
                        ; // doh!!!
                    }
                }
            }
        }
        }
        finally {
            try {
                jarFile.close();
            }
            catch (IOException e) {
                ; //doh!!!
            }
        }
    }

    private String className = "";

    private void verifyClassAdheresToDesign(Design d, InputStream in, String name, File originalClassOrJarFile) throws ClassFormatException, IOException {
        try {
            verifiedAtLeastOne = true;
            ClassParser parser = new ClassParser(in, name);
            JavaClass javaClass = parser.parse();
            className = javaClass.getClassName();
            
            if(!d.needEvalCurrentClass(className))
                return;
    
            ConstantPool pool = javaClass.getConstantPool();
            processConstantPool(pool);
            VisitorImpl visitor = new VisitorImpl(pool, this, d, task.getLocation());
            DescendingVisitor desc = new DescendingVisitor(javaClass, visitor);
            desc.visit();
        } catch(BuildException e) {
            log(Design.getWrapperMsg(originalClassOrJarFile, e.getMessage()), Project.MSG_ERR);
            designErrors.addElement(e);         
        }
    }

    private void processConstantPool(ConstantPool pool) {
        Constant[] constants = pool.getConstantPool();
        if(constants == null) {
            log("      constants=null", Project.MSG_VERBOSE);
            return;
        }
        
        log("      constants len="+constants.length, Project.MSG_VERBOSE);      
        for(int i = 0; i < constants.length; i++) {
            processConstant(pool, constants[i], i);
        }
    }
    
    private void processConstant(ConstantPool pool, Constant c, int i) {
        if(c == null) //don't know why, but constant[0] seems to be always null.
            return;

        log("      const["+i+"]="+pool.constantToString(c)+" inst="+c.getClass().getName(), Project.MSG_DEBUG); 
        byte tag = c.getTag();
        switch(tag) {
            //reverse engineered from ConstantPool.constantToString..
        case Constants.CONSTANT_Class:
            int ind   = ((ConstantClass)c).getNameIndex();
            c   = pool.getConstant(ind, Constants.CONSTANT_Utf8);
            String className = Utility.compactClassName(((ConstantUtf8)c).getBytes(), false);
            log("      classNamePre="+className, Project.MSG_DEBUG);
            className = getRidOfArray(className);
            String firstLetter = className.charAt(0)+"";
            if(primitives.contains(firstLetter))
                return;
            log("      className="+className, Project.MSG_VERBOSE);
            design.checkClass(className);
            break;
        default:
                
        }
    }
    
    private static String getRidOfArray(String className) {
        while(className.startsWith("["))
            className = className.substring(1, className.length());
        return className;
    }
    
    public static String getPackageName(String className) {
        String packageName = Package.DEFAULT;
        int index = className.lastIndexOf(".");
        if(index > 0)
            packageName = className.substring(0, index);
        //DEANDO: test the else scenario here(it is a corner case)...

        return packageName;
    }
    
    public void log(String msg, int level) {
        //if(level == Project.MSG_WARN || level == Project.MSG_INFO 
        //      || level == Project.MSG_ERR || level == Project.MSG_VERBOSE)
        //VerifyDesignTest.log(msg);
        task.log(msg, level);
    }
}
