package jdiff;

import java.util.*;
import java.io.*;
import java.text.*;

/**
 * Emit HTML based on the changes between two sets of APIs.
 *
 * See the file LICENSE.txt for copyright details.
 * @author Matthew Doar, mdoar@pobox.com
 */
public class HTMLReportGenerator {

    /** Default constructor. */
    public HTMLReportGenerator() {
    }   

    /** The Comments object for existing comments. */
    private Comments existingComments_ = null;

    /** 
     * The Comments object for freshly regenerated comments. 
     * This is populated during the generation of the report,
     * and should be like existingComments_ but with unused comments
     * marked as such, so that they can be commented out in XML when 
     * the new comments are written out to the comments file.
     */
    private Comments newComments_ = null;

    /** 
     * Accessor method for the freshly generated Comments object. 
     * The list of comments is sorted before the object is returned.
     */
    public Comments getNewComments() {
        Collections.sort(newComments_.commentsList_);
        return newComments_;
    }

    /** Generate the report. */
    public void generate(APIComparator comp, Comments existingComments) {
        String fullReportFileName = reportFileName;
        if (outputDir != null)
            fullReportFileName = outputDir + JDiff.DIR_SEP + reportFileName;
        System.out.println("JDiff: generating HTML report into the file '" + fullReportFileName + reportFileExt + "' and the subdirectory '" + fullReportFileName + "'");
        // May be null if no comments file exists yet
        existingComments_ = existingComments;
        // Where the new comments will be placed
        newComments_ = new Comments();
        // Writing to multiple files, so make sure the subdirectory exists
        File opdir = new File(fullReportFileName);
        if (!opdir.mkdir() && !opdir.exists()) {
            System.out.println("Error: could not create the subdirectory '" + fullReportFileName + "'");
            System.exit(3);
        }

        // Emit the documentation difference files
        if (!Diff.noDocDiffs) {
            // Documentation differences, one file per package
            Diff.emitDocDiffs(fullReportFileName);
        }

        // This is the top-level summary file, first in the right hand frame
        // or linked at the start to if no frames are used.
        String changesSummaryName = fullReportFileName + JDiff.DIR_SEP +
            reportFileName + "-summary" + reportFileExt;
        apiDiff = comp.apiDiff;
        try {
            FileOutputStream fos = new FileOutputStream(changesSummaryName);
            reportFile = new PrintWriter(fos);
            writeStartHTMLHeader();
            // Write out the title in he HTML header
            String oldAPIName = "Old API";
            if (apiDiff.oldAPIName_ != null)
                oldAPIName = apiDiff.oldAPIName_;
            String newAPIName = "New API";
            if (apiDiff.newAPIName_ != null)
                newAPIName = apiDiff.newAPIName_;
            if (windowTitle == null) 
                writeHTMLTitle("Android API Differences Report");
            else
                writeHTMLTitle(windowTitle);
            writeStyleSheetRef();
            writeText("</HEAD>");

            writeText("<body class=\"gc-documentation\">");

           // writeText("<div class=\"g-section g-tpl-180\">");
           // Add the nav bar for the summary page
            writeNavigationBar(reportFileName + "-summary", null, null,
                               null, 0, true,
                               apiDiff.packagesRemoved.size() != 0,
                               apiDiff.packagesAdded.size() != 0,
                               apiDiff.packagesChanged.size() != 0);

            writeText("    <div id=\"docTitleContainer\">");
            // Write the title in the body with some formatting
            if (docTitle == null) {
	                //writeText("<center>");
                writeText("<h1>Android&nbsp;API&nbsp;Differences&nbsp;Report</h1>");
            } else {
                writeText("    <h1>" + docTitle + "</h1>");
            }

            writeText("<p>This report details the changes in the core Android framework API between two <a ");  writeText("href=\"https://developer.android.com/guide/appendix/api-levels.html\" target=\"_top\">API Level</a> ");
            writeText("specifications. It shows additions, modifications, and removals for packages, classes, methods, and fields. ");
            writeText("The report also includes general statistics that characterize the extent and type of the differences.</p>");

            writeText("<p>This report is based a comparison of the Android API specifications ");
            writeText("whose API Level identifiers are given in the upper-right corner of this page. It compares a ");
            writeText("newer \"to\" API to an older \"from\" API, noting all changes relative to the ");
            writeText("older API. So, for example, API elements marked as removed are no longer present in the \"to\" ");
            writeText("API specification.</p>");

            writeText("<p>To navigate the report, use the \"Select a Diffs Index\" and \"Filter the Index\" ");
            writeText("controls on the left. The report uses text formatting to indicate <em>interface names</em>, ");
            writeText("<a href= ><code>links to reference documentation</code></a>, and <a href= >links to change ");
            writeText("description</a>. The statistics are accessible from the \"Statistics\" link in the upper-right corner.</p>");

            writeText("<p>For more information about the Android framework API and SDK, ");
            writeText("see the <a href=\"https://developer.android.com/index.html\" target=\"_top\">Android Developers site</a>.</p>");

            // Write the contents and the other files as well
            writeReport(apiDiff);
            writeHTMLFooter();
            reportFile.close();
        } catch(IOException e) {
            System.out.println("IO Error while attempting to create " + changesSummaryName);
            System.out.println("Error: " + e.getMessage());
            System.exit(1);
        }

        // Now generate all the other files for multiple frames.
        //
        // The top-level changes.html frames file where everything starts.
        String tln = fullReportFileName + reportFileExt;
        // The file for the top-left frame.
        String tlf = fullReportFileName + JDiff.DIR_SEP + 
            "jdiff_topleftframe" + reportFileExt;
        // The default file for the bottom-left frame is the one with the
        // most information in it.
        String allDiffsIndexName = fullReportFileName + JDiff.DIR_SEP + 
            "alldiffs_index"; 
        // Other indexes for the bottom-left frame.
        String packagesIndexName = fullReportFileName + JDiff.DIR_SEP + 
            "packages_index"; 
        String classesIndexName = fullReportFileName + JDiff.DIR_SEP + 
            "classes_index"; 
        String constructorsIndexName = fullReportFileName + JDiff.DIR_SEP + 
            "constructors_index"; 
        String methodsIndexName = fullReportFileName + JDiff.DIR_SEP + 
            "methods_index"; 
        String fieldsIndexName = fullReportFileName + JDiff.DIR_SEP + 
            "fields_index"; 

        HTMLFiles hf = new HTMLFiles(this);
        hf.emitTopLevelFile(tln, apiDiff);
        hf.emitTopLeftFile(tlf);
        hf.emitHelp(fullReportFileName, apiDiff);
        hf.emitStylesheet();

        HTMLIndexes h = new HTMLIndexes(this);
        h.emitAllBottomLeftFiles(packagesIndexName, classesIndexName, 
                            constructorsIndexName, methodsIndexName,
                            fieldsIndexName, allDiffsIndexName, apiDiff);

        if (doStats) {
            // The file for the statistical report.
            String sf = fullReportFileName + JDiff.DIR_SEP + 
                "jdiff_statistics" + reportFileExt;
            HTMLStatistics stats = new HTMLStatistics(this);
            stats.emitStatistics(sf, apiDiff);
        }
    }   

    /** 
     * Write the HTML report. 
     *
     * The top section describes all the packages added (with links) and 
     * removed, and the changed packages section has links which takes you 
     * to a section for each package. This pattern continues for classes and
     * constructors, methods and fields.
     */
    public void writeReport(APIDiff apiDiff) {

        // Report packages which were removed in the new API
        if (!apiDiff.packagesRemoved.isEmpty()) {
            writeTableStart("Removed Packages", 2);
            for (PackageAPI pkgAPI : apiDiff.packagesRemoved) {
                String pkgName = pkgAPI.name_;
                if (trace) System.out.println("Package " + pkgName + " was removed.");
                writePackageTableEntry(pkgName, 0, pkgAPI.doc_, false);
            }
            writeTableEnd();
        }
        
        // Report packages which were added in the new API
        if (!apiDiff.packagesAdded.isEmpty()) {
            writeTableStart("Added Packages", 2);
            for (PackageAPI pkgAPI : apiDiff.packagesAdded) {
                String pkgName = pkgAPI.name_;
                if (trace) System.out.println("Package " + pkgName + " was added.");
                writePackageTableEntry(pkgName, 1, pkgAPI.doc_, false);
            }
            writeTableEnd();

            // Now emit a separate file for each added package.
            for (PackageAPI pkgAPI : apiDiff.packagesAdded) {
                reportAddedPackage(pkgAPI);
            }
        }

        // Report packages which were changed in the new API
        if (!apiDiff.packagesChanged.isEmpty()) {
            // Emit a table of changed packages, with links to the file
            // for each package.
            writeTableStart("Changed Packages", 3);
            for (PackageDiff pkgDiff : apiDiff.packagesChanged) {
                String pkgName = pkgDiff.name_;
                if (trace) System.out.println("Package " + pkgName + " was changed.");
                writePackageTableEntry(pkgName, 2, null, false);
            }
            writeTableEnd();

            // Now emit a separate file for each changed package.
            for (PackageDiff pkgDiff : apiDiff.packagesChanged) {
                reportChangedPackage(pkgDiff);
            }
        }
            writeText("      </div>	");
            writeText("      <div id=\"footer\">");
            writeText("        <div id=\"copyright\">");
            writeText("        Except as noted, this content is licensed under ");
            writeText("        <a href=\"https://creativecommons.org/licenses/by/2.5/\"> Creative Commons Attribution 2.5</a>.");
            writeText("        For details and restrictions, see the <a href=\"https://developer.android.com/license.html\">Content License</a>.");
            writeText("        </div>");
            writeText("      <div id=\"footerlinks\">");
            writeText("      <p>");
            writeText("        <a href=\"https://www.android.com/terms.html\">Site Terms of Service</a> -");
            writeText("        <a href=\"https://www.android.com/privacy.html\">Privacy Policy</a> -");
            writeText("        <a href=\"https://www.android.com/branding.html\">Brand Guidelines</a>");
            writeText("      </p>");
            writeText("    </div>");
            writeText("    </div> <!-- end footer -->");
            writeText("    </div><!-- end doc-content -->");
            writeText("    </div> <!-- end body-content --> ");
    }

