/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.AnnotationTypeDoc;
import com.sun.javadoc.AnnotationValue;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.LanguageVersion;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ParameterizedType;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.SourcePosition;
import com.sun.javadoc.Tag;
import com.sun.javadoc.Type;
import com.sun.javadoc.TypeVariable;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
 */
public class TestCoverageDoclet {

    public static final int TYPE_FIELD = 0;
    public static final int TYPE_METHOD = 1;
    public static final int TYPE_CLASS = 2;
    public static final int TYPE_PACKAGE = 3;
    public static final int TYPE_ROOT = 4;
    public static final int VALUE_RED = 0;
    public static final int VALUE_YELLOW = 1;
    public static final int VALUE_GREEN = 2;
    public static final String[] COLORS = { "#ffa0a0", "#ffffa0", "#a0ffa0" };
    public static final String[] TYPES = { "Field", "Method", "Class", "Package", "All packages" };

    /**
     * Holds our basic output directory.
     */
    private File directory;

    private Map<ExecutableMemberDoc, AnnotationPointer> resolved =
            new HashMap<ExecutableMemberDoc, AnnotationPointer>(8192);

    /**
     * Helper class for comparing element with each other, in oder to determine
     * an order. Uses lexicographic order of names.
     */
    private class DocComparator implements Comparator<Doc> {
        public int compare(Doc elem1, Doc elem2) {
            return elem1.name().compareTo(elem2.name());
        }

        public boolean equals(Doc elem) {
            return this == elem;
        }
    }

    private class MemberComparator implements Comparator<ExecutableMemberDoc> {
        public int compare(ExecutableMemberDoc mem1, ExecutableMemberDoc mem2) {
            return mem1.toString().compareTo(mem2.toString());
        }
    }

    class MyStats {
        private String name;
        private String link;
        private int elemCnt = 0;
        private int[] ryg = new int[3];
        private String extra;

        public MyStats(int type, String name, String link) {
            this.name = name;
            this.link = link;
        }

        public void add(MyStats subStats) {
           elemCnt++;
           for (int i = 0; i < ryg.length; i++) {
               ryg[i]+= subStats.ryg[i];
           }
        }

        public int getCountFor(int color) {
            return ryg[color];
        }

        public String getStat() {
            float coverage = (float)(ryg[1]+ryg[2]) / (float)(ryg[0]+ryg[1]+ryg[2]);
            return "red: "+ryg[0]+", yellow:"+ryg[1]+", green:"+ryg[2]+",coverage:"+coverage;
        }

        public void inc(int color) {
            ryg[color]++;
        }

        public String getLink() {
            return link;
        }

        public String getName() {
            return name;
        }

        public String getExtra() {
            return extra;
        }

        public void setExtra(String extra) {
            this.extra = extra;
        }
    }

    /**
     * Holds our comparator instance for everything.
     */
    private DocComparator comparator = new DocComparator();
    private MemberComparator membercomparator = new MemberComparator();

    /**
     * Creates a new instance of the TestProgressDoclet for a given target
     * directory.
     */
    public TestCoverageDoclet(String directory) {
        this.directory = new File(directory);
    }

    /**
     * Opens a new output file and writes the usual HTML header. Directories
     * are created on demand.
     */
    private PrintWriter openFile(String name, String title) throws IOException {
        File file = new File(directory, name);
        File parent = file.getParentFile();
        parent.mkdirs();

        PrintWriter printer = new PrintWriter(new FileOutputStream(file));

        printer.println("<html>");
        printer.println("  <head>");
        printer.println("    <title>" + title + "</title>");
        printer.println("<style type=\"text/css\">\n"+
                "body { }\n"+
                "table {border-width: 0px; border: solid; border-collapse: collapse;}\n"+
                "table tr td { vertical-align:top; padding:3px; border: 1px solid black;}\n"+
                "</style>");
        printer.println("  </head>");
        printer.println("  <body>");
        printer.println("    <h1>" + title + "</h1>");

        return printer;
    }

    /**
     * Closes the given output file, writing the usual HTML footer before.
     */
    private void closeFile(PrintWriter printer) {
        printer.println("  </body>");
        printer.println("</html>");
        printer.flush();
        printer.close();
    }

    private class TablePrinter {
        private PrintWriter pr;

        public TablePrinter(PrintWriter pr) {
            this.pr = pr;
        }

