/*
 * Copyright (c) 1999, 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.doclets.internal.toolkit.util;

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

import com.sun.javadoc.*;
import com.sun.tools.doclets.internal.toolkit.*;

/**
 * Utilities Class for Doclets.
 *
 * This code is not part of an API.
 * It is implementation that is subject to change.
 * Do not use it as an API
 *
 * @author Atul M Dambalkar
 * @author Jamie Ho
 */
public class Util {

    /**
     * A mapping between characters and their
     * corresponding HTML escape character.
     */
    public static final String[][] HTML_ESCAPE_CHARS =
    {{"&", "&amp;"}, {"<", "&lt;"}, {">", "&gt;"}};

    /**
     * Name of the resource directory.
     */
    public static final String RESOURCESDIR = "resources";

    /**
     * Return array of class members whose documentation is to be generated.
     * If the member is deprecated do not include such a member in the
     * returned array.
     *
     * @param  members             Array of members to choose from.
     * @return ProgramElementDoc[] Array of eligible members for whom
     *                             documentation is getting generated.
     */
    public static ProgramElementDoc[] excludeDeprecatedMembers(
        ProgramElementDoc[] members) {
        return
            toProgramElementDocArray(excludeDeprecatedMembersAsList(members));
    }

    /**
     * Return array of class members whose documentation is to be generated.
     * If the member is deprecated do not include such a member in the
     * returned array.
     *
     * @param  members    Array of members to choose from.
     * @return List       List of eligible members for whom
     *                    documentation is getting generated.
     */
    public static List<ProgramElementDoc> excludeDeprecatedMembersAsList(
        ProgramElementDoc[] members) {
        List<ProgramElementDoc> list = new ArrayList<ProgramElementDoc>();
        for (int i = 0; i < members.length; i++) {
            if (members[i].tags("deprecated").length == 0) {
                list.add(members[i]);
            }
        }
        Collections.sort(list);
        return list;
    }

    /**
     * Return the list of ProgramElementDoc objects as Array.
     */
    public static ProgramElementDoc[] toProgramElementDocArray(List<ProgramElementDoc> list) {
        ProgramElementDoc[] pgmarr = new ProgramElementDoc[list.size()];
        for (int i = 0; i < list.size(); i++) {
            pgmarr[i] = list.get(i);
        }
        return pgmarr;
    }

