/*
 * @(#)GroboInstrumentTask.java
 *
 * Copyright (C) 2004 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 *  DEALINGS IN THE SOFTWARE.
 */

package net.sourceforge.groboutils.codecoverage.v2.ant;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.Vector;

import net.sourceforge.groboutils.codecoverage.v2.IAnalysisModule;
import net.sourceforge.groboutils.codecoverage.v2.compiler.AlreadyPostCompiledException;
import net.sourceforge.groboutils.codecoverage.v2.compiler.PostCompileClass;
import net.sourceforge.groboutils.codecoverage.v2.datastore.DirMetaDataWriter;
import net.sourceforge.groboutils.codecoverage.v2.logger.CacheDirChannelLoggerFactory;
import net.sourceforge.groboutils.codecoverage.v2.logger.DirectoryChannelLoggerFactory;
import net.sourceforge.groboutils.codecoverage.v2.logger.MinDirChannelLoggerFactory;
import net.sourceforge.groboutils.codecoverage.v2.logger.FileSingleSourceLoggerFactory;
import net.sourceforge.groboutils.codecoverage.v2.logger.NoOpChannelLoggerFactory;
import net.sourceforge.groboutils.util.io.v1.ReadByteStream;

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.taskdefs.Delete;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.util.FileUtils;



/**
 * A variation of the CoveragePostCompilerTask.  This one is intended to
 * simplify the Ant build files.  See
 * <a href="https://sourceforge.net/tracker/index.php?func=detail&aid=901588&group_id=22594&atid=375592">
 * feature request 901588</a> for details.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/04/17 08:24:39 $
 * @since     March 9, 2004
 */
public class GroboInstrumentTask extends Task
{
    private static final FileUtils FILEUTILS = FileUtils.newFileUtils();
    
    private static final String CLASSNAME_EXT = ".class";
    
    private static final String LOGGER_SAFE_1 = "safe";
    private static final String LOGGER_SAFE_2 = "dir";
    private static final String LOGGER_SAFE_3 = "directory";
    private static final String LOGGER_SAFE_CLASS =
        DirectoryChannelLoggerFactory.class.getName();
    private static final String LOGGER_KEEP_OPEN_1 = "cache";
    private static final String LOGGER_KEEP_OPEN_2 = "cachedir";
    private static final String LOGGER_KEEP_OPEN_CLASS =
        CacheDirChannelLoggerFactory.class.getName();
    private static final String LOGGER_MINDIR_1 = "fast";
    private static final String LOGGER_MINDIR_2 = "min";
    private static final String LOGGER_MINDIR_3 = "mindir";
    private static final String LOGGER_MINDIR_CLASS =
        MinDirChannelLoggerFactory.class.getName();
    private static final String LOGGER_SINGLEFILE_1 = "single file";
    private static final String LOGGER_SINGLEFILE_2 = "single";
    private static final String LOGGER_SINGLEFILE_CLASS =
        FileSingleSourceLoggerFactory.class.getName();
    private static final String LOGGER_NONE_1 = "none";
    private static final String LOGGER_NONE_CLASS =
        NoOpChannelLoggerFactory.class.getName();
    
    private static final Hashtable LOGGER_TO_CLASSNAME = new Hashtable();
    static {
        LOGGER_TO_CLASSNAME.put( LOGGER_SAFE_1, LOGGER_SAFE_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_SAFE_2, LOGGER_SAFE_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_SAFE_3, LOGGER_SAFE_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_KEEP_OPEN_1, LOGGER_KEEP_OPEN_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_KEEP_OPEN_2, LOGGER_KEEP_OPEN_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_MINDIR_1, LOGGER_MINDIR_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_MINDIR_2, LOGGER_MINDIR_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_MINDIR_3, LOGGER_MINDIR_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_SINGLEFILE_1, LOGGER_SINGLEFILE_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_SINGLEFILE_2, LOGGER_SINGLEFILE_CLASS );
        LOGGER_TO_CLASSNAME.put( LOGGER_NONE_1, LOGGER_NONE_CLASS );
    }
    
    
    /**
     * Contains all possible logger types.
     */
    public static final class LoggerAttribute extends EnumeratedAttribute
    {
        private String types[] = {
            LOGGER_SAFE_1, LOGGER_SAFE_2, LOGGER_SAFE_3,
            LOGGER_KEEP_OPEN_1, LOGGER_KEEP_OPEN_2,
            LOGGER_MINDIR_1, LOGGER_MINDIR_2, LOGGER_MINDIR_3,
            LOGGER_SINGLEFILE_1, LOGGER_SINGLEFILE_2,
            LOGGER_NONE_1
        };
        