    /**
     * Write out a quick redirection file for added packages.
     */
    public void reportAddedPackage(PackageAPI pkgAPI) {
        String pkgName = pkgAPI.name_;

        String localReportFileName = reportFileName + JDiff.DIR_SEP + "pkg_" + pkgName
                + reportFileExt;
        if (outputDir != null)
            localReportFileName = outputDir + JDiff.DIR_SEP + localReportFileName;

        try (PrintWriter pw = new PrintWriter(new FileOutputStream(localReportFileName))) {
            // Link to HTML file for the package
            String pkgRef = newDocPrefix + pkgName.replace('.', '/');
            pw.write("<html><head><meta http-equiv=\"refresh\" content=\"0;URL='" + pkgRef
                    + "/package-summary.html'\" /></head></html>");
        } catch(IOException e) {
            System.out.println("IO Error while attempting to create " + localReportFileName);
            System.out.println("Error: "+ e.getMessage());
            System.exit(1);
        }

        for (ClassAPI classAPI : pkgAPI.classes_) {
            reportAddedClass(pkgAPI.name_, classAPI);
        }
    }

    /**
     * Write out the details of a changed package in a separate file.
     */
    public void reportChangedPackage(PackageDiff pkgDiff) {
        String pkgName = pkgDiff.name_;

        PrintWriter oldReportFile = null;
        oldReportFile = reportFile;
        String localReportFileName = null;
        try {
            // Prefix package files with pkg_ because there may be a class
            // with the same name.
            localReportFileName = reportFileName + JDiff.DIR_SEP + "pkg_" + pkgName + reportFileExt;
            if (outputDir != null)
                localReportFileName = outputDir + JDiff.DIR_SEP + localReportFileName;
            FileOutputStream fos = new FileOutputStream(localReportFileName);
            reportFile = new PrintWriter(fos);
            writeStartHTMLHeader();
            writeHTMLTitle(pkgName);
            writeStyleSheetRef();
            writeText("</HEAD>");
            writeText("<BODY>");
        } catch(IOException e) {
            System.out.println("IO Error while attempting to create " + localReportFileName);
            System.out.println("Error: "+ e.getMessage());
            System.exit(1);
        }

        String pkgRef = pkgName;
        pkgRef = pkgRef.replace('.', '/');
        pkgRef = newDocPrefix + pkgRef + "/package-summary";
        // A link to the package in the new API
        String linkedPkgName = "<A HREF=\"" + pkgRef + ".html\" target=\"_top\"><font size=\"+1\"><code>" + pkgName + "</code></font></A>";
        String prevPkgRef = null;
        String nextPkgRef = null;

        writeSectionHeader("Package " + linkedPkgName, pkgName, 
                           prevPkgRef, nextPkgRef,
                           null, 1,
                           pkgDiff.classesRemoved.size() != 0, 
                           pkgDiff.classesAdded.size() != 0,
                           pkgDiff.classesChanged.size() != 0);

        // Report changes in documentation
        if (reportDocChanges && pkgDiff.documentationChange_ != null) {
            String pkgDocRef = pkgName + "/package-summary";
            pkgDocRef = pkgDocRef.replace('.', '/');
            String oldPkgRef = pkgDocRef;
            String newPkgRef = pkgDocRef;
            if (oldDocPrefix != null)
                oldPkgRef = oldDocPrefix + oldPkgRef;
            else
                oldPkgRef = null;
            newPkgRef = newDocPrefix + newPkgRef;
            if (oldPkgRef != null)
                pkgDiff.documentationChange_ += "<A HREF=\"" + oldPkgRef +
                    ".html#package_description\" target=\"_self\"><code>old</code></A> to ";
            else
                pkgDiff.documentationChange_ += "<font size=\"+1\"><code>old</code></font> to ";
            pkgDiff.documentationChange_ += "<A HREF=\"" + newPkgRef +
                ".html#package_description\" target=\"_self\"><code>new</code></A>. ";
            writeText(pkgDiff.documentationChange_);
        }

        // Report classes which were removed in the new API
        if (pkgDiff.classesRemoved.size() != 0) {
            // Determine the title for this section
            boolean hasClasses = false;
            boolean hasInterfaces = false;
            Iterator iter = pkgDiff.classesRemoved.iterator();
            while (iter.hasNext()) {
                ClassAPI classAPI = (ClassAPI)(iter.next());
                if (classAPI.isInterface_)
                    hasInterfaces = true;
                else
                    hasClasses = true;
            }
            if (hasInterfaces && hasClasses)
                writeTableStart("Removed Classes and Interfaces", 2);
            else if (!hasInterfaces && hasClasses)
                     writeTableStart("Removed Classes", 2);
            else if (hasInterfaces && !hasClasses)
                     writeTableStart("Removed Interfaces", 2);
            // Emit the table entries
            iter = pkgDiff.classesRemoved.iterator();
            while (iter.hasNext()) {
                ClassAPI classAPI = (ClassAPI)(iter.next());
                String className = classAPI.name_;
                if (trace) System.out.println("Class/Interface " + className + " was removed.");
                writeClassTableEntry(pkgName, className, 0, classAPI.isInterface_, classAPI.doc_, false);
            }
            writeTableEnd();
        }
        
        // Report classes which were added in the new API
        if (pkgDiff.classesAdded.size() != 0) {
            // Determine the title for this section
            boolean hasClasses = false;
            boolean hasInterfaces = false;
            Iterator iter = pkgDiff.classesAdded.iterator();
            while (iter.hasNext()) {
                ClassAPI classAPI = (ClassAPI)(iter.next());
                if (classAPI.isInterface_)
                    hasInterfaces = true;
                else
                    hasClasses = true;
            }
            if (hasInterfaces && hasClasses)
                writeTableStart("Added Classes and Interfaces", 2);
            else if (!hasInterfaces && hasClasses)
                     writeTableStart("Added Classes", 2);
            else if (hasInterfaces && !hasClasses)
                     writeTableStart("Added Interfaces", 2);
            // Emit the table entries
            iter = pkgDiff.classesAdded.iterator();
            while (iter.hasNext()) {
                ClassAPI classAPI = (ClassAPI)(iter.next());
                String className = classAPI.name_;
                if (trace) System.out.println("Class/Interface " + className + " was added.");
                writeClassTableEntry(pkgName, className, 1, classAPI.isInterface_, classAPI.doc_, false);
            }
            writeTableEnd();
            // Now emit a separate file for each added class and interface.
            for (ClassAPI classApi : pkgDiff.classesAdded) {
                reportAddedClass(pkgName, classApi);
            }
        }

        // Report classes which were changed in the new API
        if (pkgDiff.classesChanged.size() != 0) {
            // Determine the title for this section
            boolean hasClasses = false;
            boolean hasInterfaces = false;
            Iterator iter = pkgDiff.classesChanged.iterator();
            while (iter.hasNext()) {
                ClassDiff classDiff = (ClassDiff)(iter.next());
                if (classDiff.isInterface_)
                    hasInterfaces = true;
                else
                    hasClasses = true;
            }
            if (hasInterfaces && hasClasses)
                writeTableStart("Changed Classes and Interfaces", 2);
            else if (!hasInterfaces && hasClasses)
                     writeTableStart("Changed Classes", 2);
            else if (hasInterfaces && !hasClasses)
                     writeTableStart("Changed Interfaces", 2);
            // Emit a table of changed classes, with links to the file
            // for each class.
            iter = pkgDiff.classesChanged.iterator();
            while (iter.hasNext()) {
                ClassDiff classDiff = (ClassDiff)(iter.next());
                String className = classDiff.name_;
                if (trace) System.out.println("Package " + pkgDiff.name_ + ", class/Interface " + className + " was changed.");
                writeClassTableEntry(pkgName, className, 2, classDiff.isInterface_, null, false);
            }
            writeTableEnd();
            // Now emit a separate file for each changed class and interface.
            ClassDiff[] classDiffs = new ClassDiff[pkgDiff.classesChanged.size()];
            classDiffs = (ClassDiff[])pkgDiff.classesChanged.toArray(classDiffs);
            for (int k = 0; k < classDiffs.length; k++) {
                reportChangedClass(pkgName, classDiffs, k);
            }
        }
        
        writeSectionFooter(pkgName, prevPkgRef, nextPkgRef, null, 1);
        writeHTMLFooter();
        reportFile.close();
        reportFile = oldReportFile;
    }

    /**
     * Write out a quick redirection file for added classes.
     */
    public void reportAddedClass(String pkgName, ClassAPI classApi) {
        String className = classApi.name_;

        String localReportFileName = reportFileName + JDiff.DIR_SEP + pkgName + "." + className
                + reportFileExt;
        if (outputDir != null)
            localReportFileName = outputDir + JDiff.DIR_SEP + localReportFileName;

        try (PrintWriter pw = new PrintWriter(new FileOutputStream(localReportFileName))) {
            // Link to HTML file for the class
            String classRef = pkgName + "." + className;
            // Deal with inner classes
            if (className.indexOf('.') != -1) {
                classRef = pkgName + ".";
                classRef = classRef.replace('.', '/');
                classRef = newDocPrefix + classRef + className;
            } else {
                classRef = classRef.replace('.', '/');
                classRef = newDocPrefix + classRef;
            }

            pw.write("<html><head><meta http-equiv=\"refresh\" content=\"0;URL='" + classRef
                    + ".html'\" /></head></html>");
        } catch(IOException e) {
            System.out.println("IO Error while attempting to create " + localReportFileName);
            System.out.println("Error: "+ e.getMessage());
            System.exit(1);
        }
    }

