/*
 * @(#)GroboReportTask.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.IOException;
import java.util.Enumeration;
import java.util.Vector;

import net.sourceforge.groboutils.codecoverage.v2.IAnalysisModule;
import net.sourceforge.groboutils.codecoverage.v2.IChannelLogReader;
import net.sourceforge.groboutils.codecoverage.v2.datastore.AnalysisModuleSet;
import net.sourceforge.groboutils.codecoverage.v2.datastore.DirMetaDataReader;
import net.sourceforge.groboutils.codecoverage.v2.datastore.IMetaDataReader;
import net.sourceforge.groboutils.codecoverage.v2.logger.DirectoryChannelLogReader;
import net.sourceforge.groboutils.codecoverage.v2.report.AnalysisModuleData;
import net.sourceforge.groboutils.codecoverage.v2.report.IReportGenerator;
import net.sourceforge.groboutils.codecoverage.v2.report.XmlCombinedReportGenerator;
import net.sourceforge.groboutils.codecoverage.v2.report.XmlReportGenerator;
import net.sourceforge.groboutils.codecoverage.v2.util.ILogFilter;
import net.sourceforge.groboutils.codecoverage.v2.util.SingleLogFilter;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.EnumeratedAttribute;

import org.w3c.dom.Document;
import org.w3c.dom.Element;




/**
 * 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/07/07 09:39:09 $
 * @since     March 13, 2004
 */
public class GroboReportTask extends Task
{
    private File baselogdir;
    private File logdir;
    private File datadir;
    private Vector singleStyles = new Vector();
    private Vector comboStyles = new Vector();
    private boolean failOnError = true;
    private LogFilter filter = null;
    
    
    public static final class FilterTypeAttribute extends EnumeratedAttribute
    {
        private String[] types = { "single", "none" };
        public String[] getValues()
        {
            return this.types;
        }
    }
    
    public static class LogFilter
    {
        private String type = "none";
        
        public void setType( FilterTypeAttribute fta )
        {
            this.type = fta.getValue();
        }
        
        public ILogFilter getFilter()
        {
            if ("single".equalsIgnoreCase( this.type ))
            {
                return new SingleLogFilter();
            }
            // else
            return null;
        }
    }
    
    
    /**
     * Set the directory which contains the log and data directories.
     */
    public void setLogDir( File dir )
    {
        this.baselogdir = dir;
    }
    
    
    /**
     * Add a filter to convert the Logger's output format to the standard
     * format.  If no filter is given, then we assume that the logs are
     * already in the standard format.
     */
    public void addLogFilter( LogFilter lf )
    {
        this.filter = lf;
    }
    
    
    /**
     * 
     */
    public void addXml( XmlReportStyle xrs )
    {
        // creates an XML report for both the single and combo files.
        _addSingle( xrs );
        _addCombo( xrs );
    }
    
    
    /**
     * 
     */
    public void addXsl( SimpleXslReportStyle sxrs )
    {
        _addSingle( sxrs );
    }
    
    
    /**
     * 
     */
    public void addSourceXsl( SourceXslReportStyle sxrs )
    {
        _addCombo( sxrs );
    }
    
    
    /**
     * 
     */
    public void addSimpleHtml( SimpleHtmlReportStyle shrs )
    {
        _addSingle( shrs );
    }
    
    
    /**
     * 
     */
    public void addSimple( SimpleHtmlReportStyle shrs )
    {
        _addSingle( shrs );
    }
    
    
    /**
     * 
     */
    public void addSourceHtml( SourceHtmlReportStyle shrs )
    {
        _addCombo( shrs );
    }
    
    
    /**
     * 
     */
    public void addSource( SourceHtmlReportStyle shrs )
    {
        _addCombo( shrs );
    }
    
    
    /**
     * 
     */
    public void addFailOn( FailOnReportStyle fors )
    {
        _addCombo( fors );
    }
    
    
    public void setFailOnError( boolean f )
    {
        this.failOnError = f;
    }
    
    
    
