/* gnu.classpath.tools.doclets.AbstractDoclet
   Copyright (C) 2004, 2012 Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

GNU Classpath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */

package gnu.classpath.tools.doclets;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.Doclet;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import com.sun.javadoc.Type;

import com.sun.tools.doclets.Taglet;

import gnu.classpath.tools.taglets.GnuExtendedTaglet;
import gnu.classpath.tools.taglets.AuthorTaglet;
import gnu.classpath.tools.taglets.CodeTaglet;
import gnu.classpath.tools.taglets.DeprecatedTaglet;
import gnu.classpath.tools.taglets.GenericTaglet;
import gnu.classpath.tools.taglets.SinceTaglet;
import gnu.classpath.tools.taglets.ValueTaglet;
import gnu.classpath.tools.taglets.VersionTaglet;
import gnu.classpath.tools.taglets.TagletContext;

import gnu.classpath.tools.IOToolkit;
import gnu.classpath.tools.FileSystemClassLoader;

import java.io.File;
import java.io.IOException;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.InvocationTargetException;

import java.text.MessageFormat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 *  An abstract Doclet implementation with helpers for common tasks
 *  performed by Doclets.
 */
public abstract class AbstractDoclet
{
   /**
    *  Mapping from tag type to Taglet for user Taglets specified on
    *  the command line.
    */
   protected Map<String,Taglet> tagletMap = new LinkedHashMap<String,Taglet>();

   /**
    *  Stores the package groups specified in the user
    *  options. Contains objects of type PackageGroup.
    */
   private List<PackageGroup> packageGroups = new LinkedList<PackageGroup>();

   /**
    *  Keeps track of the tags mentioned by the user during option
    *  processiong so that an error can be emitted if a tag is
    *  mentioned more than once.
    */
   private List<Taglet> mentionedTags = new LinkedList<Taglet>();

   public static int optionLength(String option) {
      return instance.getOptionLength(option);
   }

   public static boolean validOptions(String[][] options) {
      return true;
   }

   private static AbstractDoclet instance;

   protected static void setInstance(AbstractDoclet instance)
   {
      AbstractDoclet.instance = instance;
   }

   protected abstract void run()
      throws DocletConfigurationException, IOException;

   public static boolean start(RootDoc rootDoc)
   {
      try {

         instance.startInstance(rootDoc);
         return true;
      }
      catch (DocletConfigurationException e) {
         instance.printError(e.getMessage());
         return false;
      }
      catch (Exception e) {
         e.printStackTrace();
         return false;
      }
   }

   protected RootDoc getRootDoc()
   {
      return this.rootDoc;
   }

   private RootDoc rootDoc;

   protected abstract InlineTagRenderer getInlineTagRenderer();

   private void startInstance(RootDoc rootDoc)
      throws DocletConfigurationException, IOException
   {
      this.rootDoc = rootDoc;

      // Set the default Taglet order

      registerTaglet(new VersionTaglet());
      registerTaglet(new AuthorTaglet());
      registerTaglet(new SinceTaglet(getInlineTagRenderer()));
      registerTaglet(new StandardTaglet("serial"));
      registerTaglet(new StandardTaglet("deprecated"));
      registerTaglet(new StandardTaglet("see"));
      registerTaglet(new StandardTaglet("param"));
      registerTaglet(new StandardTaglet("return"));

      registerTaglet(new ValueTaglet());
      registerTaglet(new CodeTaglet());

      // Process command line options

      for (int i=0, ilim=rootDoc.options().length; i<ilim; ++i) {

         String[] optionArr = rootDoc.options()[i];
         String _optionTag = optionArr[0];

         DocletOption option = (DocletOption)nameToOptionMap.get(_optionTag.toLowerCase());

         if (null != option) {
            option.set(optionArr);
         }
      }

      // Enable/disable standard taglets based on user input

      AuthorTaglet.setTagletEnabled(optionAuthor.getValue());
      VersionTaglet.setTagletEnabled(optionVersion.getValue());
      SinceTaglet.setTagletEnabled(!optionNoSince.getValue());
      DeprecatedTaglet.setTagletEnabled(!optionNoDeprecated.getValue());

      if (!getTargetDirectory().exists()) {
         if (!getTargetDirectory().mkdirs()) {
            throw new DocletConfigurationException("Cannot create target directory "
                                                   + getTargetDirectory());
         }
      }

      run();
   }

   public File getTargetDirectory()
   {
      return optionTargetDirectory.getValue();
   }

   private DocletOptionFile optionTargetDirectory =
     new DocletOptionFile("-d",
                          new File(System.getProperty("user.dir")));

   private DocletOptionFlag optionAuthor =
     new DocletOptionFlag("-author");

   private DocletOptionFlag optionVersion =
     new DocletOptionFlag("-version");