    /** 
     * Write out the details of a changed class in a separate file. 
     */
    public void reportChangedClass(String pkgName, ClassDiff[] classDiffs, int classIndex) {
        ClassDiff classDiff = classDiffs[classIndex];
        String className = classDiff.name_;

        PrintWriter oldReportFile = null;
        oldReportFile = reportFile;
        String localReportFileName = null;
        try {
            localReportFileName = reportFileName + JDiff.DIR_SEP + pkgName + "." + className + reportFileExt;
            if (outputDir != null)
                localReportFileName = outputDir + JDiff.DIR_SEP + localReportFileName;
            FileOutputStream fos = new FileOutputStream(localReportFileName);
            reportFile = new PrintWriter(fos);
            writeStartHTMLHeader();
            writeHTMLTitle(pkgName + "." + className);
            writeStyleSheetRef();
            writeText("</HEAD>");
            writeText("<BODY>");
        } catch(IOException e) {
            System.out.println("IO Error while attempting to create " + localReportFileName);
            System.out.println("Error: "+ e.getMessage());
            System.exit(1);
        }

        String classRef = pkgName + "." + className;
        classRef = classRef.replace('.', '/');
        if (className.indexOf('.') != -1) {
            classRef = pkgName + ".";
            classRef = classRef.replace('.', '/');
            classRef = newDocPrefix + classRef + className;
        } else {
            classRef = newDocPrefix + classRef;
        }
        // A link to the class in the new API
        String linkedClassName = "<A HREF=\"" + classRef + ".html\" target=\"_top\"><font size=\"+2\"><code>" + className + "</code></font></A>";
        String lcn = pkgName + "." + linkedClassName;
        //Links to the previous and next classes
        String prevClassRef = null;
        if (classIndex != 0) {
            prevClassRef = pkgName + "." + classDiffs[classIndex-1].name_ + reportFileExt;
        }
        // Create the HTML link to the next package
        String nextClassRef = null;
        if (classIndex < classDiffs.length - 1) {
            nextClassRef = pkgName + "." + classDiffs[classIndex+1].name_ + reportFileExt;
        }

        if (classDiff.isInterface_)
            lcn = "Interface " + lcn;
        else
            lcn = "Class " + lcn;
        boolean hasCtors = classDiff.ctorsRemoved.size() != 0 ||
            classDiff.ctorsAdded.size() != 0 ||
            classDiff.ctorsChanged.size() != 0;
        boolean hasMethods = classDiff.methodsRemoved.size() != 0 ||
            classDiff.methodsAdded.size() != 0 ||
            classDiff.methodsChanged.size() != 0;
        boolean hasFields = classDiff.fieldsRemoved.size() != 0 ||
            classDiff.fieldsAdded.size() != 0 ||
            classDiff.fieldsChanged.size() != 0;
        writeSectionHeader(lcn, pkgName, prevClassRef, nextClassRef, 
                           className, 2,
                           hasCtors, hasMethods, hasFields);

        if (classDiff.inheritanceChange_ != null)
            writeText("<p><font xsize=\"+1\">" + classDiff.inheritanceChange_ + "</font>");

        // Report changes in documentation
        if (reportDocChanges && classDiff.documentationChange_ != null) {
            String oldClassRef = null;
            if (oldDocPrefix != null) {
                oldClassRef = pkgName + "." + className;
                oldClassRef = oldClassRef.replace('.', '/');
                if (className.indexOf('.') != -1) {
                    oldClassRef = pkgName + ".";
                    oldClassRef = oldClassRef.replace('.', '/');
                    oldClassRef = oldDocPrefix + oldClassRef + className;
                } else {
                    oldClassRef = oldDocPrefix + oldClassRef;
                }
            }
            if (oldDocPrefix != null)
                classDiff.documentationChange_ += "<A HREF=\"" + oldClassRef +
                    ".html\" target=\"_self\"><code>old</code></A> to ";
            else
                classDiff.documentationChange_ += "<font size=\"+1\"><code>old</code></font> to ";
            classDiff.documentationChange_ += "<A HREF=\"" + classRef +
                ".html\" target=\"_self\"><code>new</code></A>. ";
            writeText(classDiff.documentationChange_);
        }

        if (classDiff.modifiersChange_ != null)
            writeText("<p>" + classDiff.modifiersChange_);

        reportAllCtors(pkgName, classDiff);
        reportAllMethods(pkgName, classDiff);
        reportAllFields(pkgName, classDiff);

        writeSectionFooter(pkgName, prevClassRef, nextClassRef, className, 2);
        writeHTMLFooter();
        reportFile.close();
        reportFile = oldReportFile;
    }

    /** 
     * Write out the details of constructors in a class. 
     */
    public void reportAllCtors(String pkgName, ClassDiff classDiff) {
        String className = classDiff.name_;
        writeText("<a NAME=\"constructors\"></a>"); // Named anchor
        // Report ctors which were removed in the new API
        if (classDiff.ctorsRemoved.size() != 0) {
            writeTableStart("Removed Constructors", 2);
            Iterator iter = classDiff.ctorsRemoved.iterator();
            while (iter.hasNext()) {
                ConstructorAPI ctorAPI = (ConstructorAPI)(iter.next());
                String ctorType = ctorAPI.getSignature();
                if (ctorType.compareTo("void") == 0)
                    ctorType = "";
                String id = className + "(" + ctorType + ")";
                if (trace) System.out.println("Constructor " + id + " was removed.");
                writeCtorTableEntry(pkgName, className, ctorType, 0, ctorAPI.doc_, false);
            }
            writeTableEnd();
        }

        // Report ctors which were added in the new API
        if (classDiff.ctorsAdded.size() != 0) {
            writeTableStart("Added Constructors", 2);
            Iterator iter = classDiff.ctorsAdded.iterator();
            while (iter.hasNext()) {
                ConstructorAPI ctorAPI = (ConstructorAPI)(iter.next());
                String ctorType = ctorAPI.getSignature();
                if (ctorType.compareTo("void") == 0)
                    ctorType = "";
                String id = className + "(" + ctorType + ")";
                if (trace) System.out.println("Constructor " + id + " was added.");
                writeCtorTableEntry(pkgName, className, ctorType, 1, ctorAPI.doc_, false);
            }
            writeTableEnd();
        }
        
        // Report ctors which were changed in the new API
        if (classDiff.ctorsChanged.size() != 0) {
            // Emit a table of changed classes, with links to the section
            // for each class.
            writeTableStart("Changed Constructors", 3);
            Iterator iter = classDiff.ctorsChanged.iterator();
            while (iter.hasNext()) {
                MemberDiff memberDiff = (MemberDiff)(iter.next());
                if (trace) System.out.println("Constructor for " + className +
                    " was changed from " + memberDiff.oldType_ + " to " + 
                    memberDiff.newType_);
                writeCtorChangedTableEntry(pkgName, className, memberDiff);
            }
            writeTableEnd();
        }
    }

    /** 
     * Write out the details of methods in a class. 
     */
    public void reportAllMethods(String pkgName, ClassDiff classDiff) {
        writeText("<a NAME=\"methods\"></a>"); // Named anchor
        String className = classDiff.name_;
        // Report methods which were removed in the new API
        if (classDiff.methodsRemoved.size() != 0) {
            writeTableStart("Removed Methods", 2);
            Iterator iter = classDiff.methodsRemoved.iterator();
            while (iter.hasNext()) {
                MethodAPI methodAPI = (MethodAPI)(iter.next());
                String methodName = methodAPI.name_ + "(" + methodAPI.getSignature() + ")";
                if (trace) System.out.println("Method " + methodName + " was removed.");
                writeMethodTableEntry(pkgName, className, methodAPI, 0, methodAPI.doc_, false);
            }
            writeTableEnd();
        }

        // Report methods which were added in the new API
        if (classDiff.methodsAdded.size() != 0) {
            writeTableStart("Added Methods", 2);
            Iterator iter = classDiff.methodsAdded.iterator();
            while (iter.hasNext()) {
                MethodAPI methodAPI = (MethodAPI)(iter.next());
                String methodName = methodAPI.name_ + "(" + methodAPI.getSignature() + ")";
                if (trace) System.out.println("Method " + methodName + " was added.");
                writeMethodTableEntry(pkgName, className, methodAPI, 1, methodAPI.doc_, false);
            }
            writeTableEnd();
        }
        
        // Report methods which were changed in the new API
        if (classDiff.methodsChanged.size() != 0) {
            // Emit a table of changed methods.
            writeTableStart("Changed Methods", 3);
            Iterator iter = classDiff.methodsChanged.iterator();
            while (iter.hasNext()) {
                MemberDiff memberDiff = (MemberDiff)(iter.next());
                if (trace) System.out.println("Method " + memberDiff.name_ + 
                      " was changed.");
                writeMethodChangedTableEntry(pkgName, className, memberDiff);
            }
            writeTableEnd();
        }
    }

    /** 
     * Write out the details of fields in a class. 
     */
    public void reportAllFields(String pkgName, ClassDiff classDiff) {
        writeText("<a NAME=\"fields\"></a>"); // Named anchor
        String className = classDiff.name_;
        // Report fields which were removed in the new API
        if (classDiff.fieldsRemoved.size() != 0) {
            writeTableStart("Removed Fields", 2);
            Iterator iter = classDiff.fieldsRemoved.iterator();
            while (iter.hasNext()) {
                FieldAPI fieldAPI = (FieldAPI)(iter.next());
                String fieldName = fieldAPI.name_;
                if (trace) System.out.println("Field " + fieldName + " was removed.");
                writeFieldTableEntry(pkgName, className, fieldAPI, 0, fieldAPI.doc_, false);
            }
            writeTableEnd();
        }
        
        // Report fields which were added in the new API
        if (classDiff.fieldsAdded.size() != 0) {
            writeTableStart("Added Fields", 2);
            Iterator iter = classDiff.fieldsAdded.iterator();
            while (iter.hasNext()) {
                FieldAPI fieldAPI = (FieldAPI)(iter.next());
                String fieldName = fieldAPI.name_;
                if (trace) System.out.println("Field " + fieldName + " was added.");
                writeFieldTableEntry(pkgName, className, fieldAPI, 1, fieldAPI.doc_, false);
            }
            writeTableEnd();
        }
        
        // Report fields which were changed in the new API
        if (classDiff.fieldsChanged.size() != 0) {
            // Emit a table of changed classes, with links to the section
            // for each class.
            writeTableStart("Changed Fields", 3);
            Iterator iter = classDiff.fieldsChanged.iterator();
            while (iter.hasNext()) {
                MemberDiff memberDiff = (MemberDiff)(iter.next());
                if (trace) System.out.println("Field " + pkgName + "." + className + "." + memberDiff.name_ + " was changed from " + memberDiff.oldType_ + " to " + memberDiff.newType_);
                writeFieldChangedTableEntry(pkgName, className, memberDiff);
            }
            writeTableEnd();
        }
        
    }

