/*
 * @(#)SourceXslReportStyle.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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import net.sourceforge.groboutils.codecoverage.v2.report.IXmlReportConst;
import net.sourceforge.groboutils.codecoverage.v2.report.XmlSourceReportGenerator2;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;


/**
 * This is a combo report, which means that it only works
 * with the combined coverage file.  Use of this style is really delicate.
 *
 * @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 15, 2004
 */
public class SourceXslReportStyle implements IReportStyle, IXmlReportConst
{
    private File outdir;
    private Vector srcdirs = new Vector();
    private boolean removeEmpties = false;
    private Vector rootStyles = new Vector();
    private Vector sourceStyles = new Vector();
    private Vector packageStyles = new Vector();
    private Vector files = new Vector();
    private Vector params = new Vector();
    private int totalCachedCount = 100;
    
    
    
    public static final class DirType
    {
        private File dir;
        public void setName( File name )
        {
            this.dir = name;
        }
        
        public File getDir()
        {
            return this.dir;
        }
    }
    
    
    public static final class StyleType
    {
        String url;
        File file;
        String dest;
        StyleTransformer transformer;
        int count;
        int totalCount = -1;
        
        public void setTotalCount( int totalCount )
        {
            if (this.totalCount < 0)
            {
                this.totalCount = totalCount;
                if (totalCount == 0)
                {
                    this.transformer = null;
                }
            }
        }
        
        public void setFile( File f )
                throws IOException
        {
            this.file = f;
        }
        
        public void setUrl( String resource )
        {
            this.url = resource;
        }
        