   private DocletOptionFlag optionNoSince =
     new DocletOptionFlag("-nosince");

   private DocletOptionFlag optionNoDeprecated =
     new DocletOptionFlag("-nodeprecated");

   private DocletOptionGroup optionGroup =
     new DocletOptionGroup("-group");

   private DocletOptionPackageWildcard optionNoQualifier =
     new DocletOptionPackageWildcard("-noqualifier", true);

   private DocletOptionFlag optionDocFilesSubDirs =
     new DocletOptionFlag("-docfilessubdirs");

   private DocletOptionColonSeparated optionExcludeDocFilesSubDir =
     new DocletOptionColonSeparated("-excludedocfilessubdir");

   private DocletOptionTag optionTaglet =
     new DocletOptionTag("-taglet");

   private DocletOptionTag optionTag =
     new DocletOptionTag("-tag");

   private class DocletOptionGroup
      extends DocletOption
   {
      DocletOptionGroup(String optionName)
      {
         super(optionName);
      }

      public int getLength()
      {
         return 3;
      }

      public boolean set(String[] optionArr)
      {
         try {
            PackageMatcher packageMatcher = new PackageMatcher();

            StringTokenizer tokenizer = new StringTokenizer(optionArr[2], ":");
            while (tokenizer.hasMoreTokens()) {
               String packageWildcard = tokenizer.nextToken();
               packageMatcher.addWildcard(packageWildcard);
            }

            SortedSet<PackageDoc> groupPackages = packageMatcher.filter(rootDoc.specifiedPackages());

            packageGroups.add(new PackageGroup(optionArr[1], groupPackages));

            return true;
         }
         catch (InvalidPackageWildcardException e) {
            return false;
         }
      }
   }

   private class DocletOptionTag
      extends DocletOption
   {
      DocletOptionTag(String optionName)
      {
         super(optionName);
      }

      public int getLength()
      {
         return 2;
      }

      public boolean set(String[] optionArr)
      {
         String tagSpec = optionArr[1];
         boolean validTagSpec = false;
         int ndx1 = tagSpec.indexOf(':');
         if (ndx1 < 0) {
            Taglet taglet = (Taglet)tagletMap.get(tagSpec);
            if (null == taglet) {
               printError("There is no standard tag '" + tagSpec + "'.");
            }
            else {
               if (mentionedTags.contains(taglet)) {
                  printError("Tag '" + tagSpec + "' has been added or moved before.");
               }
               else {
                  mentionedTags.add(taglet);

                  // re-append taglet
                  tagletMap.remove(tagSpec);
                  tagletMap.put(tagSpec, taglet);
               }
            }
         }
         else {
            int ndx2 = tagSpec.indexOf(':', ndx1 + 1);
            if (ndx2 > ndx1 && ndx2 < tagSpec.length() - 1) {
               String tagName = tagSpec.substring(0, ndx1);
               String tagHead = null;
               if (tagSpec.charAt(ndx2 + 1) == '\"') {
                  if (tagSpec.charAt(tagSpec.length() - 1) == '\"') {
                     tagHead = tagSpec.substring(ndx2 + 2, tagSpec.length() - 1);
                     validTagSpec = true;
                  }
               }
               else {
                  tagHead = tagSpec.substring(ndx2 + 1);
                  validTagSpec = true;
               }

               boolean tagScopeOverview = false;
               boolean tagScopePackages = false;
               boolean tagScopeTypes = false;
               boolean tagScopeConstructors = false;
               boolean tagScopeMethods = false;
               boolean tagScopeFields = false;
               boolean tagDisabled = false;

            tag_option_loop:
               for (int n=ndx1+1; n<ndx2; ++n) {
                  switch (tagSpec.charAt(n)) {
                  case 'X':
                     tagDisabled = true;
                     break;
                  case 'a':
                     tagScopeOverview = true;
                     tagScopePackages = true;
                     tagScopeTypes = true;
                     tagScopeConstructors = true;
                     tagScopeMethods = true;
                     tagScopeFields = true;
                     break;
                  case 'o':
                     tagScopeOverview = true;
                     break;
                  case 'p':
                     tagScopePackages = true;
                     break;
                  case 't':
                     tagScopeTypes = true;
                     break;
                  case 'c':
                     tagScopeConstructors = true;
                     break;
                  case 'm':
                     tagScopeMethods = true;
                     break;
                  case 'f':
                     tagScopeFields = true;
                     break;
                  default:
                     validTagSpec = false;
                     break tag_option_loop;
                  }
               }

               if (validTagSpec) {
                  GenericTaglet taglet
                     = new GenericTaglet(tagName,
                                         tagHead,
                                         tagScopeOverview,
                                         tagScopePackages,
                                         tagScopeTypes,
                                         tagScopeConstructors,
                                         tagScopeMethods,
                                         tagScopeFields);
                  taglet.setTagletEnabled(!tagDisabled);
                  taglet.register(tagletMap);
                  mentionedTags.add(taglet);
               }
            }
         }
         if (!validTagSpec) {
            printError("Value for option -tag must be in format \"<tagname>:Xaoptcmf:<taghead>\".");
         }
         return validTagSpec;
      }
   }