    /** 
     * Write the start of the HTML header, together with the current
     * date and time in an HTML comment. 
     */
    public void writeStartHTMLHeaderWithDate() {
        writeStartHTMLHeader(true);
    }

    /** Write the start of the HTML header. */
    public void writeStartHTMLHeader() {
        writeStartHTMLHeader(false);
    }

    /** Write the start of the HTML header. */
    public void writeStartHTMLHeader(boolean addDate) {
        writeText("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"https://www.w3.org/TR/html4/strict.dtd\">");
        writeText("<HTML style=\"overflow:auto;\">");
        writeText("<HEAD>");
        writeText("<meta name=\"generator\" content=\"JDiff v" + JDiff.version + "\">");
        writeText("<!-- Generated by the JDiff Javadoc doclet -->");
        writeText("<!-- (" + JDiff.jDiffLocation + ") -->");
        if (addDate)
            writeText("<!-- on " + new Date() + " -->");
        writeText("<meta name=\"description\" content=\"" + JDiff.jDiffDescription + "\">");
        writeText("<meta name=\"keywords\" content=\"" + JDiff.jDiffKeywords + "\">");
    }

    /** Write the HTML title */
    public void writeHTMLTitle(String title) {
        writeText("<TITLE>");
        writeText(title);
        writeText("</TITLE>");
    }

    /** 
     * Write the HTML style sheet reference for files in the subdirectory.
     */
    public void writeStyleSheetRef() {
        writeStyleSheetRef(false);
    }

    /**
     * Write the HTML style sheet reference. If inSameDir is set, don't add
     * "../" to the location.
     */

    public void writeStyleSheetRef(boolean inSameDir) {
        if (inSameDir) {
            writeText("<link href=\"../../../assets/android-developer-docs.css\" rel=\"stylesheet\" type=\"text/css\" />");
            writeText("<link href=\"../../assets/android-developer-docs.css\" rel=\"stylesheet\" type=\"text/css\" />");
            writeText("<link href=\"stylesheet-jdiff.css\" rel=\"stylesheet\" type=\"text/css\" />");
            writeText("<noscript>");
            writeText("<style type=\"text/css\">");
            writeText("body{overflow:auto;}");
            writeText("#body-content{position:relative; top:0;}");
            writeText("#doc-content{overflow:visible;border-left:3px solid #666;}");
            writeText("#side-nav{padding:0;}");
            writeText("#side-nav .toggle-list ul {display:block;}");
            writeText("#resize-packages-nav{border-bottom:3px solid #666;}");
            writeText("</style>");
            writeText("</noscript>");
            writeText("<style type=\"text/css\">");
            writeText("</style>");
	    } else {
            writeText("<link href=\"../../../../assets/android-developer-docs.css\" rel=\"stylesheet\" type=\"text/css\" />");
            writeText("<link href=\"../../../assets/android-developer-docs.css\" rel=\"stylesheet\" type=\"text/css\" />");
            writeText("<link href=\"../stylesheet-jdiff.css\" rel=\"stylesheet\" type=\"text/css\" />");
            writeText("<noscript>");
            writeText("<style type=\"text/css\">");
            writeText("body{overflow:auto;}");
            writeText("#body-content{position:relative; top:0;}");
            writeText("#doc-content{overflow:visible;border-left:3px solid #666;}");
            writeText("#side-nav{padding:0;}");
            writeText("#side-nav .toggle-list ul {display:block;}");
            writeText("#resize-packages-nav{border-bottom:3px solid #666;}");
            writeText("</style>");
            writeText("</noscript>");
            writeText("<style type=\"text/css\">");
            writeText("</style>");
	    }
    }

    /** Write the HTML footer. */
    public void writeHTMLFooter() {
        writeText("</BODY>");
        writeText("</HTML>");
    }

    /**
     * Write a section header, which includes a navigation bar.
     *
     * @param title Title of the header. Contains any links necessary.
     * @param packageName The name of the current package, with no slashes or
     *                    links in it. May be null
     * @param prevElemLink An HTML link to the previous element (a package or
     *                     class). May be null.
     * @param nextElemLink An HTML link to the next element (a package or
     *                     class). May be null.
     * @param className The name of the current class, with no slashes or
     *                  links in it. May be null.
     * @param level 0 = overview, 1 = package, 2 = class/interface
     */
    public void writeSectionHeader(String title, String packageName, 
                                   String prevElemLink, String nextElemLink,
                                   String className, int level, 
                                   boolean hasRemovals, 
                                   boolean hasAdditions, 
                                   boolean hasChanges) {
        writeNavigationBar(packageName, prevElemLink, nextElemLink,
                           className, level, true,
                           hasRemovals, hasAdditions, hasChanges);
        if (level != 0) {
            reportFile.println("<H2>");
            reportFile.println(title);
            reportFile.println("</H2>");
        }
    }
    
    /** 
     * Write a section footer, which includes a navigation bar. 
     * 
     * @param packageName The name of the current package, with no slashes or 
     *                    links in it. may be null
     * @param prevElemLink An HTML link to the previous element (a package or 
     *                     class). May be null.
     * @param nextElemLink An HTML link to the next element (a package or 
     *                     class). May be null.
     * @param className The name of the current class, with no slashes or 
     *                  links in it. May be null
     * @param level 0 = overview, 1 = package, 2 = class/interface
     */
    public void writeSectionFooter(String packageName,
                                   String prevElemLink, String nextElemLink,
                                   String className, int level) {
        writeText("      </div>	");
        writeText("      <div id=\"footer\">");
        writeText("        <div id=\"copyright\">");
        writeText("        Except as noted, this content is licensed under ");
        writeText("        <a href=\"https://creativecommons.org/licenses/by/2.5/\"> Creative Commons Attribution 2.5</a>.");
        writeText("        For details and restrictions, see the <a href=\"https://developer.android.com/license.html\">Content License</a>.");
        writeText("        </div>");
        writeText("      <div id=\"footerlinks\">");
        writeText("      <p>");
        writeText("        <a href=\"https://www.android.com/terms.html\">Site Terms of Service</a> -");
        writeText("        <a href=\"https://www.android.com/privacy.html\">Privacy Policy</a> -");
        writeText("        <a href=\"https://www.android.com/branding.html\">Brand Guidelines</a>");
        writeText("      </p>");
        writeText("    </div>");
        writeText("    </div> <!-- end footer -->");
        writeText("    </div><!-- end doc-content -->");
        writeText("    </div> <!-- end body-content --> ");

    }

    /**
     * Write a navigation bar section header.
     *
     * @param pkgName The name of the current package, with no slashes or
     *                links in it.
     * @param prevElemLink An HTML link to the previous element (a package or
     *                     class). May be null.
     * @param nextElemLink An HTML link to the next element (a package or
     *                     class). May be null.
     * @param className The name of the current class, with no slashes or
     *                links in it. May be null.
     * @param level 0 = overview, 1 = package, 2 = class/interface
     */

    public void writeNavigationBar(String pkgName,
                                   String prevElemLink, String nextElemLink,
                                   String className, int level,
                                   boolean upperNavigationBar,
                                   boolean hasRemovals, boolean hasAdditions,
                                   boolean hasChanges) {

        String oldAPIName = "Old API";
        if (apiDiff.oldAPIName_ != null)
            oldAPIName = apiDiff.oldAPIName_;
        String newAPIName = "New API";
        if (apiDiff.newAPIName_ != null)
            newAPIName = apiDiff.newAPIName_;

        SimpleDateFormat formatter
          = new SimpleDateFormat ("yyyy.MM.dd HH:mm");
        Date day = new Date();

	    reportFile.println("<!-- Start of nav bar -->");

	    reportFile.println("<a name=\"top\"></a>");
	    reportFile.println("<div id=\"header\" style=\"margin-bottom:0;padding-bottom:0;\">");
	    reportFile.println("<div id=\"headerLeft\">");
	    reportFile.println("<a href=\"../../../../index.html\" tabindex=\"-1\" target=\"_top\"><img src=\"../../../../assets/images/bg_logo.png\" alt=\"Android Developers\" /></a>");
	    reportFile.println("</div>");
	    reportFile.println("  <div id=\"headerRight\">");
	    reportFile.println("  <div id=\"headerLinks\">");
        reportFile.println("<!-- <img src=\"/assets/images/icon_world.jpg\" alt=\"\" /> -->");
	    reportFile.println("<span class=\"text\">");
	    reportFile.println("<!-- &nbsp;<a href=\"#\">English</a> | -->");
	    reportFile.println("<nobr><a href=\"https://developer.android.com\" target=\"_top\">Android Developers</a> | <a href=\"https://www.android.com\" target=\"_top\">Android.com</a></nobr>");
	    reportFile.println("</span>");
	    reportFile.println("</div>");
	    reportFile.println("  <div class=\"and-diff-id\" style=\"margin-top:6px;margin-right:8px;\">");
        reportFile.println("    <table class=\"diffspectable\">");
	    reportFile.println("      <tr>");
	    reportFile.println("        <td colspan=\"2\" class=\"diffspechead\">API Diff Specification</td>");
	    reportFile.println("      </tr>");
	    reportFile.println("      <tr>");
	    reportFile.println("        <td class=\"diffspec\" style=\"padding-top:.25em\">To Level:</td>");
	    reportFile.println("        <td class=\"diffvaluenew\" style=\"padding-top:.25em\">" + newAPIName + "</td>");
	    reportFile.println("      </tr>");
	    reportFile.println("      <tr>");
	    reportFile.println("        <td class=\"diffspec\">From Level:</td>");
	    reportFile.println("        <td class=\"diffvalueold\">" + oldAPIName + "</td>");
	    reportFile.println("      </tr>");
	    reportFile.println("      <tr>");
	    reportFile.println("        <td class=\"diffspec\">Generated</td>");
	    reportFile.println("        <td class=\"diffvalue\">" + formatter.format( day ) + "</td>");
	    reportFile.println("      </tr>");
 	    reportFile.println("    </table>");
 	    reportFile.println("    </div><!-- End and-diff-id -->");
        if (doStats) {
	    	reportFile.println("  <div class=\"and-diff-id\" style=\"margin-right:8px;\">");
	    	reportFile.println("    <table class=\"diffspectable\">");
	    	reportFile.println("      <tr>");
	    	reportFile.println("        <td class=\"diffspec\" colspan=\"2\"><a href=\"jdiff_statistics.html\">Statistics</a>");
	    	reportFile.println("      </tr>");
 	    	reportFile.println("    </table>");
	    	reportFile.println("  </div> <!-- End and-diff-id -->");
	    }
	    reportFile.println("  </div> <!-- End headerRight -->");
	    reportFile.println("  </div> <!-- End header -->");
	    reportFile.println("<div id=\"body-content\" xstyle=\"padding:12px;padding-right:18px;\">");
	    reportFile.println("<div id=\"doc-content\" style=\"position:relative;\">");
	    reportFile.println("<div id=\"mainBodyFluid\">");
    }

    /** Write the start of a table. */
    public void writeTableStart(String title, int colSpan) {
        reportFile.println("<p>");
        // Assumes that the first word of the title categorizes the table type
        // and that there is a space after the first word in the title
        int idx = title.indexOf(' ');
        String namedAnchor = title.substring(0, idx);
        reportFile.println("<a NAME=\"" + namedAnchor + "\"></a>"); // Named anchor
        reportFile.println("<TABLE summary=\"" + title+ "\" WIDTH=\"100%\">");
        reportFile.println("<TR>");
        reportFile.print("  <TH VALIGN=\"TOP\" COLSPAN=" + colSpan + ">");
        reportFile.println(title + "</FONT></TD>");
        reportFile.println("</TH>");
    }

    /** 
     * If a class or package name is considered to be too long for convenient
     * display, insert <br> in the middle of it at a period.
     */
    public String makeTwoRows(String name) {
        if (name.length() < 30)
            return name;
        int idx = name.indexOf(".", 20);
        if (idx == -1)
            return name;
        int len = name.length();
        String res = name.substring(0, idx+1) + "<br>" + name.substring(idx+1, len);
        return res;
    }

    /** 
     * Write a table entry for a package, with support for links to Javadoc 
     * for removed packages. 
     *
     * linkType: 0 - no link by default, 1 = link to Javadoc HTML file, 2 = link to JDiff file
     */
    public void writePackageTableEntry(String pkgName, int linkType, 
                                       String possibleComment, boolean useOld) {
        if (!useOld) {
            reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");
            reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
            reportFile.println("  <A NAME=\"" + pkgName + "\"></A>"); // Named anchor
        }
        //String shownPkgName = makeTwoRows(pkgName);
        if (linkType == 0) {
            if (oldDocPrefix == null) {
                // No link
                reportFile.print("  " + pkgName);
            } else {
                // Call this method again but this time to emit a link to the 
                // old program element.
                writePackageTableEntry(pkgName, 1, possibleComment, true);
            }
        } else if (linkType == 1) {
            // Link to HTML file for the package
            String pkgRef = pkgName;
            pkgRef = pkgRef.replace('.', '/');
            if (useOld)
                pkgRef = oldDocPrefix + pkgRef + "/package-summary";
            else
                pkgRef = newDocPrefix + pkgRef + "/package-summary";
            reportFile.println("  <nobr><A HREF=\"" + pkgRef + ".html\" target=\"_top\"><code>" + pkgName + "</code></A></nobr>");
        } else if (linkType == 2) {
            reportFile.println("  <nobr><A HREF=\"pkg_" + pkgName + reportFileExt + "\">" + pkgName + "</A></nobr>");
        }
        if (!useOld) {
            reportFile.println("  </TD>");
            emitComment(pkgName, possibleComment, linkType);
            reportFile.println("</TR>");
        }
    }

    /** 
     * Write a table entry for a class or interface. 
     *
     * linkType: 0 - no link by default, 1 = link to Javadoc HTML file, 2 = link to JDiff file
     */
    public void writeClassTableEntry(String pkgName, String className, 
                                     int linkType, boolean isInterface, 
                                     String possibleComment, boolean useOld) {
        if (!useOld) {
            reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");
            reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
            reportFile.println("  <A NAME=\"" + className + "\"></A>"); // Named anchor
        }
        String fqName = pkgName + "." + className;
        String shownClassName = makeTwoRows(className);
        if (linkType == 0) {
            if (oldDocPrefix == null) {
                // No link
                if (isInterface)
                    reportFile.println("  <I>" + shownClassName + "</I>");
                else
                    reportFile.println("  " + shownClassName);
            } else {
                writeClassTableEntry(pkgName, className, 
                                     1, isInterface, 
                                     possibleComment, true);
            }
        } else if (linkType == 1) {
            // Link to HTML file for the class
            String classRef = fqName;
            // Deal with inner classes
            if (className.indexOf('.') != -1) {
                classRef = pkgName + ".";
                classRef = classRef.replace('.', '/');
                if (useOld)
                    classRef = oldDocPrefix + classRef + className;
                else
                    classRef = newDocPrefix + classRef + className;
            } else {
                classRef = classRef.replace('.', '/');
                if (useOld)
                    classRef = oldDocPrefix + classRef;
                else
                    classRef = newDocPrefix + classRef;
            }
            reportFile.print("  <nobr><A HREF=\"" + classRef + ".html\" target=\"_top\"><code>");
            if (isInterface)
                reportFile.print("<I>" + shownClassName + "</I>");
            else
                reportFile.print(shownClassName);
            reportFile.println("</code></A></nobr>");
        } else if (linkType == 2) {
            reportFile.print("  <nobr><A HREF=\"" + fqName + reportFileExt + "\">");
            if (isInterface)
                reportFile.print("<I>" + shownClassName + "</I>");
            else
                reportFile.print(shownClassName);
            reportFile.println("</A></nobr>");
        }
        if (!useOld) {
            reportFile.println("  </TD>");
            emitComment(fqName, possibleComment, linkType);
            reportFile.println("</TR>");
        }
    }

    /** 
     * Write a table entry for a constructor. 
     *
     * linkType: 0 - no link by default, 1 = link to Javadoc HTML file
     */
    public void writeCtorTableEntry(String pkgName, String className, 
                                    String type, int linkType, 
                                    String possibleComment, boolean useOld) {
        String fqName = pkgName + "." + className;
        String shownClassName = makeTwoRows(className);
        String lt = "removed";
        if (linkType ==1)
            lt = "added";
        String commentID = fqName + ".ctor_" + lt + "(" + type + ")";
        if (!useOld) {
            reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");
            reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
            reportFile.println("  <A NAME=\"" + commentID + "\"></A>"); // Named anchor
        }
        String shortType = simpleName(type);
        if (linkType == 0) {
            if (oldDocPrefix == null) {
                // No link
                reportFile.print("  <nobr>" + className);
                emitTypeWithParens(shortType);
                reportFile.println("</nobr>");
            } else {
                writeCtorTableEntry(pkgName, className,
                                    type, 1,
                                    possibleComment, true);
            }
        } else if (linkType == 1) {
            // Link to HTML file for the package
            String memberRef = fqName.replace('.', '/');
            // Deal with inner classes
            if (className.indexOf('.') != -1) {
                memberRef = pkgName + ".";
                memberRef = memberRef.replace('.', '/');
                if (useOld) {
                    // oldDocPrefix is non-null at this point
                    memberRef = oldDocPrefix + memberRef + className;
                } else {
                    memberRef = newDocPrefix + memberRef + className;
                }
            } else {
                if (useOld) {
                    // oldDocPrefix is non-null at this point
                    memberRef = oldDocPrefix + memberRef;
                } else {
                    memberRef = newDocPrefix + memberRef;
                }
            }
            reportFile.print("  <nobr><A HREF=\"" + memberRef + ".html#" + className +
                             "(" + type + ")\" target=\"_top\"><code>" + shownClassName + "</code></A>");
            emitTypeWithParens(shortType);
            reportFile.println("</nobr>");
        }
        if (!useOld) {
            reportFile.println("  </TD>");
            emitComment(commentID, possibleComment, linkType);
            reportFile.println("</TR>");
        }
    }

    /** 
     * Write a table entry for a changed constructor.
     */
    public void writeCtorChangedTableEntry(String pkgName, String className, 
                                           MemberDiff memberDiff) {
        String fqName = pkgName + "." + className;
        String newSignature = memberDiff.newType_;
        if (newSignature.compareTo("void") == 0)
            newSignature = "";
        String commentID = fqName + ".ctor_changed(" + newSignature + ")";
        reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");
        reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
        reportFile.println("  <A NAME=\"" + commentID + "\"></A>"); // Named anchor
        String memberRef = fqName.replace('.', '/');            
        String shownClassName = makeTwoRows(className);
        // Deal with inner classes
        if (className.indexOf('.') != -1) {
            memberRef = pkgName + ".";
            memberRef = memberRef.replace('.', '/');
            memberRef = newDocPrefix + memberRef + className;
        } else {
            memberRef = newDocPrefix + memberRef;
        }
        String newType = memberDiff.newType_;
        if (newType.compareTo("void") == 0)
            newType = "";
        String shortNewType = simpleName(memberDiff.newType_);
        // Constructors have the linked name, then the type in parentheses.
        reportFile.print("  <nobr><A HREF=\"" + memberRef + ".html#" + className + "(" + newType + ")\" target=\"_top\"><code>");
        reportFile.print(shownClassName);
        reportFile.print("</code></A>");
        emitTypeWithParens(shortNewType);
        reportFile.println("  </nobr>");
        reportFile.println("  </TD>");

        // Report changes in documentation
        if (reportDocChanges && memberDiff.documentationChange_ != null) {
            String oldMemberRef = null;
            String oldType = null;
            if (oldDocPrefix != null) {
                oldMemberRef = pkgName + "." + className;
                oldMemberRef = oldMemberRef.replace('.', '/');
                if (className.indexOf('.') != -1) {
                    oldMemberRef = pkgName + ".";
                    oldMemberRef = oldMemberRef.replace('.', '/');
                    oldMemberRef = oldDocPrefix + oldMemberRef + className;
                } else {
                    oldMemberRef = oldDocPrefix + oldMemberRef;
                }
                oldType = memberDiff.oldType_;
                if (oldType.compareTo("void") == 0)
                    oldType = "";
            }
            if (oldDocPrefix != null)
                memberDiff.documentationChange_ += "<A HREF=\"" +
                    oldMemberRef + ".html#" + className + "(" + oldType +
                    ")\" target=\"_self\"><code>old</code></A> to ";
            else 
                memberDiff.documentationChange_ += "<font size=\"+1\"><code>old</code></font> to ";
            memberDiff.documentationChange_ += "<A HREF=\"" + memberRef +
                ".html#" + className + "(" + newType +
                ")\" target=\"_self\"><code>new</code></A>.<br>";
        }

        emitChanges(memberDiff, 0);
        emitComment(commentID, null, 2);

        reportFile.println("</TR>");
    }

    /**
     * Write a table entry for a method.
     *
     * linkType: 0 - no link by default, 1 = link to Javadoc HTML file
     */
    public void writeMethodTableEntry(String pkgName, String className, 
                                      MethodAPI methodAPI, int linkType, 
                                      String possibleComment, boolean useOld) {
        String fqName = pkgName + "." + className;
        String signature = methodAPI.getSignature(); 
        String methodName = methodAPI.name_;
        String lt = "removed";
        if (linkType ==1)
            lt = "added";
        String commentID = fqName + "." + methodName + "_" + lt + "(" + signature + ")";
        if (!useOld) {
            reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");
            reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
            reportFile.println("  <A NAME=\"" + commentID + "\"></A>"); // Named anchor
        }
        if (signature.compareTo("void") == 0)
            signature = "";
        String shortSignature = simpleName(signature);
        String returnType = methodAPI.returnType_;
        String shortReturnType = simpleName(returnType);
        if (linkType == 0) {
            if (oldDocPrefix == null) {
                // No link
                reportFile.print("  <nobr>");
                emitType(shortReturnType);
                reportFile.print("&nbsp;" + methodName);
                emitTypeWithParens(shortSignature);
                reportFile.println("</nobr>");
            } else {
                writeMethodTableEntry(pkgName, className, 
                                      methodAPI, 1, 
                                      possibleComment, true);
            }
        } else if (linkType == 1) {
            // Link to HTML file for the package
            String memberRef = fqName.replace('.', '/');
            // Deal with inner classes
            if (className.indexOf('.') != -1) {
                memberRef = pkgName + ".";
                memberRef = memberRef.replace('.', '/');
                if (useOld) {
                    // oldDocPrefix is non-null at this point
                    memberRef = oldDocPrefix + memberRef + className;
                } else {
                    memberRef = newDocPrefix + memberRef + className;
                }
            } else {
                if (useOld) {
                    // oldDocPrefix is non-null at this point
                    memberRef = oldDocPrefix + memberRef;
                } else {
                    memberRef = newDocPrefix + memberRef;
                }
            }
            reportFile.print("  <nobr>");
            emitType(shortReturnType);
            reportFile.print("&nbsp;<A HREF=\"" + memberRef + ".html#" + methodName +
               "(" + signature + ")\" target=\"_top\"><code>" + methodName + "</code></A>");
            emitTypeWithParens(shortSignature);
            reportFile.println("</nobr>");
        }
        if (!useOld) {
            reportFile.println("  </TD>");
            emitComment(commentID, possibleComment, linkType);
            reportFile.println("</TR>");
        }
    }

    /**
     * Write a table entry for a changed method.
     */
    public void writeMethodChangedTableEntry(String pkgName, String className, 
                                      MemberDiff memberDiff) {
        String memberName = memberDiff.name_;
        // Generally nowhere to break a member name anyway
        // String shownMemberName = makeTwoRows(memberName);
        String fqName = pkgName + "." + className;
        String newSignature = memberDiff.newSignature_;
        String commentID = fqName + "." + memberName + "_changed(" + newSignature + ")";
        reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");

        reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
        reportFile.println("  <A NAME=\"" + commentID + "\"></A>"); // Named anchor
        String memberRef = fqName.replace('.', '/');            
        // Deal with inner classes
        if (className.indexOf('.') != -1) {
            memberRef = pkgName + ".";
            memberRef = memberRef.replace('.', '/');
            memberRef = newDocPrefix + memberRef + className;
        } else {
            memberRef = newDocPrefix + memberRef;
        }
        // Javadoc generated HTML has no named anchors for methods
        // inherited from other classes, so link to the defining class' method.
        // Only copes with non-inner classes.
        if (className.indexOf('.') == -1 &&
            memberDiff.modifiersChange_ != null &&
            memberDiff.modifiersChange_.indexOf("but is now inherited from") != -1) {
            memberRef = memberDiff.inheritedFrom_;
            memberRef = memberRef.replace('.', '/');
            memberRef = newDocPrefix + memberRef;
        }

        String newReturnType = memberDiff.newType_;
        String shortReturnType = simpleName(newReturnType);
        String shortSignature = simpleName(newSignature);
        reportFile.print("  <nobr>");
        emitTypeWithNoParens(shortReturnType);
        reportFile.print("&nbsp;<A HREF=\"" + memberRef + ".html#" +
                         memberName + "(" + newSignature + ")\" target=\"_top\"><code>");
        reportFile.print(memberName);
        reportFile.print("</code></A>");
        emitTypeWithParens(shortSignature);
        reportFile.println("  </nobr>");
        reportFile.println("  </TD>");

        // Report changes in documentation
        if (reportDocChanges && memberDiff.documentationChange_ != null) {
            String oldMemberRef = null;
            String oldSignature = null;
            if (oldDocPrefix != null) {
                oldMemberRef = pkgName + "." + className;
                oldMemberRef = oldMemberRef.replace('.', '/');
                if (className.indexOf('.') != -1) {
                    oldMemberRef = pkgName + ".";
                    oldMemberRef = oldMemberRef.replace('.', '/');
                    oldMemberRef = oldDocPrefix + oldMemberRef + className;
                } else {
                    oldMemberRef = oldDocPrefix + oldMemberRef;
                }
                oldSignature = memberDiff.oldSignature_;
            }
            if (oldDocPrefix != null)
                memberDiff.documentationChange_ += "<A HREF=\"" +
                    oldMemberRef + ".html#" + memberName + "(" +
                    oldSignature + ")\" target=\"_self\"><code>old</code></A> to ";
            else
                memberDiff.documentationChange_ += "<code>old</code> to ";
            memberDiff.documentationChange_ += "<A HREF=\"" + memberRef +
                ".html#" + memberName + "(" + newSignature +
                ")\" target=\"_self\"><code>new</code></A>.<br>";
        }

        emitChanges(memberDiff, 1);
        // Get the comment from the parent class if more appropriate
        if (memberDiff.modifiersChange_ != null) {
            int parentIdx = memberDiff.modifiersChange_.indexOf("now inherited from");
            if (parentIdx != -1) {
                // Change the commentID to pick up the appropriate method
                commentID = memberDiff.inheritedFrom_ + "." + memberName +
                    "_changed(" + newSignature + ")";
            }
        }
        emitComment(commentID, null, 2);
        
        reportFile.println("</TR>");
    }

    /** 
     * Write a table entry for a field. 
     *
     * linkType: 0 - no link by default, 1 = link to Javadoc HTML file
     */
    public void writeFieldTableEntry(String pkgName, String className, 
                                     FieldAPI fieldAPI, int linkType, 
                                     String possibleComment, boolean useOld) {
        String fqName = pkgName + "." + className;
        // Fields can only appear in one table, so no need to specify _added etc
        String fieldName = fieldAPI.name_;
        // Generally nowhere to break a member name anyway
        // String shownFieldName = makeTwoRows(fieldName);
        String commentID = fqName + "." + fieldName;
        if (!useOld) {
            reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");
            reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
            reportFile.println("  <A NAME=\"" + commentID + "\"></A>"); // Named anchor
        }
        String fieldType = fieldAPI.type_;
        if (fieldType.compareTo("void") == 0)
            fieldType = "";
        String shortFieldType = simpleName(fieldType);
        if (linkType == 0) {
            if (oldDocPrefix == null) {
                // No link.
                reportFile.print("  ");
                emitType(shortFieldType);
                reportFile.println("&nbsp;" + fieldName);
            } else {
                writeFieldTableEntry(pkgName, className, 
                                     fieldAPI, 1, 
                                     possibleComment, true);
            }
        } else if (linkType == 1) {
            // Link to HTML file for the package.
            String memberRef = fqName.replace('.', '/');
            // Deal with inner classes
            if (className.indexOf('.') != -1) {
                memberRef = pkgName + ".";
                memberRef = memberRef.replace('.', '/');
                if (useOld)
                    memberRef = oldDocPrefix + memberRef + className;
                else
                    memberRef = newDocPrefix + memberRef + className;
            } else {
                if (useOld)
                    memberRef = oldDocPrefix + memberRef;
                else
                    memberRef = newDocPrefix + memberRef;
            }
            reportFile.print("  <nobr>");
            emitType(shortFieldType);
            reportFile.println("&nbsp;<A HREF=\"" + memberRef + ".html#" + fieldName +
               "\" target=\"_top\"><code>" + fieldName + "</code></A></nobr>");
        }
        if (!useOld) {
            reportFile.println("  </TD>");
            emitComment(commentID, possibleComment, linkType);
            reportFile.println("</TR>");
        }
    }

    /**
     * Write a table entry for a changed field.
     */
    public void writeFieldChangedTableEntry(String pkgName, String className,
                                            MemberDiff memberDiff) {
        String memberName = memberDiff.name_;
        // Generally nowhere to break a member name anyway
        // String shownMemberName = makeTwoRows(memberName);
        String fqName = pkgName + "." + className;
        // Fields have unique names in a class
        String commentID = fqName + "." + memberName;
        reportFile.println("<TR BGCOLOR=\"" + bgcolor + "\" CLASS=\"TableRowColor\">");

        reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"25%\">");
        reportFile.println("  <A NAME=\"" + commentID + "\"></A>"); // Named anchor
        String memberRef = fqName.replace('.', '/');            
        // Deal with inner classes
        if (className.indexOf('.') != -1) {
            memberRef = pkgName + ".";
            memberRef = memberRef.replace('.', '/');
            memberRef = newDocPrefix + memberRef + className;
        } else {
            memberRef = newDocPrefix + memberRef;
        }
        // Javadoc generated HTML has no named anchors for fields
        // inherited from other classes, so link to the defining class' field.
        // Only copes with non-inner classes.
        if (className.indexOf('.') == -1 &&
            memberDiff.modifiersChange_ != null &&
            memberDiff.modifiersChange_.indexOf("but is now inherited from") != -1) {
            memberRef = memberDiff.inheritedFrom_;
            memberRef = memberRef.replace('.', '/');
            memberRef = newDocPrefix + memberRef;
        }

        String newType = memberDiff.newType_;
        String shortNewType = simpleName(newType);
        reportFile.print("  <nobr>");
        emitTypeWithNoParens(shortNewType);
        reportFile.print("&nbsp;<A HREF=\"" + memberRef + ".html#" +
                         memberName + "\" target=\"_top\"><code>");
        reportFile.print(memberName);
        reportFile.print("</code></font></A></nobr>");
        reportFile.println("  </TD>");

        // Report changes in documentation
        if (reportDocChanges && memberDiff.documentationChange_ != null) {
            String oldMemberRef = null;
            if (oldDocPrefix != null) {
                oldMemberRef = pkgName + "." + className;
                oldMemberRef = oldMemberRef.replace('.', '/');
                if (className.indexOf('.') != -1) {
                    oldMemberRef = pkgName + ".";
                    oldMemberRef = oldMemberRef.replace('.', '/');
                    oldMemberRef = oldDocPrefix + oldMemberRef + className;
                } else {
                    oldMemberRef = oldDocPrefix + oldMemberRef;
                }
            }
            if (oldDocPrefix != null)
                memberDiff.documentationChange_ += "<A HREF=\"" +
                    oldMemberRef + ".html#" + memberName + "\" target=\"_self\"><code>old</code></A> to ";
            else
                memberDiff.documentationChange_ += "<code>old</code> to ";
            memberDiff.documentationChange_ += "<A HREF=\"" + memberRef +
                ".html#" + memberName + "\" target=\"_self\"><code>new</code></A>.<br>";
        }

        emitChanges(memberDiff, 2);
        // Get the comment from the parent class if more appropriate
        if (memberDiff.modifiersChange_ != null) {
            int parentIdx = memberDiff.modifiersChange_.indexOf("now inherited from");
            if (parentIdx != -1) {
                // Change the commentID to pick up the appropriate method
                commentID = memberDiff.inheritedFrom_ + "." + memberName;
            }
        }
        emitComment(commentID, null, 2);
        
        reportFile.println("</TR>");
    }