        public void printRow(int color, String... columns) {
            String colo = COLORS[color];
            pr.print("<tr style=\"background-color:"+colo+"\">");
            for (String col : columns) {
                pr.print("<td>"+col+"</td>");
            }
            pr.print("</tr>");
        }

        public void printRow(String... columns) {
            printRow(1, columns);
        }

        public void printPlain(String val) {
            pr.print(val);
        }

    }

    /**
     * Processes the whole list of classes that JavaDoc knows about.
     */
    private void process(RootDoc root) throws IOException {

        // 1. traverse all test-classes (those extending JUnit's TestCase)
        // and collect the annotation info. Print which test classes
        // need annotating
        PrintWriter pr = openFile("test-annotation.html", "test class annotation coverage");
        TablePrinter printer = new TablePrinter(pr);
        printer.printPlain("<table>");
        printer.printRow("className", "annotated methods", "total methods", "percentage");

        ClassDoc[] classes = root.classes();
        Arrays.sort(classes, new Comparator<ClassDoc>() {
            public int compare(ClassDoc c1, ClassDoc c2) {
                return c1.toString().compareTo(c2.toString());
            }});
        for (ClassDoc classDoc : classes) {
            if (extendsJUnitTestCase(classDoc)) {
                processTestClass(classDoc, printer);
            }
        }
        printer.printPlain("</table>");
        closeFile(pr);
        //dumpInfo();

        // 2. traverse all "normal" (non-junit) source files, for each method
        // get its status and propagate it up the tree
        MyStats stats = new MyStats(TYPE_ROOT, "All", "aaa.html");
        PrintWriter aprinter = openFile("index.html", "All packages");
        aprinter.println("Generated " + new Date().toString());
        aprinter.println("<br/><a href=\"test-annotation.html\">annotation progress of test classes</a><br/>");
        aprinter.println("<br/><a href=\"hidden-doc.html\">hidden classes and methods</a><br/>");
        aprinter.println("<br/><a href=\"interfaces.html\">interfaces</a><br/>");
        aprinter.println("<h2>Packages</h2>");
        aprinter.println("<table>");

        PrintWriter hiddenDocPr = openFile("hidden-doc.html", "hidden classes and methods list");
        TablePrinter hiddenDocPrinter = new TablePrinter(hiddenDocPr);
        hiddenDocPrinter.printPlain("<table>");
        hiddenDocPrinter.printRow("Package Name", "Class Name", "Method Name");

        PrintWriter interfacePr = openFile("interfaces.html", "interface list");
        TablePrinter interfacePrinter = new TablePrinter(interfacePr);
        interfacePrinter.printPlain("<table>");
        interfacePrinter.printRow("packageName", "className");

        PackageDoc[] packages = root.specifiedPackages();
        Arrays.sort(packages, comparator);
        for (PackageDoc pack : packages) {
            if (pack.allClasses().length != 0) {

                if (pack.name().endsWith(".cts")) {
                    // Skip the cts test packages
//                    System.out.println(">>>>>>>>>>>Skip package: " + pack.name());
                } else {
                    MyStats subStat = processPackage(pack, hiddenDocPrinter, interfacePrinter);
                    
                    System.out.println("package " + pack.name() + " has " + subStat.getCountFor(0) + " red.");
                    printStats(aprinter, subStat, true);
                    stats.add(subStat);
                }
            }
        }
        

        System.out.println("Total has " + stats.getCountFor(0) + " red.");

        interfacePrinter.printPlain("</table>");
        closeFile(interfacePr);

        hiddenDocPrinter.printPlain("</table>");
        closeFile(hiddenDocPr);

        aprinter.println("</table>");
        aprinter.println("<h2>Summary</h2>");
        aprinter.println("<table>");
        printStats(aprinter, stats, false);
        aprinter.println("</table>");

        closeFile(aprinter);
    }

    /*private void processTargetClass(ClassDoc classDoc) {
        System.out.println("class:"+classDoc);
        // show all public/protected constructors
        for (ExecutableMemberDoc constr : classDoc.constructors()) {
            if (constr.isPublic() || constr.isProtected()) {
                processTargetMC(constr);
            }
        }
        // show all public/protected methods
        for (ExecutableMemberDoc method : classDoc.methods()) {
            if (method.isPublic() || method.isProtected()) {
                processTargetMC(method);
            }
        }
    }*/

