/*
 * Copyright 2008 Google Inc.
 *
 * 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 com.google.gwt.soyc;

import com.google.gwt.core.ext.linker.CompilationMetricsArtifact;
import com.google.gwt.core.ext.linker.ModuleMetricsArtifact;
import com.google.gwt.core.ext.linker.PrecompilationMetricsArtifact;
import com.google.gwt.core.ext.soyc.impl.SizeMapRecorder;
import com.google.gwt.soyc.io.OutputDirectory;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.hash.Hashing;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A utility to make all the HTML files for one permutation.
 */
public class MakeTopLevelHtmlForPerm {
  /**
   * A dependency linker for the initial code download. It links to the
   * dependencies for the initial download.
   */
  public class DependencyLinkerForInitialCode implements DependencyLinker {
    @Override
    public String dependencyLinkForClass(String className) {
      String packageName = globalInformation.getClassToPackage().get(className);
      assert packageName != null;
      return dependenciesFileName("initial") + "#" + className;
    }
  }

  /**
   * A dependency linker for the leftovers fragment. It links to leftovers
   * status pages.
   */
  public class DependencyLinkerForLeftoversFragment implements DependencyLinker {
    @Override
    public String dependencyLinkForClass(String className) {
      return leftoversStatusFileName() + "#"
          + hashedFilenameFragment(className);
    }
  }

  /**
   * A dependency linker for the total program breakdown. It links to a split
   * status page.
   *
   */
  public class DependencyLinkerForTotalBreakdown implements DependencyLinker {
    @Override
    public String dependencyLinkForClass(String className) {
      return splitStatusFileName() + "#" + hashedFilenameFragment(className);
    }
  }

  /**
   * A dependency linker that never links to anything.
   */
  public static class NullDependencyLinker implements DependencyLinker {
    @Override
    public String dependencyLinkForClass(String className) {
      return null;
    }
  }

  interface DependencyLinker {
    String dependencyLinkForClass(String className);
  }

  /**
   * Use this class to intern strings to save space in the generated HTML. After
   * populating the map, call getJs to create a JS array of all possible methods
   * in this report.
   */
  @SuppressWarnings("serial")
  private class HtmlInterner {
    // Hashes the interned string to the number of times this string is referenced.
    Map<String, Integer> builder = new HashMap<String, Integer>();
    // Hashes the interned string to its position in the final array of interned strings.
    // Populated after the call to {@link #freeze()}
    Map<String, Integer> frozen = null;

    /**
     * Call this method after all calls to {@link #intern(String)} are complete.
     * This routine then re-orders the interned calls in order of the number of
     * times each string was referenced by intern() so that lower numbered index
     * values represent more frequently referenced strings.
     */
    public void freeze() {
      final int maxDigits = 9;
      assert (frozen == null);
      assert (builder.size() < Math.pow(10, maxDigits));

      // order the interned values with the most referenced first.
      String[] temp = new String[builder.size()];
      int index = 0;
      for (String key : builder.keySet()) {
        temp[index++] = key.format("%0" + maxDigits + "d%s", builder.get(key), key);
      }
      builder = null;
      Arrays.sort(temp);

      // strip off the numeric prefix on the key to build the frozen hash table
      index = 0;
      frozen = new LinkedHashMap<String, Integer>();
      for (int i = temp.length - 1; i >= 0; i--) {
        frozen.put(temp[i].substring(maxDigits), index++);
      }
    }

    /**
     * Stores a string for later interning. Keeps track of the number of times a
     * particular string is interned which will be used by {@link #freeze()} to
     * place the most frequently used strings at the beginning of the
     * dictionary. After a call to {@link #freeze()}, it is no longer valid to
     * call this method.
     *
     * @param key string to be added to the intern dictionary.
     */
    public void intern(String key) {
      if (builder == null) {
        throw new RuntimeException("freeze() already called.");
      }
      if (!builder.containsKey(key)) {
        builder.put(key, 1);
      } else {
        int value = builder.get(key) + 1;
        builder.put(key, value);
      }
    }

    /**
     * Displays a link for a split point that contains this code.
     */
    public void printHasCodeInSplitPoint(PrintWriter outFile, String className,
        int sp) {
      outFile.print("h(" + frozen.get(getPackageSubstring(className)) + ","
          + frozen.get(getClassSubstring(className)) + "," + sp + ");");
    }

    /**
     * Non specific message that there is code in an initial fragment.
     */
    public void printHasInitialFragment(PrintWriter outFile) {
      outFile.print("f();");
    }

    /**
     * Displays a link for code in an initial fragment.
     */
    public void printHasInitialFragment(PrintWriter outFile, String className) {
      String packageName = getPackageSubstring(className);
      outFile.print("g(" + frozen.get(packageName) + ","
          + frozen.get(getClassSubstring(className)) + ","
          + frozen.get(hashedFilenameFragment(packageName)) + ");");
    }

    public void printSomeCodeLeftover(PrintWriter outFile) {
      outFile.print("i();");
    }

    /**
     * Prints an h3 element with the class name and an anchor.
     */
    private void printClassHeader(PrintWriter outFile, String className) {
      outFile.print("e(" + frozen.get(getPackageSubstring(className)) + ","
          + frozen.get(getClassSubstring(className)) + ",'"
          + hashedFilenameFragment(className) + "');");
    }

    /**
     * Print out a single class dependency stack in the methodDependencies
     * report.
     */
    private void printDependency(PrintWriter outFile,
        Map<String, String> dependencies, String method, String depMethod) {
      String nameArray = methodDependencyString(method);
      outFile.print("b(" + nameArray + ",");
      outFile.print("[");
      Set<String> visited = Sets.newHashSet();
      String separator = "";
      while (depMethod != null) {
        String nextDep = dependencies.get(depMethod);
        // The bottom of the stack frame is not interesting.
        if (nextDep != null) {
          if (!visited.add(nextDep)) {
            break;
          }
          outFile.print(separator);
          outFile.print(methodDependencyString(depMethod));
          separator = ",";
        }
        depMethod = nextDep;
      }
      outFile.print("]);");
    }

    private String methodDependencyString(String method) {
      return "[" + frozen.get(getPackageSubstring(method)) + ","
          + frozen.get(getClassSubstring(method)) + ","
          + frozen.get(getMethodSubstring(method)) + "]";
    }

    /**
     * Prints out a class header for the methodDependendies report.
     */
    private void printDependencyClassHeader(PrintWriter outFile,
        String className) {
      outFile.print("a(" + frozen.get(getPackageSubstring(className)) + ","
          + frozen.get(getClassSubstring(className)) + ");");
    }

    /**
     * Prints a JavaScript snippet that includes the dictionary of interned
     * strings and methods to use interned strings to create lines in the
     * report.
     *
     * Call this method after invoking {@link #freeze()}.
     *
     * @param outFile open file to write the data to.
     */
    private void printInternedDataAsJs(PrintWriter outFile) {
      if (frozen == null) {
        throw new RuntimeException("freeze() not called.");
      }
      outFile.println("  var internedStrings = [");
      for (String key : frozen.keySet()) {
        outFile.print("\"" + key + "\",");
      }
      outFile.println("];");

      // array of split point descriptions
      outFile.println("  var spl = [");
      for (int fragment = 1; fragment <= globalInformation.getNumFragments(); fragment++) {
        final List<String> fragmentDescriptors = globalInformation.getFragmentDescriptors(fragment);
        String[] escapedFragmentDescriptors =
            new String[fragmentDescriptors.size()];
        for (int i = 0; i < fragmentDescriptors.size(); i++) {
          escapedFragmentDescriptors[i] =
              escapeJSString(fragmentDescriptors.get(i));
        }
        outFile.println("        '" + Joiner.on(",").join(escapedFragmentDescriptors) + "',");
      }
      outFile.println("  ];");

      // object/dictionary containing method sizes
      outFile.println("  var methodSizes = {");
      for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
        for (Entry<String, Integer> methodEntry : breakdown.methodToSize.entrySet()) {
          String methodSignature = methodEntry.getKey();
          String method = methodSignature.substring(0, methodSignature.indexOf("("));
          outFile.println("    \"" + method + "\" : " + methodEntry.getValue() + ",");
        }
      }
      outFile.println("};");

      // dropdown button image srcs
      outFile.println("  var images = {");
      outFile.println("    \"closed\" : \"images/play-g16.png\",");
      outFile.println("    \"open\" : \"images/play-g16-down.png\",");
      outFile.println("  };");

      // TODO(zundel): Most of this below is just inserting a fixed string into the code. It would
      // be easier to read and maintain if we could store the fixed part in a flat file. Use some
      // kind of HTML template?