        // may be ignored
        public void setDest( String d )
        {
            this.dest = d;
        }
        
        
        protected StyleTransformer getTransformer()
        {
            if (this.transformer != null && ++this.count > this.totalCount)
            {
                this.transformer = null;
                // fall through
            }
            return this.transformer;
        }
        
        
        protected void setTransformer( StyleTransformer st )
        {
            if (this.totalCount != 0)
            {
                this.count = 0;
                this.transformer = st;
            }
        }
        
        
        public String getURL( SourceXslReportStyle sxrs )
                throws IOException
        {
            String ret = null;
            if (this.file != null)
            {
                ret = sxrs.getStylesheetSystemIdForFile( this.file );
            }
            else
            if (this.url != null)
            {
                ret = sxrs.getStylesheetSystemIdForClass( this.url );
            }
            else
            {
                throw new BuildException(
                    "Neither 'file' nor 'url' were set for style" );
            }
            return ret;
        }
    }
    
    
    public void setStyleCacheCount( int count ) throws BuildException
    {
        if (count < 0)
        {
            throw new BuildException( "count cannot be less than 0" );
        }
        this.totalCachedCount = count;
    }
    
    
    public void setDestDir( File dir )
    {
        this.outdir = dir;
    }
    
    
    public void setSrcDir( File dir )
    {
        if (dir != null)
        {
            DirType dt = new DirType();
            dt.setName( dir );
            this.srcdirs.addElement( dt );
        }
    }

    
    public void addRootStyle( StyleType s )
    {
        if (s != null)
        {
            this.rootStyles.addElement( s );
        }
    }

    
    public void addSourceStyle( StyleType s )
    {
        if (s != null)
        {
            this.sourceStyles.addElement( s );
        }
    }

    
    public void addPackageStyle( StyleType s )
    {
        if (s != null)
        {
            this.packageStyles.addElement( s );
        }
    }

    
    public void addFile( StyleType s )
    {
        if (s != null)
        {
            this.files.addElement( s );
        }
    }
    
    
    /**
     * Add a new source directory to this report.
     */
    public void addSrcDir( DirType dt )
    {
        this.srcdirs.addElement( dt );
    }
    
    
    public void addParam( SimpleXslReportStyle.ParamType pt )
    {
        if (pt != null)
        {
            this.params.addElement( pt );
        }
    }
    
    
    public void setRemoveEmpty( boolean on )
    {
        this.removeEmpties = on;
    }
    
    
    /**
     * Called when the task is finished generating all the reports.  This
     * may be useful for styles that join all the reports together.
     */
    public void reportComplete( Project project, Vector errors )
            throws BuildException, IOException
    {
        // do nothing
    }
    
    
    public void generateReport( Project project, Document doc,
            String moduleName )
            throws BuildException, IOException
    {
        project.log( "Generating source report", Project.MSG_VERBOSE );
        if (this.removeEmpties)
        {
            project.log( "Removing empty methods and classes...",
                Project.MSG_VERBOSE );
            doc = getRemoveEmptiesStyle( project ).transform( doc );
        }
        
        setupStyles();
        
        long start = System.currentTimeMillis();
        // Transform the source files.
        // Do this first so that the directory structure exists, and so that
        // we know that all the source files exist.
        transformSources( project, doc );
        
        transformPackages( project, doc );
        transformRoots( project, doc );
        copyResources( project );
        
        project.log( "Total transform time = " +
            (System.currentTimeMillis() - start) + " ms",
            Project.MSG_VERBOSE );
    }
    
    
    protected void setupStyles()
    {
        setupStyles( this.rootStyles.elements() );
        setupStyles( this.sourceStyles.elements() );
        setupStyles( this.packageStyles.elements() );
    }
    
    
    protected void setupStyles( Enumeration e )
    {
        while (e.hasMoreElements())
        {
            StyleType st = (StyleType)e.nextElement();
            st.setTotalCount( this.totalCachedCount );
        }
    }
    
    
    /**
     * This is the biggest time eater.  This takes friggin' forever.
     */
    protected void transformSources( Project project, Document doc )
            throws IOException
    {
        project.log( "Making reports for source files", Project.MSG_VERBOSE );
        
        Element rootEl = doc.getDocumentElement();
        XmlSourceReportGenerator2 xsrg = new XmlSourceReportGenerator2( doc );
        String sources[] = xsrg.getSourceNames();
        for (int i = 0; i < sources.length; ++i)
        {
            // existence of the source file logic is in the generator.
            project.log( "Making report for source file "+sources[i],
                Project.MSG_DEBUG );
            long start = System.currentTimeMillis();
            Document srcDoc = xsrg.createXML( sources[i],
                getSourceFile( sources[i], project ) );
            srcDoc.getDocumentElement().setAttribute( "updir",
                getUpDir( sources[i] ) );
            project.log( "Generating source XML doc took "+
                (System.currentTimeMillis() - start) + " ms",
                Project.MSG_DEBUG );
            
            transform( project, srcDoc, this.sourceStyles.elements(),
                getOutFileBase( sources[i] ) );
            
            // need a better way to clean up this document...
            srcDoc = null;
            System.gc();
        }
        clearCache( this.sourceStyles.elements() );
    }
    
    
    protected void transformPackages( Project project, Document doc )
            throws IOException
    {
        // do each individual package...
        project.log( "Making reports for packages", Project.MSG_VERBOSE );
        
        DocumentBuilder builder = getDocumentBuilder();
        NodeList list = doc.getElementsByTagName( "package" );
        Element typeEl = (Element)doc.getElementsByTagName( "moduletypes" ).
            item(0);
        NodeList classes = doc.getElementsByTagName( "classcoverage" );
        for (int i = 0; i < list.getLength(); ++i)
        {
            Document ndoc = builder.newDocument();
            Element n = (Element)ndoc.importNode( list.item( i ), true );
            ndoc.appendChild( n );
            String pkgname = n.getAttribute( "name" );
            
            n.appendChild( ndoc.importNode( typeEl, true ) );
            for (int j = 0; j < classes.getLength(); ++j)
            {
                n.appendChild( ndoc.importNode( classes.item(j), true ) );
            }
            
            
            // turn the package name into a path
            String dirname = "";
            if (pkgname.length() > 0)
            {
                dirname = pkgname.replace( '.', File.separatorChar )
                    + File.separatorChar;
            }
            
            n.setAttribute( "updir", getUpDir( dirname ) );
            n = null;

            transform( project, ndoc, this.packageStyles.elements(),
                getOutFileBase( dirname ) );
            ndoc = null;
            System.gc();
        }
        clearCache( this.packageStyles.elements() );
    }
    
    
    protected void transformRoots( Project project, Document doc )
            throws IOException
    {
        transform( project, doc, this.rootStyles.elements(),
            getOutFileBase( null ) );
        clearCache( this.rootStyles.elements() );
    }
    
    
    protected void copyResources( Project project )
            throws IOException
    {
        Enumeration e = files.elements();
        while (e.hasMoreElements())
        {
            StyleType st = (StyleType)e.nextElement();
            URL url = new URL( st.getURL( this ) );
            InputStream in = url.openStream();
            if (in == null)
            {
                throw new java.io.FileNotFoundException( "URL " +
                    url );
            }
            FileOutputStream fos = new FileOutputStream(
                new File( this.outdir, st.dest ) );
            try
            {
                byte buff[] = new byte[ 4096 ];
                int size = in.read( buff, 0, 4096 );
                while (size > 0)
                {
                    fos.write( buff, 0, size );
                    size = in.read( buff, 0, 4096 );
                }
            }
            finally
            {
                in.close();
                fos.close();
            }
        }
    }
    
    
    protected void transform( Project project, Document doc,
            Enumeration styles, String baseOutName )
            throws IOException
    {
        long start = System.currentTimeMillis();
        while (styles.hasMoreElements())
        {
            StyleType s = (StyleType)styles.nextElement();
            File out = new File( baseOutName + s.dest );
            StyleTransformer st = getStyleTransformer( project, s );
            
            // ensure the directory exists
            out.getParentFile().mkdirs();
            
            st.transform( doc, out );
        }
        project.log( "Transform for all styles took " +
            (System.currentTimeMillis() - start) + " ms",
            Project.MSG_VERBOSE );
    }
    
    
    protected void clearCache( Enumeration styles )
    {
        while (styles.hasMoreElements())
        {
            StyleType s = (StyleType)styles.nextElement();
            s.setTransformer( null );
        }
        System.gc();
    }
    
    
    protected StyleTransformer getRemoveEmptiesStyle( Project project )
            throws IOException
    {
        return new StyleTransformer( project,
            getStylesheetSystemIdForClass( "xsl/remove-empty-classes.xsl" ),
            this.outdir );
    }
    
    
    
