/*
 * 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.
 */

package testprogress2;

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.ParameterizedType;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;

import testprogress2.TestMethodInformation.Color;
import testprogress2.TestMethodInformation.Level;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
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;

/**
 * this doclet generates a .html report about the test coverage of the core api
 * reading test method's annotations.
 */
public class TestCoverageDoclet {

    /**
     * color for the stats: green, yellow, red
     */
    public static final String[] COLORS = {
            "#a0ffa0", "#ffffa0", "#ff8080"
    };

    // debugging output
    private static final boolean DEBUG = false;

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

    private boolean _ignoreInterfacesAndAbstractMethods = false;

    private boolean _doIncludeDisabledTests = false;

    private boolean _acceptCompleteWithOtherStatus = false;

    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());
        }
    }

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

    private MemberComparator memberComparator = new MemberComparator();

    private Map<ClassDoc, List<TestTargetNew>> classToSpecialTargets =
        new HashMap<ClassDoc, List<TestTargetNew>>();

    /**
     * Creates a new instance of the TestProgressDoclet for a given target
     * directory.
     *
     * @param 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 filename, String title) {
        File file = new File(directory, filename);
        File parent = file.getParentFile();
        parent.mkdirs();

        PrintWriter printer;
        try {
            printer = new PrintWriter(new FileOutputStream(file));
        } catch (FileNotFoundException e) {
            throw new RuntimeException("file not found:" + e.getMessage());
        }

        printer.println("<html>");
        printer.println("  <head>");
        printer.println("    <title>" + title + "</title>");
        printer.println("<style type=\"text/css\">\n"
                + "body, body table tr td { font-size:10pt;font-family: "
                + " sans-serif; }\n" + "li { padding-bottom:2px }\n"
                + "table { width:100%; 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(String... columns) {
            pr.print("<tr style=\"background-color:white\">");
            for (String col : columns) {
                pr.print("<td>" + col + "</td>");
            }
            pr.print("</tr>");
        }

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

        public void printTableStart() {
            pr.print("<table>");
        }

        public void printTableEnd() {
            pr.print("</table>");
        }

        public void printPlainLn(String val) {
            printPlain(val);
            pr.print("<br>");
        }
    }

    /**
     * Processes the whole list of classes that JavaDoc knows about.
     */
    private void process(RootDoc root) {
        System.out.println("V 0.9a");
        String mode = getOption(root, "-f", 1, "dummy-see bash script");
        System.out.println("mode: " + mode);

        // standard mode is to include disabled tests
        _doIncludeDisabledTests = mode.contains("countdisabled");
        _acceptCompleteWithOtherStatus = mode.contains("acceptcandx");
        if (_doIncludeDisabledTests) {
            System.out.println("- including disabled tests");
        } else {
            System.out.println("- excluding disabled tests");
        }
        if (_acceptCompleteWithOtherStatus) {
            System.out.println("- accepting complete tests with partial tests");
        } else {
            System.out.println("- not accepting complete tests with partial "
                    + "tests");
        }

        // 1. traverse all test-classes (those extending JUnit's TestCase)
        // and collect the annotation info.
        // ===================================================================
        System.out.println("stage 1 - get targets from all junit test methods");
        PrintWriter pr = openFile("testcoverage/test-annotation.html",
                "test class annotation coverage");
        TablePrinter printer = new TablePrinter(pr);
        printer.printTableStart();
        printer.printRow("Test package name", "JUnit classes", "Meth", "Unt",
                "Part", "Compl", "Disab", "Broken", "ToBeFixed", "KnownFail");

        ColorStat totalStats = new ColorStat("All test packages", null);
        PackageDoc[] packages = root.specifiedPackages();
        Arrays.sort(packages, classComparator);
        for (PackageDoc pack : packages) {
            if (pack.allClasses().length != 0) {
                ColorStat packageStat = processTestPackage(pack);
                if (!packageStat.ignored) {
                    // ignore those packages which have 0 tests in it
                    printTestStats(printer, packageStat, true);
                    totalStats.add(packageStat);
                }
            }
        }
        printer.printTableEnd();

        printer.printPlainLn("<h2>Summary of all test packages</h2>");
        printer.printTableStart();
        printTestStats(printer, totalStats, false);
        printer.printTableEnd();
        closeFile(pr);

        System.out.println("stage 2 - proxy test targets to abstract classes"
                + "and interfaces");
        // 1/2 - traverse all normal (non-junit) classes for interface
        // and abstract methods implementation tests.
        ClassDoc[] classes = root.classes();
        for (ClassDoc classDoc : classes) {
            if (!extendsJUnitTestCase(classDoc)) {
                MethodDoc[] methods = classDoc.methods();
                for (MethodDoc methodDoc : methods) {
                    AnnotationPointer ap = getAnnotationPointer(methodDoc,
                            false);
                    if (ap != null) {
                        // if there are already tests targeting this method,
                        // search for abstract/interface methods which this
                        // method
                        // implements, and mark them as tested by this method,
                        // too.
                        List<MethodDoc> impls = implementingMethods(methodDoc);
                        for (MethodDoc impl : impls) {
                            AnnotationPointer apImpl = getAnnotationPointer(
                                    impl, true);
                            // add all tests which pointed to the original
                            // method.
                            apImpl.addProxiesFrom(ap);
                        }
                    }
                }
            }
        }

        // 2. traverse all "normal" (non-junit) source files
        // ===================================================================
        System.out.println("stage 3 - generating report for target api");
        pr = openFile("index.html", "All target api packages");
        printer = new TablePrinter(pr);
        printer.printPlainLn("Generated " + new Date().toString()
                + " - V0.9a<br>");
        printer.printPlainLn("<a href=\"testcoverage/test-annotation.html\">"
                + "annotation progress of test classes</a><br><br>");

        printer.printTableStart();
        printer.printRow("Package", "Classes", "Methods", "Untested",
                "Partial", "Complete", "Disabled", "Broken", "ToBeFixed", "KnownFail");

        totalStats = new ColorStat("All target packages", null);
        packages = root.specifiedPackages();
        Arrays.sort(packages, classComparator);

        int classCount = 0;
        for (PackageDoc pack : packages) {
            if (pack.allClasses().length != 0) {
                ColorStat packageStat = processPackage(pack);

                if (!packageStat.ignored) {
                    printStats(printer, packageStat, true);
                    totalStats.add(packageStat);

                    classCount += Integer.parseInt(packageStat.getExtra());
                }
            }
        }
        printer.printTableEnd();

        totalStats.setExtra("" + classCount);
        printer.printPlainLn("<h2>Summary of all target packages</h2>");
        printer.printTableStart();
        printStats(printer, totalStats, false);
        printer.printTableEnd();
        closeFile(pr);
    }

    /**
     * Returns the interface(s) method(s) and the abstract method(s) that a
     * given method implements, or an empty list if it does not implement
     * anything.
     */
    private List<MethodDoc> implementingMethods(MethodDoc doc) {
        List<MethodDoc> resultmethods = new ArrayList<MethodDoc>();
        ClassDoc clazz = doc.containingClass();
        implementedMethod0(resultmethods, doc, clazz, false);
        return resultmethods;
    }

    /**
     * Recursive helper method for finding out which interface methods or
     * abstract methods a given method implements.
     */
    private void implementedMethod0(List<MethodDoc> resultmethods,
            MethodDoc doc, ClassDoc testClass, boolean testMethods) {

        // test all methods of this class
        if (testMethods) {
            MethodDoc[] methods = testClass.methods();
            for (int j = 0; j < methods.length; j++) {
                MethodDoc methodDoc = methods[j];
                if ((methodDoc.isAbstract() || testClass.isInterface())
                        && doc.overrides(methodDoc)) {
                    resultmethods.add(methodDoc);
                }
            }
        }

        // test all implementing interfaces
        ClassDoc[] ifs = testClass.interfaces();
        for (int i = 0; i < ifs.length; i++) {
            ClassDoc iface = ifs[i];
            implementedMethod0(resultmethods, doc, iface, true);
        }

        // test the superclass
        ClassDoc superclass = testClass.superclass();
        if (superclass != null) {
            implementedMethod0(resultmethods, doc, superclass, true);
        }
    }

    private ColorStat processTestPackage(PackageDoc packageDoc) {
        ColorStat stats = new ColorStat(packageDoc.name(),
                getPackageBaseLink(packageDoc) + "/package.html");
        if (hasHideFlag(packageDoc)) {
            stats.ignored = true;
            return stats;
        }
        String file = getPackageDir("testcoverage", packageDoc)
                + "/package.html";
        PrintWriter pr = openFile(file, "Test package " + packageDoc.name());
        TablePrinter printer = new TablePrinter(pr);
        printer.printTableStart();
        printer.printRow("Class", "Extra", "Meth", "Unt", "Part", "Compl",
                "Disab", "Broken", "ToBeFixed", "KnownFail");

        ClassDoc[] classes = packageDoc.allClasses();
        Arrays.sort(classes, classComparator);
        int junitCnt = 0;
        for (ClassDoc clazz : classes) {
            if (extendsJUnitTestCase(clazz)) {
                junitCnt++;
                ColorStat subStats = processTestClass(clazz);
                printTestStats(printer, subStats, true);
                stats.add(subStats);
            } else {
                printer.printRow(clazz.name() + " ignored (no junit class): ",
                        "", "", "", "", "", "", "", "");
            }
        }
        printer.printTableEnd();

        printer.printPlainLn("<h2>Test package summary</h2>");
        printer.printTableStart();
        printStats(printer, stats, false);
        printer.printTableEnd();

        closeFile(pr);
        if (junitCnt == 0) {
            if ((packageDoc.name().contains("tests.")
                    || packageDoc.name().contains("junit.") || packageDoc
                    .name().contains(".testframework"))
                    && !(packageDoc.name().equals("junit.framework"))) {
                System.err.println("warning!: no junit classes in package '"
                        + packageDoc.name() + "' even though package name "
                        + "contains tests.,junit. or .testframework");
            }
            stats = new ColorStat(packageDoc.name(),
                    getPackageBaseLink(packageDoc) + "/package.html");
            stats.incColor(TestMethodInformation.Color.GREEN);
            stats.setExtra("Ignored since no Junit test and suites");
            stats.ignored = true;
        } else {
            stats.setExtra("" + junitCnt);
        }
        return stats;
    }

    private ColorStat processPackage(PackageDoc packageDoc) {
        ColorStat stats = new ColorStat(packageDoc.name(),
                getPackageBaseLink(packageDoc) + "/package.html");
        if (hasHideFlag(packageDoc)) {
            stats.ignored = true;
            return stats;
        }
        String file = getPackageDir("", packageDoc) + "/package.html";
        PrintWriter pr = openFile(file, "Package " + packageDoc.name());
        TablePrinter printer = new TablePrinter(pr);
        printer.printTableStart();
        printer.printRow("Class", "Extra", "Meth", "Unt", "Part", "Compl",
                "Disab", "Broken", "ToBeFixed", "KnownFail");

        ClassDoc[] classes = packageDoc.allClasses();
        Arrays.sort(classes, classComparator);
        int cnt = 0;
        int junitCnt = 0;
        for (ClassDoc clazz : classes) {
            cnt++;
            if (hasHideFlag(clazz)) {
                // ignored since it has a @hide in the javadoc on the class
                // level
            } else if (extendsJUnitTestCase(clazz)) {
                printer.printRow("ignored (junit class): " + clazz.name());
                junitCnt++;
            } else if (clazz.name().equals("AllTests")) {
                printer.printRow("ignored (junit test suite class): "
                        + clazz.name());
                junitCnt++;
            } else {
                ColorStat subStats = processClass(clazz);
                printStats(printer, subStats, true);
                stats.add(subStats);
            }
        }
        printer.printTableEnd();

        printer.printPlainLn("<h2>Target package summary</h2>");
        printer.printTableStart();
        printStats(printer, stats, false);
        printer.printTableEnd();

        closeFile(pr);
        if (junitCnt == cnt || packageDoc.name().contains("tests.")
                || packageDoc.name().contains("junit.")
                || packageDoc.name().contains(".testframework")
                || packageDoc.name().endsWith(".cts")) {
            // we only have junit classes -> mark green
            stats = new ColorStat(packageDoc.name(),
                    getPackageBaseLink(packageDoc) + "/package.html");
            stats.incColor(TestMethodInformation.Color.GREEN);
            stats
                    .setExtra(junitCnt == cnt ? "Ignored since only Junit test and "
                            + "suites"
                            : "Ignored since \"tests.\" in name - recheck");
            stats.ignored = true;
        } else {
            stats.setExtra("" + cnt);
        }
        return stats;
    }

    private boolean hasHideFlag(Doc doc) {
        // workaround for the non-recognized @hide tag in package.html
        if (doc instanceof PackageDoc) {
            String comment = doc.getRawCommentText();
            return comment != null && comment.contains("@hide");
        } else {
            Tag[] hideTags = doc.tags("hide");
            return hideTags.length > 0;
        }
    }

    private ColorStat processTestClass(ClassDoc clazz) {
        String file = getPackageDir("testcoverage", clazz.containingPackage())
                + "/" + clazz.name() + ".html";
        PrintWriter pr = openFile(file, "Test class " + clazz.qualifiedName());
        TablePrinter printer = new TablePrinter(pr);
        ColorStat classStat = new ColorStat(clazz.name(), clazz.name()
                + ".html");

        TestTargetClass testTargetClass = getTargetClass(clazz);
        ClassDoc targetClass = testTargetClass.targetClass;

        String note = "Note:";
        if (targetClass == null) {
            note += "<br>targetClass annotation missing!<br>";
        } else {
            // add untested[] annotations to statistics
            ClassOriginator co = new ClassOriginator(clazz, null);

            AnnotationDesc[] annotsC = testTargetClass.untestedMethods
                    .toArray(new AnnotationDesc[] {});
            if (annotsC.length > 0) {
                // we have "untested" refs
                ColorStat classLevelStat = new ColorStat(clazz.name(), null);
                TestMethodInformation tminfo = new TestMethodInformation(co,
                        annotsC, targetClass);
                if (tminfo.getError() != null) {
                    printer.printPlainLn("<b>Error:</b>" + tminfo.getError());
                    classLevelStat.incColor(Color.RED);
                } else {
                    linkTargets(tminfo.getTargets());
                    classLevelStat.incColor(Color.GREEN);
                }
                classStat.add(classLevelStat);
            }
        }

        printer.printPlainLn(note);

        printer.printTableStart();
        printer.printRow("Method", "Note", "Meth", "Unt", "Part", "Compl",
                "Disab", "Broken", "ToBeFixed", "KnownFail");

        int methodCnt = 0;
        // also collects test... methods from all superclasses below TestCase,
        // since those are called as well
        List<MethodDoc> testMethods = collectAllTestMethods(clazz);
        Collections.sort(testMethods, memberComparator);

        for (MethodDoc testMethod : testMethods) {
            methodCnt++;

            // Make disabled tests visible in the report so we don't forget
            // them.
            boolean disTest = testMethod.name().startsWith("_test");

            ColorStat methodStat = new ColorStat(testMethod.name(), null);
            if (disTest) {
                methodStat.incDisabledTestsCnt();
            }
            String comments = disTest ? "<b><span style=\"background:red\">"
                    + "DISABLED</span></b>" : null;

            MethodOriginator mo = new MethodOriginator(testMethod, clazz,
                    comments);
            AnnotationDesc[] annots = testMethod.annotations();
            TestMethodInformation minfo = new TestMethodInformation(mo, annots,
                    targetClass);
            linkTargets(minfo.getTargets());

            String extra = null;
            if (comments != null) {
                if (extra == null)
                    extra = "";
                extra += comments;
            }

            if (minfo.getError() != null) { // error case
                if (extra == null)
                    extra = "";
                extra += "<b>Error:</b> " + minfo.getError() + "<br>";
                methodStat.addMethodInfo(minfo);
            } else {
                if (mo.getKnownFailure() != null) {
                    methodStat.incKnownFailureCnt();
                    if (extra == null)
                        extra = "";
                    extra += mo.getKnownFailure();
                }

                // check for @BrokenTest
                if (mo.getBrokenTest() != null) {
                    // override with warning
                    methodStat.incBrokenTestCnt();
                    methodStat.incColor(Color.YELLOW);
                    if (extra == null)
                        extra = "";
                    extra += mo.getBrokenTest();
                }

                // check for @ToBeFixed
                if (mo.getToBeFixed() != null) {
                    // override with warning
                    methodStat.incToBeFixedCnt();
                    methodStat.incColor(Color.YELLOW);
                    if (extra == null) {
                        extra = "";
                    }

                    extra += mo.getToBeFixed();
                } else { // add regular stats
                    methodStat.addMethodInfo(minfo);
                }
            }
            if (extra != null) {
                methodStat.setExtra(extra);
            }

            printTestStats(printer, methodStat, false);
            classStat.add(methodStat);
        }
        printer.printTableEnd();

        printer.printPlainLn("<h2>Test class summary</h2>");
        printer.printTableStart();
        printStats(printer, classStat, false);
        printer.printTableEnd();

        closeFile(pr);
        classStat.setExtra("#methods: " + testMethods.size());
        return classStat;
    }

    private void linkTargets(List<TestTargetNew> targets) {
        for (TestTargetNew ttn : targets) {
            if (ttn.getTargetMethod() != null) {
                AnnotationPointer tar = getAnnotationPointer(ttn
                        .getTargetMethod(), true);
                tar.addTestTargetNew(ttn);
            } else if (ttn.getTargetClass() != null) {
                // some special target only pointing to a class, not a method.
                addToClassTargets(ttn.getTargetClass(), ttn);
            }
        }
    }

    private boolean isGreen(TestMethodInformation.Level level) {
        boolean lComplete = level == TestMethodInformation.Level.COMPLETE;
        boolean lSufficient = level == TestMethodInformation.Level.SUFFICIENT;
        boolean lPartialOk = level == TestMethodInformation.Level.PARTIAL_COMPLETE;
        boolean lPartial = level == TestMethodInformation.Level.PARTIAL;
        boolean lTodo = level == TestMethodInformation.Level.TODO;
        boolean lNotFeasible = level == TestMethodInformation.Level.NOT_FEASIBLE;
        boolean lNotNecessary = level == TestMethodInformation.Level.NOT_NECESSARY;

        return lComplete || lPartialOk || lSufficient || lNotFeasible
                || lNotNecessary;
    }

    private ColorStat processClass(ClassDoc clazz) {
        String file = getPackageDir("", clazz.containingPackage()) + "/"
                + clazz.name() + ".html";
        String classDesc = getClassString(clazz);
        PrintWriter pr = openFile(file, classDesc);
        TablePrinter printer = new TablePrinter(pr);
        printer.printPlain("<b>package " + clazz.containingPackage() + "</b>");
        ColorStat classStats = new ColorStat(classDesc, clazz.name() + ".html");

        // list special targets
        List<TestTargetNew> classTargets = getTargetsFor(clazz);
        if (classTargets != null) {
            printer.printPlain("<h3>Class level tests</h3>");
            printer.printPlain("<ul>");
            for (TestTargetNew ttn : classTargets) {
                String line = "<li>" + ttn.getOriginator().asString();
                Level lev = ttn.getLevel();
                line += " <font color=\""
                        + (isGreen(lev) ? "green" : "red")
                        + "\"><b>"
                        + lev.name()
                        + "</b></font>"
                        + (ttn.getNotes() != null ? "<br>Notes: "
                                + ttn.getNotes() : "") + "</li>";
                printer.printPlain(line);
            }
            printer.printPlainLn("</ul>");
        }

        printer.printPlain("<h3>Method level tests</h3>");
        printer.printTableStart();
        printer.printRow("Method", "Tested by", "Meth", "Unt", "Part", "Compl",
                "Disab", "Broken", "ToBeFixed", "KnownFail");
        ConstructorDoc[] constructors = clazz.constructors();
        Arrays.sort(constructors, classComparator);
        int cnt = 0;
        for (ConstructorDoc constructor : constructors) {
            if (!hasHideFlag(constructor) && !hasHideFlag(clazz)) {
                cnt++;
                ColorStat memberStat = processElement(constructor);
                printStats(printer, memberStat, false);
                classStats.add(memberStat);
            }
        }

        MethodDoc[] methods = clazz.methods();
        Arrays.sort(methods, classComparator);
        for (MethodDoc method : methods) {
            if (!hasHideFlag(method) && !hasHideFlag(clazz)) {
                cnt++;
                ColorStat subStat = processElement(method);
                printStats(printer, subStat, false);
                classStats.add(subStat);
            }
        }
        printer.printTableEnd();

        printer.printPlainLn("<h2>Target class summary</h2>");
        printer.printTableStart();
        printStats(printer, classStats, false);
        printer.printTableEnd();

        closeFile(pr);
        classStats.setExtra("#methods: " + cnt);

        // mark as green
        if (_ignoreInterfacesAndAbstractMethods && clazz.isInterface()) {
            classStats = new ColorStat(clazz.name()
                    + (clazz.isInterface() ? " (Interface)" : ""), clazz.name()
                    + ".html");
            int mcnt = clazz.methods().length;
            // fake all methods to green
            for (int i = 0; i < mcnt; i++) {
                classStats.incColor(TestMethodInformation.Color.GREEN);
            }
            classStats.setExtra("Ignored since interface");
        }
        return classStats;
    }

    private class TestTargetClass {
        ClassDoc targetClass;

        List<AnnotationDesc> untestedMethods = new ArrayList<AnnotationDesc>();
        // a List of @TestTargetNew annotations
    }

    private TestTargetClass getTargetClass(ClassDoc classDoc) {
        // get the class annotation which names the default test target class
        TestTargetClass ttc = new TestTargetClass();
        ClassDoc targetClass = null;
        AnnotationDesc[] cAnnots = classDoc.annotations();
        for (AnnotationDesc cAnnot : cAnnots) {
            AnnotationTypeDoc atype = cAnnot.annotationType();
            if (atype.toString().equals("dalvik.annotation.TestTargetClass")) {
                ElementValuePair[] cpairs = cAnnot.elementValues();
                for (int i = 0; i < cpairs.length; i++) {
                    ElementValuePair ev = cpairs[i];
                    String elName = ev.element().name();
                    if (elName.equals("value")) {
                        // the target class
                        AnnotationValue av = ev.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());
                    } else if (elName.equals("untestedMethods")) {
                        // TestTargetNew[] untestedMethods() default {};
                        AnnotationValue[] targets = (AnnotationValue[])ev
                                .value().value();
                        for (AnnotationValue ttn : targets) {
                            // each untested method must be a TestTargetNew
                            AnnotationDesc ttnd = (AnnotationDesc)ttn.value();
                            ttc.untestedMethods.add(ttnd);
                        }
                    }
                }
            }
        }
        ttc.targetClass = targetClass;
        return ttc;
    }

    private List<MethodDoc> collectAllTestMethods(ClassDoc classDoc) {
        List<MethodDoc> m = new ArrayList<MethodDoc>();

        ClassDoc curCl = classDoc;
        do {
            m.addAll(getJunitTestMethods(curCl));
        } while ((curCl = curCl.superclass()) != null
                && !curCl.qualifiedName().equals("junit.framework.TestCase"));
        return m;
    }

    private List<MethodDoc> getJunitTestMethods(ClassDoc classDoc) {
        List<MethodDoc> cl = new ArrayList<MethodDoc>();
        for (MethodDoc methodDoc : classDoc.methods()) {
            if (methodDoc.isPublic()
                    && (methodDoc.name().startsWith("test") || methodDoc.name()
                            .startsWith("_test"))) {
                cl.add(methodDoc);
            }
        }
        return cl;
    }

    private class ColorStat {
        private String name;

        private String link;

        private String extra;

        public boolean ignored;

        private int[] cntCol = new int[4];

        private int disabledTestsCnt = 0;

        private int brokenTestCnt = 0;

        private int toBeFixedCnt = 0;

        private int knownFailureCnt = 0;

        public String getName() {
            return name;
        }

        public String getLink() {
            return link;
        }

        public ColorStat(String name, String link) {
            this.name = name;
            this.link = link;
        }

        public void add(ColorStat subStat) {
            for (int i = 0; i < cntCol.length; i++) {
                cntCol[i] += subStat.cntCol[i];
            }
            disabledTestsCnt += subStat.disabledTestsCnt;
            brokenTestCnt += subStat.brokenTestCnt;
            toBeFixedCnt += subStat.toBeFixedCnt;
            knownFailureCnt += subStat.knownFailureCnt;
        }

        public void incDisabledTestsCnt() {
            disabledTestsCnt++;
        }

        public void incBrokenTestCnt() {
            brokenTestCnt++;
        }

        public void incToBeFixedCnt() {
            toBeFixedCnt++;
        }

        public void incKnownFailureCnt() {
            knownFailureCnt++;
        }

        public void incColor(TestMethodInformation.Color color) {
            cntCol[color.ordinal()]++;
        }

        public int getColorCnt(TestMethodInformation.Color color) {
            return cntCol[color.ordinal()];
        }

        public void addMethodInfo(TestMethodInformation minfo) {
            TestMethodInformation.Color c = minfo.getColor();
            int ord = c.ordinal();
            cntCol[ord]++;
        }

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

        public String getExtra() {
            return extra;
        }

        public int getDisabledTestsCnt() {
            return disabledTestsCnt;
        }

        public int getBrokenTestCnt() {
            return brokenTestCnt;
        }

        public int getToBeFixedCnt() {
            return toBeFixedCnt;
        }

        public int getKnownFailureCnt() {
            return knownFailureCnt;
        }
    }

    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 void addToClassTargets(ClassDoc targetClass, TestTargetNew ttn) {
        List<TestTargetNew> targets = classToSpecialTargets.get(targetClass);
        if (targets == null) {
            targets = new ArrayList<TestTargetNew>();
            classToSpecialTargets.put(targetClass, targets);
        }
        targets.add(ttn);
    }

    private List<TestTargetNew> getTargetsFor(ClassDoc targetClass) {
        return classToSpecialTargets.get(targetClass);
    }

    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 a single method/constructor.
     */
    private ColorStat processElement(ExecutableMemberDoc method) {
        if (DEBUG) System.out.println("Processing " + method);
        ColorStat memberStats = new ColorStat(getMethodString(method), null);
        TestMethodInformation.Color c = TestMethodInformation.Color.RED;
        // if flagged, consider abstract method ok also without tests
        if (_ignoreInterfacesAndAbstractMethods && method instanceof MethodDoc
                && ((MethodDoc)method).isAbstract()) {
            c = TestMethodInformation.Color.GREEN;
            memberStats.setExtra("ignored since abstract");
        } else {
            AnnotationPointer ap = getAnnotationPointer(method, false);
            int testedByCnt = 0;
            if (ap != null) {
                List<TestTargetNew> targets = ap.getTargets();
                testedByCnt = targets.size();
                if (testedByCnt == 0) {
                    throw new RuntimeException(
                            "existing annotation pointer with no entries!, "
                                    + "method:" + method);
                }
                // at least tested by one method
                String by = "<ul>";
                int completeTestCnt = 0;
                int partialOkTestCnt = 0;
                int partialTestCnt = 0;
                int todoTestCnt = 0;
                int notFeasableTestCnt = 0;
                int notNecessaryTestCnt = 0;
                int sufficientTestCnt = 0;
                // int totalCnt = targets.size();

                for (TestTargetNew target : targets) {
                    Originator originator = target.getOriginator();
                    boolean disabledTest = originator.isDisabled();
                    boolean brokenTest = originator.getBrokenTest() != null;
                    boolean toBeFixed = originator.getToBeFixed() != null;
                    boolean knownFailure = originator.getKnownFailure() != null;
                    by += "<li>" + originator.asString();
                    TestMethodInformation.Level lev;
                    if (target.isHavingProblems()) {
                        lev = TestMethodInformation.Level.TODO;
                    } else {
                        lev = target.getLevel();
                    }
                    // TODO: improve: avoid c&p by adding ColorStat.addInfo
                    // (originator) or similar
                    if (disabledTest) {
                        memberStats.incDisabledTestsCnt();
                    }
                    if (brokenTest) {
                        memberStats.incBrokenTestCnt();
                    }
                    if (toBeFixed) {
                        memberStats.incToBeFixedCnt();
                    }
                    if (knownFailure) {
                        memberStats.incKnownFailureCnt();
                    }

                    boolean lComplete = lev == TestMethodInformation.Level.COMPLETE;
                    boolean lSufficient = lev == TestMethodInformation.Level.SUFFICIENT;
                    boolean lPartialOk = lev == TestMethodInformation.Level.PARTIAL_COMPLETE;
                    boolean lPartial = lev == TestMethodInformation.Level.PARTIAL;
                    boolean lTodo = lev == TestMethodInformation.Level.TODO;
                    boolean lNotFeasible = lev == TestMethodInformation.Level.NOT_FEASIBLE;
                    boolean lNotNecessary = lev == TestMethodInformation.Level.NOT_NECESSARY;

                    by += " <font color=\""
                            + (lComplete || lPartialOk || lSufficient
                                    || lNotFeasible || lNotNecessary ? "green"
                                    : "red")
                            + "\"><b>"
                            + lev.name()
                            + "</b></font>"
                            + (target.getNotes() != null ? "<br>Notes: "
                                    + target.getNotes() : "");

                    // only count tests when they are either ok (not disabled)
                    // or if the doIncludeDisabledTests flag is set
                    if ((_doIncludeDisabledTests || !disabledTest)
                            && (!brokenTest) && (!toBeFixed)) {
                        if (lComplete) {
                            completeTestCnt++;
                        } else if (lPartialOk) {
                            partialOkTestCnt++;
                        } else if (lPartial) {
                            partialTestCnt++;
                        } else if (lTodo) {
                            todoTestCnt++;
                        } else if (lSufficient) {
                            sufficientTestCnt++;
                        } else if (lNotFeasible) {
                            notFeasableTestCnt++;
                        } else if (lNotNecessary) {
                            notNecessaryTestCnt++;
                        }
                    }

                    if (toBeFixed) {
                        partialTestCnt++;
                    }

                    if (DEBUG) {
                        System.out.println("completeTestCnt: " + completeTestCnt
                            + ", partialOkTestCnt: " + partialOkTestCnt
                            + ", partialTestCnt: " + partialTestCnt
                            + ", todoTestCnt: " + todoTestCnt
                            + ", sufficientTestCnt: " + sufficientTestCnt
                            + ", notFeasableTestCnt: " + notFeasableTestCnt
                            + ", notNecessaryTestCnt: " + notNecessaryTestCnt);
                    }
                    by += "</li>";
                }

                String warnings = "";
                // calculate the color
                int singularTestCnt = notFeasableTestCnt + notNecessaryTestCnt;
                boolean isAbstract = (method.containingClass().isInterface() ||
                        (method instanceof MethodDoc) && ((MethodDoc)method).isAbstract());

                if (_acceptCompleteWithOtherStatus
                        && (completeTestCnt > 0 || sufficientTestCnt > 0)) {
                    c = TestMethodInformation.Color.GREEN;
                } else if (_acceptCompleteWithOtherStatus
                        && (partialOkTestCnt > 1)) {
                    c = TestMethodInformation.Color.GREEN;
                } else {
                    if (singularTestCnt > 0) {
                        // we have tests which claim not_neccessary or
                        // not_feasible
                        if (targets.size() > singularTestCnt) {
                            // other tests exist
                            c = TestMethodInformation.Color.RED;
                            warnings += "<b>WARNING:</b>NOT_FEASIBLE or "
                                    + "NOT_NECESSARY together with other "
                                    + "status!<br>";
                        } else {
                            // a collection of not_necessary and/or not_feasible
                            if (notNecessaryTestCnt > 0
                                    && notFeasableTestCnt > 0) {
                                // a blend of both -> error
                                warnings += "<b>WARNING:</b>both NOT_FEASIBLE "
                                        + "and NOT_NECESSARY together!<br>";
                                c = TestMethodInformation.Color.RED;
                            } else { // just one sort of tests -> ok and
                                // sufficent
                                c = TestMethodInformation.Color.GREEN;
                            }
                        }
                    } else if (todoTestCnt > 0) {
                        c = TestMethodInformation.Color.RED;
                    } else if (partialTestCnt > 0) {
                        // at least one partial test
                        c = TestMethodInformation.Color.YELLOW;
                        if (completeTestCnt > 0 || sufficientTestCnt > 0) {
                            if (_acceptCompleteWithOtherStatus) {
                                // accept complete even if there are partials
                                c = TestMethodInformation.Color.GREEN;
                            } else if (completeTestCnt > 0) {
                                // yellow+warning: mixed PARTIAL_COMPLETE and
                                // COMPLETE status
                                warnings += "<b>WARNING</b>: mixed PARTIAL "
                                        + "and COMPLETE status<br>";
                            }
                        }
                    } else if (partialOkTestCnt > 0 || sufficientTestCnt > 0) {
                        // at least one partialOk test and no partial tests
                        c = TestMethodInformation.Color.GREEN;
                        if (partialOkTestCnt == 1) {
                            // yellow: 1 PARTIAL_COMPLETE (we need either zero
                            // or >=2 PARTIAL_OK tests)
                            warnings += "<b>WARNING</b>: only one "
                                    + "PARTIAL_COMPLETE status<br>";
                            c = TestMethodInformation.Color.YELLOW;
                        }
                    } else if (completeTestCnt > 0 || singularTestCnt == 1) {
                        // only complete tests
                        c = TestMethodInformation.Color.GREEN;
                    }

                    if (completeTestCnt > 1 && !isAbstract
                            && !_acceptCompleteWithOtherStatus) {
                        // green+warning: >1 COMPLETE (we need either 0 or 1
                        // COMPLETE, more would be strange, but possible)
                        warnings += "<b>WARNING</b>: more than one "
                                + "COMPLETE status<br>";
                        if (c != TestMethodInformation.Color.RED) {
                            c = TestMethodInformation.Color.YELLOW;
                        }
                    }
                }
                by = warnings + by;
                memberStats.setExtra(by);
            } else { // else this class has no single test that targets the
                // current method
                // handle special cases:

                // can be ok if the current method is a default constructor
                // which does not occur in the source code.
                if (method.isConstructor() && method.signature().equals("()")) {
                    if (method.position() != null) {
                        // hacky stuff - the doclet should return null for a
                        // default constructor,
                        // but instead returns a source position pointing to the
                        // same location as
                        // the class.
                        String constPos = method.position().toString();
                        String classPos = method.containingClass().position()
                                .toString();
                        if (constPos.equals(classPos)) {
                            // we have a default constructor not visible in
                            // source code -> mark green, no testing needed
                            c = TestMethodInformation.Color.GREEN;
                            memberStats
                                    .setExtra("automatically marked green "
                                            + "since implicit default "
                                            + "constructor");
                        }
                    } else {
                        // should be called for default constructors according
                        // to the doclet spec, but is never called.
                        // spec:
                        // "A default constructor returns null because it has no
                        // location in the source file"
                        // ??
                        // what about default constructors being in the source
                        // code?
                        System.err.println("warning: doclet returned null for "
                                + "source position: method:" + method);
                    }
                } else if (method.containingClass().superclass() != null
                        && method.containingClass().superclass()
                                .qualifiedName().equals("java.lang.Enum")) {
                    // check enum types
                    // <anyreturnclass> valueOf (java.lang.String) and
                    // <anyreturnclass[]> values()
                    // do not need to be tested, since they are autogenerated
                    // by the compiler
                    String sig = method.name() + method.signature();
                    if (sig.equals("valueOf(java.lang.String)")
                            || sig.equals("values()")) {
                        c = TestMethodInformation.Color.GREEN;
                        memberStats
                                .setExtra("automatically marked green since "
                                        + "generated by compiler for enums");
                    }
                }
            }
        }
        memberStats.incColor(c);
        return memberStats;
    }

    private String getMethodString(ExecutableMemberDoc method) {
        String methodDesc = (method.isPublic() ? "public " : method
                .isProtected() ? "protected " : method.isPrivate() ? "private "
                : "");

        return methodDesc + "<b>" + method.name() + "</b> "
                + method.signature();
    }

    private String getClassString(ClassDoc clazz) {
        return (clazz.isPublic() ? "public "
                : clazz.isProtected() ? "protected "
                        : clazz.isPrivate() ? "private " : "")
                + (clazz.isInterface() ? "interface" : "class")
                + " "
                + clazz.name();
    }

    private void printTestStats(TablePrinter printer, ColorStat stat,
            boolean wantLink) {
        printStats(printer, stat, wantLink);
    }

    private void printStats(TablePrinter printer, ColorStat stat,
            boolean wantLink) {
        int redCnt = stat.getColorCnt(TestMethodInformation.Color.RED);
        int yellowCnt = stat.getColorCnt(TestMethodInformation.Color.YELLOW);
        int greenCnt = stat.getColorCnt(TestMethodInformation.Color.GREEN);
        int disabledCnt = stat.getDisabledTestsCnt();
        int brokenTestCnt = stat.getBrokenTestCnt();
        int toBeFixedCnt = stat.getToBeFixedCnt();
        int knownFailureCnt = stat.getKnownFailureCnt();

        int total = redCnt + yellowCnt + greenCnt;

        String link = stat.getLink();
        String namePart;
        if (wantLink && link != null) {
            namePart = "<a href=\"" + link + "\">" + stat.getName() + "</a>";
        } else {
            namePart = stat.getName();
        }

        String extra = stat.getExtra() == null ? "" : stat.getExtra();

        int totalDots = 120;

        float toP = total == 0 ? 0 : (((float)totalDots) / total);

        int redD = (int)(toP * redCnt);
        if (redD == 0 && redCnt > 0) {
            // never let red cut to zero;
            redD = 1;
        }
        int yellowD = (int)(toP * yellowCnt);
        if (yellowD == 0 && yellowCnt > 0) {
            yellowD = 1;
        }
        int greenD = totalDots - redD - yellowD; // (int)(toP*greenCnt);

        printer.printRow(namePart, extra, "" + total, "" + redCnt, ""
                + yellowCnt, "" + greenCnt, "" + disabledCnt, ""
                + brokenTestCnt, "" + toBeFixedCnt, "" + knownFailureCnt, ""
                + (redCnt == 0 ? "" : "<span style=\"background:"
                        + COLORS[TestMethodInformation.Color.RED.ordinal()]
                        + "\">" + getDots(redD) + "</span>")
                + (yellowCnt == 0 ? "" : "<span style=\"background:"
                        + COLORS[TestMethodInformation.Color.YELLOW.ordinal()]
                        + "\">" + getDots(yellowD) + "</span>")
                + (greenCnt == 0 && total > 0 ? ""
                        : "<span style=\"background:"
                                + COLORS[TestMethodInformation.Color.GREEN
                                        .ordinal()] + "\">" + getDots(greenD)
                                + "</span>")
                + "&nbsp;&nbsp;&nbsp;<span style=\"background:blue\">"
                + getDots(total / 10) + "</span>");
    }

    private String getDots(int cnt) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < cnt; i++) {
            sb.append("&nbsp;");
        }
        return sb.toString();
    }

    /**
     * Returns the directory for a given package. Basically converts embedded
     * dots in the name into slashes.
     */

    private String getPackageBaseLink(PackageDoc pack) {
        return pack.name().replace('.', '/');
    }

    private File getPackageDir(String prefix, PackageDoc pack) {
        if (pack == null || pack.name() == null || "".equals(pack.name())) {
            return new File(prefix + "/" + ".");
        } else {
            return new File(prefix + "/" + 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.
     *
     * @param option the options
     * @return an int
     */
    public static int optionLength(String option) {
        if ("-d".equals(option)) {
            return 2;
        }
        if ("-f".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.
     *
     * @return the version of the language
     */
    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.
     *
     * @param root
     * @return whether an error occurred
     */
    public static boolean start(RootDoc root) {
        try {
            String target = getOption(root, "-d", 1, ".");
            TestCoverageDoclet doclet = new TestCoverageDoclet(target);
            doclet.process(root);

            File file = new File(target, "index.html");
            System.out.println("Please see complete report in " + 
                    file.getAbsolutePath());
            
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
        return true;
    }

}