      // function to print a class header in the methodDependencies report
      // see printDependencyClassHeader()
      outFile.println("  function a(packageRef, classRef) {");
      outFile.println("    var className = internedStrings[packageRef] + \".\" + "
          + "internedStrings[classRef];");
      outFile.println("    document.write(\"<div class='main'>\");");
      outFile.println("    document.write(\"<table class='soyc-table'>\");");
      outFile.println("    document.write(\"<thead>\");");
      outFile.println("    document.write(\"<th><a class='soyc-class-name' "
          + "name='\" + className + \"'>"
          + "Class: \" + className + \"</a></th>\");");
      outFile.println("    document.write(\"<th class='soyc-numerical-col-header'>Size "
          + "<span class='soyc-th-units'>(bytes)</span></th>\");");
      outFile.println("    document.write(\"</thead>\");");
      outFile.println("  }");

      outFile.println("  function swapShowHide(elementName) {");
      outFile.println("    hp = document.getElementById(elementName);");
      outFile.println("    arrow = document.getElementById(\"dropdown-\" + elementName);");
      outFile.println("    if (hp.style.display !== \"none\" && hp.style.display "
          + "!== \"inline\") {");
      outFile.println("      hp.style.display = \"inline\";");
      outFile.println("      arrow.src = images[\"open\"];");
      outFile.println("    } else if (hp.style.display === \"none\") {");
      outFile.println("      hp.style.display = \"inline\";");
      outFile.println("      arrow.src = images[\"open\"];");
      outFile.println("    } else {");
      outFile.println("      hp.style.display = \"none\";");
      outFile.println("      arrow.src = images[\"closed\"];");
      outFile.println("    }");
      outFile.println("  }");

      // function to print a single dependency in the methodDependencies report
      // see printDependency()
      outFile.println("  function b(c, deps) {");
      outFile.println("    var methodName = internedStrings[c[0]] + \".\" + internedStrings[c[1]] "
          + "+ \"::\" + internedStrings[c[2]];");
      outFile.println("    var methodSize = methodSizes[methodName];");
      outFile.println("    if (methodSize === undefined) methodSize = \"--\";");
      outFile.println("    var callstackId = \"callstack-\" + methodName;");
      outFile.println("    document.write(\"<tr>\");");
      outFile.println("    document.write(\"<td>\");");
      outFile.println("    document.write(\"<img onclick='swapShowHide(\\\"\" + "
          + "callstackId + \"\\\")'"
          + "id='dropdown-\" + callstackId + \"' "
          + "class='dropdown-img' "
          + "src=\" + images[\"closed\"] + \">\");");
      outFile.println("    document.write(\"<a class='toggle soyc-call-stack-link' "
          + "onclick='swapShowHide(\\\"\" + callstackId + \"\\\")'>\" + methodName + \"</a>\");");
      outFile.println("    document.write(\"<ul id='\" + callstackId + \"' "
          + "class='soyc-call-stack-list'>\");");
      outFile.println("    for (var i = 0; i < deps.length ; i++) {");
      outFile.println("      var s = deps[i];");
      outFile.println("      document.write(\"<li>\" + internedStrings[s[0]] + \".\" + "
          + "internedStrings[s[1]] + \"::\" + internedStrings[s[2]] + \"</li>\");");
      outFile.println("    }");
      outFile.println("    document.write(\"</ul>\");");
      outFile.println("    document.write(\"</td>\");");
      outFile.println("    document.write(\"<td class='soyc-numerical-col'>\" + "
          + "methodSize + \"</td>\");");
      outFile.println("    document.write(\"</tr>\");");
      outFile.println("  }");

      // follows all method dependency stacks
      outFile.println("  function j() {");
      outFile.println("    document.write(\"</table></div>\");");
      outFile.println("  }");

      // leftovers status line
      outFile.println("  function c(packageRef,classRef,packageHashRef) {");
      outFile.println("    var packageName = internedStrings[packageRef];");
      outFile.println("    var className = packageName + \".\" + internedStrings[classRef];");
      outFile.println("    var d1 = 'methodDependencies-total-" + getPermutationId() + ".html';");
      outFile.println("    document.write(\"<ul class='soyc-excl'>\");");
      outFile.println("    document.write(\"<li><a href='\" + d1 + \"#\" + className + \"'>"
          + "See why it's live</a></li>\");");
      outFile.println("    for (var sp = 1; sp <= "
          + globalInformation.getNumFragments() + "; sp++) {");
      outFile.println("      var d2 = 'methodDependencies-sp' + sp + '-"
          + getPermutationId() + ".html';");
      outFile.println("      document.write(\"<li><a href='\" + d2 + \"#\" + className +\"'>"
          + " See why it's not exclusive to s.p. #\" + sp + \" (\" + spl[sp - 1] + \")"
          + "</a></li>\");");
      outFile.println("    }");
      outFile.println("    document.write(\"</ul>\");");
      outFile.println("  }");

      // leftovers status package header line
      outFile.println("  function d(packageRef) {");
      outFile.println("    document.write(\"<div class='soyc-pkg-break'>Package: \" + "
          + "internedStrings[packageRef] + \"</div>\");");
      outFile.println("  }");

      // leftovers status class header line
      outFile.println("  function e(packageRef,classRef,classHashRef) {");
      outFile.println("    document.write(\"<a name='\" + classHashRef + \"'></a><h3>\" + "
          + "internedStrings[packageRef] + \".\" + internedStrings[classRef] + \"</h3>\");");
      outFile.println("  }");

      // split point has a class with code in the initial fragment - no link
      outFile.println("  function f() {");
      outFile.println("    document.write(\"<p>Some code is included in the initial fragment"
          + "</p>\");");
      outFile.println("  }");

      // split point has a class with code in the initial fragment
      outFile.println("  function g(packageRef, classRef, packageHashRef) {");
      outFile.println("    document.write(\"<p>Some code is included in the initial fragment "
          + "(<a href='methodDependencies-initial-\" + internedStrings[packageHashRef] + \"-"
          + getPermutationId()
          + ".html#\" + internedStrings[packageRef] + \".\" + "
          + "internedStrings[classRef] + \"'> See why</a>)</p>\");");
      outFile.println("  }");

      // split point has code from class
      outFile.println("  function h(packageRef, classRef, sp) {");
      outFile.println("    document.write(\"<p>Some code downloads with split point \" + sp + "
          + "\": \" + spl[sp - 1] + \"</p>\");");
      outFile.println("  }");

      // some code is left over
      outFile.println("  function i() {");
      outFile.println("    document.write(\"<p>Some code is left over:</p>\");");
      outFile.println("  }");
    }

    /**
     * Prints links to each split point showing why a leftover fragment isn't
     * exclusive.
     */
    private void printLeftoversStatus(PrintWriter outFile, String packageName,
        String className) {
      outFile.println("c(" + frozen.get(packageName) + ","
          + frozen.get(getClassSubstring(className)) + ","
          + frozen.get(hashedFilenameFragment(packageName)) + ");");
    }