    public void execute()
            throws BuildException
    {
        try
        {
            setupDirectories();
        }
        catch (IOException ioe)
        {
            throw new BuildException( "Error setting up the directories.", ioe );
        }
        
        
        IMetaDataReader mdr = createMetaDataReader();
        IReportGenerator rg = new XmlReportGenerator();
        boolean errors = false;
        Vector reports = new Vector();
        Vector errorList = new Vector();
        try
        {
            AnalysisModuleSet ams = mdr.getAnalysisModuleSet();
            IAnalysisModule amL[] = ams.getAnalysisModules();
            for (int i = 0; i < amL.length; ++i)
            {
                try
                {
                    IChannelLogReader clr =
                        createChannelLogReader( amL[i], ams );
                    AnalysisModuleData amd =
                        new AnalysisModuleData( amL[i], mdr, clr );
                    Element rootEl = createReport( amL[i], amd, rg );
                    if (rootEl == null)
                    {
                        log( "Creating the report returned null", Project.MSG_WARN );
                        errors = true;
                    }
                    else
                    {
                        Document doc = rootEl.getOwnerDocument();
                        reports.addElement( doc );
                        processSingleStyles( doc, amL[i].getMeasureName() );
                    }
                }
                catch (IllegalArgumentException iae)
                {
                    iae.printStackTrace();
                    log( iae.getMessage(), Project.MSG_WARN );
                    errors = true;
                }
            }
            finishSingleStyles( errorList );
        }
        catch (IOException e)
        {
            throw new BuildException(
                "I/O Exception while creating a report.", e, getLocation() );
        }
        finally
        {
            try
            {
                mdr.close();
            }
            catch (IOException e)
            {
                throw new BuildException(
                    "I/O Exception while closing meta-data reader.", e,
                    getLocation() );
            }
        }
        rg = null;
        mdr = null;
        try
        {
            processCombos( reports, errorList );
        }
        catch (IOException e)
        {
            throw new BuildException(
                "I/O Exception while creating a report.", e, getLocation() );
        }
        
        
        if (errors && this.failOnError)
        {
            throw new BuildException(
                "No coverage logs were generated, or the logs "+
                "are not located under '"+this.logdir+"'.",
                getLocation() );
        }
        
        if (errorList.size() > 0)
        {
            StringBuffer sb = new StringBuffer();
            Enumeration e = errorList.elements();
            while (e.hasMoreElements())
            {
                sb.append( "; " ).append( (String)e.nextElement() );
            }
            if (this.failOnError)
            {
                throw new BuildException(
                    "Error conditions caused failure" + sb.toString() );
            }
            else
            {
                log( "Warning: " + sb.toString(), Project.MSG_WARN );
            }
        }
    }
    

    
    
    private void processCombos( Vector reports, Vector errorList )
            throws BuildException, IOException
    {
        // only process the combos (and thus create the uber XML file)
        // if needed.
        if (this.comboStyles.size() > 0)
        {
            // Create the uber document
            Document docs[] = new Document[ reports.size() ];
            reports.copyInto( docs );
            
            // a memory conservation item.  This means this can only be
            // the last method to run with the reports from execute()
            reports.removeAllElements();
            
            XmlCombinedReportGenerator gen = new XmlCombinedReportGenerator();
            log( "Creating combined coverage report", Project.MSG_INFO );
            Element ret = null;
            try
            {
                ret = gen.createReport( docs );
            }
            catch (IllegalArgumentException iae)
            {
                // throw new BuildException( iae );
                
                // this is a hack: really, the underlying code should be made
                // more robust.
                iae.printStackTrace();
                log( iae.getMessage(), Project.MSG_WARN );
                ret = null;
            }
            docs = null;
            
            if (ret != null)
            {
                processComboStyles( ret.getOwnerDocument() );
                finishComboStyles( errorList );
            }
        }
    }

    
    
    private void processSingleStyles( Document doc, String moduleName )
            throws BuildException, IOException
    {
        processStyles( doc, moduleName, this.singleStyles.elements() );
    }
    
    
    private void finishSingleStyles( Vector errorList )
            throws BuildException, IOException
    {
        finishStyles( this.singleStyles.elements(), errorList );
    }
    
    
    private void processComboStyles( Document doc )
            throws BuildException, IOException
    {
        processStyles( doc, "all", this.comboStyles.elements() );
    }
    
    
    private void finishComboStyles( Vector errorList )
            throws BuildException, IOException
    {
        finishStyles( this.comboStyles.elements(), errorList );
    }
    
    
    private void processStyles( Document doc, String moduleName,
            Enumeration styles )
            throws BuildException, IOException
    {
        while (styles.hasMoreElements())
        {
            IReportStyle rs = (IReportStyle)styles.nextElement();
            rs.generateReport( getProject(), doc, moduleName );
        }
    }
    
    
    private void finishStyles( Enumeration styles, Vector errors )
            throws BuildException, IOException
    {
        while (styles.hasMoreElements())
        {
            IReportStyle rs = (IReportStyle)styles.nextElement();
            rs.reportComplete( getProject(), errors );
        }
    }
    
    
    