        public String[] getValues()
        {
            return this.types;
        }
    }
    
    
    /**
     * Used for associating a key with a value for the properties.
     */
    public static final class LoggerProperty
    {
        String key;
        String value;
        public void setKey( String k )
        {
            this.key = k;
        }
        public void setValue( String v )
        {
            this.value = v;
        }
        public void setLocation( File f )
        {
            this.value = f.getAbsolutePath();
        }
    }
    
    
    private static final String HANDLEEXISTING_REPLACE = "replace";
    private static final String HANDLEEXISTING_KEEP = "keep";
    private static final String HANDLEEXISTING_REMOVE_ALL = "clean";
    
    /**
     * Contains all possible HandleExisting types.
     */
    public static final class HandleExistingAttribute
            extends EnumeratedAttribute
    {
        private String types[] = {
            HANDLEEXISTING_REPLACE, HANDLEEXISTING_KEEP,
            HANDLEEXISTING_REMOVE_ALL
        };
        public String[] getValues()
        {
            return types;
        }
    }
    
    
    
    
    private Vector filesets = new Vector();
    private Vector loggerProps = new Vector();
    private File datadir = null;
    private File logdir = null;
    private File outfiledir = null;
    private File baselogdir = null;
    private String logger = LOGGER_SAFE_1;
    private String loggerClass = null;
    private Vector analysisModules = new Vector();
    private String handleExisting = HANDLEEXISTING_REPLACE;

    

    /**
     * Add a new fileset instance to this compilation. Whatever the fileset is,
     * only filename that are <tt>.class</tt> will be considered as
     * 'candidates'.  Currently, jar files are not read; you'll have to
     * uncompress them to a directory before running this step.
     *
     * @param     fs the new fileset containing the rules to get the testcases.
     */
    public void addFileSet( FileSet fs )
    {
        this.filesets.addElement(fs);
    }
    
    
    /**
     * Set the type of logger to use.  This defaults to the "safe"
     * logger, which is JDK agnostic.
     */
    public void setLogger( LoggerAttribute la )
    {
        this.logger = la.getValue();
    }
    
    
    /**
     * Allow the user to specify a logger factory class name.  If this
     * is specified, it overrides any value set by the "logger" attribute.
     */
    public void setLoggerFactory( String name )
    {
        if (name.indexOf(".") < 0)
        {
            this.loggerClass =
                "net.sourceforge.groboutils.codecoverage.v2.logger."+
                name;
        }
        else
        {
            this.loggerClass = name;
        }
    }
    
    
    /**
     * Sets the directory in which all the data accumulated from the
     * post compilation step will be placed, and the logging output
     * as well.  This should be a directory dedicated just to the output data.
     * If the directory doesn't exist when the task runs, it will be
     * created.
     */
    public void setLogDir( File f )
    {
        this.baselogdir = f;
    }
    
    
    /**
     * Sets the directory in which all the recompiled class files will be
     * placed.  This directory should never be confused with the original
     * class file location.
     */
    public void setDestDir( File f )
    {
        this.outfiledir = f;
    }
    
    
    /**
     * Creates a new analysis module.
     */
    public void addMeasure( AnalysisModuleType amt )
    {
        this.analysisModules.addElement( amt );
    }
    
    
    /**
     * Adds a property to the logger properties file.
     */
    public void addLoggerProp( LoggerProperty lp )
    {
        this.loggerProps.addElement( lp );
    }
    
    
    /**
     * Sets the behavior when classes get post-compiled - should the
     * previous post-compiled class be replaced, kept, or should all
     * the previous data be cleaned?
     */
    public void setIfExists( HandleExistingAttribute hea )
    {
        this.handleExisting = hea.getValue();
    }
    
    
    