    /**
     * Emit all changes associated with a MemberDiff as an entry in a table.
     *
     * @param memberType 0 = ctor, 1 = method, 2 = field
     */
    public void emitChanges(MemberDiff memberDiff, int memberType){
        reportFile.println("  <TD VALIGN=\"TOP\" WIDTH=\"30%\">");
        boolean hasContent = false;
        // The type or return type changed
        if (memberDiff.oldType_.compareTo(memberDiff.newType_) != 0) {
            String shortOldType = simpleName(memberDiff.oldType_);
            String shortNewType = simpleName(memberDiff.newType_);
            if (memberType == 1) {
                reportFile.print("Change in return type from ");
            } else {
                reportFile.print("Change in type from ");
            }
            if (shortOldType.compareTo(shortNewType) == 0) {
                // The types differ in package name, so use the full name
                shortOldType = memberDiff.oldType_;
                shortNewType = memberDiff.newType_;
            }
            emitType(shortOldType);
            reportFile.print(" to ");
            emitType(shortNewType);
            reportFile.println(".<br>");
            hasContent = true;
        }
        // The signatures changed - only used by methods
        if (memberType == 1 &&
            memberDiff.oldSignature_ != null && 
            memberDiff.newSignature_ != null && 
            memberDiff.oldSignature_.compareTo(memberDiff.newSignature_) != 0) {
            String shortOldSignature = simpleName(memberDiff.oldSignature_);
            String shortNewSignature = simpleName(memberDiff.newSignature_);
            if (shortOldSignature.compareTo(shortNewSignature) == 0) {
                // The signatures differ in package names, so use the full form
                shortOldSignature = memberDiff.oldSignature_;
                shortNewSignature = memberDiff.newSignature_;
            }
            if (hasContent)
                reportFile.print(" "); 
            reportFile.print("Change in signature from ");
            if (shortOldSignature.compareTo("") == 0)
                shortOldSignature = "void";
            emitType(shortOldSignature);
            reportFile.print(" to ");
            if (shortNewSignature.compareTo("") == 0)
                shortNewSignature = "void";
            emitType(shortNewSignature);
            reportFile.println(".<br>");
            hasContent = true;
        }
        // The exceptions are only non-null in methods and constructors
        if (memberType != 2 &&
            memberDiff.oldExceptions_ != null && 
            memberDiff.newExceptions_ != null && 
            memberDiff.oldExceptions_.compareTo(memberDiff.newExceptions_) != 0) {
            if (hasContent)
                reportFile.print(" "); 
            // If either one of the exceptions has no spaces in it, or is 
            // equal to "no exceptions", then just display the whole 
            // exceptions texts.
            int spaceInOld = memberDiff.oldExceptions_.indexOf(" ");
            if (memberDiff.oldExceptions_.compareTo("no exceptions") == 0)
                spaceInOld = -1;
            int spaceInNew = memberDiff.newExceptions_.indexOf(" ");
            if (memberDiff.newExceptions_.compareTo("no exceptions") == 0)
                spaceInNew = -1;
            if (spaceInOld == -1 || spaceInNew == -1) {
                reportFile.print("Change in exceptions thrown from ");
                emitException(memberDiff.oldExceptions_); 
                reportFile.print(" to " );
                emitException(memberDiff.newExceptions_); 
                reportFile.println(".<br>");
            } else {
                // Too many exceptions become unreadable, so just show the 
                // individual changes. Catch the case where exceptions are
                // just reordered.
                boolean firstChange = true;
                int numRemoved = 0;
                StringTokenizer stOld = new StringTokenizer(memberDiff.oldExceptions_, ", ");
                while (stOld.hasMoreTokens()) {
                    String oldException = stOld.nextToken();
                    if (!memberDiff.newExceptions_.startsWith(oldException) &&
                        !(memberDiff.newExceptions_.indexOf(", " + oldException) != -1)) {
                        if (firstChange) {
                            reportFile.print("Change in exceptions: ");
                            firstChange = false;
                        }
                        if (numRemoved != 0)
                            reportFile.print(", ");
                        emitException(oldException);
                        numRemoved++;
                    }
                }
                if (numRemoved == 1)
                    reportFile.print(" was removed.");
                else if (numRemoved > 1)
                    reportFile.print(" were removed.");
                
                int numAdded = 0;
                StringTokenizer stNew = new StringTokenizer(memberDiff.newExceptions_, ", ");
                while (stNew.hasMoreTokens()) {
                    String newException = stNew.nextToken();
                    if (!memberDiff.oldExceptions_.startsWith(newException) &&
                        !(memberDiff.oldExceptions_.indexOf(", " + newException) != -1)) {
                        if (firstChange) {
                            reportFile.print("Change in exceptions: ");
                            firstChange = false;
                        }
                        if (numAdded != 0)
                            reportFile.println(", ");    
                        else
                            reportFile.println(" ");
                        emitException(newException);
                        numAdded++;
                    }
                }
                if (numAdded == 1)
                    reportFile.print(" was added");
                else if (numAdded > 1)
                    reportFile.print(" were added");
                else if (numAdded == 0 && numRemoved == 0 && firstChange)
                    reportFile.print("Exceptions were reordered");
                reportFile.println(".<br>");
            }
            // Note the changes between a comma-separated list of Strings
            hasContent = true;
        }

        if (memberDiff.documentationChange_ != null) {
            if (hasContent)
                reportFile.print(" "); 
            reportFile.print(memberDiff.documentationChange_);
            hasContent = true;
        }
            
        // Last, so no need for a <br>
        if (memberDiff.modifiersChange_ != null) {
            if (hasContent)
                reportFile.print(" "); 
            reportFile.println(memberDiff.modifiersChange_);
            hasContent = true;
        }
        reportFile.println("  </TD>");
    }

