/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2001-2002 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Ant", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */
package org.apache.tools.ant.taskdefs.optional.sitraka;

import java.io.File;
import java.io.FileInputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Vector;
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;
import org.xml.sax.InputSource;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.ClassFile;
import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.ClassPathLoader;
import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.MethodInfo;
import org.apache.tools.ant.taskdefs.optional.sitraka.bytecode.Utils;

/**
 * Little hack to process XML report from JProbe. It will fix
 * some reporting errors from JProbe 3.0 and makes use of a reference
 * classpath to add classes/methods that were not reported by JProbe
 * as being used (ie loaded)
 *
 * @author <a href="sbailliez@imediation.com">Stephane Bailliez</a>
 */
public class XMLReport {
    /** task caller, can be null, used for logging purpose */
    private Task task;

    /** the XML file to process just from CovReport */
    private File file;

    /** jprobe home path. It is used to get the DTD */
    private File jprobeHome;

    /** parsed document */
    private Document report;

    /** mapping of class names to <code>ClassFile</code>s from the reference classpath.  It is used to filter the JProbe report. */
    private Hashtable classFiles;

    /** mapping package name / package node for faster access */
    private Hashtable pkgMap;

    /** mapping classname / class node for faster access */
    private Hashtable classMap;

    /** method filters */
    private ReportFilters filters;

    /** create a new XML report, logging will be on stdout */
    public XMLReport(File file) {
        this(null, file);
    }

    /** create a new XML report, logging done on the task */
    public XMLReport(Task task, File file) {
        this.file = file;
        this.task = task;
    }

    /** set the JProbe home path. Used to get the DTD */
    public void setJProbehome(File home) {
        jprobeHome = home;
    }

    /** set the  */
    public void setReportFilters(ReportFilters filters) {
        this.filters = filters;
    }


    /** create node maps so that we can access node faster by their name */
    protected void createNodeMaps() {
        pkgMap = new Hashtable();
        classMap = new Hashtable();
        // create a map index of all packages by their name
        // @todo can be done faster by direct access.
        NodeList packages = report.getElementsByTagName("package");
        final int pkglen = packages.getLength();
        log("Indexing " + pkglen + " packages");
        for (int i = pkglen - 1; i > -1; i--) {
            Element pkg = (Element) packages.item(i);
            String pkgname = pkg.getAttribute("name");

            int nbclasses = 0;
            // create a map index of all classes by their fully
            // qualified name.
            NodeList classes = pkg.getElementsByTagName("class");
            final int classlen = classes.getLength();
            log("Indexing " + classlen + " classes in package " + pkgname);
            for (int j = classlen - 1; j > -1; j--) {
                Element clazz = (Element) classes.item(j);
                String classname = clazz.getAttribute("name");
                if (pkgname != null && pkgname.length() != 0) {
                    classname = pkgname + "." + classname;
                }

                int nbmethods = 0;
                NodeList methods = clazz.getElementsByTagName("method");
                final int methodlen = methods.getLength();
                for (int k = methodlen - 1; k > -1; k--) {
                    Element meth = (Element) methods.item(k);
                    StringBuffer methodname = new StringBuffer(meth.getAttribute("name"));
                    methodname.delete(methodname.toString().indexOf("("), methodname.toString().length());
                    String signature = classname + "." + methodname + "()";
                    if (filters.accept(signature)) {
                        log("kept method:" + signature);
                        nbmethods++;
                    } else {
                        clazz.removeChild(meth);
                    }
                }
                // if we don't keep any method, we don't keep the class
                if (nbmethods != 0 && classFiles.containsKey(classname)) {
                    log("Adding class '" + classname + "'");
                    classMap.put(classname, clazz);
                    nbclasses++;
                } else {
                    pkg.removeChild(clazz);
                }
            }
            if (nbclasses != 0) {
                log("Adding package '" + pkgname + "'");
                pkgMap.put(pkgname, pkg);
            } else {
                pkg.getParentNode().removeChild(pkg);
            }
        }
        log("Indexed " + classMap.size() + " classes in " + pkgMap.size() + " packages");
    }