   private DocletOption[] commonOptions =
      {
         optionTargetDirectory,
         optionAuthor,
         optionVersion,
         optionNoSince,
         optionNoDeprecated,
         optionGroup,
         optionDocFilesSubDirs,
         optionExcludeDocFilesSubDir,
         optionTaglet,
         optionTag,
      };

   private void registerOptions()
   {
      if (!optionsRegistered) {
         for (int i=0; i<commonOptions.length; ++i) {
            DocletOption option = commonOptions[i];
            registerOption(option);
         }
         DocletOption[] docletOptions = getOptions();
         for (int i=0; i<docletOptions.length; ++i) {
            DocletOption option = docletOptions[i];
            registerOption(option);
         }
         optionsRegistered = true;
      }
   }

   protected abstract DocletOption[] getOptions();

   private boolean optionsRegistered = false;

   private void registerOption(DocletOption option)
   {
      nameToOptionMap.put(option.getName(), option);
   }

   private Map<String,DocletOption> nameToOptionMap = new HashMap<String,DocletOption>();

   private int getOptionLength(String optionName)
   {
      registerOptions();
      DocletOption option = nameToOptionMap.get(optionName.toLowerCase());
      if (null != option) {
         return option.getLength();
      }
      else {
         return -1;
      }
   }

   protected List<ClassDoc> getKnownDirectSubclasses(ClassDoc classDoc)
   {
      List<ClassDoc> result = new LinkedList<ClassDoc>();
      if (!"java.lang.Object".equals(classDoc.qualifiedName())) {
         ClassDoc[] classes = rootDoc.classes();
         for (int i=0; i<classes.length; ++i) {
            if (classDoc == classes[i].superclass()) {
               result.add(classes[i]);
            }
         }
      }
      return result;
   }

   protected static class IndexKey
      implements Comparable<IndexKey>
   {
      private String name;
      private String lowerName;

      public IndexKey(String name)
      {
         this.name = name;
         this.lowerName = name.toLowerCase();
      }

      public boolean equals(Object other)
      {
         return this.lowerName.equals(((IndexKey)other).lowerName);
      }

      public int hashCode()
      {
         return lowerName.hashCode();
      }

      public int compareTo(IndexKey ik)
      {
         return lowerName.compareTo(ik.lowerName);
      }

      public String getName()
      {
         return name;
      }
   }

   private Map<Character,List<Doc>> categorizedIndex;

   protected Map<Character,List<Doc>> getCategorizedIndex()
   {
      if (null == categorizedIndex) {
         categorizedIndex = new LinkedHashMap<Character,List<Doc>>();

         Map<IndexKey,Doc> indexMap = getIndexByName();
         LinkedList<IndexKey> keys = new LinkedList<IndexKey>(); //indexMap.keySet().size());
         keys.addAll(indexMap.keySet());
         Collections.sort(keys);
         Iterator<IndexKey> it = keys.iterator(); //indexMap.keySet().iterator();
         char previousCategoryLetter = '\0';
         Character keyLetter = null;
         while (it.hasNext()) {
            IndexKey key = it.next();
            char firstChar = Character.toUpperCase(key.getName().charAt(0));
            if (firstChar != previousCategoryLetter) {
               keyLetter = new Character(firstChar);
               previousCategoryLetter = firstChar;
               categorizedIndex.put(keyLetter, new LinkedList<Doc>());
            }
            List<Doc> letterList = categorizedIndex.get(keyLetter);
            letterList.add(indexMap.get(key));
         }
      }

      return categorizedIndex;
   }


   private Map<IndexKey,Doc> indexByName;