    /** 
     * Emit a string which is an exception by surrounding it with 
     * &lt;code&gt; tags.
     * If there is a space in the type, e.g. &quot;String, File&quot;, then
     * surround it with parentheses too. Do not add &lt;code&gt; tags or 
     * parentheses if the String is "no exceptions".
     */
    public void emitException(String ex) {
        if (ex.compareTo("no exceptions") == 0) {
            reportFile.print(ex);
        } else {
            if (ex.indexOf(' ') != -1) {
                reportFile.print("(<code>" + ex + "</code>)");
            } else {
                reportFile.print("<code>" + ex + "</code>");
            }
        }
    }

    /** 
     * Emit a string which is a type by surrounding it with &lt;code&gt; tags.
     * If there is a space in the type, e.g. &quot;String, File&quot;, then
     * surround it with parentheses too.
     */
    public void emitType(String type) {
        if (type.compareTo("") == 0)
            return;
        if (type.indexOf(' ') != -1) {
            reportFile.print("(<code>" + type + "</code>)");
        } else {
            reportFile.print("<code>" + type + "</code>");
        }
    }

    /** 
     * Emit a string which is a type by surrounding it with &lt;code&gt; tags.
     * Also surround it with parentheses too. Used to display methods' 
     * parameters.
     * Suggestions for where a browser should break the 
     * text are provided with &lt;br> and &ltnobr> tags.
     */
    public static void emitTypeWithParens(String type) {
        emitTypeWithParens(type, true);
    }