    /** create the whole new document */
    public Document createDocument(String[] classPath) throws Exception {

        // Iterate over the classpath to identify reference classes
        classFiles = new Hashtable();
        ClassPathLoader cpl = new ClassPathLoader(classPath);
        Enumeration enum = cpl.loaders();
        while (enum.hasMoreElements()) {
            ClassPathLoader.FileLoader fl = (ClassPathLoader.FileLoader) enum.nextElement();
            ClassFile[] classes = fl.getClasses();
            log("Processing " + classes.length + " classes in " + fl.getFile());
            // process all classes
            for (int i = 0; i < classes.length; i++) {
                classFiles.put(classes[i].getFullName(), classes[i]);
            }
        }

        // Load the JProbe coverage XML report
        DocumentBuilder dbuilder = newBuilder();
        InputSource is = new InputSource(new FileInputStream(file));
        if (jprobeHome != null) {
            File dtdDir = new File(jprobeHome, "dtd");
            is.setSystemId("file:///" + dtdDir.getAbsolutePath() + "/");
        }
        report = dbuilder.parse(is);
        report.normalize();

        // create maps for faster node access (also filters out unwanted nodes)
        createNodeMaps();

        // Make sure each class from the reference path ends up in the report
        Enumeration classes = classFiles.elements();
        while (classes.hasMoreElements()) {
            ClassFile cf = (ClassFile) classes.nextElement();
            serializeClass(cf);
        }
        // update the document with the stats
        update();
        return report;
    }

    /**
     * JProbe does not put the java.lang prefix for classes
     * in this package, so used this nice method so that
     * I have the same signature for methods
     */
    protected String getMethodSignature(MethodInfo method) {
        StringBuffer buf = new StringBuffer(method.getName());
        buf.append("(");
        String[] params = method.getParametersType();
        for (int i = 0; i < params.length; i++) {
            String type = params[i];
            int pos = type.lastIndexOf('.');
            if (pos != -1) {
                String pkg = type.substring(0, pos);
                if ("java.lang".equals(pkg)) {
                    params[i] = type.substring(pos + 1);
                }
            }
            buf.append(params[i]);
            if (i != params.length - 1) {
                buf.append(", ");
            }
        }
        buf.append(")");
        return buf.toString();
    }

    /**
     * Convert to a CovReport-like signature - &lt;classname&gt;&#046;&lt;method&gt;().
     */
    protected String getMethodSignature(ClassFile clazz, MethodInfo method) {
        StringBuffer buf = new StringBuffer(clazz.getFullName());
        buf.append(".");
        buf.append(method.getName());
        buf.append("()");
        return buf.toString();
    }

    /**
     * Do additional work on an element to remove abstract methods that
     * are reported by JProbe 3.0
     */
    protected void removeAbstractMethods(ClassFile classFile, Element classNode) {
        MethodInfo[] methods = classFile.getMethods();
        Hashtable methodNodeList = getMethods(classNode);
        // assert xmlMethods.size() == methods.length()
        final int size = methods.length;
        for (int i = 0; i < size; i++) {
            MethodInfo method = methods[i];
            String methodSig = getMethodSignature(method);
            Element methodNode = (Element) methodNodeList.get(methodSig);
            if (methodNode != null &&
                    Utils.isAbstract(method.getAccessFlags())) {
                log("\tRemoving abstract method " + methodSig);
                classNode.removeChild(methodNode);
            }
        }
    }

    /** create an empty method element with its cov.data values */
    protected Element createMethodElement(MethodInfo method) {
        String methodsig = getMethodSignature(method);
        Element methodElem = report.createElement("method");
        methodElem.setAttribute("name", methodsig);
        // create the method cov.data element
        Element methodData = report.createElement("cov.data");
        methodElem.appendChild(methodData);
        methodData.setAttribute("calls", "0");
        methodData.setAttribute("hit_lines", "0");
        methodData.setAttribute("total_lines", String.valueOf(method.getNumberOfLines()));
        return methodElem;
    }

    /** create an empty package element with its default cov.data (0) */
    protected Element createPackageElement(String pkgname) {
        Element pkgElem = report.createElement("package");
        pkgElem.setAttribute("name", pkgname);
        // create the package cov.data element / default
        // must be updated at the end of the whole process
        Element pkgData = report.createElement("cov.data");
        pkgElem.appendChild(pkgData);
        pkgData.setAttribute("calls", "0");
        pkgData.setAttribute("hit_methods", "0");
        pkgData.setAttribute("total_methods", "0");
        pkgData.setAttribute("hit_lines", "0");
        pkgData.setAttribute("total_lines", "0");
        return pkgElem;
    }