   protected Map<IndexKey,Doc> getIndexByName()
   {
      if (null == indexByName) {
         // Create index

         // Collect index

         indexByName = new HashMap<IndexKey,Doc>(); //TreeMap();

         // Add packages to index

         PackageDoc[] packages = rootDoc.specifiedPackages();
         for (int i=0, ilim=packages.length; i<ilim; ++i) {
            PackageDoc c = packages[i];
            if (c.name().length() > 0) {
               indexByName.put(new IndexKey(c.name()), c);
            }
         }

         // Add classes, fields and methods to index

         ClassDoc[] sumclasses = rootDoc.classes();
         for (int i=0, ilim=sumclasses.length; i<ilim; ++i) {
            ClassDoc c = sumclasses[i];
            if (null == c.containingClass()) {
               indexByName.put(new IndexKey(c.name() + " " + c.containingPackage().name()), c);
            }
            else {
               indexByName.put(new IndexKey(c.name().substring(c.containingClass().name().length() + 1)
                                            + " " + c.containingClass().name() + " " + c.containingPackage().name()), c);
            }
            FieldDoc[] fields = c.fields();
            for (int j=0, jlim=fields.length; j<jlim; ++j) {
               indexByName.put(new IndexKey(fields[j].name() + " " + fields[j].containingClass().name() + " " + fields[j].containingPackage().name()), fields[j]);
            }
            MethodDoc[] methods = c.methods();
            for (int j=0, jlim=methods.length; j<jlim; ++j) {
               MethodDoc method = methods[j];
               indexByName.put(new IndexKey(method.name() + method.signature() + " " + method.containingClass().name() + " " + method.containingPackage().name()), method);
            }
            ConstructorDoc[] constructors = c.constructors();
            for (int j=0, jlim=constructors.length; j<jlim; ++j) {
               ConstructorDoc constructor = constructors[j];
               indexByName.put(new IndexKey(constructor.name() + constructor.signature() + " " + constructor.containingClass().name() + " " + constructor.containingPackage().name()), constructor);
            }
         }
      }
      return indexByName;
   }

   private void registerTaglet(Taglet taglet)
   {
      tagletMap.put(taglet.getName(), taglet);
   }

   protected void printTaglets(Tag[] tags, TagletContext context, TagletPrinter output, boolean inline)
   {
      for (Iterator<String> it = tagletMap.keySet().iterator(); it.hasNext(); ) {
         String tagName = it.next();
         Taglet taglet = tagletMap.get(tagName);
         Doc doc = context.getDoc();
         if (inline == taglet.isInlineTag()
             && ((doc == null
                  && taglet.inOverview())
                 || (doc != null
                     && ((doc.isConstructor() && taglet.inConstructor())
                         || (doc.isField() && taglet.inField())
                         || (doc.isMethod() && taglet.inMethod())
                         || (doc instanceof PackageDoc && taglet.inPackage())
                         || ((doc.isClass() || doc.isInterface()) && taglet.inType()))))) {

            List<Tag> tagsOfThisType = new LinkedList<Tag>();
            for (int i=0; i<tags.length; ++i) {
               if (tags[i].name().substring(1).equals(tagName)) {
                  tagsOfThisType.add(tags[i]);
               }
            }

            Tag[] tagletTags = tagsOfThisType.toArray(new Tag[tagsOfThisType.size()]);

            String tagletString;
            if (taglet instanceof StandardTaglet) {
               tagletString = renderTag(tagName, tagletTags, context);
            }
            else if (taglet instanceof GnuExtendedTaglet) {
               tagletString = ((GnuExtendedTaglet)taglet).toString(tagletTags, context);
            }
            else {
               tagletString = taglet.toString(tagletTags);
            }
            if (null != tagletString) {
               output.printTagletString(tagletString);
            }
         }
      }
   }

   protected void printInlineTaglet(Tag tag, TagletContext context, TagletPrinter output)
   {
      Taglet taglet = (Taglet)tagletMap.get(tag.name().substring(1));
      if (null != taglet) {
         String tagletString;
         if (taglet instanceof GnuExtendedTaglet) {
            tagletString = ((GnuExtendedTaglet)taglet).toString(tag, context);
         }
         else {
            tagletString = taglet.toString(tag);
         }
         if (null != tagletString) {
            output.printTagletString(tagletString);
         }
      }
      else {
         printWarning("Unknown tag: " + tag.name());
      }
   }

   protected void printMainTaglets(Tag[] tags, TagletContext context, TagletPrinter output)
   {
      printTaglets(tags, context, output, false);
   }

   /**
    *  @param usedClassToPackagesMap  ClassDoc to (PackageDoc to (UsageType to (Set of Doc)))
    */
   private void addUsedBy(Map<ClassDoc,Map<PackageDoc,Map<UsageType,Set<Doc>>>> usedClassToPackagesMap,
                          ClassDoc usedClass, UsageType usageType, Doc user, PackageDoc userPackage)
   {
      Map<PackageDoc,Map<UsageType,Set<Doc>>> packageToUsageTypeMap = usedClassToPackagesMap.get(usedClass);
      if (null == packageToUsageTypeMap) {
         packageToUsageTypeMap = new HashMap<PackageDoc,Map<UsageType,Set<Doc>>>();
         usedClassToPackagesMap.put(usedClass, packageToUsageTypeMap);
      }

      Map<UsageType,Set<Doc>> usageTypeToUsersMap = packageToUsageTypeMap.get(userPackage);
      if (null == usageTypeToUsersMap) {
        usageTypeToUsersMap = new TreeMap<UsageType,Set<Doc>>();
         packageToUsageTypeMap.put(userPackage, usageTypeToUsersMap);
      }

      Set<Doc> userSet = usageTypeToUsersMap.get(usageType);
      if (null == userSet) {
         userSet = new TreeSet<Doc>(); // FIXME: we need the collator from Main here
         usageTypeToUsersMap.put(usageType, userSet);
      }
      userSet.add(user);
   }