    /**
     * Even though these are public, they take an interface, and so can't
     * be called through the Ant script.
     */
    public void _addSingle( IReportStyle rs )
    {
        if (rs != null)
        {
            this.singleStyles.addElement( rs );
        }
    }
    
    
    /**
     * Even though these are public, they take an interface, and so can't
     * be called through the Ant script.
     */
    public void _addCombo( IReportStyle rs )
    {
        if (rs != null)
        {
            this.comboStyles.addElement( rs );
        }
    }
    
    
    private Element createReport( IAnalysisModule am,
            AnalysisModuleData amd, IReportGenerator rg )
            throws IOException, BuildException
    {
        log( "Creating coverage report for module "+am.getMeasureName(),
            Project.MSG_INFO );
        Element ret = null;
        try
        {
            ret = rg.createReport( am, amd );
        }
        catch (IllegalArgumentException iae)
        {
            // throw new BuildException( iae );
            
            // this is a hack: really, the underlying code should be made
            // more robust.
            iae.printStackTrace();
            log( iae.getMessage(), Project.MSG_WARN );
            ret = null;
        }
        
        return ret;
    }
    
    
    private IChannelLogReader createChannelLogReader( IAnalysisModule am,
            AnalysisModuleSet ams )
            throws IOException
    {
        short mi = ams.getAnalysisModuleIndex( am );
        IChannelLogReader clr = new DirectoryChannelLogReader( this.logdir,
            mi );
        return clr;
    }
    
    
    private IMetaDataReader createMetaDataReader()
            throws BuildException
    {
        try
        {
            return new DirMetaDataReader( this.datadir );
        }
        catch (IOException e)
        {
            throw new BuildException( "I/O error creating meta-data reader.",
                e, getLocation() );
        }
    }


    
    /**
     * setup the directories in the logdir - ensure we have adequate
     * setup protections.  This saves some head banging in figuring out
     * why bad exceptions are thrown from the Ant script.
     */
    private void setupDirectories()
            throws IOException, BuildException
    {
        // check specifications
        if (this.baselogdir == null)
        {
            throw new BuildException( "Did not specify attribute 'datadir'." );
        }
        if (this.datadir == null)
        {
            this.datadir = new File( this.baselogdir, "data" );
        }
        if (this.logdir == null)
        {
            this.logdir = new File( this.baselogdir, "logs" );
        }
        
        
        if (!this.datadir.exists() ||
            !this.datadir.isDirectory())
        {
            throw new BuildException("Data directory setting ("+
                this.datadir+") does not exist or is not a directory.");
        }
        if (!this.logdir.exists())
        {
            this.logdir.mkdirs();
        }
        if (!this.logdir.isDirectory())
        {
            throw new BuildException("Log directory setting ("+
                this.logdir+") is not a directory.");
        }
        String modules[] = this.datadir.list();
        if (modules == null || modules.length <= 0)
        {
            throw new BuildException("There are no module data directories in "+
                this.datadir+".");
        }
        
        String indicies[] = this.logdir.list();
        if (indicies == null)
        {
            indicies = new String[0];
        }
        int count = modules.length;
        
        for (int i = 0; i <= count; ++i)
        {
            String dirname = Integer.toString(i);
            boolean found = false;
            for (int j = 0; j < indicies.length; ++j)
            {
                if (indicies[j].equals( dirname ))
                {
                    found = true;
                    break;
                }
            }
            if (!found)
            {
                File f = new File( this.logdir, dirname );
                f.mkdirs();
            }
        }
        
        if (this.filter != null)
        {
            ILogFilter lf = this.filter.getFilter();
            if (lf != null)
            {
                lf.process( count, this.logdir );
            }
        }
    }
}