    /**
     * Perform the task
     */
    public void execute()
            throws BuildException
    {
        // pre-check
        setupDirectories();
        
        ClassFile classFiles[] = getFilenames();
        IAnalysisModule modules[] = getAnalysisModules();
        
        try
        {
            log( "Writing meta-data to directory '"+this.datadir+"'.",
                Project.MSG_VERBOSE );
            DirMetaDataWriter dmdw = new DirMetaDataWriter( this.datadir );
            try
            {
                PostCompileClass pcc = new PostCompileClass( dmdw, modules );
                for (int i = 0; i < classFiles.length; ++i)
                {
                    if (HANDLEEXISTING_REPLACE.equals( this.handleExisting ))
                    {
                        cleanupClass( classFiles[i], modules );
                    }
                    
                    File infile = classFiles[i].srcFile;
                    String filename = classFiles[i].filename;
                    
                    // create the output class file, and ensure that
                    // its directory structure exists before creating it
                    File outfile = new File( this.outfiledir, filename );
                    log( "Recompiling class '"+infile+"' to file '"+
                        outfile+"'.", Project.MSG_VERBOSE );
                    File parent = outfile.getParentFile();
                    if (!parent.exists())
                    {
                        parent.mkdirs();
                    }
                    
                    // need some code handle the situation where the
                    // outfile may be the same as the infile.  This will
                    // also allow us to correctly handle the situation of
                    // an exception not properly creating the instrumented
                    // class.  See bug 929332.
                    File tmpout = FILEUTILS.createTempFile( outfile.getName(),
                        ".tmp", parent );
                    FileOutputStream fos = new FileOutputStream( tmpout );
                    
                    try
                    {
                        pcc.postCompile( filename, readFile( infile ), fos );
                        fos.close();
                        fos = null;
                        FILEUTILS.copyFile( tmpout, outfile );
                    }
                    catch (AlreadyPostCompiledException apce)
                    {
                        // see bug 903837
                        log( "Ignoring '"+infile+"': it has already been "+
                            "post-compiled.", Project.MSG_INFO );
                    }
                    finally
                    {
                        if (fos != null)
                        {
                            fos.close();
                        }
                        if (tmpout.exists())
                        {
                            tmpout.delete();
                        }
                    }
                }
            }
            finally
            {
                dmdw.close();
            }
        }
        catch (IOException ioe)
        {
            throw new BuildException( "I/O exception during execution.",
                ioe, getLocation() );
        }
        
        try
        {
            generatePropertyFile( this.outfiledir, modules.length );
        }
        catch (IOException ioe)
        {
            throw new BuildException( "I/O exception during execution.",
                ioe, getLocation() );
        }
    }
    
    
    private void setupDirectories()
            throws BuildException
    {
        if (this.baselogdir == null)
        {
            throw new BuildException( "Attribute 'logdir' was never set." );
        }
        if (this.outfiledir == null)
        {
            throw new BuildException(
                "Attribute 'destdir' was never set." );
        }
        
        if (this.datadir == null)
        {
            this.datadir = new File( this.baselogdir, "data" );
        }
        if (this.logdir == null)
        {
            this.logdir = new File( this.baselogdir, "logs" );
        }
        
        
        // bug 906316: ensure the directories exist...
        if (!this.datadir.exists())
        {
            this.datadir.mkdirs();
        }
        else
        if (HANDLEEXISTING_REMOVE_ALL.equals( this.handleExisting ))
        {
            removeDir( this.datadir );
            this.datadir.mkdirs();
            
            removeDir( this.logdir );
        }
        
        
        if (!this.outfiledir.exists())
        {
            this.outfiledir.mkdirs();
        }
    }
    
    
    
    /**
     * 
     */
    private IAnalysisModule[] getAnalysisModules()
            throws BuildException
    {
        final Vector v = new Vector();
        final Enumeration enum = this.analysisModules.elements();
        while (enum.hasMoreElements())
        {
            AnalysisModuleType amt = (AnalysisModuleType)enum.nextElement();
            IAnalysisModule am = amt.getAnalysisModule();
            v.addElement( am );
        }
        final IAnalysisModule[] amL = new IAnalysisModule[ v.size() ];
        v.copyInto( amL );
        return amL;
    }
    
    
    
    
    /**
     * Iterate over all filesets and return the filename of all files
     * that end with <tt>.class</tt> (case insensitive). This is to avoid
     * trying to parse a non-class file.
     *
     * @return an array of filenames to parse.
     */
    private ClassFile[] getFilenames()
    {
        Vector v = new Vector();
        final int size = this.filesets.size();
        for (int j = 0; j < size; j++) 
        {
            FileSet fs = (FileSet)filesets.elementAt( j );
            DirectoryScanner ds = fs.getDirectoryScanner( getProject() );
            File baseDir = ds.getBasedir();
            ds.scan();
            String[] f = ds.getIncludedFiles();
            for (int k = 0; k < f.length; k++) 
            {
                String pathname = f[k];
                if (pathname.toLowerCase().endsWith( CLASSNAME_EXT )) 
                {
                    // this isn't right
                    v.addElement( new ClassFile( baseDir, pathname ) );
                }
            }
        }

        ClassFile[] files = new ClassFile[v.size()];
        v.copyInto(files);
        return files;
    }
    
    
    /**
     * Contains the data for the class file.
     */
    private static final class ClassFile
    {
        public File srcFile;
        public String filename;
        