   /**
    *  Create the cross reference database.
    */
   private Map collectUsage() {

      Map<ClassDoc,Map<PackageDoc,Map<UsageType,Set<Doc>>>> _usedClassToPackagesMap =
        new HashMap<ClassDoc,Map<PackageDoc,Map<UsageType,Set<Doc>>>>();

      ClassDoc[] classes = rootDoc.classes();
      for (int i = 0, ilim = classes.length; i < ilim; ++ i) {
         ClassDoc clazz = classes[i];

         if (clazz.isInterface()) {
            // classes implementing
            InterfaceRelation relation
               = (InterfaceRelation)getInterfaceRelations().get(clazz);
            Iterator<ClassDoc> it = relation.implementingClasses.iterator();
            while (it.hasNext()) {
               ClassDoc implementor = it.next();
               addUsedBy(_usedClassToPackagesMap,
                         clazz, UsageType.CLASS_IMPLEMENTING, implementor, implementor.containingPackage());
            }
         }
         else {
            // classes derived from
            for (ClassDoc superclass = clazz.superclass(); superclass != null;
                 superclass = superclass.superclass()) {
               addUsedBy(_usedClassToPackagesMap,
                         superclass, UsageType.CLASS_DERIVED_FROM, clazz, clazz.containingPackage());
            }
         }

         FieldDoc[] fields = clazz.fields();
         for (int j = 0, jlim = fields.length; j < jlim; ++ j) {
            FieldDoc field = fields[j];

            // fields of type
            ClassDoc fieldType = field.type().asClassDoc();
            if (null != fieldType) {
               addUsedBy(_usedClassToPackagesMap,
                         fieldType, UsageType.FIELD_OF_TYPE,
                         field, clazz.containingPackage());
            }
         }

         MethodDoc[] methods = clazz.methods();
         for (int j = 0, jlim = methods.length; j < jlim; ++ j) {
            MethodDoc method = methods[j];

            // methods with return type

            ClassDoc returnType = method.returnType().asClassDoc();
            if (null != returnType) {
               addUsedBy(_usedClassToPackagesMap,
                         returnType, UsageType.METHOD_WITH_RETURN_TYPE,
                         method, clazz.containingPackage());
            }
            Parameter[] parameters = method.parameters();
            for (int k=0; k<parameters.length; ++k) {

               // methods with parameter type

               Parameter parameter = parameters[k];
               ClassDoc parameterType = parameter.type().asClassDoc();
               if (null != parameterType) {
                  addUsedBy(_usedClassToPackagesMap,
                            parameterType, UsageType.METHOD_WITH_PARAMETER_TYPE,
                            method, clazz.containingPackage());
               }
            }

            // methods which throw

            ClassDoc[] thrownExceptions = method.thrownExceptions();
            for (int k = 0, klim = thrownExceptions.length; k < klim; ++ k) {
               ClassDoc thrownException = thrownExceptions[k];
               addUsedBy(_usedClassToPackagesMap,
                         thrownException, UsageType.METHOD_WITH_THROWN_TYPE,
                         method, clazz.containingPackage());
            }
         }

         ConstructorDoc[] constructors = clazz.constructors();
         for (int j = 0, jlim = constructors.length; j < jlim; ++ j) {

            ConstructorDoc constructor = constructors[j];

            Parameter[] parameters = constructor.parameters();
            for (int k = 0, klim = parameters.length; k < klim; ++ k) {

               // constructors with parameter type

               Parameter parameter = parameters[k];
               ClassDoc parameterType = parameter.type().asClassDoc();
               if (null != parameterType) {
                  addUsedBy(_usedClassToPackagesMap,
                            parameterType, UsageType.CONSTRUCTOR_WITH_PARAMETER_TYPE,
                            constructor, clazz.containingPackage());
               }
            }

            // constructors which throw

            ClassDoc[] thrownExceptions = constructor.thrownExceptions();
            for (int k = 0, klim = thrownExceptions.length; k < klim; ++ k) {
               ClassDoc thrownException = thrownExceptions[k];
               addUsedBy(_usedClassToPackagesMap,
                         thrownException, UsageType.CONSTRUCTOR_WITH_THROWN_TYPE,
                         constructor, clazz.containingPackage());
            }
         }
      }
      return _usedClassToPackagesMap;
   }