    /** create an empty class element with its default cov.data (0) */
    protected Element createClassElement(ClassFile classFile) {
        // create the class element
        Element classElem = report.createElement("class");
        classElem.setAttribute("name", classFile.getName());
        // source file possibly does not exist in the bytecode
        if (null != classFile.getSourceFile()) {
            classElem.setAttribute("source", classFile.getSourceFile());
        }
        // create the cov.data elem
        Element classData = report.createElement("cov.data");
        classElem.appendChild(classData);
        // create the class cov.data element
        classData.setAttribute("calls", "0");
        classData.setAttribute("hit_methods", "0");
        classData.setAttribute("total_methods", "0");
        classData.setAttribute("hit_lines", "0");
        classData.setAttribute("total_lines", "0");
        return classElem;
    }

    /** serialize a classfile into XML */
    protected void serializeClass(ClassFile classFile) {
        // the class already is reported so ignore it
        String fullclassname = classFile.getFullName();
        log("Looking for '" + fullclassname + "'");
        Element clazz = (Element) classMap.get(fullclassname);

        // ignore classes that are already reported, all the information is
        // already there.
        if (clazz != null) {
            log("Ignoring " + fullclassname);
            removeAbstractMethods(classFile, clazz);
            return;
        }

        // ignore interfaces files, there is no code in there to cover.
        if (Utils.isInterface(classFile.getAccess())) {
            return;
        }

        Vector methods = getFilteredMethods(classFile);
        // no need to process, there are no methods to add for this class.
        if (methods.size() == 0) {
            return;
        }

        String pkgname = classFile.getPackage();
        // System.out.println("Looking for package " + pkgname);
        Element pkgElem = (Element) pkgMap.get(pkgname);
        if (pkgElem == null) {
            pkgElem = createPackageElement(pkgname);
            report.getDocumentElement().appendChild(pkgElem);
            pkgMap.put(pkgname, pkgElem); // add the pkg to the map
        }
        // this is a brand new class, so we have to create a new node

        // create the class element
        Element classElem = createClassElement(classFile);
        pkgElem.appendChild(classElem);

        int total_lines = 0;
        int total_methods = 0;
        final int count = methods.size();
        for (int i = 0; i < count; i++) {
            // create the method element
            MethodInfo method = (MethodInfo) methods.elementAt(i);
            if (Utils.isAbstract(method.getAccessFlags())) {
                continue; // no need to report abstract methods
            }
            Element methodElem = createMethodElement(method);
            classElem.appendChild(methodElem);
            total_lines += method.getNumberOfLines();
            total_methods++;
        }
        // create the class cov.data element
        Element classData = getCovDataChild(classElem);
        classData.setAttribute("total_methods", String.valueOf(total_methods));
        classData.setAttribute("total_lines", String.valueOf(total_lines));

        // add itself to the node map
        classMap.put(fullclassname, classElem);
    }

    protected Vector getFilteredMethods(ClassFile classFile) {
        MethodInfo[] methodlist = classFile.getMethods();
        Vector methods = new Vector(methodlist.length);
        for (int i = 0; i < methodlist.length; i++) {
            MethodInfo method = methodlist[i];
            String signature = getMethodSignature(classFile, method);
            if (filters.accept(signature)) {
                methods.addElement(method);
                log("keeping " + signature);
            } else {
//              log("discarding " + signature);
            }
        }
        return methods;
    }