    /*private void dumpInfo() {
        for (Map.Entry<ExecutableMemberDoc, AnnotationPointer> entry : resolved.entrySet()) {
            ExecutableMemberDoc mdoc = entry.getKey();
            AnnotationPointer ap = entry.getValue();
            System.out.println("----- entry -----------------------");
            System.out.println("target:"+mdoc.toString());
            System.out.println("=");
            for (MethodDoc meth : ap.testMethods) {
                System.out.println("test method:"+meth);
            }
        }
    }*/

    private void processTestClass(ClassDoc classDoc, TablePrinter printer) {
        // System.out.println("Processing >>> " + classDoc);
        // collects all testinfo-annotation info of this class
        ClassDoc targetClass = null;
        // get the class annotation which names the default test target class
        AnnotationDesc[] cAnnots = classDoc.annotations();
        for (AnnotationDesc cAnnot : cAnnots) {

            AnnotationTypeDoc atype = cAnnot.annotationType();
            if (atype.toString().equals("dalvik.annotation.TestTargetClass")) {
                // single member annot with one child 'value'
                ElementValuePair[] cpairs = cAnnot.elementValues();
                ElementValuePair evp = cpairs[0];
                AnnotationValue av = evp.value();
                Object obj = av.value();

                // value must be a class doc
                if (obj instanceof ClassDoc) {
                    targetClass = (ClassDoc) obj;
                } else if (obj instanceof ParameterizedType) {
                    targetClass = ((ParameterizedType)obj).asClassDoc();
                }
                else throw new RuntimeException("annotation elem value is of type "+obj.getClass().getName());
            }
        }

        // now visit all methods (junit test methods - therefore we need not visit the constructors
        AnnotStat ast = new AnnotStat();

        //System.out.println("checking:"+classDoc.qualifiedName());

        MethodDoc[] methods = classDoc.methods();
        String note = "";
        if (targetClass == null) {
            note += "<br/>targetClass annotation missing!<br/>";
        }

        for (MethodDoc methodDoc : methods) {
            // ignore if it is not a junit test method
            if (!methodDoc.name().startsWith("test")) continue;
            if (classDoc.qualifiedName().equals("tests.api.java.io.BufferedInputStreamTest")) {
                //System.out.println("method: "+methodDoc.toString());
            }

            if (targetClass == null) {
                // if the targetClass is missing, count all methods as non-annotated
                ast.incMethodCnt(false);
            } else {
                String error = processTestMethod(methodDoc, ast, targetClass);
                if (error != null) {
                    note+="<br/><b>E:</b> "+error;
                }
            }
        }

        int man = ast.cntMethodWithAnnot;
        int mto = ast.cntAllMethods;
        float perc = mto==0? 100f : ((float)man)/mto * 100f;

        printer.printRow(man==mto && note.equals("")? 2:0, classDoc.qualifiedName(), ""+ast.cntMethodWithAnnot, ""+ast.cntAllMethods,
                ""+perc+ note);

    }

    private class AnnotStat {
        int cntMethodWithAnnot = 0;
        int cntAllMethods = 0;
        /**
         * @param correctAnnot
         */
        public void incMethodCnt(boolean correctAnnot) {
            cntAllMethods++;
            if (correctAnnot) {
                cntMethodWithAnnot++;
            }
        }
    }

    // points from one targetMethod to 0..n testMethods which test the target method
    private class AnnotationPointer {
        AnnotationPointer(ExecutableMemberDoc targetMethod) {
            this.targetMethod = targetMethod;
        }

        final ExecutableMemberDoc targetMethod;
        List<MethodDoc> testMethods = new ArrayList<MethodDoc>();

        public void addTestMethod(MethodDoc testMethod) {
            if (testMethods.contains(testMethod)) {
                System.out.println("warn: testMethod refers more than once to the targetMethod, testMethod="+testMethod);
            } else {
                testMethods.add(testMethod);
            }
        }
    }