   private Map<ClassDoc,Map<PackageDoc,Map<UsageType,Set<Doc>>>> usedClassToPackagesMap = null;

   protected Map<PackageDoc,Map<UsageType,Set<Doc>>> getUsageOfClass(ClassDoc classDoc)
   {
      if (null == this.usedClassToPackagesMap) {
         this.usedClassToPackagesMap = collectUsage();
      }
      return this.usedClassToPackagesMap.get(classDoc);
   }

   protected static class UsageType
      implements Comparable<UsageType>
   {
      public static final UsageType CLASS_DERIVED_FROM = new UsageType("class-derived-from");
      public static final UsageType CLASS_IMPLEMENTING = new UsageType("class-implementing");
      public static final UsageType FIELD_OF_TYPE = new UsageType("field-of-type");
      public static final UsageType METHOD_WITH_RETURN_TYPE = new UsageType("method-with-return-type");
      public static final UsageType METHOD_WITH_PARAMETER_TYPE = new UsageType("method-with-parameter-type");
      public static final UsageType METHOD_WITH_THROWN_TYPE = new UsageType("method-with-thrown-type");
      public static final UsageType CONSTRUCTOR_WITH_PARAMETER_TYPE = new UsageType("constructor-with-parameter-type");
      public static final UsageType CONSTRUCTOR_WITH_THROWN_TYPE = new UsageType("constructor-with-thrown-type");
      private String id;

      private UsageType(String id)
      {
         this.id = id;
      }

      public int compareTo(UsageType ut)
      {
         return this.id.compareTo(ut.id);
      }

      public String toString() {
         return "UsageType{id=" + id + "}";
      }

      public String getId() {
         return id;
      }
   }

   private ResourceBundle resources;

   protected String getString(String key)
   {
      if (null == resources) {
         Locale currentLocale = Locale.getDefault();

         resources
            = ResourceBundle.getBundle("htmldoclet.HtmlDoclet", currentLocale);
      }

      return resources.getString(key);
   }

   protected String format(String key, String value1)
   {
      return MessageFormat.format(getString(key), new Object[] { value1 });
   }

   protected List<PackageGroup> getPackageGroups()
   {
      return packageGroups;
   }

   protected void copyDocFiles(File sourceDir, File targetDir)
      throws IOException
   {
      File sourceDocFiles = new File(sourceDir, "doc-files");
      File targetDocFiles = new File(targetDir, "doc-files");

      if (sourceDocFiles.exists()) {
         IOToolkit.copyDirectory(sourceDocFiles,
                                 targetDocFiles,
                                 optionDocFilesSubDirs.getValue(),
                                 optionExcludeDocFilesSubDir.getComponents());
      }
   }

   private Set sourcePaths;

   /**
    *  Try to determine the source directory for the given package by
    *  looking at the path specified by -sourcepath, or the current
    *  directory if -sourcepath hasn't been specified.
    *
    *  @throws IOException if the source directory couldn't be
    *  located.
    *
    *  @return List of File
    */
   protected List getPackageSourceDirs(PackageDoc packageDoc)
      throws IOException
   {
      if (null == sourcePaths) {
         for (int i=0; i<rootDoc.options().length; ++i) {
            if ("-sourcepath".equals(rootDoc.options()[i][0])
                || "-s".equals(rootDoc.options()[i][0])) {
               sourcePaths = new LinkedHashSet();
               String sourcepathString = rootDoc.options()[i][1];
               StringTokenizer st = new StringTokenizer(sourcepathString, File.pathSeparator);
               while (st.hasMoreTokens()) {
                  sourcePaths.add(new File(st.nextToken()));
               }
            }
         }
         if (null == sourcePaths) {
            sourcePaths = new LinkedHashSet();
            sourcePaths.add(new File(System.getProperty("user.dir")));
         }
      }

      String packageSubDir = packageDoc.name().replace('.', File.separatorChar);
      Iterator it = sourcePaths.iterator();
      List result = new LinkedList();
      while (it.hasNext()) {
         File pathComponent = (File)it.next();
         File packageDir = new File(pathComponent, packageSubDir);
         if (packageDir.exists()) {
            result.add(packageDir);
         }
      }
      if (result.isEmpty()) {
         throw new IOException("Couldn't locate source directory for package " + packageDoc.name());
      }
      else {
         return result;
      }
   }

   protected File getSourceFile(ClassDoc classDoc)
      throws IOException
   {
      List packageDirs = getPackageSourceDirs(classDoc.containingPackage());
      Iterator it = packageDirs.iterator();
      while (it.hasNext()) {
         File packageDir = (File)it.next();
         File sourceFile = new File(packageDir, getOuterClassDoc(classDoc).name() + ".java");
         if (sourceFile.exists()) {
            return sourceFile;
         }
      }

      throw new IOException("Couldn't locate source file for class " + classDoc.qualifiedTypeName());
   }