    /** 
     * Emit a string which is a type by surrounding it with &lt;code&gt; tags.
     * Also surround it with parentheses too. Used to display methods' 
     * parameters.
     */
    public static void emitTypeWithParens(String type, boolean addBreaks) {
        if (type.compareTo("") == 0)
            reportFile.print("()");
        else {
            int idx = type.indexOf(", ");
            if (!addBreaks || idx == -1) {
                reportFile.print("(<code>" + type + "</code>)");
            } else {
                // Make the browser break text at reasonable places
                String sepType = null;
                StringTokenizer st = new StringTokenizer(type, ", ");
                while (st.hasMoreTokens()) {
                    String p = st.nextToken();
                    if (sepType == null)
                        sepType = p;
                    else
                        sepType += ",</nobr> " + p + "<nobr>";
                }
                reportFile.print("(<code>" + sepType + "<nobr></code>)");
            }
        }
    }

    /**
     * Emit a string which is a type by surrounding it with &lt;code&gt; tags.
     * Do not surround it with parentheses. Used to display methods' return
     * types and field types.
     */
    public static void emitTypeWithNoParens(String type) {
        if (type.compareTo("") != 0)
            reportFile.print("<code>" + type + "</code>");
    }

    /** 
     * Return a String with the simple names of the classes in fqName.
     * &quot;java.lang.String&quot; becomes &quot;String&quot;, 
     * &quotjava.lang.String, java.io.File&quot becomes &quotString, File&quot;
     * and so on. If fqName is null, return null. If fqName is &quot;&quot;, 
     * return &quot;&quot;. 
     */
    public static String simpleName(String fqNames) {
        if (fqNames == null)
            return null;
        String res = "";
        boolean hasContent = false;
        // We parse the string step by step to ensure we take
        // fqNames that contains generics parameter in a whole.
        ArrayList<String> fqNamesList = new ArrayList<String>();
        int genericParametersDepth = 0;
        StringBuffer buffer = new StringBuffer();
        for (int i=0; i<fqNames.length(); i++) {
          char c = fqNames.charAt(i);
          if ('<' == c) {
            genericParametersDepth++;
          }
          if ('>' == c) {
            genericParametersDepth--;
          }
          if (',' != c || genericParametersDepth > 0) {
            buffer.append(c);
          } else if (',' == c) {
            fqNamesList.add(buffer.toString().trim());
            buffer = new StringBuffer(buffer.length());
          }
        }
        fqNamesList.add(buffer.toString().trim());
        for (String fqName : fqNamesList) {
            // Assume this will be used inside a <nobr> </nobr> set of tags.
            if (hasContent)
                res += ", "; 
            hasContent = true;
            // Look for text within '<' and '>' in case this is a invocation of a generic
            
            int firstBracket = fqName.indexOf('<');
            int lastBracket = fqName.lastIndexOf('>');
            String genericParameter = null;
            if (firstBracket != -1 && lastBracket != -1) {
              genericParameter = simpleName(fqName.substring(firstBracket + 1, lastBracket));
              fqName = fqName.substring(0, firstBracket);              
            }
            
            int lastDot = fqName.lastIndexOf('.');
            if (lastDot < 0) {
                res += fqName; // Already as simple as possible
            } else {
                res += fqName.substring(lastDot+1);
            }
            if (genericParameter != null)
              res += "&lt;" + genericParameter + "&gt;";            
        }
        return res;
    }

