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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Collection;

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

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


/**
 * Generates an XML report about a Java source file, containing the source
 * file's line-by-line source, and the coverage information on that class.
 * <p>
 * Unlike the other generators, this class must be instantiated for a
 * particular coverage report, and will be reused to make each sub-source
 * file.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/06/08 20:55:35 $
 * @since     November 26, 2003
 */
public class XmlSourceReportGenerator2 implements IXmlSourceReportConst
{
    private static final org.apache.log4j.Logger LOG =
        org.apache.log4j.Logger.getLogger( XmlCombinedReportGenerator.class );
    
    
    private SourceNodeMap sourceNodeMap = new SourceNodeMap();
    private Node moduleTypesNode;
    
    private static class SourceNodes
    {
        public List coverNodes = new LinkedList();
        public Map uncoveredMarkNodesByLine = new HashMap();
        public List noLine = new LinkedList();
        
        public void addCoverNode( Element e )
        {
            // we're going to move the marks out of 
            this.coverNodes.add( e );
            NodeList nl = e.getElementsByTagName( T_MARK );
            for (int i = 0; i < nl.getLength(); ++i)
            {
                Node node = nl.item(i);
                if (node instanceof Element)
                {
                    Element me = (Element)node;
                    if (!"true".equals( me.getAttribute( A_COVERED ) ))
                    {
                        Element methodNode = (Element)me.getParentNode();
                        me.setAttribute( A_METHODSIGNATURE,
                            methodNode.getAttribute( A_METHODSIGNATURE ) );
                        me.setAttribute( A_METHODSIGNATURE_REAL,
                            methodNode.getAttribute( A_METHODSIGNATURE_REAL ) );
                        String line = me.getAttribute( A_SOURCELINE );
                        List marks;
                        if ("-1".equals( line ))
                        {
                            marks = this.noLine;
                        }
                        else
                        {
                            marks = (List)this.uncoveredMarkNodesByLine.
                                get( line );
                            if (marks == null)
                            {
                                marks = new LinkedList();
                                this.uncoveredMarkNodesByLine.put( line, marks );
                            }
                        }
                        marks.add( me );
                    }
                }
            }
        }
        
        
        public void copyClassCoverInto( Element rootEl, Document doc )
        {
            copyNodesInto( this.coverNodes, rootEl, doc );
        }
        
        
        public int getNoLineMarkCount()
        {
            return this.noLine.size();
        }
        
        
        public void copyMarksInto( int lineNo, Element rootEl, Document doc )
        {
            List marks;
            if (lineNo < 0)
            {
                marks = this.noLine;
            }
            else
            {
                marks = (List)this.uncoveredMarkNodesByLine.get(
                    Integer.toString( lineNo ) );
            }
            copyNodesInto( marks, rootEl, doc );
        }
        
        
        public boolean copyAllMarksInto( Element rootEl, Document doc )
        {
            boolean added = false;
            Iterator iter = this.uncoveredMarkNodesByLine.keySet().iterator();
            while (iter.hasNext())
            {
                added = copyNodesInto( (List)this.uncoveredMarkNodesByLine.
                    get( iter.next() ), rootEl, doc );
            }
            return added;
        }
        
        
        private boolean copyNodesInto( Collection src, Element rootEl, Document doc )
        {
            boolean added =false;
            if (src != null)
            {
                Iterator iter = src.iterator();
                while (iter.hasNext())
                {
                    added = true;
                    rootEl.appendChild( copyNode( (Node)iter.next(), doc ) );
                }
            }
            return added;
        }
    }
    
    private static class SourceNodeMap
    {
        private Map sourceFileMap = new HashMap();
        public void addClassCoverageNode( Node n )
        {
            if (n instanceof Element)
            {
                Element e = (Element)n;
                String name = getSourceName( e );
                SourceNodes sn = getSource( name );
                if (sn == null)
                {
                    sn = new SourceNodes();
                    this.sourceFileMap.put( name, sn );
                }
                sn.addCoverNode( e );
            }
        }
        
        public SourceNodes getSource( String name )
        {
            return (SourceNodes)this.sourceFileMap.get( name );
        }
        
        public String[] getSourceNames()
        {
            Set keys = this.sourceFileMap.keySet();
            return (String[])keys.toArray( new String[ keys.size() ] );
        }
    }
    