   protected void printError(String error)
   {
      if (null != rootDoc) {
         rootDoc.printError(error);
      }
      else {
         System.err.println("ERROR: "+error);
      }
   }

   protected void printWarning(String warning)
   {
      if (null != rootDoc) {
         rootDoc.printWarning(warning);
      }
      else {
         System.err.println("WARNING: "+warning);
      }
   }

   protected void printNotice(String notice)
   {
      if (null != rootDoc) {
         rootDoc.printNotice(notice);
      }
      else {
         System.err.println(notice);
      }
   }

   protected static ClassDoc getOuterClassDoc(ClassDoc classDoc)
   {
      while (null != classDoc.containingClass()) {
         classDoc = classDoc.containingClass();
      }
      return classDoc;
   }

   private SortedSet allPackages;

   protected Set getAllPackages()
   {
      if (null == this.allPackages) {
         allPackages = new TreeSet();
         PackageDoc[] specifiedPackages = rootDoc.specifiedPackages();
         for (int i=0; i<specifiedPackages.length; ++i) {
            allPackages.add(specifiedPackages[i]);
         }
         ClassDoc[] specifiedClasses = rootDoc.specifiedClasses();
         for (int i=0; i<specifiedClasses.length; ++i) {
            allPackages.add(specifiedClasses[i].containingPackage());
         }
      }
      return this.allPackages;
   }

   protected boolean omitPackageQualifier(PackageDoc packageDoc)
   {
      if (!optionNoQualifier.isSpecified()) {
         return false;
      }
      else {
         return optionNoQualifier.match(packageDoc);
      }
   }

   protected String possiblyQualifiedName(Type type)
   {
      if (null == type.asClassDoc()
          || !omitPackageQualifier(type.asClassDoc().containingPackage())) {
         return type.qualifiedTypeName();
      }
      else {
         return type.typeName();
      }
   }

   protected static class InterfaceRelation
   {
      public Set superInterfaces;
      public Set subInterfaces;
      public Set implementingClasses;

      public InterfaceRelation()
      {
         superInterfaces = new TreeSet();
         subInterfaces = new TreeSet();
         implementingClasses = new TreeSet();
      }
   }

   private void addAllInterfaces(ClassDoc classDoc, Set allInterfaces)
   {
      ClassDoc[] interfaces = classDoc.interfaces();
      for (int i=0; i<interfaces.length; ++i) {
         allInterfaces.add(interfaces[i]);
         addAllInterfaces(interfaces[i], allInterfaces);
      }
   }

   private Map allSubClasses;

   protected Map getAllSubClasses()
   {
      if (null == allSubClasses) {
         allSubClasses = new HashMap();

         ClassDoc[] classDocs = getRootDoc().classes();
         for (int i=0; i<classDocs.length; ++i) {
            if (!classDocs[i].isInterface()) {
               for (ClassDoc cd = classDocs[i].superclass();
                    null != cd;
                    cd = cd.superclass()) {

                  if (!cd.qualifiedTypeName().equals("java.lang.Object")) {
                     List subClasses = (List)allSubClasses.get(cd);
                     if (null == subClasses) {
                        subClasses = new LinkedList();
                        allSubClasses.put(cd, subClasses);
                     }
                     subClasses.add(classDocs[i]);
                  }
               }
            }
         }
      }
      return allSubClasses;
   }

   private Map interfaceRelations;

   private void addToInterfaces(ClassDoc classDoc, ClassDoc[] interfaces)
   {
      for (int i=0; i<interfaces.length; ++i) {
         InterfaceRelation interfaceRelation
            = (InterfaceRelation)interfaceRelations.get(interfaces[i]);
         if (null == interfaceRelation) {
            interfaceRelation = new InterfaceRelation();
            interfaceRelations.put(interfaces[i], interfaceRelation);
         }
         interfaceRelation.implementingClasses.add(classDoc);
         addToInterfaces(classDoc, interfaces[i].interfaces());
      }
   }