    /** 
     * Find any existing comment and emit it. Add the new comment to the
     * list of new comments. The first instance of the string "@first" in 
     * a hand-written comment will be replaced by the first sentence from 
     * the associated doc block, if such exists. Also replace @link by
     * an HTML link.
     *
     * @param commentID The identifier for this comment.
     * @param possibleComment A possible comment from another source.
     * @param linkType 0 = remove, 1 = add, 2 = change
     */
    public void emitComment(String commentID, String possibleComment, 
                            int linkType) {
        if (noCommentsOnRemovals && linkType == 0) {
            reportFile.println("  <TD>&nbsp;</TD>");
            return;
        }
        if (noCommentsOnAdditions && linkType == 1) {
            reportFile.println("  <TD>&nbsp;</TD>");
            return;
        }
        if (noCommentsOnChanges && linkType == 2) {
            reportFile.println("  <TD>&nbsp;</TD>");
            return;
        }

        // We have to use this global hash table because the *Diff classes
        // do not store the possible comment from the new *API object.
        if (!noCommentsOnChanges && possibleComment == null) {
            possibleComment = (String)Comments.allPossibleComments.get(commentID);
        }
        // Just use the first sentence of the possible comment.
        if (possibleComment != null) {
            int fsidx = RootDocToXML.endOfFirstSentence(possibleComment, false);
            if (fsidx != -1 && fsidx != 0)
                possibleComment = possibleComment.substring(0, fsidx+1);
        }

        String comment = Comments.getComment(existingComments_, commentID);
        if (comment.compareTo(Comments.placeHolderText) == 0) {
            if (possibleComment != null && 
                possibleComment.indexOf("InsertOtherCommentsHere") == -1)
                reportFile.println("  <TD VALIGN=\"TOP\">" + possibleComment + "</TD>");
            else
                reportFile.println("  <TD>&nbsp;</TD>");
        } else {
            int idx = comment.indexOf("@first");
            if (idx == -1) {
                reportFile.println("  <TD VALIGN=\"TOP\">" + Comments.convertAtLinks(comment, "", null, null) + "</TD>");
            } else {
                reportFile.print("  <TD VALIGN=\"TOP\">" + comment.substring(0, idx));
                if (possibleComment != null && 
                    possibleComment.indexOf("InsertOtherCommentsHere") == -1)
                    reportFile.print(possibleComment);
                reportFile.println(comment.substring(idx + 6) + "</TD>");
            }
        }
        SingleComment newComment = new SingleComment(commentID, comment);
        newComments_.addComment(newComment);
    }
    
    /** Write the end of a table. */
    public void writeTableEnd() {
        reportFile.println("</TABLE>");
        reportFile.println("&nbsp;");
    } 

    /** Write a newline out. */
    public void writeText() {
        reportFile.println();
    } 

    /** Write some text out. */
    public void writeText(String text) {
        reportFile.println(text);
    } 

    /** Emit some non-breaking space for indentation. */
    public void indent(int indent) {
        for (int i = 0; i < indent; i++)
            reportFile.print("&nbsp;");
    } 

    /** 
     * The name of the file to which the top-level HTML file is written, 
     * and also the name of the subdirectory where most of the HTML appears,
     * and also a prefix for the names of some of the files in that 
     * subdirectory.
     */
    static String reportFileName = "changes";
    
    /** 
     * The suffix of the file to which the HTML output is currently being 
     * written. 
     */
    static String reportFileExt = ".html";
    
    /** 
     * The file to which the HTML output is currently being written. 
     */
    static PrintWriter reportFile = null;

    /** 
     * The object which represents the top of the tree of differences
     * between two APIs. It is only used indirectly when emitting a
     * navigation bar.
     */
    static APIDiff apiDiff = null;

    /** 
     * If set, then do not suggest comments for removals from the first 
     * sentence of the doc block of the old API. 
     */
    public static boolean noCommentsOnRemovals = false;

    /** 
     * If set, then do not suggest comments for additions from the first 
     * sentence of the doc block of the new API. 
     */
    public static boolean noCommentsOnAdditions = false;

    /** 
     * If set, then do not suggest comments for changes from the first 
     * sentence of the doc block of the new API. 
     */
    public static boolean noCommentsOnChanges = false;

    /**
     * If set, then report changes in documentation (Javadoc comments)
     * between the old and the new API. The default is that this is not set.
     */
    public static boolean reportDocChanges = false;

    /**
     * Define the prefix for HTML links to the existing set of Javadoc-
     * generated documentation for the new API. E.g. For J2SE1.3.x, use
     * "https://java.sun.com/j2se/1.3/docs/api/"
     */
    public static String newDocPrefix = "../";

    /**
     * Define the prefix for HTML links to the existing set of Javadoc-
     * generated documentation for the old API.
     */
    public static String oldDocPrefix = null;

    /** To generate statistical output, set this to true. */
    public static boolean doStats = false;

    /** 
     * The destination directory for output files.
     */
    public static String outputDir = null;
    
    /** 
     * The destination directory for comments files (if not specified, uses outputDir)
     */
    public static String commentsDir = null;

    /** 
     * The title used on the first page of the report. By default, this is 
     * &quot;API Differences Between &lt;name of old API&gt; and 
     * &lt;name of new API&gt;&quot;. It can be
     * set by using the -doctitle option.
     */
    public static String docTitle = null;

    /** 
     * The browser window title for the report. By default, this is 
     * &quot;API Differences Between &lt;name of old API&gt; and 
     * &lt;name of new API&gt;&quot;. It can be
     * set by using the -windowtitle option.
     */
    public static String windowTitle = null;

    /** The desired background color for JDiff tables. */
    static final String bgcolor = "#FFFFFF";

    /** Set to enable debugging output. */
    private static final boolean trace = false;

}