    private String processTestMethod(MethodDoc methodDoc, AnnotStat ast, ClassDoc targetClass) {
        //System.out.println("processing method: " + methodDoc);
        // get all per-method-annotation
        boolean correctAnnot = false;
        AnnotationDesc[] annots = methodDoc.annotations();
        for (AnnotationDesc annot : annots) {
            if (annot.annotationType().toString().equals("dalvik.annotation.TestInfo")) {
                ElementValuePair[] pairs = annot.elementValues();
                for (ElementValuePair kv : pairs) {
                    if (kv.element().qualifiedName().equals("dalvik.annotation.TestInfo.targets")) {
                        // targets is an [] type
                        AnnotationValue[] targets = (AnnotationValue[]) kv.value().value();
                        for (AnnotationValue tval : targets) {
                            // the test targets must be annotations themselves
                            AnnotationDesc targetAnnot = (AnnotationDesc) tval.value();
                            ExecutableMemberDoc targetMethod = getTargetMethod(targetAnnot, targetClass);
                            if (targetMethod != null) {
                                AnnotationPointer tar = getAnnotationPointer(targetMethod, true);
                                tar.addTestMethod(methodDoc);
                                correctAnnot = true;
                            } else {
                                ast.incMethodCnt(false);
                                return "error: could not resolve targetMethod for class "+targetClass+", annotation was:"+targetAnnot+", testMethod = "+methodDoc.toString();
                            }
                        }
                    }
                }
            } // else some other annotation
        }
        ast.incMethodCnt(correctAnnot);
        return null;
    }

    private AnnotationPointer getAnnotationPointer(ExecutableMemberDoc targetMethod, boolean create) {
        AnnotationPointer ap = resolved.get(targetMethod);
        if (create && ap == null) {
            ap = new AnnotationPointer(targetMethod);
            resolved.put(targetMethod, ap);
        }
        return ap;
    }

    private ExecutableMemberDoc getTargetMethod(AnnotationDesc targetAnnot,
            ClassDoc targetClass) {
        // targetAnnot like @android.annotation.TestTarget(methodName="group", methodArgs=int.class)
        ElementValuePair[] pairs = targetAnnot.elementValues();
        String methodName = null;
        String args = "";
        for (ElementValuePair kval : pairs) {
            if (kval.element().name().equals("methodName")) {
                methodName = (String) kval.value().value();
            } else if (kval.element().name().equals("methodArgs")) {
                AnnotationValue[] vals = (AnnotationValue[]) kval.value().value();
                for (int i = 0; i < vals.length; i++) {
                    AnnotationValue arg = vals[i];
                    String argV;
                    if (arg.value() instanceof ClassDoc) {
                       ClassDoc cd = (ClassDoc)arg.value();
                       argV = cd.qualifiedName();
                    } else { // primitive type or array type
                        // is there a nicer way to do this?
                        argV = arg.toString();
                    }
                    // strip .class out of args since signature does not contain those
                    if (argV.endsWith(".class")) {
                        argV = argV.substring(0, argV.length()-6);
                    }
                    args+= (i>0? ",":"") + argV;
                }
            }
        }
        // both methodName and methodArgs != null because of Annotation definition

        String refSig = methodName+"("+args+")";
        //System.out.println("Check " + refSig);
        // find the matching method in the target class
        // check all methods
        for (ExecutableMemberDoc mdoc : targetClass.methods()) {
            if (equalsSignature(mdoc, refSig)) {
                return mdoc;
            }
        }
        // check constructors, too
        for (ExecutableMemberDoc mdoc : targetClass.constructors()) {
            if (equalsSignature(mdoc, refSig)) {
                return mdoc;
            }
        }
        return null;
    }

    private boolean equalsSignature(ExecutableMemberDoc mdoc, String refSignature) {
        Parameter[] params = mdoc.parameters();
        String targs = "";
        for (int i = 0; i < params.length; i++) {
            Parameter parameter = params[i];
            // check for generic type types
            Type ptype = parameter.type();
            TypeVariable typeVar = ptype.asTypeVariable();
            String ptname;
            if (typeVar != null) {
                ptname = "java.lang.Object"; // the default fallback
                Type[] bounds = typeVar.bounds();
                if (bounds.length > 0) {
                    ClassDoc typeClass = bounds[0].asClassDoc();
                    ptname = typeClass.qualifiedName();
                }
            } else {
                // regular var
                //ptname = parameter.type().qualifiedTypeName();
                ptname = parameter.type().toString();

                //System.out.println("quali:"+ptname);
                //ptname = parameter.typeName();
                // omit type signature
                ptname = ptname.replaceAll("<.*>","");
            }
            targs+= (i>0? ",":"") + ptname;
        }
        String testSig = mdoc.name()+"("+targs+")";

        //return testSig.equals(refSignature);
        if (testSig.equals(refSignature)) {
            //System.out.println("found: Sig:"+testSig);
            return true;
        } else {
            //System.out.println("no match: ref = "+refSignature+", test = "+testSig);
            return false;
        }
    }