    /**
     * Prints a div containing the package name in a blue block.
     */
    private void printPackageHeader(PrintWriter outFile, String packageName) {
      outFile.print("d(" + frozen.get(packageName) + ");");
    }
  }

  /**
   * By a convention shared with the compiler, the initial download is fragment
   * number 0.
   */
  private static final int FRAGMENT_NUMBER_INITIAL_DOWNLOAD = 0;

  /**
   * Just within this file, the convention is that the total program is fragment
   * number -1.
   */
  private static final int FRAGMENT_NUMBER_TOTAL_PROGRAM = -1;

  /**
   * A pattern describing the name of dependency graphs for code fragments
   * corresponding to a specific split point. These can be either exclusive
   * fragments or fragments of code for split points in the initial load
   * sequence.
   */
  private static final Pattern PATTERN_SP_INT = Pattern.compile("sp([0-9]+)");

  public static void makeTopLevelHtmlForAllPerms(
      Map<String, List<String>> allPermsInfo, OutputDirectory outDir)
      throws IOException {
    PrintWriter outFile = new PrintWriter(outDir.getOutputStream("index.html"));
    addStandardHtmlProlog(outFile, "Compile report", "Compile report",
        "Overview of permutations");
    outFile.println("<ul>");

    // in order to print these in ascending order, we have to sort by
    // integers
    SortedSet<Integer> sortedPermIds = new TreeSet<Integer>();
    for (String permutationId : allPermsInfo.keySet()) {
      sortedPermIds.add(Integer.parseInt(permutationId));
    }

    for (Integer sortedPermId : sortedPermIds) {
      String permutationId = Integer.toString(sortedPermId);
      List<String> permutationInfoList = allPermsInfo.get(permutationId);
      outFile.print("<li>Permutation " + permutationId);

      for (String desc : permutationInfoList) {
        outFile.println("  (" + desc + ")");
      }
      outFile.println("<ul>");
      outFile.println("<li>");
      outFile.println("<a href=\"SoycDashboard-" + permutationId
          + "-index.html\">Split Point Report</a>");
      outFile.println("</li>");
      outFile.println("<li>");
      outFile.println("<a href=\"CompilerMetrics-" + permutationId
          + "-index.html\">Compiler Metrics</a>");
      outFile.println("</li>");

      outFile.println("</ul>");
      outFile.println("</li>");
    }
    outFile.println("</ul>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  /**
   * @return given "com.foo.myClass" or "com.foo.myClass::myMethod" returns
   *         "myClass"
   */
  static String getClassSubstring(String fullMethodName) {
    if (fullMethodName.length() == 0) {
      return "";
    }
    int startIndex = getPackageSubstring(fullMethodName).length() + 1;
    int endIndex = fullMethodName.indexOf("::");
    if (endIndex == -1) {
      endIndex = fullMethodName.length();
    }
    if (startIndex > endIndex || startIndex > fullMethodName.length()) {
      return "";
    }
    return fullMethodName.substring(startIndex, endIndex);
  }

  /**
   * @return given "com.foo.myClass::myMethod" returns "myMethod"
   */
  static String getMethodSubstring(String fullMethodName) {
    int index = fullMethodName.indexOf("::");
    if (index == -1) {
      return "";
    }
    index += 2;
    if (index >= fullMethodName.length()) {
      return "";
    }
    return fullMethodName.substring(index);
  }

  /**
   * @return given "com.foo.myClass" or "com.foo.myClass::myMethod" returns
   *         "com.foo"
   */
  static String getPackageSubstring(String fullMethodName) {
    int endIndex = fullMethodName.lastIndexOf('.');
    if (endIndex == -1) {
      endIndex = fullMethodName.length();
    }
    return fullMethodName.substring(0, endIndex);
  }

  private static void addSmallHtmlProlog(final PrintWriter outFile, String title) {
    outFile.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
    outFile.println("\"http://www.w3.org/TR/html4/strict.dtd\">");
    outFile.println("<html>");
    outFile.println("<head>");
    outFile.println("<title>");
    outFile.println(title);
    outFile.println("</title>");
    outFile.println("<style type=\"text/css\" media=\"screen\">");
    outFile.println("@import url('goog.css');");
    outFile.println("@import url('inlay.css');");
    outFile.println("@import url('soyc.css');");
    outFile.println("</style>");
    outFile.println("</head>");
  }

  private static void addStandardHtmlEnding(final PrintWriter out) {
    out.println("</div>");
    out.println("</body>");
    out.println("</html>");
  }

  private static void addStandardHtmlProlog(final PrintWriter outFile,
      String title, String header1, String header2) {
    outFile.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
    outFile.println("\"http://www.w3.org/TR/html4/strict.dtd\">");
    outFile.println("<html>");
    outFile.println("<head>");
    outFile.println("<script type=\"text/javascript\">");
    outFile.println("function show(elementName)");
    outFile.println("{");
    outFile.println("hp = document.getElementById(elementName);");
    outFile.println("hp.style.visibility = \"Visible\";");
    outFile.println("}");
    outFile.println("function hide(elementName)");
    outFile.println("{");
    outFile.println("hp = document.getElementById(elementName);");
    outFile.println("hp.style.visibility = \"Hidden\";");
    outFile.println("}");
    outFile.println("</script>");
    outFile.println("<title>");
    outFile.println(title);
    outFile.println("</title>");
    outFile.println("<style type=\"text/css\" media=\"screen\">");
    outFile.println("@import url('goog.css');");
    outFile.println("@import url('inlay.css');");
    outFile.println("@import url('soyc.css');");
    outFile.println("</style>");
    outFile.println("</head>");
    outFile.println("<body>");
    outFile.println("<div class=\"g-doc\">");
    outFile.println("<div id=\"hd\" class=\"g-section g-tpl-50-50 g-split\">");
    outFile.println("<div class=\"g-unit g-first\">");
    outFile.println("<p>");
    outFile.println("<a href=\"index.html\" id=\"gwt-logo\" class=\"soyc-ir\">");
    outFile.println("<span>Google Web Toolkit</span>");
    outFile.println("</a>");
    outFile.println("</p>");
    outFile.println("</div>");
    outFile.println("<div class=\"g-unit\">");
    outFile.println("</div>");
    outFile.println("</div>");
    outFile.println("<div id=\"soyc-appbar-lrg\">");
    outFile.println("<div class=\"g-section g-tpl-75-25 g-split\">");
    outFile.println("<div class=\"g-unit g-first\">");
    outFile.println("<h1>" + header1 + "</h1>");
    outFile.println("</div>");
    outFile.println("<div class=\"g-unit\"></div>");
    outFile.println("</div>");
    outFile.println("</div>");
    outFile.println("<div id=\"bd\">");
    if (header2 != null && header2.length() > 0) {
      outFile.println("<h2>" + header2 + "</h2>");
    }
  }

  private static String classesInPackageFileName(SizeBreakdown breakdown,
      String permutationId) {
    return breakdown.getId() + "_" + permutationId + "_Classes.html";
  }

  private String escapeJSString(String str) {
    // TODO(rluble): make into a Util routine.
    return str.replaceAll("/", "\\/");
  }

  private static String escapeXml(String unescaped) {
    // TODO(rluble): see why SizeMapRecorder.escapeXML is different from Util.escapeXML. If two
    // different versions are needed at all move SizeMapRecorder.escapeXML to Util.
    return SizeMapRecorder.escapeXml(unescaped);
  }

  /**
   * Convert a potentially long string into a short file name. The current
   * implementation simply hashes the long name.
   */
  private static String hashedFilenameFragment(String longFileName) {
    return Hashing.murmur3_128().hashString(longFileName, StandardCharsets.UTF_8).toString();
  }

  private static String headerLineForBreakdown(SizeBreakdown breakdown) {
    return "(Analyzing code subset: " + breakdown.getDescription() + ")";
  }

  private static String shellFileName(SizeBreakdown breakdown,
      String permutationId) {
    return breakdown.getId() + "-" + permutationId + "-overallBreakdown.html";
  }

  /**
   * Global information for this permutation.
   */
  private final GlobalInformation globalInformation;

  private final OutputDirectory outDir;

  MakeTopLevelHtmlForPerm(GlobalInformation globalInformation,
      OutputDirectory outDir) {
    this.globalInformation = globalInformation;
    this.outDir = outDir;
  }

  public void makeBreakdownShell(SizeBreakdown breakdown) throws IOException {
    Map<String, CodeCollection> nameToCodeColl = breakdown.nameToCodeColl;
    Map<String, LiteralsCollection> nameToLitColl = breakdown.nameToLitColl;
    String packageBreakdownFileName = makePackageHtml(breakdown);
    String codeTypeBreakdownFileName = makeCodeTypeHtml(breakdown,
        nameToCodeColl, nameToLitColl);

    PrintWriter outFile = new PrintWriter(getOutFile(shellFileName(breakdown,
        getPermutationId())));

    addStandardHtmlProlog(outFile, "Application breakdown analysis",
        "Application breakdown analysis", "");

    String popupName = "packageBreakdownPopup";
    String popupTitle = "Package breakdown";
    String popupBody = "The package breakdown blames pieces of JavaScript "
        + "code on Java packages wherever possible.  Note that this is not possible for all "
        + "code, so the sizes of the packages here will not normally add up to the full code "
        + "size.  More specifically, the sum will exclude strings, whitespace, and a few pieces "
        + "of JavaScript code that are produced during compilation but cannot be attributed to "
        + "any Java package.";

    outFile.println("<h2>");
    addPopupLink(outFile, popupName, popupTitle, null);
    outFile.println("</h2></div>");
    addPopup(outFile, popupName, popupTitle, popupBody);
    outFile.println("<iframe class='soyc-iframe-package' src=\""
        + packageBreakdownFileName + "\" scrolling=auto></iframe>");

    popupName = "codeTypeBreakdownPopup";
    popupTitle = "Code Type Breakdown";
    popupBody = "The code type breakdown breaks down the JavaScript code according to its "
        + "type or function.  For example, it tells you how much of your code can be attributed to "
        + "JRE, GWT-RPC, etc.  As above, strings and some other JavaScript snippets are not "
        + "included in the breakdown.";
    outFile.println("<h2>");
    addPopupLink(outFile, popupName, popupTitle, null);
    outFile.println("</h2>");
    addPopup(outFile, popupName, popupTitle, popupBody);

    outFile.println("<iframe class='soyc-iframe-code' src=\""
        + codeTypeBreakdownFileName + "\" scrolling=auto></iframe>");
    outFile.println("</div>");

    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  public void makeCodeTypeClassesHtmls(SizeBreakdown breakdown)
      throws IOException {
    HashMap<String, CodeCollection> nameToCodeColl = breakdown.nameToCodeColl;

    for (String codeType : nameToCodeColl.keySet()) {

      // construct file name
      String outFileName = breakdown.getId() + "_" + codeType + "-"
          + getPermutationId() + "Classes.html";

      float sumSize = 0f;
      TreeMap<Integer, Set<String>> sortedClasses = new TreeMap<Integer, Set<String>>(
          Collections.reverseOrder());

      for (String className : nameToCodeColl.get(codeType).classes) {
        if (breakdown.classToSize.containsKey(className)) {
          int curSize = 0;
          if (breakdown.classToSize.containsKey(className)) {
            curSize = breakdown.classToSize.get(className);
          }
          if (curSize != 0) {
            if (sortedClasses.containsKey(curSize)) {
              Set<String> existingSet = sortedClasses.get(curSize);
              existingSet.add(className);
              sortedClasses.put(curSize, existingSet);
            } else {
              Set<String> newSet = new TreeSet<String>();
              newSet.add(className);
              sortedClasses.put(curSize, newSet);
            }
            sumSize += curSize;
          }
        }
      }

      final PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
      addStandardHtmlProlog(outFile, "Classes in package " + codeType,
          "Classes in package " + codeType, headerLineForBreakdown(breakdown));
      outFile.println("<table class=\"soyc-table\">");
      outFile.println("<colgroup>");
      outFile.println("<col id=\"soyc-splitpoint-type-col\">");
      outFile.println("<col id=\"soyc-splitpoint-size-col\">");
      outFile.println("</colgroup>");
      outFile.println("<thead>");
      outFile.println("<th>Code type</th>");
      outFile.println("<th>");
      outFile.println("<th class=\"soyc-numerical-col-header\">");
      outFile.println("Size <span class=\"soyc-th-units\">(Bytes)</span>");
      outFile.println("</th>");
      outFile.println("<th class=\"soyc-numerical-col-header\">% of total</th>");
      outFile.println("</thead>");

      NumberFormat bytesFormatter = NumberFormat.getInstance();
      bytesFormatter.setGroupingUsed(true);

      NumberFormat percentFormatter = NumberFormat.getPercentInstance();
      percentFormatter.setMinimumFractionDigits(1);
      percentFormatter.setMaximumFractionDigits(1);

      for (Integer size : sortedClasses.keySet()) {
        Set<String> classNames = sortedClasses.get(size);
        for (String className : classNames) {
          float perc = (float) size / sumSize;
          outFile.println("<tr>");
          outFile.println("<td>" + className + "</a></td>");
          outFile.println("<td class=\"soyc-bargraph-col\">");
          outFile.println("<div class=\"soyc-bar-graph goog-inline-block\">");
          outFile.println(getProgressBarHtml(perc));
          outFile.println("</div>");
          outFile.println("</td>");
          outFile.println("<td class=\"soyc-numerical-col\">");
          outFile.println(bytesFormatter.format(size));
          outFile.println("</td>");
          outFile.println("<td class=\"soyc-percent-col\">" +
              percentFormatter.format(perc) + "</td>");
          outFile.println("</tr>");
        }
      }
      addStandardHtmlEnding(outFile);
      outFile.close();
    }
  }

  public void makeCompilerMetricsPermFiles(ModuleMetricsArtifact moduleMetrics,
      PrecompilationMetricsArtifact precompilationMetrics,
      CompilationMetricsArtifact compilationMetrics) throws IOException {
    String outFileName = "CompilerMetrics-"
        + precompilationMetrics.getPermutationBase() + "-index.html";
    PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    String title = "Compiler Metrics for Permutation "
        + compilationMetrics.getPermutationId();
    addStandardHtmlProlog(outFile, title, title, "Build Time Metrics");

    NumberFormat elapsedFormatter = NumberFormat.getInstance();
    elapsedFormatter.setGroupingUsed(true);
    elapsedFormatter.setMinimumFractionDigits(3);
    elapsedFormatter.setMaximumFractionDigits(3);

    outFile.println("<div id=\"bd\">");
    int permutationId = compilationMetrics.getPermutationId();

    // Build Time Metrics
    outFile.println("<table class=\"soyc-table\">");
    outFile.println("<colgroup>");
    outFile.println("<col id=\"soyc-buildTimePhase-col\">");
    outFile.println("<col id=\"soyc-buildTimeElapsed-col\">");
    outFile.println("</colgroup>");
    outFile.println("<thead>");
    outFile.println("<th>Phase</th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">Elapsed Time</th>");
    outFile.println("</thead>");

    outFile.println("<tr>");
    outFile.println("<td>");
    outFile.println("Module Analysis");
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">"
        + elapsedFormatter.format(moduleMetrics.getElapsedMilliseconds() / 1000.0) + " s");
    outFile.println("</td>");
    outFile.println("</tr>");

    outFile.println("<tr>");
    outFile.println("<td>");
    outFile.println("Precompile (may include Module Analysis)");
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">"
        + elapsedFormatter.format(precompilationMetrics.getElapsedMilliseconds() / 1000.0)
        + " s");
    outFile.println("</td>");
    outFile.println("</tr>");

    outFile.println("<tr>");
    outFile.println("<td>");
    outFile.println("Compile");
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">"
        + elapsedFormatter.format(compilationMetrics.getElapsedMilliseconds() / 1000.0)
        + " s");
    outFile.println("</td>");
    outFile.println("</tr>");
    outFile.println("</table>");

    NumberFormat referencesFormatter = NumberFormat.getInstance();
    referencesFormatter.setGroupingUsed(true);

    outFile.println("<p></p>");
    outFile.println("<h2>Source/Type Metrics</h2>");

    outFile.println("<table class=\"soyc-table\">");
    outFile.println("<colgroup>");
    outFile.println("<col id=\"soyc-typeList-col\">");
    outFile.println("<col id=\"soyc-typeReferences-col\">");
    outFile.println("</colgroup>");
    outFile.println("<thead>");
    outFile.println("<th>Description</th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">References</th>");
    outFile.println("</thead>");

    String sourcesFileName = "CompilerMetrics-sources.html";
    outFile.println("<tr>");
    outFile.println("<td>");
    String popupName = "compilerMetricsSourceFiles";
    String popupTitle = "Source files";
    String popupBody = "All source files on the module source path.";
    addPopupLink(outFile, popupName, popupTitle, sourcesFileName);
    addPopup(outFile, popupName, popupTitle, popupBody);
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">");
    outFile.println(referencesFormatter.format(moduleMetrics.getSourceFiles().length));
    outFile.println("</td>");
    outFile.println("</tr>");
    makeCompilerMetricsSources(sourcesFileName, moduleMetrics, popupBody);

    String initialTypesFileName = "CompilerMetrics-initialTypes-"
        + permutationId + ".html";
    outFile.println("<tr>");
    outFile.println("<td>");
    popupName = "compilerMetricsInitialTypes";
    popupTitle = "Initial Type Oracle Types";
    popupBody = "All types in the type oracle after compiling sources on the source path.";
    addPopupLink(outFile, popupName, popupTitle, initialTypesFileName);
    addPopup(outFile, popupName, popupTitle, popupBody);
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">");
    outFile.println(referencesFormatter.format(moduleMetrics.getInitialTypes().length));
    outFile.println("</td>");
    outFile.println("</tr>");
    makeCompilerMetricsInitialTypeOracleTypes(initialTypesFileName,
        moduleMetrics, popupBody);

    String finalTypesFileName = "CompilerMetrics-finalTypes-" + permutationId
        + ".html";
    outFile.println("<tr>");
    outFile.println("<td>");
    popupName = "compilerMetricsFinalTypes";
    popupTitle = "Final Type Oracle Types";
    popupBody = "All types in the type oracle after constructing the Java AST.";
    addPopupLink(outFile, popupName, popupTitle, finalTypesFileName);
    addPopup(outFile, popupName, popupTitle, popupBody);
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">");
    outFile.println(referencesFormatter.format(
        precompilationMetrics.getFinalTypeOracleTypes().length));
    outFile.println("</td>");
    outFile.println("</tr>");
    makeCompilerMetricsFinalTypeOracleTypes(finalTypesFileName,
        precompilationMetrics, popupBody);

    String[] generatedTypes = getGeneratedTypes(moduleMetrics,
        precompilationMetrics);
    String generatedTypesFileName = "CompilerMetrics-generatedTypes-"
        + permutationId + ".html";
    outFile.println("<tr>");
    outFile.println("<td>");
    popupName = "compilerMetricsGeneratedTypes";
    popupTitle = "GeneratedTypes";
    popupBody = "Types that were added to the type oracle while running generators.";
    addPopupLink(outFile, popupName, popupTitle, generatedTypesFileName);
    addPopup(outFile, popupName, popupTitle, popupBody);
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">");
    outFile.println(referencesFormatter.format(generatedTypes.length));
    outFile.println("</td>");
    outFile.println("</tr>");
    makeCompilerMetricsGeneratedTypes(generatedTypesFileName, generatedTypes,
        popupBody);

    String astFileName = "CompilerMetrics-ast-" + permutationId + ".html";
    outFile.println("<tr>");
    outFile.println("<td>");
    popupName = "compilerMetricsAstTypes";
    popupTitle = "AST Referenced Types";
    popupBody = "All types referenced by the Java AST after performing "
        + "reachability analysis from the module EntryPoint.";
    addPopupLink(outFile, popupName, popupTitle, astFileName);
    addPopup(outFile, popupName, popupTitle, popupBody);
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">");
    outFile.println(referencesFormatter.format(precompilationMetrics.getAstTypes().length));
    outFile.println("</td>");
    outFile.println("</tr>");
    makeCompilerMetricsAstTypes(astFileName, precompilationMetrics, popupBody);

    String[] unreferencedTypes = getUnreferencedTypes(precompilationMetrics);
    String unreferencedFileName = "CompilerMetrics-unreferencedTypes-"
        + permutationId + ".html";
    outFile.println("<tr>");
    outFile.println("<td>");
    popupName = "compilerMetricsUnreferenceTypes";
    popupTitle = "Unreferenced Types";
    popupBody = "Types that were on the initial source path but never referenced in "
        + "the Java AST.";
    addPopupLink(outFile, popupName, popupTitle, unreferencedFileName);
    addPopup(outFile, popupName, popupTitle, popupBody);
    outFile.println("</td>");
    outFile.println("<td class=\"soyc-numerical-col\">");
    outFile.println(referencesFormatter.format(unreferencedTypes.length));
    outFile.println("</td>");
    outFile.println("</tr>");
    makeCompilerMetricsUnreferencedTypes(unreferencedFileName,
        unreferencedTypes, popupBody);
    outFile.println("</table>");

    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  public void makeDependenciesHtml() throws IOException {
    for (String depGraphName : globalInformation.dependencies.keySet()) {
      makeDependenciesHtml(depGraphName,
          globalInformation.dependencies.get(depGraphName));
    }
  }

  public void makeLeftoverStatusPages() throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(leftoversStatusFileName()));
    addStandardHtmlProlog(outFile, "Leftovers page", "Leftovers page", "");
    outFile.println("<div id=\"bd\">");
    outFile.println("<p>These classes have some leftover code, neither initial nor "
        + "exclusive to any split point:</p>");
    String curPackageName = "";
    HtmlInterner interner = new HtmlInterner();

    for (String className : globalInformation.getClassToPackage().keySet()) {
      String packageName = globalInformation.getClassToPackage().get(className);
      interner.intern(packageName);
      interner.intern(getClassSubstring(className));
      interner.intern(hashedFilenameFragment(packageName));
    }
    interner.freeze();

    outFile.println("<script language=\"javascript\">");
    interner.printInternedDataAsJs(outFile);

    for (String className : globalInformation.getClassToPackage().keySet()) {
      String packageName = globalInformation.getClassToPackage().get(className);
      if (packageName.compareTo("") == 0
          || packageName.compareTo(curPackageName) != 0) {
        curPackageName = packageName;
        interner.printPackageHeader(outFile, packageName);
      }
      interner.printClassHeader(outFile, className);
      if (globalInformation.dependencies != null) {
        interner.printLeftoversStatus(outFile, packageName, className);
      }
    }
    outFile.println("</script>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  public void makeLiteralsClassesTableHtmls(SizeBreakdown breakdown)
      throws IOException {
    Map<String, LiteralsCollection> nameToLitColl = breakdown.nameToLitColl;
    for (String literalType : nameToLitColl.keySet()) {
      String outFileName = literalType + "-" + getPermutationId() + "Lits.html";
      final PrintWriter outFile = new PrintWriter(getOutFile(breakdown.getId()
          + "_" + outFileName));
      addStandardHtmlProlog(outFile, "Literals of type " + literalType,
          "Literals of type " + literalType, headerLineForBreakdown(breakdown));
      outFile.println("<table width=\"80%\" style=\"font-size: 11pt;\" bgcolor=\"white\">");
      for (String literal : nameToLitColl.get(literalType).literals) {
        if (literal.trim().length() == 0) {
          literal = "[whitespace only string]";
        }
        String escliteral = escapeXml(literal);
        outFile.println("<tr>");
        outFile.println("<td>" + escliteral + "</td>");
        outFile.println("</tr>");
      }
      outFile.println("</table>");
      outFile.println("<center>");
      addStandardHtmlEnding(outFile);
      outFile.close();
    }
  }

  /**
   * Make size breakdowns for each package for one code collection.
   */
  public void makePackageClassesHtmls(SizeBreakdown breakdown,
      DependencyLinker depLinker) throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(classesInPackageFileName(
        breakdown, getPermutationId())));
    addStandardHtmlProlog(outFile, "Classes in  " + breakdown.getDescription(),
        "Classes in " + breakdown.getDescription(),
        headerLineForBreakdown(breakdown));

    String[] packageNames = globalInformation.getPackageToClasses().keySet().toArray(
        new String[0]);
    Arrays.sort(packageNames);
    for (String packageName : packageNames) {
      TreeMap<Integer, Set<String>> sortedClasses = new TreeMap<Integer, Set<String>>(
          Collections.reverseOrder());
      int sumSize = 0;
      for (String className : globalInformation.getPackageToClasses().get(
          packageName)) {
        int curSize = 0;
        if (!breakdown.classToSize.containsKey(className)) {
          // This class not present in this code collection
        } else {
          curSize = breakdown.classToSize.get(className);
        }
        if (curSize != 0f) {
          if (sortedClasses.containsKey(curSize)) {
            Set<String> existingSet = sortedClasses.get(curSize);
            existingSet.add(className);
            sortedClasses.put(curSize, existingSet);
          } else {
            Set<String> newSet = new TreeSet<String>();
            newSet.add(className);
            sortedClasses.put(curSize, newSet);
          }
          sumSize += curSize;
        }
      }

      if (sortedClasses.size() > 0) {
        outFile.println("<p>");
        outFile.println("<table class=\"soyc-table\">");
        outFile.print("<colgroup>");
        outFile.print("<col id=\"soyc-splitpoint-type-col\">");
        outFile.print("<col id=\"soyc-splitpoint-size-col\">");
        outFile.println("</colgroup>");
        outFile.print("<thead>");
        outFile.print("<a name=\"" + hashedFilenameFragment(packageName)
            + "\"></a><th>Package: " + packageName + "</th>");
        outFile.println("<th></th>");
        outFile.println("<th class=\"soyc-numerical-col-header\">");
        outFile.println("Size <span class=\"soyc-th-units\">(Bytes)</span>");
        outFile.println("</th>");
        outFile.println("<th class=\"soyc-numerical-col-header\">% of total</th>");
        outFile.print("</thead>");

        NumberFormat bytesFormatter = NumberFormat.getInstance();
        bytesFormatter.setGroupingUsed(true);

        NumberFormat percentFormatter = NumberFormat.getPercentInstance();
        percentFormatter.setMinimumFractionDigits(1);
        percentFormatter.setMaximumFractionDigits(1);

        for (Integer size : sortedClasses.keySet()) {
          Set<String> classNames = sortedClasses.get(size);
          for (String className : classNames) {
            String drillDownFileName = depLinker.dependencyLinkForClass(className);
            float perc = (float) size / (float) sumSize;
            outFile.println("<tr>");
            if (drillDownFileName == null) {
              outFile.println("<td>" + className + "</td>");
            } else {
              outFile.println("<td><a href=\"" + drillDownFileName
                  + "\" target=\"_top\">" + className + "</a></td>");
            }
            outFile.println("<td class=\"soyc-bargraph-col\">");
            outFile.println("<div class=\"soyc-bar-graph goog-inline-block\">");
            outFile.println(getProgressBarHtml(perc));
            outFile.println("</div>");
            outFile.println("</td>");
            outFile.println("<td class=\"soyc-numerical-col\">");
            outFile.println(bytesFormatter.format(size));
            outFile.println("</td>");
            outFile.println("<td class=\"soyc-percent-col\">" +
                percentFormatter.format(perc) + "</td>");
            outFile.println("</tr>");
          }
        }
        outFile.println("</table>");
        outFile.println("</p>");
      }
    }
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  public void makeSplitStatusPages() throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(splitStatusFileName()));

    addStandardHtmlProlog(outFile, "Split point status", "Split point status",
        "");
    outFile.println("<div id=\"bd\">");

    HtmlInterner interner = new HtmlInterner();
    for (String className : globalInformation.getClassToPackage().keySet()) {
      String packageName = globalInformation.getClassToPackage().get(className);
      interner.intern(packageName);
      interner.intern(getClassSubstring(className));
      interner.intern(hashedFilenameFragment(packageName));
    }
    interner.freeze();

    outFile.println("<script language=\"javascript\">");
    interner.printInternedDataAsJs(outFile);

    String curPackageName = "";
    for (String className : globalInformation.getClassToPackage().keySet()) {

      String packageName = globalInformation.getClassToPackage().get(className);
      if (packageName.compareTo("") == 0
          || packageName.compareTo(curPackageName) != 0) {
        curPackageName = packageName;
        interner.printPackageHeader(outFile, packageName);
      }

      interner.printClassHeader(outFile, className);

      if (globalInformation.getInitialCodeBreakdown().classToSize.containsKey(className)) {
        if (globalInformation.dependencies != null) {
          interner.printHasInitialFragment(outFile, className);
        } else {
          interner.printHasInitialFragment(outFile);
        }
      }
      for (int sp : splitPointsWithClass(className)) {
        interner.printHasCodeInSplitPoint(outFile, className, sp);
      }
      if (globalInformation.getLeftoversBreakdown().classToSize.containsKey(className)) {
        interner.printSomeCodeLeftover(outFile);
        interner.printLeftoversStatus(outFile, packageName, className);
      }
    }
    outFile.println("</script>");

    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  public void makeTopLevelShell() throws IOException {

    String permutationId = getPermutationId();
    PrintWriter outFile = new PrintWriter(getOutFile("SoycDashboard" + "-"
        + getPermutationId() + "-index.html"));

    addStandardHtmlProlog(outFile, "Compile report: Permutation "
        + permutationId, "Compile report: Permutation " + permutationId, "");

    NumberFormat bytesFormatter = NumberFormat.getInstance();
    bytesFormatter.setGroupingUsed(true);

    outFile.println("<div id=\"bd\">");
    outFile.println("<div id=\"soyc-summary\" class=\"g-section\">");
    outFile.println("<dl>");
    outFile.println("<dt>Full code size</dt>");
    outFile.println("<dd class=\"value\">"
        + bytesFormatter.format(globalInformation.getTotalCodeBreakdown().sizeAllCode)
        + " Bytes</dd>");
    outFile.println("<dd class=\"report\"><a href=\"total-" + permutationId
        + "-overallBreakdown.html\">Report</a></dd>");

    outFile.println("</dl>");
    outFile.println("<dl>");
    outFile.println("<dt>Initial download size</dt>");
    // TODO(kprobst) -- add percentage here: (48%)</dd>");
    outFile.println("<dd class=\"value\">"
        + bytesFormatter.format(globalInformation.getInitialCodeBreakdown().sizeAllCode)
        + " Bytes</dd>");
    outFile.println("<dd class=\"report\"><a href=\"initial-" + permutationId
        + "-overallBreakdown.html\">Report</a></dd>");
    outFile.println("</dl>");
    outFile.println("<dl>");

    outFile.println("<dt>Left over code</dt>");
    outFile.println("<dd class=\"value\">"
        + bytesFormatter.format(globalInformation.getLeftoversBreakdown().sizeAllCode)
        + " Bytes</dd>");
    outFile.println("<dd class=\"report\"><a href=\"leftovers-" + permutationId
        + "-overallBreakdown.html\">Report</a></dd>");
    outFile.println("</dl>");
    outFile.println("</div>");
    outFile.println("<table id=\"soyc-table-splitpoints\" class=\"soyc-table\">");
    outFile.println("<caption>");

    outFile.println("<strong>Split Points</strong>");
    outFile.println("</caption>");
    outFile.println("<colgroup>");
    outFile.println("<col id=\"soyc-splitpoint-number-col\">");
    outFile.println("<col id=\"soyc-splitpoint-location-col\">");
    outFile.println("<col id=\"soyc-splitpoint-size-col\">");
    outFile.println("</colgroup>");
    outFile.println("<thead>");

    outFile.println("<th>#</th>");
    outFile.println("<th>Location</th>");
    outFile.println("<th></th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">");
    outFile.println("Size <span class=\"soyc-th-units\">(Bytes)</span>");
    outFile.println("</th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">% of total</th>");
    outFile.println("</thead>");
    outFile.println("<tbody>");

    NumberFormat percentFormatter = NumberFormat.getPercentInstance();
    percentFormatter.setMinimumFractionDigits(1);
    percentFormatter.setMaximumFractionDigits(1);

    if (globalInformation.getNumFragments() >= 1) {

      int numFragments = globalInformation.getNumFragments();
      int maxSize = globalInformation.getTotalCodeBreakdown().sizeAllCode;

      for (int fragment = FRAGMENT_NUMBER_TOTAL_PROGRAM; fragment <= numFragments + 1; fragment++) {
        SizeBreakdown breakdown;
        if (fragment == FRAGMENT_NUMBER_TOTAL_PROGRAM
            || fragment == numFragments + 1 // leftovers
            || fragment == FRAGMENT_NUMBER_INITIAL_DOWNLOAD) {
          continue;
        }

        breakdown = globalInformation.fragmentCodeBreakdown(fragment);

        String drillDownFileName = shellFileName(breakdown, getPermutationId());
        final List<String> fragmentDescriptors = globalInformation.getFragmentDescriptors(fragment);
        String[] escapedFragmentDescriptors =
            new String[fragmentDescriptors.size()];
        for (int i = 0; i < fragmentDescriptors.size(); i++) {
          escapedFragmentDescriptors[i] =
              escapeXml(fragmentDescriptors.get(i));
        }

        String fragmentDescription = Joiner.on("<BR>").join(escapedFragmentDescriptors);

        int size = breakdown.sizeAllCode;
        float perc = (float) size / (float) maxSize;

        outFile.println("<tr>");
        outFile.println("<td>" + fragment + "</td>");
        outFile.print("<td><a href=\"" + drillDownFileName + "\">" + fragmentDescription
            + "</a></td>");
        outFile.println("<td class=\"soyc-bargraph-col\">");
        outFile.println("<div class=\"soyc-bar-graph goog-inline-block\">");
        outFile.println(getProgressBarHtml(perc));
        outFile.println("</div>");
        outFile.println("</td>");
        outFile.println("<td class=\"soyc-numerical-col\">");
        outFile.println(bytesFormatter.format(size));
        outFile.println("</td>");
        outFile.println("<td class=\"soyc-percent-col\">" +
            percentFormatter.format(perc) + "</td>");
        outFile.println("</tr>");
      }
    }
    outFile.println("</tbody>");
    outFile.println("</table>");
    outFile.println("</div>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  @SuppressWarnings("checkstyle:SpaceAfterColon")
  private String getProgressBarHtml(float perc) {
    return "<div style=\"width:" + (perc * 100.0)
        + "%;\" class=\"soyc-bar-graph-fill goog-inline-block\"></div>";
  }

  private void addPopup(PrintWriter outFile, String popupName,
      String popupTitle, String popupBody) {
    outFile.println("<div class=\"soyc-popup\" id=\"" + popupName + "\">");
    outFile.println("<table>");
    outFile.println("<tr><th><b>" + popupTitle + "</b></th></tr>");
    outFile.println("<tr><td>" + popupBody + "</td></tr>");
    outFile.println("</table>");
    outFile.println("</div>");
  }

  private void addPopupLink(PrintWriter outFile, String popupName,
      String popupTitle, String href) {
    outFile.println("<a ");
    if (href != null) {
      outFile.println("href=\"" + href + "\"");
    }
    outFile.println("style=\"cursor:default;\" onMouseOver=\"show('"
        + popupName + "');\" " + "onMouseOut=\"hide('" + popupName + "');\">"
        + popupTitle + "</a>");
  }

  /**
   * Returns a file name for the dependencies list.
   */
  private String dependenciesFileName(String depGraphName) {
    return "methodDependencies-" + depGraphName + "-" + getPermutationId() + ".html";
  }

  private String[] getGeneratedTypes(ModuleMetricsArtifact moduleMetrics,
      PrecompilationMetricsArtifact precompilationMetrics) {
    List<String> initialTypes = Lists.newArrayList(moduleMetrics.getInitialTypes());
    Set<String> generatedTypes = Sets.newHashSet(precompilationMetrics.getFinalTypeOracleTypes());
    generatedTypes.removeAll(initialTypes);
    String[] results = generatedTypes.toArray(new String[generatedTypes.size()]);
    Arrays.sort(results);
    return results;
  }

  /**
   * Return a {@link java.io.File} object for a file to be emitted into the
   * output directory.
   */
  private OutputStream getOutFile(String localFileName) throws IOException {
    return outDir.getOutputStream(localFileName);
  }

  /**
   * @return the ID for the current permutation
   */
  private String getPermutationId() {
    return globalInformation.getPermutationId();
  }

  private String[] getUnreferencedTypes(
      PrecompilationMetricsArtifact precompilationMetrics) {
    List<String> astTypes = Lists.newArrayList(precompilationMetrics.getAstTypes());
    Set<String> unreferencedTypes = Sets.newHashSet(
        precompilationMetrics.getFinalTypeOracleTypes());
    unreferencedTypes.removeAll(astTypes);
    String[] results = unreferencedTypes.toArray(new String[unreferencedTypes.size()]);
    Arrays.sort(results);
    return results;
  }

  /**
   * Describe the code covered by the dependency graph with the supplied name.
   */
  private String inferDepGraphDescription(String depGraphName) {
    if (depGraphName.equals("initial")) {
      return "Initially Live Code";
    }

    if (depGraphName.equals("total")) {
      return "All Code";
    }

    Matcher matcher = PATTERN_SP_INT.matcher(depGraphName);
    if (matcher.matches()) {
      int splitPoint = Integer.valueOf(matcher.group(1));
      if (isInitialSplitPoint(splitPoint)) {
        return "Code Becoming Live at Split Point " + splitPoint;
      } else {
        return "Code not Exclusive to Split Point " + splitPoint;
      }
    }

    throw new RuntimeException("Unexpected dependency graph name: "
        + depGraphName);
  }

  /**
   * Returns whether a split point is initial or not.
   *
   * @param splitPoint
   * @returns true of the split point is initial, false otherwise
   */
  private boolean isInitialSplitPoint(int splitPoint) {
    return globalInformation.getInitialFragmentLoadSequence().contains(
        splitPoint);
  }

  /**
   * Makes a file name for a leftovers status file.
   *
   * @return the file name of the leftovers status file
   */
  private String leftoversStatusFileName() {
    return "leftoverStatus-" + getPermutationId() + ".html";
  }

  /**
   * Produces an HTML file that breaks down by code type.
   *
   * @param breakdown
   * @param nameToCodeColl
   * @param nameToLitColl
   * @return the name of the produced file
   * @throws IOException
   */
  private String makeCodeTypeHtml(SizeBreakdown breakdown,
      Map<String, CodeCollection> nameToCodeColl,
      Map<String, LiteralsCollection> nameToLitColl) throws IOException {
    String outFileName = breakdown.getId() + "-" + getPermutationId()
        + "-codeTypeBreakdown.html";
    int sumSize = 0;
    TreeMap<Integer, Set<String>> sortedCodeTypes = new TreeMap<Integer, Set<String>>(
        Collections.reverseOrder());

    for (String codeType : nameToCodeColl.keySet()) {
      int curSize = nameToCodeColl.get(codeType).getCumSize(breakdown);
      sumSize += curSize;
      if (curSize != 0) {
        if (sortedCodeTypes.containsKey(curSize)) {
          Set<String> existingSet = sortedCodeTypes.get(curSize);
          existingSet.add(codeType);
          sortedCodeTypes.put(curSize, existingSet);
        } else {
          Set<String> newSet = new TreeSet<String>();
          newSet.add(codeType);
          sortedCodeTypes.put(curSize, newSet);
        }
      }
    }

    final PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    addSmallHtmlProlog(outFile, "Code breakdown");
    outFile.println("<body class=\"soyc-breakdown\">");
    outFile.println("<div class=\"g-doc\">");

    outFile.println("<table class=\"soyc-table\">");
    outFile.println("<colgroup>");
    outFile.println("<col id=\"soyc-splitpoint-type-col\">");
    outFile.println("<col id=\"soyc-splitpoint-size-col\">");
    outFile.println("</colgroup>");
    outFile.println("<thead>");
    outFile.println("<th>Type</th>");
    outFile.println("<th></th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">");
    outFile.println("Size <span class=\"soyc-th-units\">(Bytes)</span>");
    outFile.println("</th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">% of total</th>");
    outFile.println("</thead>");

    NumberFormat bytesFormatter = NumberFormat.getInstance();
    bytesFormatter.setGroupingUsed(true);

    NumberFormat percentFormatter = NumberFormat.getPercentInstance();
    percentFormatter.setMinimumFractionDigits(1);
    percentFormatter.setMaximumFractionDigits(1);

    for (Integer size : sortedCodeTypes.keySet()) {
      Set<String> codeTypes = sortedCodeTypes.get(size);
      for (String codeType : codeTypes) {
        String drillDownFileName = breakdown.getId() + "_" + codeType + "-"
            + getPermutationId() + "Classes.html";
        float perc = (float) size / (float) sumSize;
        outFile.println("<tr>");
        outFile.println("<td><a href=\"" + drillDownFileName
            + "\" target=\"_top\">" + codeType + "</a></td>");
        outFile.println("<td class=\"soyc-bargraph-col\">");
        outFile.println("<div class=\"soyc-bar-graph goog-inline-block\">");
        outFile.println(getProgressBarHtml(perc));
        outFile.println("</div>");
        outFile.println("</td>");
        outFile.println("<td class=\"soyc-numerical-col\">");
        outFile.println(bytesFormatter.format(size));
        outFile.println("</td>");
        outFile.println("<td class=\"soyc-percent-col\">" +
            percentFormatter.format(perc) + "</td>");
        outFile.println("</tr>");
      }
    }
    outFile.println("</table>");

    int stringSize = nameToLitColl.get("string").size;
    String drillDownFileName = breakdown.getId() + "_string-"
        + getPermutationId() + "Lits.html";
    outFile.println("<p class=\"soyc-breakdown-strings\">" + stringSize
        + " bytes occupied by <a href=\"" + drillDownFileName
        + "\" target=\"_top\">Strings</a></p>");
    int unaccountedForSize = breakdown.sizeAllCode - sumSize - stringSize;
    outFile.println("<p class=\"soyc-breakdown-strings\">"
        + unaccountedForSize
        + " bytes of the JavaScript output cannot be attributed to any package or code type.</p>");
    addStandardHtmlEnding(outFile);
    outFile.close();
    return outFileName;
  }

  private void makeCompilerMetricsAstTypes(String outFileName,
      PrecompilationMetricsArtifact precompilationMetrics, String helpText)
      throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    String title = "AST Types";
    addStandardHtmlProlog(outFile, title, title, "");
    outFile.println("<p>");
    outFile.println(helpText);
    outFile.println("</p>");
    outFile.println("<pre>");
    String[] types = precompilationMetrics.getAstTypes();
    Arrays.sort(types);
    for (String type : types) {
      outFile.println(type);
    }
    outFile.println("</pre>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  private void makeCompilerMetricsFinalTypeOracleTypes(String outFileName,
      PrecompilationMetricsArtifact precompilationMetrics, String helpText)
      throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    String title = "Final Type Oracle Types";
    addStandardHtmlProlog(outFile, title, title, "");
    outFile.println("<p>");
    outFile.println(helpText);
    outFile.println("</p>");
    outFile.println("<pre>");
    String[] types = precompilationMetrics.getFinalTypeOracleTypes();
    Arrays.sort(types);
    for (String type : types) {
      outFile.println(type);
    }
    outFile.println("</pre>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  private void makeCompilerMetricsGeneratedTypes(String outFileName,
      String[] generatedTypes, String helpText) throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    String title = "Generated Types";
    addStandardHtmlProlog(outFile, title, title, "");
    outFile.println("<p>");
    outFile.println(helpText);
    outFile.println("</p>");
    outFile.println("<pre>");
    for (String type : generatedTypes) {
      outFile.println(type);
    }
    outFile.println("</pre>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  private void makeCompilerMetricsInitialTypeOracleTypes(String outFileName,
      ModuleMetricsArtifact moduleMetrics, String helpText) throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    String title = "Initial Type Oracle Types (built from source path)";
    addStandardHtmlProlog(outFile, title, title, "");
    outFile.println("<p>");
    outFile.println(helpText);
    outFile.println("</p>");
    outFile.println("<pre>");
    String[] types = moduleMetrics.getInitialTypes();
    Arrays.sort(types);
    for (String type : types) {
      outFile.println(type);
    }
    outFile.println("</pre>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  private void makeCompilerMetricsSources(String outFileName,
      ModuleMetricsArtifact moduleMetrics, String helpText) throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    String title = "Sources on Source Path";
    addStandardHtmlProlog(outFile, title, title, "");
    outFile.println("<p>");
    outFile.println(helpText);
    outFile.println("</p>");
    outFile.println("<pre>");
    String[] sources = moduleMetrics.getSourceFiles();
    Arrays.sort(sources);
    for (String source : sources) {
      outFile.println(source);
    }
    outFile.println("</pre>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  private void makeCompilerMetricsUnreferencedTypes(String outFileName,
      String[] unreferencedTypes, String helpText) throws IOException {
    PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    String title = "Unreferenced Types";
    addStandardHtmlProlog(outFile, title, title, "");
    outFile.println("<p>");
    outFile.println(helpText);
    outFile.println("</p>");
    outFile.println("<pre>");
    for (String type : unreferencedTypes) {
      outFile.println(type);
    }
    outFile.println("</pre>");
    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  private static void internMethod(HtmlInterner interner,  String method) {
    interner.intern(getPackageSubstring(method));
    interner.intern(getClassSubstring(method));
    interner.intern(getMethodSubstring(method));
  }

  /**
   * Produces an HTML file that displays dependencies.
   *
   * @param depGraphName name of dependency graph
   * @param dependencies map of dependencies
   * @throws IOException
   */
  private void makeDependenciesHtml(String depGraphName,
      Map<String, String> dependencies) throws IOException {
    String curPackageName = "";

    HtmlInterner interner = new HtmlInterner();

    for (Entry<String, String> dependencyEntry : dependencies.entrySet()) {
      internMethod(interner, dependencyEntry.getKey());
      internMethod(interner, dependencyEntry.getValue());
    }
    interner.freeze();

    // Write out the interned data values as a script element
    String jsFileName = "methodDependencies-" + depGraphName + "-"
        + getPermutationId() + ".js";
    PrintWriter outFile = new PrintWriter(getOutFile(jsFileName));
    interner.printInternedDataAsJs(outFile);
    outFile.close();

    List<String> classesInPackage = new ArrayList<String>();
    for (String method : dependencies.keySet()) {
      classesInPackage.add(method);
    }
    makeDependenciesInternedHtml(depGraphName,
        classesInPackage, dependencies, interner, jsFileName);
  }

  /**
   * Produces an HTML file that displays dependencies.
   *
   * @param depGraphName name of dependency graph
   * @param dependencies map of dependencies
   * @throws IOException
   */
  private void makeDependenciesInternedHtml(String depGraphName,
      List<String> classesInSplitPoint,
      Map<String, String> dependencies, HtmlInterner interner, String jsFileName)
      throws IOException {
    String depGraphDescription = inferDepGraphDescription(depGraphName);
    PrintWriter outFile = null;
    String curClassName = "";

    String outFileName = dependenciesFileName(depGraphName);
    outFile = new PrintWriter(getOutFile(outFileName));

    addStandardHtmlProlog(outFile, "Method Dependencies for "
        + depGraphDescription,
        "Method Dependencies for " + depGraphDescription, null);

    outFile.print("<script src=\"" + jsFileName
        + "\" language=\"javascript\" ></script>");

    // Write out the HTML
    outFile.print("<script>");
    for (String method : classesInSplitPoint) {
      // this key set is already in alphabetical order
      // get the package of this method, i.e., everything up to .[A-Z]

      String className = method.replaceAll("::.*", "");
      String depMethod = dependencies.get(method);
      if (curClassName.compareTo(className) != 0) {
        curClassName = className;
        if (method != classesInSplitPoint.get(0)) {
          outFile.print("j();"); // close the previous table if not the first
        }
        interner.printDependencyClassHeader(outFile, className);
      }
      interner.printDependency(outFile, dependencies, method, depMethod);
    }
    outFile.println("</script>");

    addStandardHtmlEnding(outFile);
    outFile.close();
  }

  /**
   * Produces an HTML file that shows information about a package.
   *
   * @param breakdown
   * @return the name of the HTML file
   */
  private String makePackageHtml(SizeBreakdown breakdown) throws IOException {
    String outFileName = breakdown.getId() + "-" + getPermutationId() + "-"
        + "packageBreakdown.html";
    Map<String, Integer> packageToPartialSize = breakdown.packageToSize;
    TreeMap<Integer, Set<String>> sortedPackages = new TreeMap<Integer, Set<String>>(
        Collections.reverseOrder());
    float sumSize = 0f;
    for (String packageName : packageToPartialSize.keySet()) {
      Integer curSize = packageToPartialSize.get(packageName);
      if (sortedPackages.containsKey(curSize)) {
        Set<String> existingSet = sortedPackages.get(curSize);
        existingSet.add(packageName);
        sortedPackages.put(curSize, existingSet);
      } else {
        Set<String> newSet = new TreeSet<String>();
        newSet.add(packageName);
        sortedPackages.put(curSize, newSet);
      }
      sumSize += curSize;
    }

    final PrintWriter outFile = new PrintWriter(getOutFile(outFileName));
    addSmallHtmlProlog(outFile, "Package breakdown");
    outFile.println("<body class=\"soyc-breakdown\">");
    outFile.println("<div class=\"g-doc\">");

    outFile.println("<table class=\"soyc-table\">");
    outFile.println("<colgroup>");
    outFile.println("<col id=\"soyc-splitpoint-type-col\">");
    outFile.println("<col id=\"soyc-splitpoint-size-col\">");
    outFile.println("</colgroup>");
    outFile.println("<thead>");
    outFile.println("<th>Packages (Sorted by size)</th>");
    outFile.println("<th></th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">");
    outFile.println("Size <span class=\"soyc-th-units\">(Bytes)</span>");
    outFile.println("</th>");
    outFile.println("<th class=\"soyc-numerical-col-header\">% of total</th>");
    outFile.println("</thead>");

    NumberFormat bytesFormatter = NumberFormat.getInstance();
    bytesFormatter.setGroupingUsed(true);

    NumberFormat percentFormatter = NumberFormat.getPercentInstance();
    percentFormatter.setMinimumFractionDigits(1);
    percentFormatter.setMaximumFractionDigits(1);

    for (int size : sortedPackages.keySet()) {
      if (size == 0) {
        continue;
      }
      Set<String> packageNames = sortedPackages.get(size);
      for (String packageName : packageNames) {
        String drillDownFileName = classesInPackageFileName(breakdown,
            getPermutationId())
            + "#" + hashedFilenameFragment(packageName);
        float perc = size / sumSize;
        outFile.println("<tr>");
        outFile.println("<td><a href=\"" + drillDownFileName
            + "\" target=\"_top\">" + packageName + "</a></td>");
        outFile.println("<td class=\"soyc-bargraph-col\">");
        outFile.println("<div class=\"soyc-bar-graph goog-inline-block\">");
        outFile.println(getProgressBarHtml(perc));
        outFile.println("</div>");
        outFile.println("</td>");
        outFile.println("<td class=\"soyc-numerical-col\">");
        outFile.println(bytesFormatter.format(size));
        outFile.println("</td>");
        outFile.println("<td class=\"soyc-percent-col\">" +
            percentFormatter.format(perc) + "</td>");
        outFile.println("</tr>");
      }
    }

    outFile.println("</table>");
    addStandardHtmlEnding(outFile);
    outFile.close();
    return outFileName;
  }

  /**
   * Find which split points include code belonging to <code>className</code>.
   */
  private Iterable<Integer> splitPointsWithClass(String className) {
    List<Integer> sps = new ArrayList<Integer>();
    for (int sp = 1; sp <= globalInformation.getNumFragments(); sp++) {
      Map<String, Integer> classToSize = globalInformation.fragmentCodeBreakdown(sp).classToSize;
      if (classToSize.containsKey(className)) {
        sps.add(sp);
      }
    }
    return sps;
  }

  private String splitStatusFileName() {
    return "splitStatus-" + getPermutationId() + ".html";
  }
}