        public ClassFile( File baseDir, String filename )
        {
            if (baseDir == null || filename == null)
            {
                throw new IllegalArgumentException("no null args.");
            }
            this.filename = filename;
            this.srcFile = new File( baseDir, filename );
        }
    }
    
    
    /**
     * Create the property file for the logger.
     */
    private void generatePropertyFile( File outfiledir, int moduleCount )
            throws IOException
    {
        Properties props = new Properties();
        if (this.loggerClass != null)
        {
            props.setProperty( "factory", this.loggerClass );
        }
        else
        {
            String factoryClass = (String)LOGGER_TO_CLASSNAME.get(
                this.logger );
            if (LOGGER_NONE_CLASS.equals( factoryClass ))
            {
                // feature: specifying a logger type (not class) of
                // none means don't create the property file.
                return;
            }
            props.setProperty( "factory", factoryClass );
        }
        
        if (this.logdir != null)
        {
            props.setProperty( "logger.dir",
                this.logdir.getAbsolutePath() );
        }
        
        Enumeration enum = loggerProps.elements();
        while (enum.hasMoreElements())
        {
            LoggerProperty lp = (LoggerProperty)enum.nextElement();
            if (lp.key == null)
            {
                throw new BuildException( "No key given for loggerprop." );
            }
            if (lp.value == null)
            {
                throw new BuildException(
                    "No value or location given for loggerprop key \""
                    + lp.key + "\"." );
            }
            props.setProperty( "logger." + lp.key, lp.value );
        }
        
        props.setProperty( "channel-count",
            Integer.toString( moduleCount ) );
        
        FileOutputStream fos = new FileOutputStream(
            new File( outfiledir, "grobocoverage.properties" ) );
        try
        {
            props.store( fos, "CodeCoverage setup file" );
        }
        finally
        {
            fos.close();
        }
    }
    
    
    /**
     * 
     */
    private byte[] readFile( File file )
            throws IOException
    {
        FileInputStream fis = new FileInputStream( file );
        try
        {
            byte[] outfile = ReadByteStream.readByteStream( fis );
            return outfile;
        }
        finally
        {
            fis.close();
        }
    }
    
    
    /**
     * Recursively removes a directory's contents
     */
    private void removeDir( File d )
    {
        MyDelete md = new MyDelete();
        md.setProject( getProject() );
        md.setFailOnError( false );
        md.removeDir2( d );
    }
    
    /** all we care about is making the removeDirectory public */
    private static final class MyDelete extends Delete
    {
        // save some execution time by not overriding removeDir, but
        // instead have a different method that calls out to the
        // protected method.
        public void removeDir2( File f )
        {
            if (f != null && f.exists())
            {
                if (f.isDirectory())
                {
                    super.removeDir( f );
                }
                else
                {
                    f.delete();
                }
            }
        }
    }
    
    
    /**
     * Cleanup the datafiles for the given class.  The implementation
     * is rather hacky.
     */
    private void cleanupClass( ClassFile cf, IAnalysisModule[] modules )
            throws IOException
    {
        String cfName = cf.filename.replace( File.separatorChar, '.' );
        if (cfName.toLowerCase().endsWith( CLASSNAME_EXT ))
        {
            cfName = cfName.substring( 0,
                cfName.length() - CLASSNAME_EXT.length() );
        }
        // be sure to add in the last '-', otherwise inner-classes and
        // other classes that start with the same text may be accidentally
        // deleted.  This was happening in the testInstrument5 test.
        cfName = cfName + "-";
        
        
        for (int amIndex = 0; amIndex < modules.length; ++amIndex)
        {
            // analysis module data dir
            File amdd = new File( this.datadir,
                modules[ amIndex ].getMeasureName() );
            File list[] = amdd.listFiles();
            if (list != null)
            {
                for (int i = 0; i < list.length; ++i)
                {
                    // names are of the form:
                    //   [full class name]-[CRC].[type].txt
                    String name = list[i].getName();
                    if (name.startsWith( cfName ))
                    {
                        // delete all for this class!!!
                        list[i].delete();
                    }
                }
            }
        }
    }
}

