/*
 * @(#)XmlReportGenerator.java
 *
 * Copyright (C) 2002,2003 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.report;

import java.io.IOException;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

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

import net.sourceforge.groboutils.codecoverage.v2.IAnalysisMetaData;
import net.sourceforge.groboutils.codecoverage.v2.IAnalysisModule;
import net.sourceforge.groboutils.codecoverage.v2.datastore.ClassRecord;
import net.sourceforge.groboutils.codecoverage.v2.datastore.MarkRecord;
import net.sourceforge.groboutils.codecoverage.v2.util.ClassSignatureUtil;

import org.apache.bcel.classfile.Utility;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


/**
 * Creates an XML formatted report, suitable for framing.
 * <p>
 * Re-written to use DOM for XML creation; see bug 847334.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/04/15 05:48:26 $
 * @since     December 18, 2002
 * @see       IAnalysisMetaData
 */
public class XmlReportGenerator implements IReportGenerator, IXmlReportConst
{
    private static final org.apache.log4j.Logger LOG =
        org.apache.log4j.Logger.getLogger( XmlReportGenerator.class );
    
    
    private static final NumberFormat DFORMAT = NumberFormat.getInstance();
    private static final NumberFormat IFORMAT = NumberFormat.getInstance();
    static {
        DFORMAT.setMaximumFractionDigits( 2 );
        DFORMAT.setMinimumFractionDigits( 2 );
        IFORMAT.setMaximumFractionDigits( 0 );
    }
    
    private DocumentBuilder docBuilder = getDocumentBuilder();
    
    private class CoverageCount
    {
        public CoverageCount() {}
        public CoverageCount( boolean covered, byte weight )
        {
            int cc = 0;
            int tcc = 1;
            if (covered)
            {
                cc = 1;
            }
            this.coveredCount = cc;
            this.totalCount = tcc;
            this.weightedCoverage =
                ((double)weight + (double)Byte.MAX_VALUE + 1.0)
                * (double)(cc + 1) / (double)Byte.MAX_VALUE * 2.0;
        }
        
        public int coveredCount;
        public int totalCount;
        public double weightedCoverage;
        
        public void add( CoverageCount b )
        {
            this.coveredCount += b.coveredCount;
            this.totalCount += b.totalCount;
            
            this.weightedCoverage += b.weightedCoverage;
        }
        
        
        public double getCoveredPercent()
        {
            if (this.totalCount == 0)
            {
                return 100.0;
            }
            return ((double)this.coveredCount / (double)this.totalCount) *
                100.0;
        }
        
        
        public double getWeightedValue()
        {
            if (this.totalCount == 0)
            {
                return 100.0;
            }
            return (this.weightedCoverage / (double)this.totalCount) * 100.0;
        }
    }
    
    
    