    /**
     * Return true if a non-public member found in the given array.
     *
     * @param  members Array of members to look into.
     * @return boolean True if non-public member found, false otherwise.
     */
    public static boolean nonPublicMemberFound(ProgramElementDoc[] members) {
        for (int i = 0; i < members.length; i++) {
            if (!members[i].isPublic()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Search for the given method in the given class.
     *
     * @param  cd        Class to search into.
     * @param  method    Method to be searched.
     * @return MethodDoc Method found, null otherwise.
     */
    public static MethodDoc findMethod(ClassDoc cd, MethodDoc method) {
        MethodDoc[] methods = cd.methods();
        for (int i = 0; i < methods.length; i++) {
            if (executableMembersEqual(method, methods[i])) {
                return methods[i];

            }
        }
        return null;
    }

    /**
     * @param member1 the first method to compare.
     * @param member2 the second method to compare.
     * @return true if member1 overrides/hides or is overriden/hidden by member2.
     */
    public static boolean executableMembersEqual(ExecutableMemberDoc member1,
            ExecutableMemberDoc member2) {
        if (! (member1 instanceof MethodDoc && member2 instanceof MethodDoc))
            return false;

        MethodDoc method1 = (MethodDoc) member1;
        MethodDoc method2 = (MethodDoc) member2;
        if (method1.isStatic() && method2.isStatic()) {
            Parameter[] targetParams = method1.parameters();
            Parameter[] currentParams;
            if (method1.name().equals(method2.name()) &&
                   (currentParams = method2.parameters()).length ==
                targetParams.length) {
                int j;
                for (j = 0; j < targetParams.length; j++) {
                    if (! (targetParams[j].typeName().equals(
                              currentParams[j].typeName()) ||
                                   currentParams[j].type() instanceof TypeVariable ||
                                   targetParams[j].type() instanceof TypeVariable)) {
                        break;
                    }
                }
                if (j == targetParams.length) {
                    return true;
                }
            }
            return false;
        } else {
                return method1.overrides(method2) ||
                method2.overrides(method1) ||
                                member1 == member2;
        }
    }

    /**
     * According to
     * <cite>The Java&trade; Language Specification</cite>,
     * all the outer classes and static inner classes are core classes.
     */
    public static boolean isCoreClass(ClassDoc cd) {
        return cd.containingClass() == null || cd.isStatic();
    }

    public static boolean matches(ProgramElementDoc doc1,
            ProgramElementDoc doc2) {
        if (doc1 instanceof ExecutableMemberDoc &&
            doc2 instanceof ExecutableMemberDoc) {
            ExecutableMemberDoc ed1 = (ExecutableMemberDoc)doc1;
            ExecutableMemberDoc ed2 = (ExecutableMemberDoc)doc2;
            return executableMembersEqual(ed1, ed2);
        } else {
            return doc1.name().equals(doc2.name());
        }
    }

    /**
     * Copy source file to destination file.
     *
     * @throws SecurityException
     * @throws IOException
     */
    public static void copyFile(File destfile, File srcfile)
        throws IOException {
        byte[] bytearr = new byte[512];
        int len = 0;
        FileInputStream input = new FileInputStream(srcfile);
        File destDir = destfile.getParentFile();
        destDir.mkdirs();
        FileOutputStream output = new FileOutputStream(destfile);
        try {
            while ((len = input.read(bytearr)) != -1) {
                output.write(bytearr, 0, len);
                }
        } catch (FileNotFoundException exc) {
        } catch (SecurityException exc) {
            } finally {
            input.close();
            output.close();
            }
        }

    /**
     * Copy the given directory contents from the source package directory
     * to the generated documentation directory. For example for a package
     * java.lang this method find out the source location of the package using
     * {@link SourcePath} and if given directory is found in the source
     * directory structure, copy the entire directory, to the generated
     * documentation hierarchy.
     *
     * @param configuration The configuration of the current doclet.
     * @param path The relative path to the directory to be copied.
     * @param dir The original directory name to copy from.
     * @param overwrite Overwrite files if true.
     */
    public static void copyDocFiles(Configuration configuration,
            String path, String dir, boolean overwrite) {
        if (checkCopyDocFilesErrors(configuration, path, dir)) {
            return;
        }
        String destname = configuration.docFileDestDirName;
        File srcdir = new File(path + dir);
        if (destname.length() > 0 && !destname.endsWith(
               DirectoryManager.URL_FILE_SEPARATOR)) {
            destname += DirectoryManager.URL_FILE_SEPARATOR;
        }
        String dest = destname + dir;
        try {
            File destdir = new File(dest);
            DirectoryManager.createDirectory(configuration, dest);
            String[] files = srcdir.list();
            for (int i = 0; i < files.length; i++) {
                File srcfile = new File(srcdir, files[i]);
                File destfile = new File(destdir, files[i]);
                if (srcfile.isFile()) {
                    if(destfile.exists() && ! overwrite) {
                        configuration.message.warning((SourcePosition) null,
                                "doclet.Copy_Overwrite_warning",
                                srcfile.toString(), destdir.toString());
                    } else {
                        configuration.message.notice(
                            "doclet.Copying_File_0_To_Dir_1",
                            srcfile.toString(), destdir.toString());
                        Util.copyFile(destfile, srcfile);
                    }
                } else if(srcfile.isDirectory()) {
                    if(configuration.copydocfilesubdirs
                        && ! configuration.shouldExcludeDocFileDir(
                          srcfile.getName())){
                        copyDocFiles(configuration, path, dir +
                                    DirectoryManager.URL_FILE_SEPARATOR + srcfile.getName(),
                                overwrite);
                    }
                }
            }
        } catch (SecurityException exc) {
            throw new DocletAbortException();
        } catch (IOException exc) {
            throw new DocletAbortException();
        }
    }

    /**
     * Given the parameters for copying doc-files, check for errors.
     *
     * @param configuration The configuration of the current doclet.
     * @param path The relative path to the directory to be copied.
     * @param dirName The original directory name to copy from.
     */
    private static boolean checkCopyDocFilesErrors (Configuration configuration,
            String path, String dirName) {
        if ((configuration.sourcepath == null || configuration.sourcepath.length() == 0) &&
               (configuration.destDirName == null || configuration.destDirName.length() == 0)) {
            //The destination path and source path are definitely equal.
            return true;
        }
        File sourcePath, destPath = new File(configuration.destDirName);
        StringTokenizer pathTokens = new StringTokenizer(
            configuration.sourcepath == null ? "" : configuration.sourcepath,
            File.pathSeparator);
        //Check if the destination path is equal to the source path.  If yes,
        //do not copy doc-file directories.
        while(pathTokens.hasMoreTokens()){
            sourcePath = new File(pathTokens.nextToken());
            if(destPath.equals(sourcePath)){
                return true;
            }
        }
        //Make sure the doc-file being copied exists.
        File srcdir = new File(path + dirName);
        if (! srcdir.exists()) {
            return true;
        }
        return false;
    }

    /**
     * Copy a file in the resources directory to the destination
     * directory (if it is not there already).  If
     * <code>overwrite</code> is true and the destination file
     * already exists, overwrite it.
     *
     * @param configuration  Holds the destination directory and error message
     * @param resourcefile   The name of the resource file to copy
     * @param overwrite      A flag to indicate whether the file in the
     *                       destination directory will be overwritten if
     *                       it already exists.
     */
    public static void copyResourceFile(Configuration configuration,
            String resourcefile, boolean overwrite) {
        String destresourcesdir = configuration.destDirName + RESOURCESDIR;
        copyFile(configuration, resourcefile, RESOURCESDIR, destresourcesdir,
                overwrite, false);
    }

    /**
     * Copy a file from a source directory to a destination directory
     * (if it is not there already). If <code>overwrite</code> is true and
     * the destination file already exists, overwrite it.
     *
     * @param configuration Holds the error message
     * @param file The name of the file to copy
     * @param source The source directory
     * @param destination The destination directory where the file needs to be copied
     * @param overwrite A flag to indicate whether the file in the
     *                  destination directory will be overwritten if
     *                  it already exists.
     * @param replaceNewLine true if the newline needs to be replaced with platform-
     *                  specific newline.
     */
    public static void copyFile(Configuration configuration, String file, String source,
            String destination, boolean overwrite, boolean replaceNewLine) {
        DirectoryManager.createDirectory(configuration, destination);
        File destfile = new File(destination, file);
        if(destfile.exists() && (! overwrite)) return;
        try {
            InputStream in = Configuration.class.getResourceAsStream(
                    source + DirectoryManager.URL_FILE_SEPARATOR + file);
            if(in==null) return;
            OutputStream out = new FileOutputStream(destfile);
            try {
                if (!replaceNewLine) {
                    byte[] buf = new byte[2048];
                    int n;
                    while((n = in.read(buf))>0) out.write(buf,0,n);
                } else {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
                    try {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            writer.write(line);
                            writer.write(DocletConstants.NL);
                        }
                    } finally {
                        reader.close();
                        writer.close();
                    }
                }
            } finally {
                in.close();
                out.close();
            }
        } catch (IOException ie) {
            ie.printStackTrace(System.err);
            throw new DocletAbortException();
        }
    }

    /**
     * Given a PackageDoc, return the source path for that package.
     * @param configuration The Configuration for the current Doclet.
     * @param pkgDoc The package to seach the path for.
     * @return A string representing the path to the given package.
     */
    public static String getPackageSourcePath(Configuration configuration,
            PackageDoc pkgDoc){
        try{
            String pkgPath = DirectoryManager.getDirectoryPath(pkgDoc);
            String completePath = new SourcePath(configuration.sourcepath).
                getDirectory(pkgPath) + DirectoryManager.URL_FILE_SEPARATOR;
            //Make sure that both paths are using the same separators.
            completePath = Util.replaceText(completePath, File.separator,
                    DirectoryManager.URL_FILE_SEPARATOR);
            pkgPath = Util.replaceText(pkgPath, File.separator,
                    DirectoryManager.URL_FILE_SEPARATOR);
            return completePath.substring(0, completePath.lastIndexOf(pkgPath));
        } catch (Exception e){
            return "";
        }
    }

    /**
     * We want the list of types in alphabetical order.  However, types are not
     * comparable.  We need a comparator for now.
     */
    private static class TypeComparator implements Comparator<Type> {
        public int compare(Type type1, Type type2) {
            return type1.qualifiedTypeName().toLowerCase().compareTo(
                type2.qualifiedTypeName().toLowerCase());
        }
    }

    /**
     * For the class return all implemented interfaces including the
     * superinterfaces of the implementing interfaces, also iterate over for
     * all the superclasses. For interface return all the extended interfaces
     * as well as superinterfaces for those extended interfaces.
     *
     * @param  type       type whose implemented or
     *                    super interfaces are sought.
     * @param  configuration the current configuration of the doclet.
     * @param  sort if true, return list of interfaces sorted alphabetically.
     * @return List of all the required interfaces.
     */
    public static List<Type> getAllInterfaces(Type type,
            Configuration configuration, boolean sort) {
        Map<ClassDoc,Type> results = sort ? new TreeMap<ClassDoc,Type>() : new LinkedHashMap<ClassDoc,Type>();
        Type[] interfaceTypes = null;
        Type superType = null;
        if (type instanceof ParameterizedType) {
            interfaceTypes = ((ParameterizedType) type).interfaceTypes();
            superType = ((ParameterizedType) type).superclassType();
        } else if (type instanceof ClassDoc) {
            interfaceTypes = ((ClassDoc) type).interfaceTypes();
            superType = ((ClassDoc) type).superclassType();
        } else {
            interfaceTypes = type.asClassDoc().interfaceTypes();
            superType = type.asClassDoc().superclassType();
        }

        for (int i = 0; i < interfaceTypes.length; i++) {
            Type interfaceType = interfaceTypes[i];
            ClassDoc interfaceClassDoc = interfaceType.asClassDoc();
            if (! (interfaceClassDoc.isPublic() ||
                (configuration == null ||
                isLinkable(interfaceClassDoc, configuration)))) {
                continue;
            }
            results.put(interfaceClassDoc, interfaceType);
            List<Type> superInterfaces = getAllInterfaces(interfaceType, configuration, sort);
            for (Iterator<Type> iter = superInterfaces.iterator(); iter.hasNext(); ) {
                Type t = iter.next();
                results.put(t.asClassDoc(), t);
            }
        }
        if (superType == null)
            return new ArrayList<Type>(results.values());
        //Try walking the tree.
        addAllInterfaceTypes(results,
            superType,
            superType instanceof ClassDoc ?
                ((ClassDoc) superType).interfaceTypes() :
                ((ParameterizedType) superType).interfaceTypes(),
            false, configuration);
        List<Type> resultsList = new ArrayList<Type>(results.values());
        if (sort) {
                Collections.sort(resultsList, new TypeComparator());
        }
        return resultsList;
    }

    public static List<Type> getAllInterfaces(Type type, Configuration configuration) {
        return getAllInterfaces(type, configuration, true);
    }

    private static void findAllInterfaceTypes(Map<ClassDoc,Type> results, ClassDoc c, boolean raw,
            Configuration configuration) {
        Type superType = c.superclassType();
        if (superType == null)
            return;
        addAllInterfaceTypes(results, superType,
                superType instanceof ClassDoc ?
                ((ClassDoc) superType).interfaceTypes() :
                ((ParameterizedType) superType).interfaceTypes(),
                raw, configuration);
    }

    private static void findAllInterfaceTypes(Map<ClassDoc,Type> results, ParameterizedType p,
            Configuration configuration) {
        Type superType = p.superclassType();
        if (superType == null)
            return;
        addAllInterfaceTypes(results, superType,
                superType instanceof ClassDoc ?
                ((ClassDoc) superType).interfaceTypes() :
                ((ParameterizedType) superType).interfaceTypes(),
                false, configuration);
    }

    private static void addAllInterfaceTypes(Map<ClassDoc,Type> results, Type type,
            Type[] interfaceTypes, boolean raw,
            Configuration configuration) {
        for (int i = 0; i < interfaceTypes.length; i++) {
            Type interfaceType = interfaceTypes[i];
            ClassDoc interfaceClassDoc = interfaceType.asClassDoc();
            if (! (interfaceClassDoc.isPublic() ||
                (configuration != null &&
                isLinkable(interfaceClassDoc, configuration)))) {
                continue;
            }
            if (raw)
                interfaceType = interfaceType.asClassDoc();
            results.put(interfaceClassDoc, interfaceType);
            List<Type> superInterfaces = getAllInterfaces(interfaceType, configuration);
            for (Iterator<Type> iter = superInterfaces.iterator(); iter.hasNext(); ) {
                Type superInterface = iter.next();
                results.put(superInterface.asClassDoc(), superInterface);
            }
        }
        if (type instanceof ParameterizedType)
            findAllInterfaceTypes(results, (ParameterizedType) type, configuration);
        else if (((ClassDoc) type).typeParameters().length == 0)
            findAllInterfaceTypes(results, (ClassDoc) type, raw, configuration);
        else
            findAllInterfaceTypes(results, (ClassDoc) type, true, configuration);
    }

    /**
     * Enclose in quotes, used for paths and filenames that contains spaces
     */
    public static String quote(String filepath) {
        return ("\"" + filepath + "\"");
    }

    /**
     * Given a package, return it's name.
     * @param packageDoc the package to check.
     * @return the name of the given package.
     */
    public static String getPackageName(PackageDoc packageDoc) {
        return packageDoc == null || packageDoc.name().length() == 0 ?
            DocletConstants.DEFAULT_PACKAGE_NAME : packageDoc.name();
    }

    /**
     * Given a package, return it's file name without the extension.
     * @param packageDoc the package to check.
     * @return the file name of the given package.
     */
    public static String getPackageFileHeadName(PackageDoc packageDoc) {
        return packageDoc == null || packageDoc.name().length() == 0 ?
            DocletConstants.DEFAULT_PACKAGE_FILE_NAME : packageDoc.name();
    }

    /**
     * Given a string, replace all occurraces of 'newStr' with 'oldStr'.
     * @param originalStr the string to modify.
     * @param oldStr the string to replace.
     * @param newStr the string to insert in place of the old string.
     */
    public static String replaceText(String originalStr, String oldStr,
            String newStr) {
        if (oldStr == null || newStr == null || oldStr.equals(newStr)) {
            return originalStr;
        }
        return originalStr.replace(oldStr, newStr);
    }

    /**
     * Given a string, escape all special html characters and
     * return the result.
     *
     * @param s The string to check.
     * @return the original string with all of the HTML characters
     * escaped.
     *
     * @see #HTML_ESCAPE_CHARS
     */
    public static String escapeHtmlChars(String s) {
        String result = s;
        for (int i = 0; i < HTML_ESCAPE_CHARS.length; i++) {
            result = Util.replaceText(result,
                    HTML_ESCAPE_CHARS[i][0], HTML_ESCAPE_CHARS[i][1]);
        }
        return result;
    }

    /**
     * Given a string, strips all html characters and
     * return the result.
     *
     * @param rawString The string to check.
     * @return the original string with all of the HTML characters
     * stripped.
     *
     */
    public static String stripHtml(String rawString) {
        // remove HTML tags
        rawString = rawString.replaceAll("\\<.*?>", " ");
        // consolidate multiple spaces between a word to a single space
        rawString = rawString.replaceAll("\\b\\s{2,}\\b", " ");
        // remove extra whitespaces
        return rawString.trim();
    }

    /**
     * Create the directory path for the file to be generated, construct
     * FileOutputStream and OutputStreamWriter depending upon docencoding.
     *
     * @param path The directory path to be created for this file.
     * @param filename File Name to which the PrintWriter will do the Output.
     * @param docencoding Encoding to be used for this file.
     * @exception IOException Exception raised by the FileWriter is passed on
     * to next level.
     * @exception UnsupportedEncodingException Exception raised by the
     * OutputStreamWriter is passed on to next level.
     * @return Writer Writer for the file getting generated.
     * @see java.io.FileOutputStream
     * @see java.io.OutputStreamWriter
     */
    public static Writer genWriter(Configuration configuration,
            String path, String filename,
            String docencoding)
        throws IOException, UnsupportedEncodingException {
        FileOutputStream fos;
        if (path != null) {
            DirectoryManager.createDirectory(configuration, path);
            fos = new FileOutputStream(((path.length() > 0)?
                                                  path + File.separator: "") + filename);
        } else {
            fos = new FileOutputStream(filename);
        }
        if (docencoding == null) {
            return new OutputStreamWriter(fos);
        } else {
            return new OutputStreamWriter(fos, docencoding);
        }
    }

    /**
     * Given an annotation, return true if it should be documented and false
     * otherwise.
     *
     * @param annotationDoc the annotation to check.
     *
     * @return true return true if it should be documented and false otherwise.
     */
    public static boolean isDocumentedAnnotation(AnnotationTypeDoc annotationDoc) {
        AnnotationDesc[] annotationDescList = annotationDoc.annotations();
        for (int i = 0; i < annotationDescList.length; i++) {
            if (annotationDescList[i].annotationType().qualifiedName().equals(
                   java.lang.annotation.Documented.class.getName())){
                return true;
            }
        }
        return false;
    }

    /**
     * Given a string, return an array of tokens.  The separator can be escaped
     * with the '\' character.  The '\' character may also be escaped by the
     * '\' character.
     *
     * @param s         the string to tokenize.
     * @param separator the separator char.
     * @param maxTokens the maxmimum number of tokens returned.  If the
     *                  max is reached, the remaining part of s is appended
     *                  to the end of the last token.
     *
     * @return an array of tokens.
     */
    public static String[] tokenize(String s, char separator, int maxTokens) {
        List<String> tokens = new ArrayList<String>();
        StringBuilder  token = new StringBuilder ();
        boolean prevIsEscapeChar = false;
        for (int i = 0; i < s.length(); i += Character.charCount(i)) {
            int currentChar = s.codePointAt(i);
            if (prevIsEscapeChar) {
                // Case 1:  escaped character
                token.appendCodePoint(currentChar);
                prevIsEscapeChar = false;
            } else if (currentChar == separator && tokens.size() < maxTokens-1) {
                // Case 2:  separator
                tokens.add(token.toString());
                token = new StringBuilder();
            } else if (currentChar == '\\') {
                // Case 3:  escape character
                prevIsEscapeChar = true;
            } else {
                // Case 4:  regular character
                token.appendCodePoint(currentChar);
            }
        }
        if (token.length() > 0) {
            tokens.add(token.toString());
        }
        return tokens.toArray(new String[] {});
    }

    /**
     * Return true if this class is linkable and false if we can't link to the
     * desired class.
     * <br>
     * <b>NOTE:</b>  You can only link to external classes if they are public or
     * protected.
     *
     * @param classDoc the class to check.
     * @param configuration the current configuration of the doclet.
     *
     * @return true if this class is linkable and false if we can't link to the
     * desired class.
     */
    public static boolean isLinkable(ClassDoc classDoc,
            Configuration configuration) {
        return
            ((classDoc.isIncluded() && configuration.isGeneratedDoc(classDoc))) ||
            (configuration.extern.isExternal(classDoc) &&
                (classDoc.isPublic() || classDoc.isProtected()));
    }

    /**
     * Given a class, return the closest visible super class.
     *
     * @param classDoc the class we are searching the parent for.
     * @param configuration the current configuration of the doclet.
     * @return the closest visible super class.  Return null if it cannot
     *         be found (i.e. classDoc is java.lang.Object).
     */
    public static Type getFirstVisibleSuperClass(ClassDoc classDoc,
            Configuration configuration) {
        if (classDoc == null) {
            return null;
        }
        Type sup = classDoc.superclassType();
        ClassDoc supClassDoc = classDoc.superclass();
        while (sup != null &&
                  (! (supClassDoc.isPublic() ||
                              isLinkable(supClassDoc, configuration))) ) {
            if (supClassDoc.superclass().qualifiedName().equals(supClassDoc.qualifiedName()))
                break;
            sup = supClassDoc.superclassType();
            supClassDoc = supClassDoc.superclass();
        }
        if (classDoc.equals(supClassDoc)) {
            return null;
        }
        return sup;
    }

    /**
     * Given a class, return the closest visible super class.
     *
     * @param classDoc the class we are searching the parent for.
     * @param configuration the current configuration of the doclet.
     * @return the closest visible super class.  Return null if it cannot
     *         be found (i.e. classDoc is java.lang.Object).
     */
    public static ClassDoc getFirstVisibleSuperClassCD(ClassDoc classDoc,
            Configuration configuration) {
        if (classDoc == null) {
            return null;
        }
        ClassDoc supClassDoc = classDoc.superclass();
        while (supClassDoc != null &&
                  (! (supClassDoc.isPublic() ||
                              isLinkable(supClassDoc, configuration))) ) {
            supClassDoc = supClassDoc.superclass();
        }
        if (classDoc.equals(supClassDoc)) {
            return null;
        }
        return supClassDoc;
    }

    /**
     * Given a ClassDoc, return the name of its type (Class, Interface, etc.).
     *
     * @param cd the ClassDoc to check.
     * @param lowerCaseOnly true if you want the name returned in lower case.
     *                      If false, the first letter of the name is capatilized.
     * @return
     */
    public static String getTypeName(Configuration config,
        ClassDoc cd, boolean lowerCaseOnly) {
        String typeName = "";
        if (cd.isOrdinaryClass()) {
            typeName = "doclet.Class";
        } else if (cd.isInterface()) {
            typeName = "doclet.Interface";
        } else if (cd.isException()) {
            typeName = "doclet.Exception";
        } else if (cd.isError()) {
            typeName = "doclet.Error";
        } else if (cd.isAnnotationType()) {
            typeName = "doclet.AnnotationType";
        } else if (cd.isEnum()) {
            typeName = "doclet.Enum";
        }
        return config.getText(
            lowerCaseOnly ? typeName.toLowerCase() : typeName);
    }

    /**
     * Given a string, replace all tabs with the appropriate
     * number of spaces.
     * @param tabLength the length of each tab.
     * @param s the String to scan.
     */
    public static void replaceTabs(int tabLength, StringBuilder s) {
        if (whitespace == null || whitespace.length() < tabLength)
            whitespace = String.format("%" + tabLength + "s", " ");
        int index = 0;
        while ((index = s.indexOf("\t", index)) != -1) {
            int spaceCount = tabLength - index % tabLength;
            s.replace(index, index+1, whitespace.substring(0, spaceCount));
            index += spaceCount;
        }
    }
    private static String whitespace;

    /**
     * The documentation for values() and valueOf() in Enums are set by the
     * doclet.
     */
    public static void setEnumDocumentation(Configuration configuration,
            ClassDoc classDoc) {
        MethodDoc[] methods = classDoc.methods();
        for (int j = 0; j < methods.length; j++) {
            MethodDoc currentMethod = methods[j];
            if (currentMethod.name().equals("values") &&
                currentMethod.parameters().length == 0) {
                currentMethod.setRawCommentText(
                    configuration.getText("doclet.enum_values_doc", classDoc.name()));
            } else if (currentMethod.name().equals("valueOf") &&
                currentMethod.parameters().length == 1) {
                Type paramType = currentMethod.parameters()[0].type();
                if (paramType != null &&
                    paramType.qualifiedTypeName().equals(String.class.getName())) {
                    currentMethod.setRawCommentText(
                        configuration.getText("doclet.enum_valueof_doc"));
                }
            }
        }
    }

    /**
     *  Return true if the given Doc is deprecated.
     *
     * @param doc the Doc to check.
     * @return true if the given Doc is deprecated.
     */
    public static boolean isDeprecated(Doc doc) {
        if (doc.tags("deprecated").length > 0) {
            return true;
        }
        AnnotationDesc[] annotationDescList;
        if (doc instanceof PackageDoc)
            annotationDescList = ((PackageDoc)doc).annotations();
        else
            annotationDescList = ((ProgramElementDoc)doc).annotations();
        for (int i = 0; i < annotationDescList.length; i++) {
            if (annotationDescList[i].annotationType().qualifiedName().equals(
                   java.lang.Deprecated.class.getName())){
                return true;
            }
        }
        return false;
    }
}