    /** update the count of the XML, that is accumulate the stats on
     * methods, classes and package so that the numbers are valid
     * according to the info that was appended to the XML.
     */
    protected void update() {
        int calls = 0;
        int hit_methods = 0;
        int total_methods = 0;
        int hit_lines = 0;
        int total_lines = 0;

        // use the map for access, all nodes should be there
        Enumeration enum = pkgMap.elements();
        while (enum.hasMoreElements()) {
            Element pkgElem = (Element) enum.nextElement();
            String pkgname = pkgElem.getAttribute("name");
            Element[] classes = getClasses(pkgElem);
            int pkg_calls = 0;
            int pkg_hit_methods = 0;
            int pkg_total_methods = 0;
            int pkg_hit_lines = 0;
            int pkg_total_lines = 0;
            //System.out.println("Processing package '" + pkgname + "': " + classes.length + " classes");
            for (int j = 0; j < classes.length; j++) {
                Element clazz = classes[j];
                String classname = clazz.getAttribute("name");
                if (pkgname != null && pkgname.length() != 0) {
                    classname = pkgname + "." + classname;
                }
                // there's only cov.data as a child so bet on it
                Element covdata = getCovDataChild(clazz);
                try {
                    pkg_calls += Integer.parseInt(covdata.getAttribute("calls"));
                    pkg_hit_methods += Integer.parseInt(covdata.getAttribute("hit_methods"));
                    pkg_total_methods += Integer.parseInt(covdata.getAttribute("total_methods"));
                    pkg_hit_lines += Integer.parseInt(covdata.getAttribute("hit_lines"));
                    pkg_total_lines += Integer.parseInt(covdata.getAttribute("total_lines"));
                } catch (NumberFormatException e) {
                    log("Error parsing '" + classname + "' (" + j + "/" + classes.length + ") in package '" + pkgname + "'");
                    throw e;
                }
            }
            Element covdata = getCovDataChild(pkgElem);
            covdata.setAttribute("calls", String.valueOf(pkg_calls));
            covdata.setAttribute("hit_methods", String.valueOf(pkg_hit_methods));
            covdata.setAttribute("total_methods", String.valueOf(pkg_total_methods));
            covdata.setAttribute("hit_lines", String.valueOf(pkg_hit_lines));
            covdata.setAttribute("total_lines", String.valueOf(pkg_total_lines));
            calls += pkg_calls;
            hit_methods += pkg_hit_methods;
            total_methods += pkg_total_methods;
            hit_lines += pkg_hit_lines;
            total_lines += pkg_total_lines;
        }
        Element covdata = getCovDataChild(report.getDocumentElement());
        covdata.setAttribute("calls", String.valueOf(calls));
        covdata.setAttribute("hit_methods", String.valueOf(hit_methods));
        covdata.setAttribute("total_methods", String.valueOf(total_methods));
        covdata.setAttribute("hit_lines", String.valueOf(hit_lines));
        covdata.setAttribute("total_lines", String.valueOf(total_lines));
    }

    protected Element getCovDataChild(Element parent) {
        NodeList children = parent.getChildNodes();
        int len = children.getLength();
        for (int i = 0; i < len; i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element elem = (Element) child;
                if ("cov.data".equals(elem.getNodeName())) {
                    return elem;
                }
            }
        }
        throw new NoSuchElementException("Could not find 'cov.data' element in parent '" + parent.getNodeName() + "'");
    }

    protected Hashtable getMethods(Element clazz) {
        Hashtable map = new Hashtable();
        NodeList children = clazz.getChildNodes();
        int len = children.getLength();
        for (int i = 0; i < len; i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element elem = (Element) child;
                if ("method".equals(elem.getNodeName())) {
                    String name = elem.getAttribute("name");
                    map.put(name, elem);
                }
            }
        }
        return map;
    }

    protected Element[] getClasses(Element pkg) {
        Vector v = new Vector();
        NodeList children = pkg.getChildNodes();
        int len = children.getLength();
        for (int i = 0; i < len; i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element elem = (Element) child;
                if ("class".equals(elem.getNodeName())) {
                    v.addElement(elem);
                }
            }
        }
        Element[] elems = new Element[v.size()];
        v.copyInto(elems);
        return elems;

    }

    protected Element[] getPackages(Element snapshot) {
        Vector v = new Vector();
        NodeList children = snapshot.getChildNodes();
        int len = children.getLength();
        for (int i = 0; i < len; i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element elem = (Element) child;
                if ("package".equals(elem.getNodeName())) {
                    v.addElement(elem);
                }
            }
        }
        Element[] elems = new Element[v.size()];
        v.copyInto(elems);
        return elems;
    }

    private static DocumentBuilder newBuilder() {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setIgnoringComments(true);
            factory.setValidating(false);
            return factory.newDocumentBuilder();
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public void log(String message) {
        if (task == null) {
            //System.out.println(message);
        } else {
            task.log(message, Project.MSG_DEBUG);
        }
    }

}