   protected Map getInterfaceRelations()
   {
      if (null == interfaceRelations) {
         interfaceRelations = new HashMap();

         ClassDoc[] classDocs = getRootDoc().classes();
         for (int i=0; i<classDocs.length; ++i) {
            if (classDocs[i].isInterface()) {
               InterfaceRelation relation = new InterfaceRelation();
               addAllInterfaces(classDocs[i], relation.superInterfaces);
               interfaceRelations.put(classDocs[i], relation);
            }
         }

         Iterator it = interfaceRelations.keySet().iterator();
         while (it.hasNext()) {
            ClassDoc interfaceDoc = (ClassDoc)it.next();
            InterfaceRelation relation
               = (InterfaceRelation)interfaceRelations.get(interfaceDoc);
            Iterator superIt = relation.superInterfaces.iterator();
            while (superIt.hasNext()) {
               ClassDoc superInterfaceDoc = (ClassDoc)superIt.next();
               InterfaceRelation superRelation
                  = (InterfaceRelation)interfaceRelations.get(superInterfaceDoc);
               if (null != superRelation) {
                  superRelation.subInterfaces.add(interfaceDoc);
               }
            }
         }

         for (int i=0; i<classDocs.length; ++i) {
            if (!classDocs[i].isInterface()) {
               for (ClassDoc cd = classDocs[i]; null != cd; cd = cd.superclass()) {
                  addToInterfaces(classDocs[i], cd.interfaces());
               }
            }
         }
      }

      return interfaceRelations;
   }

   private Map sortedMethodMap = new HashMap();

   protected MethodDoc[] getSortedMethods(ClassDoc classDoc)
   {
      MethodDoc[] result = (MethodDoc[])sortedMethodMap.get(classDoc);
      if (null == result) {
         MethodDoc[] methods = classDoc.methods();
         result = (MethodDoc[])methods.clone();
         Arrays.sort(result);
         return result;
      }
      return result;
   }

   private Map sortedConstructorMap = new HashMap();

   protected ConstructorDoc[] getSortedConstructors(ClassDoc classDoc)
   {
      ConstructorDoc[] result = (ConstructorDoc[])sortedConstructorMap.get(classDoc);
      if (null == result) {
         ConstructorDoc[] constructors = classDoc.constructors();
         result = (ConstructorDoc[])constructors.clone();
         Arrays.sort(result);
         return result;
      }
      return result;
   }

   private Map sortedFieldMap = new HashMap();

   protected FieldDoc[] getSortedFields(ClassDoc classDoc)
   {
      FieldDoc[] result = (FieldDoc[])sortedFieldMap.get(classDoc);
      if (null == result) {
         FieldDoc[] fields = classDoc.fields();
         result = (FieldDoc[])fields.clone();
         Arrays.sort(result);
         return result;
      }
      return result;
   }

   private Map sortedInnerClassMap = new HashMap();

   protected ClassDoc[] getSortedInnerClasses(ClassDoc classDoc)
   {
      ClassDoc[] result = (ClassDoc[])sortedInnerClassMap.get(classDoc);
      if (null == result) {
         ClassDoc[] innerClasses = classDoc.innerClasses();
         result = (ClassDoc[])innerClasses.clone();
         Arrays.sort(result);
         return result;
      }
      return result;
   }

   protected abstract String renderTag(String tagName, Tag[] tags, TagletContext context);

   protected abstract String getDocletVersion();

   protected SortedSet getThrownExceptions(ExecutableMemberDoc execMemberDoc)
   {
      SortedSet result = new TreeSet();
      ClassDoc[] thrownExceptions = execMemberDoc.thrownExceptions();
      for (int j=0; j<thrownExceptions.length; ++j) {
         result.add(thrownExceptions[j]);
      }
      return result;
   }

   protected boolean isUncheckedException(ClassDoc classDoc)
   {
      if (classDoc.isException()) {
         while (null != classDoc) {
            if (classDoc.qualifiedTypeName().equals("java.lang.RuntimeException")) {
               return true;
            }
            classDoc = classDoc.superclass();
         }
         return false;
      }
      else {
         return false;
      }
   }

   protected FieldDoc findField(ClassDoc classDoc, String fieldName)
   {
      for (ClassDoc cd = classDoc; cd != null; cd = cd.superclass()) {
         FieldDoc[] fields = cd.fields(false);
         for (int i=0; i<fields.length; ++i) {
            if (fields[i].name().equals(fieldName)) {
               return fields[i];
            }
         }
      }
      return null;
   }

   private Map implementedInterfacesCache = new HashMap();

   protected Set getImplementedInterfaces(ClassDoc classDoc)
   {
      Set result = (Set)implementedInterfacesCache.get(classDoc);
      if (null == result) {
         result = new TreeSet();

         for (ClassDoc cd = classDoc; cd != null; cd = cd.superclass()) {
            ClassDoc[] interfaces = cd.interfaces();
            for (int i=0; i<interfaces.length; ++i) {
               result.add(interfaces[i]);
               InterfaceRelation relation
                  = (InterfaceRelation)getInterfaceRelations().get(interfaces[i]);
               if (null != relation) {
                  result.addAll(relation.superInterfaces);
               }
            }
         }

         implementedInterfacesCache.put(classDoc, result);
      }

      return result;
   }

   protected boolean isSinglePackage()
   {
      return getAllPackages().size() <= 1;
   }

   protected PackageDoc getSinglePackage()
   {
      return (PackageDoc)getAllPackages().iterator().next();
   }
}