    private boolean extendsJUnitTestCase(ClassDoc classDoc) {
        //junit.framework.TestCase.java
        ClassDoc curClass = classDoc;
        while ((curClass = curClass.superclass()) != null) {
            if (curClass.toString().equals("junit.framework.TestCase")) {
                return true;
            }
        }

        return false;
    }

    /**
     * Processes the details of a single package.
     * @param hiddenDocPrinter
     * @param excludedClassPrinter
     * @param interfacePrinter
     */
    private MyStats processPackage(PackageDoc pack, TablePrinter hiddenDocPrinter,
            TablePrinter interfacePrinter) throws IOException {
        String file = getPackageDir(pack) + "/package.html";
        PrintWriter printer = openFile(file, "Package " + pack.name());

        MyStats stats = new MyStats(TYPE_PACKAGE, pack.name(), file);
        printer.println("<table>");

        ClassDoc[] classes = pack.allClasses();
        Arrays.sort(classes, comparator);
        for (ClassDoc clazz : classes) {
            if (extendsJUnitTestCase(clazz)) {
                printer.println("<tr><td>ignored(junit):"+clazz.name()+"</td></tr>");
            } else if (isHiddenClass(clazz)) {
                hiddenDocPrinter.printRow(pack.name(), clazz.name(), "*");
            } else if (clazz.isInterface()) {
                interfacePrinter.printRow(pack.name(), clazz.name());
            } else {
                MyStats subStats = processClass(clazz, hiddenDocPrinter);
                printStats(printer, subStats, true);
                stats.add(subStats);
            }
        }
        printer.println("</table>");
        closeFile(printer);
        return stats;
    }

    private boolean isHiddenClass(ClassDoc clazz) {
        if (clazz == null) {
            return false;
        }

        if (isHiddenDoc(clazz)) {
            return true;
        }

        // If outter class is hidden, this class should be hidden as well
        return isHiddenClass(clazz.containingClass());
    }

    private boolean isHiddenDoc(Doc doc) {
        // Since currently we have two kinds of annotations to mark a class as hide:
        //  1. @hide
        //  2. {@hide}
        // So we should consider both conditions.
        for (Tag t : doc.tags()) {
            if (t.name().equals("@hide")) {
                return true;
            }
        }

        for (Tag t : doc.inlineTags()) {
            if (t.name().equals("@hide")) {
                return true;
            }
        }

        return false;
    }
    
    private MyStats processClass(ClassDoc clazz, TablePrinter hiddenDocPrinter) throws IOException {
        //System.out.println("Process source class: " + clazz);
        String file = getPackageDir(clazz.containingPackage()) + "/" + clazz.name() + ".html";
        PrintWriter printer = openFile(file, "Class " + clazz.name());

        String packageName = clazz.containingPackage().name();
        String className = clazz.name();
        
        MyStats stats = new MyStats(TYPE_CLASS, className, className+".html");
        printer.println("<table><tr><td>name</td><td>tested by</td></tr>");
        ConstructorDoc[] constructors = clazz.constructors();
        Arrays.sort(constructors, comparator);
        for (ConstructorDoc constructor : constructors) {
            //System.out.println("constructor: " + constructor);
            if (isHiddenDoc(constructor)) {
                hiddenDocPrinter.printRow(packageName, className, constructor.name());
            } else if (!isGeneratedConstructor(constructor)) {
                MyStats subStat = processElement(constructor);
                printStats(printer, subStat, false);
                stats.add(subStat);
            }
        }

        MethodDoc[] methods = clazz.methods();
        Arrays.sort(methods, comparator);
        for (MethodDoc method : methods) {
            //System.out.println("method: " + method);
            if ("finalize".equals(method.name())) {
                // Skip finalize method
            } else if (isHiddenDoc(method)) {
                hiddenDocPrinter.printRow(packageName, className, method.name());
            } else if (method.isAbstract()) {
                // Skip abstract method
            } else {
                MyStats subStat = processElement(method);
                printStats(printer, subStat, false);
                stats.add(subStat);
            }
        }

        printer.println("</table>");
        closeFile(printer);
        return stats;
    }