    public XmlSourceReportGenerator2( Document coverageReport )
    {
        if (coverageReport == null)
        {
            throw new IllegalArgumentException( "No null args" );
        }
        
        NodeList list = coverageReport.getDocumentElement().
            getElementsByTagName( T_CLASSCOVERAGE );
        for (int i = 0; i < list.getLength(); ++i)
        {
            this.sourceNodeMap.addClassCoverageNode( list.item(i) );
        }
        
        this.moduleTypesNode = coverageReport.getDocumentElement().
            getElementsByTagName( "moduletypes" ).item(0);
    }
    
    
    public String[] getSourceNames()
    {
        return this.sourceNodeMap.getSourceNames();
    }
    
    
    /**
     * Sends the generated report using the given module
     * and data set.
     *
     * @return the root element generated.
     */
    public Document createXML( String srcName, File srcFile )
            throws IOException
    {
        SourceNodes coverageNodes = this.sourceNodeMap.getSource( srcName );
        if (coverageNodes == null)
        {
            throw new IllegalArgumentException( "No such source: "+
                srcName );
        }
        
        // this is a bit slower, but it conserves some memory resources.
        DocumentBuilder docBuilder = getDocumentBuilder();
        Document doc = docBuilder.newDocument();
        Element rootEl = doc.createElement( T_JAVACOVERAGE );
        doc.appendChild( rootEl );
        rootEl.setAttribute( A_JAVACLASS, srcName );
        rootEl.appendChild( copyNode( this.moduleTypesNode, doc ) );
        
        parseSourceFile( srcFile, rootEl, doc, coverageNodes );
        coverageNodes.copyClassCoverInto( rootEl, doc );
        
        return doc;
    }
    
    
    protected void parseSourceFile( File src, Element parent, Document doc,
            SourceNodes sourceNode )
            throws IOException
    {
        // everyone needs this.
        Element nolineEl = null;
        if (sourceNode.getNoLineMarkCount() > 0)
        {
            nolineEl = doc.createElement( T_NO_LINE );
            sourceNode.copyMarksInto( -1, nolineEl, doc );
        }
        
        // If the file doesn't exist, don't fail.  Just put it in the report.
        if (src == null || !src.exists() || !src.isFile())
        {
            Element nosrcEl = doc.createElement( T_NO_SOURCE );
            parent.appendChild( nosrcEl );
            
            // System.out.println( "No source file" );
            if (nolineEl != null)
            {
                nosrcEl.appendChild( nolineEl );
                nolineEl.setAttribute( A_INDEX, Integer.toString( -1 ) );
                nolineEl.setAttribute( A_SRC, "Could not find source file." );
            }
            
            Element nsmEl = doc.createElement( T_NO_SOURCE_MARKS );
            if (sourceNode.copyAllMarksInto( nsmEl, doc ))
            {
                nosrcEl.appendChild( nsmEl );
            }
            
            return;
        }
        
        // else, parse the source file.
        Element srcEl = doc.createElement( T_SOURCE );
        parent.appendChild( srcEl );
        if (nolineEl != null)
        {
            srcEl.appendChild( nolineEl );
        }
        
        
        BufferedReader in = new BufferedReader( new FileReader( src ) );
        try
        {
            int lineNo = 0;
            for (String line = in.readLine();
                    line != null;
                    line = in.readLine())
            {
                ++lineNo;
                
                // should color-code the line according to Java syntax.
                // But, we can't guarantee that the source is Java!  It
                // might be NetRexx or any number of other source types.
                Element lineEl = doc.createElement( T_LINE );
                srcEl.appendChild( lineEl );
                
                lineEl.setAttribute( A_INDEX, Integer.toString( lineNo ) );
                lineEl.setAttribute( A_SRC, line );
                
                sourceNode.copyMarksInto( lineNo, lineEl, doc );
            }
        }
        finally
        {
            if (in != null)
            {
                in.close();
            }
        }
    }
    
    
    protected static Node copyNode( Node el, Document doc )
    {
        return doc.importNode( el, true );
    }
    
    
    
    //------------------------------------------------------------------------
    // Private methods
    
    
    private static DocumentBuilder getDocumentBuilder()
    {
        try
        {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder();
        }
        catch (Exception exc)
        {
            throw new ExceptionInInitializerError(exc);
        }
    }
    
    
    private static String getSourceName( Element coverageEl )
    {
        String pkg = coverageEl.getAttribute( A_PACKAGE );
        String src = coverageEl.getAttribute( A_SOURCEFILE );
        return makeSourceName( pkg, src );
    }
    
    
    private static String makeSourceName( String pkg, String srcFile )
    {
        if (pkg == null || pkg.length() <= 0)
        {
            return srcFile;
        }
        pkg = pkg.replace( '.', File.separatorChar );
        return pkg + File.separatorChar + srcFile;
    }
}