    /**
     * Returns the first file from the list of source directories that
     * contains the given sourcefile.
     */
    protected File getSourceFile( String sourceName, Project project )
    {
        Enumeration enum = this.srcdirs.elements();
        while (enum.hasMoreElements())
        {
            File f = ((DirType)enum.nextElement()).getDir();
            if (f != null)
            {
                File sf = new File( f, sourceName );
                if (sf.exists() && sf.isFile())
                {
                    return sf;
                }
            }
        }
        project.log( "Could not find a corresponding file for source '"+
            sourceName+"'.", Project.MSG_INFO );
        return null;
    }
    
    
    protected String getOutFileBase( String baseName )
    {
        File f;
        if (baseName == null)
        {
            f = this.outdir;
        }
        else
        {
            f = new File( this.outdir, baseName );
        }
        
        return f.getAbsolutePath();
    }
    
    
    protected String getUpDir( String filename )
    {
        StringBuffer reldir = new StringBuffer();
        int pos = filename.indexOf( File.separatorChar );
        while (pos >= 0)
        {
            reldir.append( "../" );
            pos = filename.indexOf( File.separatorChar, pos+1 );
        }
        return reldir.toString();
    }
    
    
    protected StyleTransformer getStyleTransformer( Project project,
            StyleType s )
            throws IOException
    {
        StyleTransformer st = s.getTransformer();
        if (st != null)
        {
            return st;
        }
        
        // otherwise, generate the cache.
        String url = s.getURL( this );
        st = new StyleTransformer( project, url,
            this.outdir );
        Enumeration e = this.params.elements();
        while (e.hasMoreElements())
        {
            ((SimpleXslReportStyle.ParamType)e.nextElement()).
                updateParameter( st, project );
        }
        project.log( "Transforming with stylesheet "+url,
            Project.MSG_DEBUG );
        
        // Possibly keep a cache of the style, for performance reasons.
        s.setTransformer( st );
        return st;
    }
    
    
    // ----------------------------------------------------------------
    
    
    
    protected String getStylesheetSystemIdForFile( File f )
            throws IOException
    {
        if (f == null || !f.exists())
        {
            throw new java.io.FileNotFoundException(
                "Could not find file '" + f + "'" );
        }
        URL url = new URL( "file", "", f.getAbsolutePath() );
        return url.toExternalForm();
    }
    
    
    protected String getStylesheetSystemIdForClass( String name )
            throws IOException
    {
        URL url = getClass().getResource( name );
        if (url == null)
        {
            throw new java.io.FileNotFoundException(
                "Could not find jar resource " + name);
        }
        return url.toExternalForm();
    }
    
    
    private static DocumentBuilder getDocumentBuilder()
    {
        try
        {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder();
        }
        catch (Exception ex)
        {
            throw new ExceptionInInitializerError( ex );
        }
    }
}

