/*
 * Copyright (C) 2010 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.doclava;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

public class Stubs {
  public static void writeStubsAndApi(String stubsDir, String apiFile, String keepListFile,
      String removedApiFile, HashSet<String> stubPackages) {
    // figure out which classes we need
    final HashSet<ClassInfo> notStrippable = new HashSet<ClassInfo>();
    ClassInfo[] all = Converter.allClasses();
    PrintStream apiWriter = null;
    PrintStream keepListWriter = null;
    PrintStream removedApiWriter = null;

    if (apiFile != null) {
      try {
        File xml = new File(apiFile);
        xml.getParentFile().mkdirs();
        apiWriter = new PrintStream(new BufferedOutputStream(new FileOutputStream(xml)));
      } catch (FileNotFoundException e) {
        Errors.error(Errors.IO_ERROR, new SourcePositionInfo(apiFile, 0, 0),
            "Cannot open file for write.");
      }
    }
    if (keepListFile != null) {
      try {
        File keepList = new File(keepListFile);
        keepList.getParentFile().mkdirs();
        keepListWriter = new PrintStream(new BufferedOutputStream(new FileOutputStream(keepList)));
      } catch (FileNotFoundException e) {
        Errors.error(Errors.IO_ERROR, new SourcePositionInfo(keepListFile, 0, 0),
            "Cannot open file for write.");
      }
    }
    if (removedApiFile != null) {
      try {
        File removedApi = new File(removedApiFile);
        removedApi.getParentFile().mkdirs();
        removedApiWriter = new PrintStream(
            new BufferedOutputStream(new FileOutputStream(removedApi)));
      } catch (FileNotFoundException e) {
        Errors.error(Errors.IO_ERROR, new SourcePositionInfo(removedApiFile, 0, 0),
            "Cannot open file for write");
      }
    }
    // If a class is public or protected, not hidden, and marked as included,
    // then we can't strip it
    for (ClassInfo cl : all) {
      if (cl.checkLevel() && cl.isIncluded()) {
        cantStripThis(cl, notStrippable, "0:0");
      }
    }

    // complain about anything that looks includeable but is not supposed to
    // be written, e.g. hidden things
    for (ClassInfo cl : notStrippable) {
      if (!cl.isHiddenOrRemoved()) {
        for (MethodInfo m : cl.selfMethods()) {
          if (m.isHiddenOrRemoved()) {
            Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Reference to unavailable method "
                + m.name());
          } else if (m.isDeprecated()) {
            // don't bother reporting deprecated methods
            // unless they are public
            Errors.error(Errors.DEPRECATED, m.position(), "Method " + cl.qualifiedName() + "."
                + m.name() + " is deprecated");
          }

          ClassInfo hiddenClass = findHiddenClasses(m.returnType());
          if (null != hiddenClass) {
            if (hiddenClass.qualifiedName() == m.returnType().asClassInfo().qualifiedName()) {
              // Return type is hidden
              Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Method " + cl.qualifiedName()
                  + "." + m.name() + " returns unavailable type " + hiddenClass.name());
            } else {
              // Return type contains a generic parameter
              Errors.error(Errors.HIDDEN_TYPE_PARAMETER, m.position(), "Method " + cl.qualifiedName()
                  + "." + m.name() + " returns unavailable type " + hiddenClass.name()
                  + " as a type parameter");
            }
          }

          for (ParameterInfo p :  m.parameters()) {
            TypeInfo t = p.type();
            if (!t.isPrimitive()) {
              hiddenClass = findHiddenClasses(t);
              if (null != hiddenClass) {
                if (hiddenClass.qualifiedName() == t.asClassInfo().qualifiedName()) {
                  // Parameter type is hidden
                  Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(),
                      "Parameter of unavailable type " + t.fullName() + " in " + cl.qualifiedName()
                      + "." + m.name() + "()");
                } else {
                  // Parameter type contains a generic parameter
                  Errors.error(Errors.HIDDEN_TYPE_PARAMETER, m.position(),
                      "Parameter uses type parameter of unavailable type " + t.fullName() + " in "
                      + cl.qualifiedName() + "." + m.name() + "()");
                }
              }
            }
          }
        }

        // annotations are handled like methods
        for (MethodInfo m : cl.annotationElements()) {
          if (m.isHiddenOrRemoved()) {
            Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Reference to unavailable annotation "
                + m.name());
          }

          ClassInfo returnClass = m.returnType().asClassInfo();
          if (returnClass != null && returnClass.isHiddenOrRemoved()) {
            Errors.error(Errors.UNAVAILABLE_SYMBOL, m.position(), "Annotation '" + m.name()
                + "' returns unavailable type " + returnClass.name());
          }

          for (ParameterInfo p :  m.parameters()) {
            TypeInfo t = p.type();
            if (!t.isPrimitive()) {
              if (t.asClassInfo().isHiddenOrRemoved()) {
                Errors.error(Errors.UNAVAILABLE_SYMBOL, p.position(),
                    "Reference to unavailable annotation class " + t.fullName());
              }
            }
          }
        }
      } else if (cl.isDeprecated()) {
        // not hidden, but deprecated
        Errors.error(Errors.DEPRECATED, cl.position(), "Class " + cl.qualifiedName()
            + " is deprecated");
      }
    }

    // packages contains all the notStrippable classes mapped by their containing packages
    HashMap<PackageInfo, List<ClassInfo>> packages = new HashMap<PackageInfo, List<ClassInfo>>();
    final HashSet<Pattern> stubPackageWildcards = extractWildcards(stubPackages);
    for (ClassInfo cl : notStrippable) {
      if (!cl.isDocOnly()) {
        if (shouldWriteStub(cl.containingPackage().name(), stubPackages, stubPackageWildcards)) {
          // write out the stubs
          if (stubsDir != null) {
            writeClassFile(stubsDir, notStrippable, cl);
          }
          // build class list for api file or keep list file
          if (apiWriter != null || keepListWriter != null) {
            if (packages.containsKey(cl.containingPackage())) {
              packages.get(cl.containingPackage()).add(cl);
            } else {
              ArrayList<ClassInfo> classes = new ArrayList<ClassInfo>();
              classes.add(cl);
              packages.put(cl.containingPackage(), classes);
            }
          }
        }
      }
    }
    // write out the Api
    if (apiWriter != null) {
      writeApi(apiWriter, packages, notStrippable);
      apiWriter.close();
    }

    // write out the keep list
    if (keepListWriter != null) {
      writeKeepList(keepListWriter, packages, notStrippable);
      keepListWriter.close();
    }

    HashMap<PackageInfo, List<ClassInfo>> allPackageClassMap =
        new HashMap<PackageInfo, List<ClassInfo>>();
    for (ClassInfo cl : Converter.allClasses()) {
      if (allPackageClassMap.containsKey(cl.containingPackage())) {
        allPackageClassMap.get(cl.containingPackage()).add(cl);
      } else {
        ArrayList<ClassInfo> classes = new ArrayList<ClassInfo>();
        classes.add(cl);
        allPackageClassMap.put(cl.containingPackage(), classes);
      }
    }
    // write out the removed Api
    if (removedApiWriter != null) {
      writeRemovedApi(removedApiWriter, allPackageClassMap, notStrippable);
      removedApiWriter.close();
    }
  }

  private static boolean shouldWriteStub(final String packageName,
          final HashSet<String> stubPackages, final HashSet<Pattern> stubPackageWildcards) {
    if (stubPackages == null) {
      // There aren't any stub packages set, write all stubs
      return true;
    }
    if (stubPackages.contains(packageName)) {
      // Stub packages contains package, return true
      return true;
    }
    if (stubPackageWildcards != null) {
      // Else, we will iterate through the wildcards to see if there's a match
      for (Pattern wildcard : stubPackageWildcards) {
        if (wildcard.matcher(packageName).matches()) {
          return true;
        }
      }
    }
    return false;
  }

  private static HashSet<Pattern> extractWildcards(HashSet<String> stubPackages) {
    HashSet<Pattern> wildcards = null;
    if (stubPackages != null) {
      for (Iterator<String> i = stubPackages.iterator(); i.hasNext();) {
        final String pkg = i.next();
        if (pkg.indexOf('*') != -1) {
          if (wildcards == null) {
            wildcards = new HashSet<Pattern>();
          }
          // Add the compiled wildcard, replacing * with the regex equivalent
          wildcards.add(Pattern.compile(pkg.replace("*", ".*?")));
          // And remove the raw wildcard from the packages
          i.remove();
        }
      }
    }
    return wildcards;
  }

  private static ClassInfo findHiddenClasses(TypeInfo ti) {
    ClassInfo ci = ti.asClassInfo();
    if (ci == null) return null;
    if (ci.isHiddenOrRemoved()) return ci;
    if (ti.typeArguments() != null) {
      for (TypeInfo tii : ti.typeArguments()) {
        // Avoid infinite recursion in the case of Foo<T extends Foo>
        if (tii.qualifiedTypeName() != ti.qualifiedTypeName()) {
          ClassInfo hiddenClass = findHiddenClasses(tii);
          if (hiddenClass != null) return hiddenClass;
        }
      }
    }
    return null;
  }

  public static void cantStripThis(ClassInfo cl, HashSet<ClassInfo> notStrippable, String why) {

    if (!notStrippable.add(cl)) {
      // slight optimization: if it already contains cl, it already contains
      // all of cl's parents
      return;
    }
    cl.setReasonIncluded(why);

    // cant strip annotations
    /*
     * if (cl.annotations() != null){ for (AnnotationInstanceInfo ai : cl.annotations()){ if
     * (ai.type() != null){ cantStripThis(ai.type(), notStrippable, "1:" + cl.qualifiedName()); } }
     * }
     */
    // cant strip any public fields or their generics
    if (cl.selfFields() != null) {
      for (FieldInfo fInfo : cl.selfFields()) {
        if (fInfo.type() != null) {
          if (fInfo.type().asClassInfo() != null) {
            cantStripThis(fInfo.type().asClassInfo(), notStrippable, "2:" + cl.qualifiedName());
          }
          if (fInfo.type().typeArguments() != null) {
            for (TypeInfo tTypeInfo : fInfo.type().typeArguments()) {
              if (tTypeInfo.asClassInfo() != null) {
                cantStripThis(tTypeInfo.asClassInfo(), notStrippable, "3:" + cl.qualifiedName());
              }
            }
          }
        }
      }
    }
    // cant strip any of the type's generics
    if (cl.asTypeInfo() != null) {
      if (cl.asTypeInfo().typeArguments() != null) {
        for (TypeInfo tInfo : cl.asTypeInfo().typeArguments()) {
          if (tInfo.asClassInfo() != null) {
            cantStripThis(tInfo.asClassInfo(), notStrippable, "4:" + cl.qualifiedName());
          }
        }
      }
    }
    // cant strip any of the annotation elements
    // cantStripThis(cl.annotationElements(), notStrippable);
    // take care of methods
    cantStripThis(cl.allSelfMethods(), notStrippable);
    cantStripThis(cl.allConstructors(), notStrippable);
    // blow the outer class open if this is an inner class
    if (cl.containingClass() != null) {
      cantStripThis(cl.containingClass(), notStrippable, "5:" + cl.qualifiedName());
    }
    // blow open super class and interfaces
    ClassInfo supr = cl.realSuperclass();
    if (supr != null) {
      if (supr.isHiddenOrRemoved()) {
        // cl is a public class declared as extending a hidden superclass.
        // this is not a desired practice but it's happened, so we deal
        // with it by finding the first super class which passes checklevel for purposes of
        // generating the doc & stub information, and proceeding normally.
        cl.init(cl.asTypeInfo(), cl.realInterfaces(), cl.realInterfaceTypes(), cl.innerClasses(),
            cl.allConstructors(), cl.allSelfMethods(), cl.annotationElements(), cl.allSelfFields(),
            cl.enumConstants(), cl.containingPackage(), cl.containingClass(),
            supr.superclass(), supr.superclassType(), cl.annotations());
        Errors.error(Errors.HIDDEN_SUPERCLASS, cl.position(), "Public class " + cl.qualifiedName()
            + " stripped of unavailable superclass " + supr.qualifiedName());
      } else {
        cantStripThis(supr, notStrippable, "6:" + cl.realSuperclass().name() + cl.qualifiedName());
        if (supr.isPrivate()) {
          Errors.error(Errors.PRIVATE_SUPERCLASS, cl.position(), "Public class "
              + cl.qualifiedName() + " extends private class " + supr.qualifiedName());
        }
      }
    }
  }

  private static void cantStripThis(ArrayList<MethodInfo> mInfos, HashSet<ClassInfo> notStrippable) {
    // for each method, blow open the parameters, throws and return types. also blow open their
    // generics
    if (mInfos != null) {
      for (MethodInfo mInfo : mInfos) {
        if (mInfo.getTypeParameters() != null) {
          for (TypeInfo tInfo : mInfo.getTypeParameters()) {
            if (tInfo.asClassInfo() != null) {
              cantStripThis(tInfo.asClassInfo(), notStrippable, "8:"
                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
            }
          }
        }
        if (mInfo.parameters() != null) {
          for (ParameterInfo pInfo : mInfo.parameters()) {
            if (pInfo.type() != null && pInfo.type().asClassInfo() != null) {
              cantStripThis(pInfo.type().asClassInfo(), notStrippable, "9:"
                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
              if (pInfo.type().typeArguments() != null) {
                for (TypeInfo tInfoType : pInfo.type().typeArguments()) {
                  if (tInfoType.asClassInfo() != null) {
                    ClassInfo tcl = tInfoType.asClassInfo();
                    if (tcl.isHiddenOrRemoved()) {
                      Errors
                          .error(Errors.UNAVAILABLE_SYMBOL, mInfo.position(),
                              "Parameter of hidden type " + tInfoType.fullName() + " in "
                                  + mInfo.containingClass().qualifiedName() + '.' + mInfo.name()
                                  + "()");
                    } else {
                      cantStripThis(tcl, notStrippable, "10:"
                          + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
                    }
                  }
                }
              }
            }
          }
        }
        for (ClassInfo thrown : mInfo.thrownExceptions()) {
          cantStripThis(thrown, notStrippable, "11:" + mInfo.realContainingClass().qualifiedName()
              + ":" + mInfo.name());
        }
        if (mInfo.returnType() != null && mInfo.returnType().asClassInfo() != null) {
          cantStripThis(mInfo.returnType().asClassInfo(), notStrippable, "12:"
              + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
          if (mInfo.returnType().typeArguments() != null) {
            for (TypeInfo tyInfo : mInfo.returnType().typeArguments()) {
              if (tyInfo.asClassInfo() != null) {
                cantStripThis(tyInfo.asClassInfo(), notStrippable, "13:"
                    + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
              }
            }
          }
        }
      }
    }
  }

  static String javaFileName(ClassInfo cl) {
    String dir = "";
    PackageInfo pkg = cl.containingPackage();
    if (pkg != null) {
      dir = pkg.name();
      dir = dir.replace('.', '/') + '/';
    }
    return dir + cl.name() + ".java";
  }

  static void writeClassFile(String stubsDir, HashSet<ClassInfo> notStrippable, ClassInfo cl) {
    // inner classes are written by their containing class
    if (cl.containingClass() != null) {
      return;
    }

    // Work around the bogus "Array" class we invent for
    // Arrays.copyOf's Class<? extends T[]> newType parameter. (http://b/2715505)
    if (cl.containingPackage() != null
        && cl.containingPackage().name().equals(PackageInfo.DEFAULT_PACKAGE)) {
      return;
    }

    String filename = stubsDir + '/' + javaFileName(cl);
    File file = new File(filename);
    ClearPage.ensureDirectory(file);

    PrintStream stream = null;
    try {
      stream = new PrintStream(new BufferedOutputStream(new FileOutputStream(file)));
      writeClassFile(stream, notStrippable, cl);
    } catch (FileNotFoundException e) {
      System.err.println("error writing file: " + filename);
    } finally {
      if (stream != null) {
        stream.close();
      }
    }
  }

  static void writeClassFile(PrintStream stream, HashSet<ClassInfo> notStrippable, ClassInfo cl) {
    PackageInfo pkg = cl.containingPackage();
    if (pkg != null) {
      stream.println("package " + pkg.name() + ";");
    }
    writeClass(stream, notStrippable, cl);
  }

  static void writeClass(PrintStream stream, HashSet<ClassInfo> notStrippable, ClassInfo cl) {
    writeAnnotations(stream, cl.annotations(), cl.isDeprecated());

    stream.print(cl.scope() + " ");
    if (cl.isAbstract() && !cl.isAnnotation() && !cl.isInterface()) {
      stream.print("abstract ");
    }
    if (cl.isStatic()) {
      stream.print("static ");
    }
    if (cl.isFinal() && !cl.isEnum()) {
      stream.print("final ");
    }
    if (false) {
      stream.print("strictfp ");
    }

    HashSet<String> classDeclTypeVars = new HashSet();
    String leafName = cl.asTypeInfo().fullName(classDeclTypeVars);
    int bracket = leafName.indexOf('<');
    if (bracket < 0) bracket = leafName.length() - 1;
    int period = leafName.lastIndexOf('.', bracket);
    if (period < 0) period = -1;
    leafName = leafName.substring(period + 1);

    String kind = cl.kind();
    stream.println(kind + " " + leafName);

    TypeInfo base = cl.superclassType();

    if (!"enum".equals(kind)) {
      if (base != null && !"java.lang.Object".equals(base.qualifiedTypeName())) {
        stream.println("  extends " + base.fullName(classDeclTypeVars));
      }
    }

    List<TypeInfo> usedInterfaces = new ArrayList<TypeInfo>();
    for (TypeInfo iface : cl.realInterfaceTypes()) {
      if (notStrippable.contains(iface.asClassInfo()) && !iface.asClassInfo().isDocOnly()) {
        usedInterfaces.add(iface);
      }
    }
    if (usedInterfaces.size() > 0 && !cl.isAnnotation()) {
      // can java annotations extend other ones?
      if (cl.isInterface() || cl.isAnnotation()) {
        stream.print("  extends ");
      } else {
        stream.print("  implements ");
      }
      String comma = "";
      for (TypeInfo iface : usedInterfaces) {
        stream.print(comma + iface.fullName(classDeclTypeVars));
        comma = ", ";
      }
      stream.println();
    }

    stream.println("{");

    ArrayList<FieldInfo> enumConstants = cl.enumConstants();
    int N = enumConstants.size();
    int i = 0;
    for (FieldInfo field : enumConstants) {
      if (!field.constantLiteralValue().equals("null")) {
        stream.println(field.name() + "(" + field.constantLiteralValue()
            + (i == N - 1 ? ");" : "),"));
      } else {
        stream.println(field.name() + "(" + (i == N - 1 ? ");" : "),"));
      }
      i++;
    }

    for (ClassInfo inner : cl.getRealInnerClasses()) {
      if (notStrippable.contains(inner) && !inner.isDocOnly()) {
        writeClass(stream, notStrippable, inner);
      }
    }


    for (MethodInfo method : cl.constructors()) {
      if (!method.isDocOnly()) {
        writeMethod(stream, method, true);
      }
    }

    boolean fieldNeedsInitialization = false;
    boolean staticFieldNeedsInitialization = false;
    for (FieldInfo field : cl.selfFields()) {
      if (!field.isDocOnly()) {
        if (!field.isStatic() && field.isFinal() && !fieldIsInitialized(field)) {
          fieldNeedsInitialization = true;
        }
        if (field.isStatic() && field.isFinal() && !fieldIsInitialized(field)) {
          staticFieldNeedsInitialization = true;
        }
      }
    }

    // The compiler includes a default public constructor that calls the super classes
    // default constructor in the case where there are no written constructors.
    // So, if we hide all the constructors, java may put in a constructor
    // that calls a nonexistent super class constructor. So, if there are no constructors,
    // and the super class doesn't have a default constructor, write in a private constructor
    // that works. TODO -- we generate this as protected, but we really should generate
    // it as private unless it also exists in the real code.
    if ((cl.constructors().isEmpty() && (!cl.getNonWrittenConstructors().isEmpty() ||
        fieldNeedsInitialization)) && !cl.isAnnotation() && !cl.isInterface() && !cl.isEnum()) {
      // Errors.error(Errors.HIDDEN_CONSTRUCTOR,
      // cl.position(), "No constructors " +
      // "found and superclass has no parameterless constructor.  A constructor " +
      // "that calls an appropriate superclass constructor " +
      // "was automatically written to stubs.\n");
      stream.println(cl.leafName() + "() { " + superCtorCall(cl, null) + "throw new"
          + " RuntimeException(\"Stub!\"); }");
    }

    for (MethodInfo method : cl.allSelfMethods()) {
      if (cl.isEnum()) {
        if (("values".equals(method.name()) && "()".equals(method.signature())) ||
            ("valueOf".equals(method.name()) &&
            "(java.lang.String)".equals(method.signature()))) {
          // skip these two methods on enums, because they're synthetic,
          // although for some reason javadoc doesn't mark them as synthetic,
          // maybe because they still want them documented
          continue;
        }
      }
      if (!method.isDocOnly()) {
        writeMethod(stream, method, false);
      }
    }
    // Write all methods that are hidden or removed, but override abstract methods or interface methods.
    // These can't be hidden.
    List<MethodInfo> hiddenAndRemovedMethods = cl.getHiddenMethods();
    hiddenAndRemovedMethods.addAll(cl.getRemovedMethods());
    for (MethodInfo method : hiddenAndRemovedMethods) {
      MethodInfo overriddenMethod =
          method.findRealOverriddenMethod(method.name(), method.signature(), notStrippable);
      ClassInfo classContainingMethod =
          method.findRealOverriddenClass(method.name(), method.signature());
      if (overriddenMethod != null && !overriddenMethod.isHiddenOrRemoved() &&
          !overriddenMethod.isDocOnly() &&
          (overriddenMethod.isAbstract() || overriddenMethod.containingClass().isInterface())) {
        method.setReason("1:" + classContainingMethod.qualifiedName());
        cl.addMethod(method);
        writeMethod(stream, method, false);
      }
    }

    for (MethodInfo element : cl.annotationElements()) {
      if (!element.isDocOnly()) {
        writeAnnotationElement(stream, element);
      }
    }

    for (FieldInfo field : cl.selfFields()) {
      if (!field.isDocOnly()) {
        writeField(stream, field);
      }
    }

    if (staticFieldNeedsInitialization) {
      stream.print("static { ");
      for (FieldInfo field : cl.selfFields()) {
        if (!field.isDocOnly() && field.isStatic() && field.isFinal() && !fieldIsInitialized(field)
            && field.constantValue() == null) {
          stream.print(field.name() + " = " + field.type().defaultValue() + "; ");
        }
      }
      stream.println("}");
    }

    stream.println("}");
  }


  static void writeMethod(PrintStream stream, MethodInfo method, boolean isConstructor) {
    String comma;

    writeAnnotations(stream, method.annotations(), method.isDeprecated());

    stream.print(method.scope() + " ");
    if (method.isStatic()) {
      stream.print("static ");
    }
    if (method.isFinal()) {
      stream.print("final ");
    }
    if (method.isAbstract()) {
      stream.print("abstract ");
    }
    if (method.isSynchronized()) {
      stream.print("synchronized ");
    }
    if (method.isNative()) {
      stream.print("native ");
    }
    if (false /* method.isStictFP() */) {
      stream.print("strictfp ");
    }

    stream.print(method.typeArgumentsName(new HashSet()) + " ");

    if (!isConstructor) {
      stream.print(method.returnType().fullName(method.typeVariables()) + " ");
    }
    String n = method.name();
    int pos = n.lastIndexOf('.');
    if (pos >= 0) {
      n = n.substring(pos + 1);
    }
    stream.print(n + "(");
    comma = "";
    int count = 1;
    int size = method.parameters().size();
    for (ParameterInfo param : method.parameters()) {
      stream.print(comma + fullParameterTypeName(method, param.type(), count == size) + " "
          + param.name());
      comma = ", ";
      count++;
    }
    stream.print(")");

    comma = "";
    if (method.thrownExceptions().size() > 0) {
      stream.print(" throws ");
      for (ClassInfo thrown : method.thrownExceptions()) {
        stream.print(comma + thrown.qualifiedName());
        comma = ", ";
      }
    }
    if (method.isAbstract() || method.isNative() || method.containingClass().isInterface()) {
      stream.println(";");
    } else {
      stream.print(" { ");
      if (isConstructor) {
        stream.print(superCtorCall(method.containingClass(), method.thrownExceptions()));
      }
      stream.println("throw new RuntimeException(\"Stub!\"); }");
    }
  }

  static void writeField(PrintStream stream, FieldInfo field) {
    writeAnnotations(stream, field.annotations(), field.isDeprecated());

    stream.print(field.scope() + " ");
    if (field.isStatic()) {
      stream.print("static ");
    }
    if (field.isFinal()) {
      stream.print("final ");
    }
    if (field.isTransient()) {
      stream.print("transient ");
    }
    if (field.isVolatile()) {
      stream.print("volatile ");
    }

    stream.print(field.type().fullName());
    stream.print(" ");
    stream.print(field.name());

    if (fieldIsInitialized(field)) {
      stream.print(" = " + field.constantLiteralValue());
    }

    stream.println(";");
  }

  static boolean fieldIsInitialized(FieldInfo field) {
    return (field.isFinal() && field.constantValue() != null)
        || !field.type().dimension().equals("") || field.containingClass().isInterface();
  }

  // Returns 'true' if the method is an @Override of a visible parent
  // method implementation, and thus does not affect the API.
  static boolean methodIsOverride(HashSet<ClassInfo> notStrippable, MethodInfo mi) {
    // Abstract/static/final methods are always listed in the API description
    if (mi.isAbstract() || mi.isStatic() || mi.isFinal()) {
      return false;
    }

    // Find any relevant ancestor declaration and inspect it
    MethodInfo om = mi.findSuperclassImplementation(notStrippable);
    if (om != null) {
      // Visibility mismatch is an API change, so check for it
      if (mi.mIsPrivate == om.mIsPrivate && mi.mIsPublic == om.mIsPublic
          && mi.mIsProtected == om.mIsProtected) {
        // Look only for overrides of an ancestor class implementation,
        // not of e.g. an abstract or interface method declaration
        if (!om.isAbstract()) {
          // If the parent is hidden or removed, we can't rely on it to provide
          // the API
          if (!om.isHiddenOrRemoved()) {
            // If the only "override" turns out to be in our own class
            // (which sometimes happens in concrete subclasses of
            // abstract base classes), it's not really an override
            if (!mi.mContainingClass.equals(om.mContainingClass)) {
              return true;
            }
          }
        }
      }
    }
    return false;
  }

  static boolean canCallMethod(ClassInfo from, MethodInfo m) {
    if (m.isPublic() || m.isProtected()) {
      return true;
    }
    if (m.isPackagePrivate()) {
      String fromPkg = from.containingPackage().name();
      String pkg = m.containingClass().containingPackage().name();
      if (fromPkg.equals(pkg)) {
        return true;
      }
    }
    return false;
  }

  // call a constructor, any constructor on this class's superclass.
  static String superCtorCall(ClassInfo cl, ArrayList<ClassInfo> thrownExceptions) {
    ClassInfo base = cl.realSuperclass();
    if (base == null) {
      return "";
    }
    HashSet<String> exceptionNames = new HashSet<String>();
    if (thrownExceptions != null) {
      for (ClassInfo thrown : thrownExceptions) {
        exceptionNames.add(thrown.name());
      }
    }
    ArrayList<MethodInfo> ctors = base.constructors();
    MethodInfo ctor = null;
    // bad exception indicates that the exceptions thrown by the super constructor
    // are incompatible with the constructor we're using for the sub class.
    Boolean badException = false;
    for (MethodInfo m : ctors) {
      if (canCallMethod(cl, m)) {
        if (m.thrownExceptions() != null) {
          for (ClassInfo thrown : m.thrownExceptions()) {
            if (!exceptionNames.contains(thrown.name())) {
              badException = true;
            }
          }
        }
        if (badException) {
          badException = false;
          continue;
        }
        // if it has no args, we're done
        if (m.parameters().isEmpty()) {
          return "";
        }
        ctor = m;
      }
    }
    if (ctor != null) {
      String result = "";
      result += "super(";
      ArrayList<ParameterInfo> params = ctor.parameters();
      for (ParameterInfo param : params) {
        TypeInfo t = param.type();
        if (t.isPrimitive() && t.dimension().equals("")) {
          String n = t.simpleTypeName();
          if (("byte".equals(n) || "short".equals(n) || "int".equals(n) || "long".equals(n)
              || "float".equals(n) || "double".equals(n))
              && t.dimension().equals("")) {
            result += "0";
          } else if ("char".equals(n)) {
            result += "'\\0'";
          } else if ("boolean".equals(n)) {
            result += "false";
          } else {
            result += "<<unknown-" + n + ">>";
          }
        } else {
          // put null in each super class method. Cast null to the correct type
          // to avoid collisions with other constructors. If the type is generic
          // don't cast it
          result +=
              (!t.isTypeVariable() ? "(" + t.qualifiedTypeName() + t.dimension() + ")" : "")
                  + "null";
        }
        if (param != params.get(params.size()-1)) {
          result += ",";
        }
      }
      result += "); ";
      return result;
    } else {
      return "";
    }
  }

    /**
     * Write out the given list of annotations. If the {@code isDeprecated}
     * flag is true also write out a {@code @Deprecated} annotation if it did not
     * already appear in the list of annotations. (This covers APIs that mention
     * {@code @deprecated} in their documentation but fail to add
     * {@code @Deprecated} as an annotation.
     * <p>
     * {@code @Override} annotations are deliberately skipped.
     */
  static void writeAnnotations(PrintStream stream, List<AnnotationInstanceInfo> annotations,
          boolean isDeprecated) {
    assert annotations != null;
    for (AnnotationInstanceInfo ann : annotations) {
      // Skip @Override annotations: the stubs do not need it and in some cases it leads
      // to compilation errors with the way the stubs are generated
      if (ann.type() != null && ann.type().qualifiedName().equals("java.lang.Override")) {
        continue;
      }
      if (!ann.type().isHiddenOrRemoved()) {
        stream.println(ann.toString());
        if (isDeprecated && ann.type() != null
            && ann.type().qualifiedName().equals("java.lang.Deprecated")) {
          isDeprecated = false; // Prevent duplicate annotations
        }
      }
    }
    if (isDeprecated) {
      stream.println("@Deprecated");
    }
  }

  static void writeAnnotationElement(PrintStream stream, MethodInfo ann) {
    stream.print(ann.returnType().fullName());
    stream.print(" ");
    stream.print(ann.name());
    stream.print("()");
    AnnotationValueInfo def = ann.defaultAnnotationElementValue();
    if (def != null) {
      stream.print(" default ");
      stream.print(def.valueString());
    }
    stream.println(";");
  }

  static void writeXML(PrintStream xmlWriter, HashMap<PackageInfo, List<ClassInfo>> allClasses,
      HashSet<ClassInfo> notStrippable) {
    // extract the set of packages, sort them by name, and write them out in that order
    Set<PackageInfo> allClassKeys = allClasses.keySet();
    PackageInfo[] allPackages = allClassKeys.toArray(new PackageInfo[allClassKeys.size()]);
    Arrays.sort(allPackages, PackageInfo.comparator);

    xmlWriter.println("<api>");
    for (PackageInfo pack : allPackages) {
      writePackageXML(xmlWriter, pack, allClasses.get(pack), notStrippable);
    }
    xmlWriter.println("</api>");
  }

  public static void writeXml(PrintStream xmlWriter, Collection<PackageInfo> pkgs) {
    final PackageInfo[] packages = pkgs.toArray(new PackageInfo[pkgs.size()]);
    Arrays.sort(packages, PackageInfo.comparator);

    HashSet<ClassInfo> notStrippable = new HashSet();
    for (PackageInfo pkg: packages) {
      for (ClassInfo cl: pkg.allClasses().values()) {
        notStrippable.add(cl);
      }
    }
    xmlWriter.println("<api>");
    for (PackageInfo pkg: packages) {
      writePackageXML(xmlWriter, pkg, pkg.allClasses().values(), notStrippable);
    }
    xmlWriter.println("</api>");
  }

  static void writePackageXML(PrintStream xmlWriter, PackageInfo pack,
      Collection<ClassInfo> classList, HashSet<ClassInfo> notStrippable) {
    ClassInfo[] classes = classList.toArray(new ClassInfo[classList.size()]);
    Arrays.sort(classes, ClassInfo.comparator);
    // Work around the bogus "Array" class we invent for
    // Arrays.copyOf's Class<? extends T[]> newType parameter. (http://b/2715505)
    if (pack.name().equals(PackageInfo.DEFAULT_PACKAGE)) {
      return;
    }
    xmlWriter.println("<package name=\"" + pack.name() + "\"\n"
    // + " source=\"" + pack.position() + "\"\n"
        + ">");
    for (ClassInfo cl : classes) {
      writeClassXML(xmlWriter, cl, notStrippable);
    }
    xmlWriter.println("</package>");


  }

  static void writeClassXML(PrintStream xmlWriter, ClassInfo cl, HashSet<ClassInfo> notStrippable) {
    String scope = cl.scope();
    String deprecatedString = "";
    String declString = (cl.isInterface()) ? "interface" : "class";
    if (cl.isDeprecated()) {
      deprecatedString = "deprecated";
    } else {
      deprecatedString = "not deprecated";
    }
    xmlWriter.println("<" + declString + " name=\"" + cl.name() + "\"");
    if (!cl.isInterface() && !cl.qualifiedName().equals("java.lang.Object")) {
      xmlWriter.println(" extends=\""
          + ((cl.realSuperclass() == null) ? "java.lang.Object" : cl.realSuperclass()
              .qualifiedName()) + "\"");
    }
    xmlWriter.println(" abstract=\"" + cl.isAbstract() + "\"\n" + " static=\"" + cl.isStatic()
        + "\"\n" + " final=\"" + cl.isFinal() + "\"\n" + " deprecated=\"" + deprecatedString
        + "\"\n" + " visibility=\"" + scope + "\"\n"
        // + " source=\"" + cl.position() + "\"\n"
        + ">");

    ArrayList<ClassInfo> interfaces = cl.realInterfaces();
    Collections.sort(interfaces, ClassInfo.comparator);
    for (ClassInfo iface : interfaces) {
      if (notStrippable.contains(iface)) {
        xmlWriter.println("<implements name=\"" + iface.qualifiedName() + "\">");
        xmlWriter.println("</implements>");
      }
    }

    ArrayList<MethodInfo> constructors = cl.constructors();
    Collections.sort(constructors, MethodInfo.comparator);
    for (MethodInfo mi : constructors) {
      writeConstructorXML(xmlWriter, mi);
    }

    ArrayList<MethodInfo> methods = cl.allSelfMethods();
    Collections.sort(methods, MethodInfo.comparator);
    for (MethodInfo mi : methods) {
      if (!methodIsOverride(notStrippable, mi)) {
        writeMethodXML(xmlWriter, mi);
      }
    }

    ArrayList<FieldInfo> fields = cl.selfFields();
    Collections.sort(fields, FieldInfo.comparator);
    for (FieldInfo fi : fields) {
      writeFieldXML(xmlWriter, fi);
    }
    xmlWriter.println("</" + declString + ">");

  }

  static void writeMethodXML(PrintStream xmlWriter, MethodInfo mi) {
    String scope = mi.scope();

    String deprecatedString = "";
    if (mi.isDeprecated()) {
      deprecatedString = "deprecated";
    } else {
      deprecatedString = "not deprecated";
    }
    xmlWriter.println("<method name=\""
        + mi.name()
        + "\"\n"
        + ((mi.returnType() != null) ? " return=\""
            + makeXMLcompliant(fullParameterTypeName(mi, mi.returnType(), false)) + "\"\n" : "")
        + " abstract=\"" + mi.isAbstract() + "\"\n" + " native=\"" + mi.isNative() + "\"\n"
        + " synchronized=\"" + mi.isSynchronized() + "\"\n" + " static=\"" + mi.isStatic() + "\"\n"
        + " final=\"" + mi.isFinal() + "\"\n" + " deprecated=\"" + deprecatedString + "\"\n"
        + " visibility=\"" + scope + "\"\n"
        // + " source=\"" + mi.position() + "\"\n"
        + ">");

    // write parameters in declaration order
    int numParameters = mi.parameters().size();
    int count = 0;
    for (ParameterInfo pi : mi.parameters()) {
      count++;
      writeParameterXML(xmlWriter, mi, pi, count == numParameters);
    }

    // but write exceptions in canonicalized order
    ArrayList<ClassInfo> exceptions = mi.thrownExceptions();
    Collections.sort(exceptions, ClassInfo.comparator);
    for (ClassInfo pi : exceptions) {
      xmlWriter.println("<exception name=\"" + pi.name() + "\" type=\"" + pi.qualifiedName()
          + "\">");
      xmlWriter.println("</exception>");
    }
    xmlWriter.println("</method>");
  }

  static void writeConstructorXML(PrintStream xmlWriter, MethodInfo mi) {
    String scope = mi.scope();
    String deprecatedString = "";
    if (mi.isDeprecated()) {
      deprecatedString = "deprecated";
    } else {
      deprecatedString = "not deprecated";
    }
    xmlWriter.println("<constructor name=\"" + mi.name() + "\"\n" + " type=\""
        + mi.containingClass().qualifiedName() + "\"\n" + " static=\"" + mi.isStatic() + "\"\n"
        + " final=\"" + mi.isFinal() + "\"\n" + " deprecated=\"" + deprecatedString + "\"\n"
        + " visibility=\"" + scope + "\"\n"
        // + " source=\"" + mi.position() + "\"\n"
        + ">");

    int numParameters = mi.parameters().size();
    int count = 0;
    for (ParameterInfo pi : mi.parameters()) {
      count++;
      writeParameterXML(xmlWriter, mi, pi, count == numParameters);
    }

    ArrayList<ClassInfo> exceptions = mi.thrownExceptions();
    Collections.sort(exceptions, ClassInfo.comparator);
    for (ClassInfo pi : exceptions) {
      xmlWriter.println("<exception name=\"" + pi.name() + "\" type=\"" + pi.qualifiedName()
          + "\">");
      xmlWriter.println("</exception>");
    }
    xmlWriter.println("</constructor>");
  }

  static void writeParameterXML(PrintStream xmlWriter, MethodInfo method, ParameterInfo pi,
      boolean isLast) {
    xmlWriter.println("<parameter name=\"" + pi.name() + "\" type=\""
        + makeXMLcompliant(fullParameterTypeName(method, pi.type(), isLast)) + "\">");
    xmlWriter.println("</parameter>");
  }

  static void writeFieldXML(PrintStream xmlWriter, FieldInfo fi) {
    String scope = fi.scope();
    String deprecatedString = "";
    if (fi.isDeprecated()) {
      deprecatedString = "deprecated";
    } else {
      deprecatedString = "not deprecated";
    }
    // need to make sure value is valid XML
    String value = makeXMLcompliant(fi.constantLiteralValue());

    String fullTypeName = makeXMLcompliant(fi.type().fullName());

    xmlWriter.println("<field name=\"" + fi.name() + "\"\n" + " type=\"" + fullTypeName + "\"\n"
        + " transient=\"" + fi.isTransient() + "\"\n" + " volatile=\"" + fi.isVolatile() + "\"\n"
        + (fieldIsInitialized(fi) ? " value=\"" + value + "\"\n" : "") + " static=\""
        + fi.isStatic() + "\"\n" + " final=\"" + fi.isFinal() + "\"\n" + " deprecated=\""
        + deprecatedString + "\"\n" + " visibility=\"" + scope + "\"\n"
        // + " source=\"" + fi.position() + "\"\n"
        + ">");
    xmlWriter.println("</field>");
  }

  static String makeXMLcompliant(String s) {
    String returnString = "";
    returnString = s.replaceAll("&", "&amp;");
    returnString = returnString.replaceAll("<", "&lt;");
    returnString = returnString.replaceAll(">", "&gt;");
    returnString = returnString.replaceAll("\"", "&quot;");
    returnString = returnString.replaceAll("'", "&apos;");
    return returnString;
  }

  static void writeRemovedApi(PrintStream apiWriter, HashMap<PackageInfo,
      List<ClassInfo>> allPackageClassMap, Set<ClassInfo> notStrippable) {
    final PackageInfo[] packages = allPackageClassMap.keySet().toArray(new PackageInfo[0]);
    Arrays.sort(packages, PackageInfo.comparator);
    for (PackageInfo pkg : packages) {
      // beware that pkg.allClasses() has no class in it at the moment
      final List<ClassInfo> classes = allPackageClassMap.get(pkg);
      Collections.sort(classes, ClassInfo.comparator);
      boolean hasWrittenPackageHead = false;
      for (ClassInfo cl : classes) {
        if (cl.hasRemovedSelfMembers()) {
          if (!hasWrittenPackageHead) {
            hasWrittenPackageHead = true;
            apiWriter.print("package ");
            apiWriter.print(pkg.qualifiedName());
            apiWriter.print(" {\n\n");
          }
          writeClassRemovedSelfMembers(apiWriter, cl, notStrippable);
        }
      }

      // the package contains some classes with some removed members
      if (hasWrittenPackageHead) {
        apiWriter.print("}\n\n");
      }
    }
  }

  /**
   * Write the removed members of the class to removed.txt
   */
  private static void writeClassRemovedSelfMembers(PrintStream apiWriter, ClassInfo cl,
      Set<ClassInfo> notStrippable) {
    apiWriter.print("  ");
    apiWriter.print(cl.scope());
    if (cl.isStatic()) {
      apiWriter.print(" static");
    }
    if (cl.isFinal()) {
      apiWriter.print(" final");
    }
    if (cl.isAbstract()) {
      apiWriter.print(" abstract");
    }
    if (cl.isDeprecated()) {
      apiWriter.print(" deprecated");
    }
    apiWriter.print(" ");
    apiWriter.print(cl.isInterface() ? "interface" : "class");
    apiWriter.print(" ");
    apiWriter.print(cl.name());

    if (!cl.isInterface()
        && !"java.lang.Object".equals(cl.qualifiedName())
        && cl.realSuperclass() != null
        && !"java.lang.Object".equals(cl.realSuperclass().qualifiedName())) {
      apiWriter.print(" extends ");
      apiWriter.print(cl.realSuperclass().qualifiedName());
    }

    ArrayList<ClassInfo> interfaces = cl.realInterfaces();
    Collections.sort(interfaces, ClassInfo.comparator);
    boolean first = true;
    for (ClassInfo iface : interfaces) {
      if (notStrippable.contains(iface)) {
        if (first) {
          apiWriter.print(" implements");
          first = false;
        }
        apiWriter.print(" ");
        apiWriter.print(iface.qualifiedName());
      }
    }

    apiWriter.print(" {\n");

    List<MethodInfo> constructors = cl.getRemovedConstructors();
    for (MethodInfo mi : constructors) {
      writeConstructorApi(apiWriter, mi);
    }

    List<MethodInfo> methods = cl.getRemovedSelfMethods();
    for (MethodInfo mi : methods) {
      writeMethodApi(apiWriter, mi);
    }

    List<FieldInfo> enums = cl.getRemovedSelfEnumConstants();
    for (FieldInfo fi : enums) {
      writeFieldApi(apiWriter, fi, "enum_constant");
    }

    List<FieldInfo> fields = cl.getRemovedSelfFields();
    for (FieldInfo fi : fields) {
      writeFieldApi(apiWriter, fi, "field");
    }

    apiWriter.print("  }\n\n");
  }

  public static void writeApi(PrintStream apiWriter, Collection<PackageInfo> pkgs) {
    final PackageInfo[] packages = pkgs.toArray(new PackageInfo[pkgs.size()]);
    Arrays.sort(packages, PackageInfo.comparator);

    HashSet<ClassInfo> notStrippable = new HashSet();
    for (PackageInfo pkg: packages) {
      for (ClassInfo cl: pkg.allClasses().values()) {
        notStrippable.add(cl);
      }
    }
    for (PackageInfo pkg: packages) {
      writePackageApi(apiWriter, pkg, pkg.allClasses().values(), notStrippable);
    }
  }

  static void writeApi(PrintStream apiWriter, HashMap<PackageInfo, List<ClassInfo>> allClasses,
      HashSet<ClassInfo> notStrippable) {
    // extract the set of packages, sort them by name, and write them out in that order
    Set<PackageInfo> allClassKeys = allClasses.keySet();
    PackageInfo[] allPackages = allClassKeys.toArray(new PackageInfo[allClassKeys.size()]);
    Arrays.sort(allPackages, PackageInfo.comparator);

    for (PackageInfo pack : allPackages) {
      writePackageApi(apiWriter, pack, allClasses.get(pack), notStrippable);
    }
  }

  static void writePackageApi(PrintStream apiWriter, PackageInfo pack,
      Collection<ClassInfo> classList, HashSet<ClassInfo> notStrippable) {
    // Work around the bogus "Array" class we invent for
    // Arrays.copyOf's Class<? extends T[]> newType parameter. (http://b/2715505)
    if (pack.name().equals(PackageInfo.DEFAULT_PACKAGE)) {
      return;
    }

    apiWriter.print("package ");
    apiWriter.print(pack.qualifiedName());
    apiWriter.print(" {\n\n");

    ClassInfo[] classes = classList.toArray(new ClassInfo[classList.size()]);
    Arrays.sort(classes, ClassInfo.comparator);
    for (ClassInfo cl : classes) {
      writeClassApi(apiWriter, cl, notStrippable);
    }

    apiWriter.print("}\n\n");
  }

  static void writeClassApi(PrintStream apiWriter, ClassInfo cl, HashSet<ClassInfo> notStrippable) {
    boolean first;

    apiWriter.print("  ");
    apiWriter.print(cl.scope());
    if (cl.isStatic()) {
      apiWriter.print(" static");
    }
    if (cl.isFinal()) {
      apiWriter.print(" final");
    }
    if (cl.isAbstract()) {
      apiWriter.print(" abstract");
    }
    if (cl.isDeprecated()) {
      apiWriter.print(" deprecated");
    }
    apiWriter.print(" ");
    apiWriter.print(cl.isInterface() ? "interface" : "class");
    apiWriter.print(" ");
    apiWriter.print(cl.name());

    if (!cl.isInterface()
        && !"java.lang.Object".equals(cl.qualifiedName())
        && cl.realSuperclass() != null
        && !"java.lang.Object".equals(cl.realSuperclass().qualifiedName())) {
      apiWriter.print(" extends ");
      apiWriter.print(cl.realSuperclass().qualifiedName());
    }

    ArrayList<ClassInfo> interfaces = cl.realInterfaces();
    Collections.sort(interfaces, ClassInfo.comparator);
    first = true;
    for (ClassInfo iface : interfaces) {
      if (notStrippable.contains(iface)) {
        if (first) {
          apiWriter.print(" implements");
          first = false;
        }
        apiWriter.print(" ");
        apiWriter.print(iface.qualifiedName());
      }
    }

    apiWriter.print(" {\n");

    ArrayList<MethodInfo> constructors = cl.constructors();
    Collections.sort(constructors, MethodInfo.comparator);
    for (MethodInfo mi : constructors) {
      writeConstructorApi(apiWriter, mi);
    }

    ArrayList<MethodInfo> methods = cl.allSelfMethods();
    Collections.sort(methods, MethodInfo.comparator);
    for (MethodInfo mi : methods) {
      if (!methodIsOverride(notStrippable, mi)) {
        writeMethodApi(apiWriter, mi);
      }
    }

    ArrayList<FieldInfo> enums = cl.enumConstants();
    Collections.sort(enums, FieldInfo.comparator);
    for (FieldInfo fi : enums) {
      writeFieldApi(apiWriter, fi, "enum_constant");
    }

    ArrayList<FieldInfo> fields = cl.selfFields();
    Collections.sort(fields, FieldInfo.comparator);
    for (FieldInfo fi : fields) {
      writeFieldApi(apiWriter, fi, "field");
    }

    apiWriter.print("  }\n\n");
  }

  static void writeConstructorApi(PrintStream apiWriter, MethodInfo mi) {
    apiWriter.print("    ctor ");
    apiWriter.print(mi.scope());
    if (mi.isDeprecated()) {
      apiWriter.print(" deprecated");
    }
    apiWriter.print(" ");
    apiWriter.print(mi.name());

    writeParametersApi(apiWriter, mi, mi.parameters());
    writeThrowsApi(apiWriter, mi.thrownExceptions());
    apiWriter.print(";\n");
  }

  static void writeMethodApi(PrintStream apiWriter, MethodInfo mi) {
    apiWriter.print("    method ");
    apiWriter.print(mi.scope());
    if (mi.isStatic()) {
      apiWriter.print(" static");
    }
    if (mi.isFinal()) {
      apiWriter.print(" final");
    }
    if (mi.isAbstract()) {
      apiWriter.print(" abstract");
    }
    if (mi.isDeprecated()) {
      apiWriter.print(" deprecated");
    }
    if (mi.isSynchronized()) {
      apiWriter.print(" synchronized");
    }
    apiWriter.print(" ");
    if (mi.returnType() == null) {
      apiWriter.print("void");
    } else {
      apiWriter.print(fullParameterTypeName(mi, mi.returnType(), false));
    }
    apiWriter.print(" ");
    apiWriter.print(mi.name());

    writeParametersApi(apiWriter, mi, mi.parameters());
    writeThrowsApi(apiWriter, mi.thrownExceptions());

    apiWriter.print(";\n");
  }

  static void writeParametersApi(PrintStream apiWriter, MethodInfo method,
      ArrayList<ParameterInfo> params) {
    apiWriter.print("(");

    for (ParameterInfo pi : params) {
      if (pi != params.get(0)) {
        apiWriter.print(", ");
      }
      apiWriter.print(fullParameterTypeName(method, pi.type(), pi == params.get(params.size()-1)));
      // turn on to write the names too
      if (false) {
        apiWriter.print(" ");
        apiWriter.print(pi.name());
      }
    }

    apiWriter.print(")");
  }

  static void writeThrowsApi(PrintStream apiWriter, ArrayList<ClassInfo> exceptions) {
    // write in a canonical order
    exceptions = (ArrayList<ClassInfo>) exceptions.clone();
    Collections.sort(exceptions, ClassInfo.comparator);
    //final int N = exceptions.length;
    boolean first = true;
    for (ClassInfo ex : exceptions) {
      // Turn this off, b/c we need to regenrate the old xml files.
      if (true || !"java.lang.RuntimeException".equals(ex.qualifiedName())
          && !ex.isDerivedFrom("java.lang.RuntimeException")) {
        if (first) {
          apiWriter.print(" throws ");
          first = false;
        } else {
          apiWriter.print(", ");
        }
        apiWriter.print(ex.qualifiedName());
      }
    }
  }

  static void writeFieldApi(PrintStream apiWriter, FieldInfo fi, String label) {
    apiWriter.print("    ");
    apiWriter.print(label);
    apiWriter.print(" ");
    apiWriter.print(fi.scope());
    if (fi.isStatic()) {
      apiWriter.print(" static");
    }
    if (fi.isFinal()) {
      apiWriter.print(" final");
    }
    if (fi.isDeprecated()) {
      apiWriter.print(" deprecated");
    }
    if (fi.isTransient()) {
      apiWriter.print(" transient");
    }
    if (fi.isVolatile()) {
      apiWriter.print(" volatile");
    }

    apiWriter.print(" ");
    apiWriter.print(fi.type().fullName());

    apiWriter.print(" ");
    apiWriter.print(fi.name());

    Object val = null;
    if (fi.isConstant() && fieldIsInitialized(fi)) {
      apiWriter.print(" = ");
      apiWriter.print(fi.constantLiteralValue());
      val = fi.constantValue();
    }

    apiWriter.print(";");

    if (val != null) {
      if (val instanceof Integer && "char".equals(fi.type().qualifiedTypeName())) {
        apiWriter.format(" // 0x%04x '%s'", val,
            FieldInfo.javaEscapeString("" + ((char)((Integer)val).intValue())));
      } else if (val instanceof Byte || val instanceof Short || val instanceof Integer) {
        apiWriter.format(" // 0x%x", val);
      } else if (val instanceof Long) {
        apiWriter.format(" // 0x%xL", val);
      }
    }

    apiWriter.print("\n");
  }

  static void writeKeepList(PrintStream keepListWriter,
      HashMap<PackageInfo, List<ClassInfo>> allClasses, HashSet<ClassInfo> notStrippable) {
    // extract the set of packages, sort them by name, and write them out in that order
    Set<PackageInfo> allClassKeys = allClasses.keySet();
    PackageInfo[] allPackages = allClassKeys.toArray(new PackageInfo[allClassKeys.size()]);
    Arrays.sort(allPackages, PackageInfo.comparator);

    for (PackageInfo pack : allPackages) {
      writePackageKeepList(keepListWriter, pack, allClasses.get(pack), notStrippable);
    }
  }

  static void writePackageKeepList(PrintStream keepListWriter, PackageInfo pack,
      Collection<ClassInfo> classList, HashSet<ClassInfo> notStrippable) {
    // Work around the bogus "Array" class we invent for
    // Arrays.copyOf's Class<? extends T[]> newType parameter. (http://b/2715505)
    if (pack.name().equals(PackageInfo.DEFAULT_PACKAGE)) {
      return;
    }

    ClassInfo[] classes = classList.toArray(new ClassInfo[classList.size()]);
    Arrays.sort(classes, ClassInfo.comparator);
    for (ClassInfo cl : classes) {
      writeClassKeepList(keepListWriter, cl, notStrippable);
    }
  }

  static void writeClassKeepList(PrintStream keepListWriter, ClassInfo cl,
      HashSet<ClassInfo> notStrippable) {
    keepListWriter.print("-keep class ");
    keepListWriter.print(to$Class(cl.qualifiedName()));

    keepListWriter.print(" {\n");

    ArrayList<MethodInfo> constructors = cl.constructors();
    Collections.sort(constructors, MethodInfo.comparator);
    for (MethodInfo mi : constructors) {
      writeConstructorKeepList(keepListWriter, mi);
    }

    keepListWriter.print("\n");

    ArrayList<MethodInfo> methods = cl.allSelfMethods();
    Collections.sort(methods, MethodInfo.comparator);
    for (MethodInfo mi : methods) {
      // allSelfMethods is the non-hidden and visible methods. See Doclava.checkLevel.
      writeMethodKeepList(keepListWriter, mi);
    }

    keepListWriter.print("\n");

    ArrayList<FieldInfo> enums = cl.enumConstants();
    Collections.sort(enums, FieldInfo.comparator);
    for (FieldInfo fi : enums) {
      writeFieldKeepList(keepListWriter, fi);
    }

    keepListWriter.print("\n");

    ArrayList<FieldInfo> fields = cl.selfFields();
    Collections.sort(fields, FieldInfo.comparator);
    for (FieldInfo fi : fields) {
      writeFieldKeepList(keepListWriter, fi);
    }

    keepListWriter.print("}\n\n");
  }

  static void writeConstructorKeepList(PrintStream keepListWriter, MethodInfo mi) {
    keepListWriter.print("    ");
    String name = mi.name();
    name = name.replace(".", "$");
    keepListWriter.print(name);

    writeParametersKeepList(keepListWriter, mi, mi.parameters());
    keepListWriter.print(";\n");
  }

  static void writeMethodKeepList(PrintStream keepListWriter, MethodInfo mi) {
    keepListWriter.print("    ");
    keepListWriter.print(mi.scope());
    if (mi.isStatic()) {
      keepListWriter.print(" static");
    }
    if (mi.isAbstract()) {
      keepListWriter.print(" abstract");
    }
    if (mi.isSynchronized()) {
      keepListWriter.print(" synchronized");
    }
    keepListWriter.print(" ");
    if (mi.returnType() == null) {
      keepListWriter.print("void");
    } else {
      keepListWriter.print(getCleanTypeName(mi.returnType()));
    }
    keepListWriter.print(" ");
    keepListWriter.print(mi.name());

    writeParametersKeepList(keepListWriter, mi, mi.parameters());

    keepListWriter.print(";\n");
  }

  static void writeParametersKeepList(PrintStream keepListWriter, MethodInfo method,
      ArrayList<ParameterInfo> params) {
    keepListWriter.print("(");

    for (ParameterInfo pi : params) {
      if (pi != params.get(0)) {
        keepListWriter.print(", ");
      }
      keepListWriter.print(getCleanTypeName(pi.type()));
    }

    keepListWriter.print(")");
  }

  static void writeFieldKeepList(PrintStream keepListWriter, FieldInfo fi) {
    keepListWriter.print("    ");
    keepListWriter.print(fi.scope());
    if (fi.isStatic()) {
      keepListWriter.print(" static");
    }
    if (fi.isTransient()) {
      keepListWriter.print(" transient");
    }
    if (fi.isVolatile()) {
      keepListWriter.print(" volatile");
    }

    keepListWriter.print(" ");
    keepListWriter.print(getCleanTypeName(fi.type()));

    keepListWriter.print(" ");
    keepListWriter.print(fi.name());

    keepListWriter.print(";\n");
  }

  static String fullParameterTypeName(MethodInfo method, TypeInfo type, boolean isLast) {
    String fullTypeName = type.fullName(method.typeVariables());
    if (isLast && method.isVarArgs()) {
      // TODO: note that this does not attempt to handle hypothetical
      // vararg methods whose last parameter is a list of arrays, e.g.
      // "Object[]...".
      fullTypeName = type.fullNameNoDimension(method.typeVariables()) + "...";
    }
    return fullTypeName;
  }

  static String to$Class(String name) {
    int pos = 0;
    while ((pos = name.indexOf('.', pos)) > 0) {
      String n = name.substring(0, pos);
      if (Converter.obtainClass(n) != null) {
        return n + (name.substring(pos).replace('.', '$'));
      }
      pos = pos + 1;
    }
    return name;
  }

  static String getCleanTypeName(TypeInfo t) {
      return t.isPrimitive() ? t.simpleTypeName() + t.dimension() :
              to$Class(t.asClassInfo().qualifiedName() + t.dimension());
  }
}