    /**
     * Determines whether a constructor has been automatically generated and is
     * thus not present in the original source. The only way to find out seems
     * to compare the source position against the one of the class. If they're
     * equal, the constructor does not exist. It's a bit hacky, but it works.
     */
    private boolean isGeneratedConstructor(ConstructorDoc doc) {
        SourcePosition constPos = doc.position();
        SourcePosition classPos = doc.containingClass().position();

        return ("" + constPos).equals("" + classPos);
    }

    /**
     * Processes a single method/constructor.
     */
    private MyStats processElement(ExecutableMemberDoc method) {
        //int color = getColor(doc)
        //derived.add(subStats)
        AnnotationPointer ap = getAnnotationPointer(method, false);
        MyStats stats = new MyStats(TYPE_METHOD, "<b>"+method.name() + "</b> "+method.signature(), null);
        int refCnt = 0;
        if (ap != null) {
            refCnt = ap.testMethods.size();
            String by = "";
            List<MethodDoc> testM = ap.testMethods;
            Collections.sort(testM, membercomparator);
            for (MethodDoc teme : testM) {
                by+= "<br/>"+teme.toString();
            }
            stats.setExtra(by);
        } // else this class has no single test that targets one of its method

        if (refCnt == 0) {
            stats.inc(VALUE_RED);
        } else if (refCnt == 1) {
            stats.inc(VALUE_YELLOW);
        } else {
            stats.inc(VALUE_GREEN);
        }
        return stats;
    }

    /**
     * Prints a single row to a stats table.
     */
    private void printStats(PrintWriter printer, MyStats info, boolean wantLink) {
        int red = info.getCountFor(VALUE_RED);
        int yellow = info.getCountFor(VALUE_YELLOW);

        printer.println("<tr>");
      
        // rule for coloring:
        // if red > 0 -> red
        // if yellow > 0 -> yellow
        // else green
        int color;
        if (red > 0) {
            color = VALUE_RED;
        } else if (yellow > 0) {
            color = VALUE_YELLOW;
        } else {
            color = VALUE_GREEN;
        }

        printer.println("<td bgcolor=\""+COLORS[color]+"\">");
        String link = info.getLink();
        if (wantLink && link != null) {
            printer.print("<a href=\"" + link + "\">" + info.getName() + "</a>");
        } else {
            printer.print(info.getName());
        }
        printer.println(" ("+info.getStat()+") </td>");
        if (info.getExtra()!=null) {
            printer.println("<td>"+info.getExtra()+"</td>");
        }
        printer.println("</tr>");
    }

    /**
     * Returns the directory for a given package. Basically converts embedded
     * dots in the name into slashes.
     */
    private File getPackageDir(PackageDoc pack) {
        if (pack == null || pack.name() == null || "".equals(pack.name())) {
            return new File(".");
        } else {
            return new File(pack.name().replace('.', '/'));
        }
    }

    /**
     * Called by JavaDoc to find our which command line arguments are supported
     * and how many parameters they take. Part of the JavaDoc API.
     */
    public static int optionLength(String option) {
        if ("-d".equals(option)) {
            return 2;
        } else {
            return 0;
        }
    }

    /**
     * Called by JavaDoc to query a specific command line argument. Part of the
     * JavaDoc API.
     */
    private static String getOption(RootDoc root, String option, int index, String defValue) {
        String[][] allOptions = root.options();
        for (int i = 0; i < allOptions.length; i++) {
            if (allOptions[i][0].equals(option)) {
                return allOptions[i][index];
            }
        }
        return defValue;
    }

    /**
     * Called by JavaDoc to find out which Java version we claim to support.
     * Part of the JavaDoc API.
     */
    public static LanguageVersion languageVersion() {
        return LanguageVersion.JAVA_1_5;
    }

    /**
     * The main entry point called by JavaDoc after all required information has
     * been collected. Part of the JavaDoc API.
     */
    public static boolean start(RootDoc root) {
        try {
            String target = getOption(root, "-d", 1, ".");
            TestCoverageDoclet doclet = new TestCoverageDoclet(target);
            doclet.process(root);

        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
        return true;
    }

}