    /**
     * Sends the generated report to <tt>out</tt> using the given module
     * and data set.
     *
     * @return the root element generated.
     */
    public Element createReport( IAnalysisModule module,
            AnalysisModuleData data )
            throws IOException
    {
        if (module == null || data == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        
        Document doc = this.docBuilder.newDocument();
        
        // get the list of all classes under consideration
        final String classSigs[] = data.getClassSignatures();
        Map packageCounts = loadPackageCounts( classSigs );
        
        Element rootEl = createRootNode( module, doc );
        
        // get each class's count, and append the output for each
        for (int i = 0; i < classSigs.length; ++i)
        {
            LOG.info( "Reading class "+classSigs[i]+"." );
            ClassMarkSet cms = data.createClassMarkSet( classSigs[i] );
            CoverageCount classCount = writeClass( cms,
                data.getClassRecord( classSigs[i] ), rootEl, doc );
            CoverageCount pkgCount = (CoverageCount)packageCounts.get(
                getPackageName( classSigs[i] ) );
            pkgCount.add( classCount );
        }
        
        final CoverageCount totalCount = new CoverageCount();
        Iterator iter = packageCounts.keySet().iterator();
        while (iter.hasNext())
        {
            String pkgName = (String)iter.next();
            CoverageCount pkgCount = (CoverageCount)packageCounts.get(
                pkgName );
            writePackage( pkgName, pkgCount, rootEl, doc );
            totalCount.add( pkgCount );
        }
        // don't need this memory anymore
        packageCounts = null;
        writeCoverage( totalCount, rootEl, doc );
        
        return rootEl;
    }
    
    
    
    private Element createRootNode( IAnalysisModule module, Document doc )
    {
        if (module == null || doc == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        
        Element rootEl = doc.createElement( T_COVERAGEREPORT );
        doc.appendChild( rootEl );
        rootEl.setAttribute( A_MEASURE, module.getMeasureName() );
        rootEl.setAttribute( A_UNIT, module.getMeasureUnit() );
        
        return rootEl;
    }
    
    
    private CoverageCount writeClass( ClassMarkSet cms, ClassRecord cr,
            Element rootEl, Document doc )
    {
        if (cr == null || cms == null || rootEl == null || doc == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        
        final String classSig = cr.getClassSignature();
        final String className = cr.getClassName();
        
        Element classEl = doc.createElement( T_CLASSCOVERAGE );
        rootEl.appendChild( classEl );
        classEl.setAttribute( A_CLASSSIGNATURE, classSig );
        classEl.setAttribute( A_CLASSNAME, className );
        classEl.setAttribute( A_PACKAGE, getPackageName( classSig ) );
        classEl.setAttribute( A_SOURCEFILE, cr.getSourceFileName() );
        
        final CoverageCount classCoverage = writeMethods( cms, classEl, doc );
        
        writeCoverage( classCoverage, classEl, doc );
        
        return classCoverage;
    }
    
    
    private void writePackage( String packageName, CoverageCount pkgCount,
            Element parent, Document doc )
    {
        if (packageName == null || pkgCount == null || parent == null ||
                doc == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        
        Element packEl = doc.createElement( T_PACKAGE );
        parent.appendChild( packEl );
        packEl.setAttribute( A_NAME, packageName );
        
        writeCoverage( pkgCount, packEl, doc );
    }
    
    
    private CoverageCount writeMethods( ClassMarkSet cms, Element parent,
            Document doc )
    {
        if (cms == null || parent == null || doc == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        
        final CoverageCount classCoverage = new CoverageCount();
        final String methodList[] = cms.getMethodSignatures();
        for (int i = 0; i < methodList.length; ++i)
        {
            String methodSig = methodList[i];
            CoverageCount methodCoverage = new CoverageCount();
            
            Element methEl = doc.createElement( T_METHODCOVERAGE );
            parent.appendChild( methEl );
            methEl.setAttribute( A_METHODSIGNATURE, translate( methodSig ) );
            methEl.setAttribute( A_METHODSIGNATURE_REAL, methodSig );
            
            // covered marks
            MarkRecord marks[] = cms.getCoveredMarksForMethod( methodSig );
            for (int markI = 0; markI < marks.length; ++markI)
            {
                CoverageCount cc = writeMark( marks[ markI ], true,
                    methEl, doc );
                methodCoverage.add( cc );
            }
            
            // uncovered marks
            // (note that putting this allocation in the same array is a bit
            // of a memory conservation)
            marks = cms.getNotCoveredMarksForMethod( methodSig );
            for (int markI = 0; markI < marks.length; ++markI)
            {
                CoverageCount cc = writeMark( marks[ markI ], false,
                    methEl, doc );
                methodCoverage.add( cc );
            }
            
            writeCoverage( methodCoverage, methEl, doc );
            
            classCoverage.add( methodCoverage );
        }
        
        return classCoverage;
    }
    
    
    private CoverageCount writeMark( MarkRecord mr, boolean covered,
            Element parent, Document doc )
    {
        if (mr == null || parent == null || doc == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        
        final IAnalysisMetaData amd = mr.getAnalysisMetaData();
        Element markEl = doc.createElement( T_MARK );
        parent.appendChild( markEl );
        markEl.setAttribute( A_COVERED, covered ? "true" : "false" );
        markEl.setAttribute( A_TEXT, amd.getCoveredFormattedText() );
        markEl.setAttribute( A_WEIGHT, formatNumber( amd.getInstructionWeight() ) );
        markEl.setAttribute( A_SOURCELINE, formatNumber( mr.getLineNumber() ) );
        
        CoverageCount cc = new CoverageCount( covered,
            amd.getInstructionWeight() );
        return cc;
    }
    
    
    private void writeCoverage( CoverageCount cc, Element parent,
            Document doc )
    {
        if (cc == null || parent == null || doc == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        Element cover = doc.createElement( T_COVER );
        parent.appendChild( cover );
        
        cover.setAttribute( A_COVERED_DISP, format( cc.coveredCount ) );
        cover.setAttribute( A_COVERED, formatNumber( cc.coveredCount ) );
        cover.setAttribute( A_TOTAL_DISP, format( cc.totalCount ) );
        cover.setAttribute( A_TOTAL, formatNumber( cc.totalCount ) );
        cover.setAttribute( A_PERCENTCOVERED_DISP, format( cc.getCoveredPercent() ) );
        cover.setAttribute( A_PERCENTCOVERED, formatNumber( cc.getCoveredPercent() ) );
        cover.setAttribute( A_WEIGHTEDPERCENT_DISP, format( cc.getWeightedValue() ) );
        cover.setAttribute( A_WEIGHTEDPERCENT, formatNumber( cc.getWeightedValue() ) );
    }
    
    
    
    //-----------------------------------------------------------------------
    // Helper / utilities
    
    
    private Map loadPackageCounts( String[] classSigs )
    {
        PackageSorter ps = new PackageSorter();
        ps.addClassSignatures( classSigs );
        String[] pkgs = ps.getPackages();
        
        Map counts = new HashMap();
        for (int i = 0; i < pkgs.length; ++i)
        {
            counts.put( pkgs[i], new CoverageCount() );
        }
        return counts;
    }
    
    
    private String getPackageName( String className )
    {
        return PackageSorter.getPackageName( className );
    }
    
    
    private String getClassName( String classSig )
    {
        return ClassSignatureUtil.getInstance().getClassName( classSig );
    }
    
    
    private String format( int i )
    {
        return IFORMAT.format( (long)i );
    }
    
    
    private String format( double d )
    {
        return DFORMAT.format( d );
    }
    
    
    private String formatNumber( int i )
    {
        return Integer.toString( i );
    }
    
    
    private String formatNumber( double d )
    {
        return Double.toString( d );
    }
    
    
    private static DocumentBuilder getDocumentBuilder()
    {
        try
        {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder();
        }
        catch (Exception ex)
        {
            throw new ExceptionInInitializerError( ex );
        }
    }
    
    
    static String translate( String methodSig )
    {
        if (methodSig == null)
        {
            return null;
        }
        
        
        // Translate both the method name and the method arguments
        int pos1 = methodSig.indexOf( '(' );
        if (pos1 < 0)
        {
            return methodSig;
        }
        int pos2 = methodSig.indexOf( ')' );
        if (pos2 < 0)
        {
            return methodSig;
        }
        String mname = methodSig.substring( 0, pos1 );
        
        // translate the method names - should this be done???
        if ("<init>".equals( mname ))
        {
            mname = "[constructor]";
        }
        else
        if ("<clinit>".equals( mname ))
        {
            return "[static initializer]";
        }
        
        StringBuffer sig = new StringBuffer( mname );
        sig.append( '(' );
        String s = methodSig.substring( pos1, pos2+1 );
        if (!"()".equals( s ))
        {
            String parms[] = Utility.methodSignatureArgumentTypes( s );
            if (parms.length > 0)
            {
                sig.append( parms[0] );
                for (int i = 1; i < parms.length; ++i)
                {
                    sig.append( ", " );
                    sig.append( parms[i] );
                }
            }
        }
        sig.append( ')' );
        
        // don't include return value
        return new String( sig );
    }
}

