/* gnu.classpath.tools.doclets.AbstractDoclet
   Copyright (C) 2004 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. */

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 tagletMap = new LinkedHashMap();

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

   /**
    *  The current classpath for loading taglet classes.
    */
   private String tagletPath;

   /**
    *  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 mentionedTags = new LinkedList();

   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 optionNoEmailWarn = 
     new DocletOptionFlag("-noemailwarn");

   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 DocletOptionTagletPath optionTagletPath = 
     new DocletOptionTagletPath("-tagletpath");

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

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

   private class DocletOptionTaglet
      extends DocletOption
   {
      DocletOptionTaglet(String optionName)
      {
         super(optionName);
      }
      
      public int getLength()
      {
         return 2;
      }

      public boolean set(String[] optionArr)
      {

         boolean tagletLoaded = false;

         String useTagletPath = AbstractDoclet.this.tagletPath;
         if (null == useTagletPath) {
            useTagletPath = System.getProperty("java.class.path");
         }

         try {
            Class tagletClass;
            try {
               tagletClass
                  = new FileSystemClassLoader(useTagletPath).loadClass(optionArr[1]);
            }
            catch (ClassNotFoundException e) {
               // If not found on specified tagletpath, try default classloader
               tagletClass
                  = Class.forName(optionArr[1]);
            }
            Method registerTagletMethod
               = tagletClass.getDeclaredMethod("register", new Class[] { java.util.Map.class });

            if (!registerTagletMethod.getReturnType().equals(Void.TYPE)) {
               printError("Taglet class '" + optionArr[1] + "' found, but register method doesn't return void.");
            }
            else if (registerTagletMethod.getExceptionTypes().length > 0) {
               printError("Taglet class '" + optionArr[1] + "' found, but register method contains throws clause.");
            }
            else if ((registerTagletMethod.getModifiers() & (Modifier.STATIC | Modifier.PUBLIC | Modifier.ABSTRACT)) != (Modifier.STATIC | Modifier.PUBLIC)) {
               printError("Taglet class '" + optionArr[1] + "' found, but register method isn't public static, or is abstract..");
            }
            else {
               Map tempMap = new HashMap();
               registerTagletMethod.invoke(null, new Object[] { tempMap });
               tagletLoaded = true;
               String name = (String)tempMap.keySet().iterator().next();
               Taglet taglet = (Taglet)tempMap.get(name);
               tagletMap.put(name, taglet);
               mentionedTags.add(taglet);
            }
         }
         catch (NoSuchMethodException e) {
            printError("Taglet class '" + optionArr[1] + "' found, but doesn't contain the register method.");
         }
         catch (SecurityException e) {
            printError("Taglet class '" + optionArr[1] + "' cannot be loaded: " + e.getMessage());
         }
         catch (InvocationTargetException e) {
            printError("Taglet class '" + optionArr[1] + "' found, but register method throws exception: " + e.toString());
         }
         catch (IllegalAccessException e) {
            printError("Taglet class '" + optionArr[1] + "' found, but there was a problem when accessing the register method: " + e.toString());
         }
         catch (IllegalArgumentException e) {
            printError("Taglet class '" + optionArr[1] + "' found, but there was a problem when accessing the register method: " + e.toString());
         }
         catch (ClassNotFoundException e) {
            printError("Taglet class '" + optionArr[1] + "' cannot be found.");
         }
         return tagletLoaded;
      }
   }

   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 groupPackages = packageMatcher.filter(rootDoc.specifiedPackages());

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

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


   private class DocletOptionTagletPath
      extends DocletOption
   {
      DocletOptionTagletPath(String optionName)
      {
         super(optionName);
      }
      
      public int getLength()
      {
         return 2;
      }

      public boolean set(String[] optionArr)
      {
         AbstractDoclet.this.tagletPath = optionArr[1];
         return true;
      }
   }

   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,
         optionTagletPath,
         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 nameToOptionMap = new HashMap();

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

   protected List getKnownDirectSubclasses(ClassDoc classDoc)
   {
      List result = new LinkedList();
      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
   {
      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(Object other)
      {
         return lowerName.compareTo(((IndexKey)other).lowerName);
      }

      public String getName()
      {
         return name;
      }
   }
   
   private Map categorizedIndex;

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

      return categorizedIndex;
   }


   private Map indexByName;

   protected Map getIndexByName()
   {
      if (null == indexByName) {
         // Create index

         // Collect index
            
         indexByName = new HashMap(); //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 it = tagletMap.keySet().iterator(); it.hasNext(); ) {
         String tagName = (String)it.next();
         Object o = tagletMap.get(tagName);
         Taglet taglet = (Taglet)o;
         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 tagsOfThisType = new LinkedList();
            for (int i=0; i<tags.length; ++i) {
               if (tags[i].name().substring(1).equals(tagName)) {
                  tagsOfThisType.add(tags[i]);
               }
            }

            Tag[] tagletTags = (Tag[])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 usedClassToPackagesMap,
                          ClassDoc usedClass, UsageType usageType, Doc user, PackageDoc userPackage)
   {
      Map packageToUsageTypeMap = (Map)usedClassToPackagesMap.get(usedClass);
      if (null == packageToUsageTypeMap) {
         packageToUsageTypeMap = new HashMap();
         usedClassToPackagesMap.put(usedClass, packageToUsageTypeMap);
      }

      Map usageTypeToUsersMap = (Map)packageToUsageTypeMap.get(userPackage);
      if (null == usageTypeToUsersMap) {
         usageTypeToUsersMap = new TreeMap();
         packageToUsageTypeMap.put(userPackage, usageTypeToUsersMap);
      }

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

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

      Map _usedClassToPackagesMap = new HashMap();

      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 it = relation.implementingClasses.iterator();
            while (it.hasNext()) {
               ClassDoc implementor = (ClassDoc)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 usedClassToPackagesMap = null;

   protected Map getUsageOfClass(ClassDoc classDoc)
   {
      if (null == this.usedClassToPackagesMap) {
         this.usedClassToPackagesMap = collectUsage();
      }
      return (Map)this.usedClassToPackagesMap.get(classDoc);
   }

   protected static class UsageType
      implements Comparable
   {
      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(Object other)
      {
         return this.id.compareTo(((UsageType)other).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 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 1 == getAllPackages().size();
   }

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