/*
 * Copyright (c) 1997, 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.javadoc;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.util.HashSet;
import java.util.Set;
import javax.tools.FileObject;
import javax.tools.JavaFileManager.Location;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;

import com.sun.javadoc.*;

import static com.sun.javadoc.LanguageVersion.*;

import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Kinds;
import com.sun.tools.javac.code.Scope;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.*;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.ClassType;
import com.sun.tools.javac.code.TypeTags;

import com.sun.tools.javac.comp.AttrContext;
import com.sun.tools.javac.comp.Env;

import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCImport;
import com.sun.tools.javac.tree.TreeInfo;

import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import com.sun.tools.javac.util.Position;

import static com.sun.tools.javac.code.Kinds.*;

/**
 * Represents a java class and provides access to information
 * about the class, the class' comment and tags, and the
 * members of the class.  A ClassDocImpl only exists if it was
 * processed in this run of javadoc.  References to classes
 * which may or may not have been processed in this run are
 * referred to using Type (which can be converted to ClassDocImpl,
 * if possible).
 *
 * @see Type
 *
 * @since 1.2
 * @author Robert Field
 * @author Neal Gafter (rewrite)
 * @author Scott Seligman (generics, enums, annotations)
 */

public class ClassDocImpl extends ProgramElementDocImpl implements ClassDoc {

    public final ClassType type;        // protected->public for debugging
    protected final ClassSymbol tsym;

    boolean isIncluded = false;         // Set in RootDocImpl

    private SerializedForm serializedForm;

    /**
     * Constructor
     */
    public ClassDocImpl(DocEnv env, ClassSymbol sym) {
        this(env, sym, null, null, null);
    }

    /**
     * Constructor
     */
    public ClassDocImpl(DocEnv env, ClassSymbol sym, String documentation,
                        JCClassDecl tree, Position.LineMap lineMap) {
        super(env, sym, documentation, tree, lineMap);
        this.type = (ClassType)sym.type;
        this.tsym = sym;
    }

    /**
     * Returns the flags in terms of javac's flags
     */
    protected long getFlags() {
        return getFlags(tsym);
    }

    /**
     * Returns the flags of a ClassSymbol in terms of javac's flags
     */
    static long getFlags(ClassSymbol clazz) {
        while (true) {
            try {
                return clazz.flags();
            } catch (CompletionFailure ex) {
                // quietly ignore completion failures
            }
        }
    }

    /**
     * Is a ClassSymbol an annotation type?
     */
    static boolean isAnnotationType(ClassSymbol clazz) {
        return (getFlags(clazz) & Flags.ANNOTATION) != 0;
    }

    /**
     * Identify the containing class
     */
    protected ClassSymbol getContainingClass() {
        return tsym.owner.enclClass();
    }

    /**
     * Return true if this is a class, not an interface.
     */
    @Override
    public boolean isClass() {
        return !Modifier.isInterface(getModifiers());
    }

    /**
     * Return true if this is a ordinary class,
     * not an enumeration, exception, an error, or an interface.
     */
    @Override
    public boolean isOrdinaryClass() {
        if (isEnum() || isInterface() || isAnnotationType()) {
            return false;
        }
        for (Type t = type; t.tag == TypeTags.CLASS; t = env.types.supertype(t)) {
            if (t.tsym == env.syms.errorType.tsym ||
                t.tsym == env.syms.exceptionType.tsym) {
                return false;
            }
        }
        return true;
    }

    /**
     * Return true if this is an enumeration.
     * (For legacy doclets, return false.)
     */
    @Override
    public boolean isEnum() {
        return (getFlags() & Flags.ENUM) != 0
               &&
               !env.legacyDoclet;
    }

    /**
     * Return true if this is an interface, but not an annotation type.
     * Overridden by AnnotationTypeDocImpl.
     */
    @Override
    public boolean isInterface() {
        return Modifier.isInterface(getModifiers());
    }

    /**
     * Return true if this is an exception class
     */
    @Override
    public boolean isException() {
        if (isEnum() || isInterface() || isAnnotationType()) {
            return false;
        }
        for (Type t = type; t.tag == TypeTags.CLASS; t = env.types.supertype(t)) {
            if (t.tsym == env.syms.exceptionType.tsym) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return true if this is an error class
     */
    @Override
    public boolean isError() {
        if (isEnum() || isInterface() || isAnnotationType()) {
            return false;
        }
        for (Type t = type; t.tag == TypeTags.CLASS; t = env.types.supertype(t)) {
            if (t.tsym == env.syms.errorType.tsym) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return true if this is a throwable class
     */
    public boolean isThrowable() {
        if (isEnum() || isInterface() || isAnnotationType()) {
            return false;
        }
        for (Type t = type; t.tag == TypeTags.CLASS; t = env.types.supertype(t)) {
            if (t.tsym == env.syms.throwableType.tsym) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return true if this class is abstract
     */
    public boolean isAbstract() {
        return Modifier.isAbstract(getModifiers());
    }

    /**
     * Returns true if this class was synthesized by the compiler.
     */
    public boolean isSynthetic() {
        return (getFlags() & Flags.SYNTHETIC) != 0;
    }

    /**
     * Return true if this class is included in the active set.
     * A ClassDoc is included iff either it is specified on the
     * commandline, or if it's containing package is specified
     * on the command line, or if it is a member class of an
     * included class.
     */

    public boolean isIncluded() {
        if (isIncluded) {
            return true;
        }
        if (env.shouldDocument(tsym)) {
            // Class is nameable from top-level and
            // the class and all enclosing classes
            // pass the modifier filter.
            if (containingPackage().isIncluded()) {
                return isIncluded=true;
            }
            ClassDoc outer = containingClass();
            if (outer != null && outer.isIncluded()) {
                return isIncluded=true;
            }
        }
        return false;
    }

    /**
     * Return the package that this class is contained in.
     */
    @Override
    public PackageDoc containingPackage() {
        PackageDocImpl p = env.getPackageDoc(tsym.packge());
        if (p.setDocPath == false) {
            FileObject docPath;
            try {
                Location location = env.fileManager.hasLocation(StandardLocation.SOURCE_PATH)
                    ? StandardLocation.SOURCE_PATH : StandardLocation.CLASS_PATH;

                docPath = env.fileManager.getFileForInput(
                        location, p.qualifiedName(), "package.html");
            } catch (IOException e) {
                docPath = null;
            }

            if (docPath == null) {
                // fall back on older semantics of looking in same directory as
                // source file for this class
                SourcePosition po = position();
                if (env.fileManager instanceof StandardJavaFileManager &&
                        po instanceof SourcePositionImpl) {
                    URI uri = ((SourcePositionImpl) po).filename.toUri();
                    if ("file".equals(uri.getScheme())) {
                        File f = new File(uri);
                        File dir = f.getParentFile();
                        if (dir != null) {
                            File pf = new File(dir, "package.html");
                            if (pf.exists()) {
                                StandardJavaFileManager sfm = (StandardJavaFileManager) env.fileManager;
                                docPath = sfm.getJavaFileObjects(pf).iterator().next();
                            }
                        }

                    }
                }
            }

            p.setDocPath(docPath);
        }
        return p;
    }

    /**
     * Return the class name without package qualifier - but with
     * enclosing class qualifier - as a String.
     * <pre>
     * Examples:
     *  for java.util.Hashtable
     *  return Hashtable
     *  for java.util.Map.Entry
     *  return Map.Entry
     * </pre>
     */
    public String name() {
        return getClassName(tsym, false);
    }

    /**
     * Return the qualified class name as a String.
     * <pre>
     * Example:
     *  for java.util.Hashtable
     *  return java.util.Hashtable
     *  if no qualifier, just return flat name
     * </pre>
     */
    public String qualifiedName() {
        return getClassName(tsym, true);
    }

    /**
     * Return unqualified name of type excluding any dimension information.
     * <p>
     * For example, a two dimensional array of String returns 'String'.
     */
    public String typeName() {
        return name();
    }

    /**
     * Return qualified name of type excluding any dimension information.
     *<p>
     * For example, a two dimensional array of String
     * returns 'java.lang.String'.
     */
    public String qualifiedTypeName() {
        return qualifiedName();
    }

    /**
     * Return the simple name of this type.
     */
    public String simpleTypeName() {
        return tsym.name.toString();
    }

    /**
     * Return the qualified name and any type parameters.
     * Each parameter is a type variable with optional bounds.
     */
    @Override
    public String toString() {
        return classToString(env, tsym, true);
    }

    /**
     * Return the class name as a string.  If "full" is true the name is
     * qualified, otherwise it is qualified by its enclosing class(es) only.
     */
    static String getClassName(ClassSymbol c, boolean full) {
        if (full) {
            return c.getQualifiedName().toString();
        } else {
            String n = "";
            for ( ; c != null; c = c.owner.enclClass()) {
                n = c.name + (n.equals("") ? "" : ".") + n;
            }
            return n;
        }
    }

    /**
     * Return the class name with any type parameters as a string.
     * Each parameter is a type variable with optional bounds.
     * If "full" is true all names are qualified, otherwise they are
     * qualified by their enclosing class(es) only.
     */
    static String classToString(DocEnv env, ClassSymbol c, boolean full) {
        StringBuilder s = new StringBuilder();
        if (!c.isInner()) {             // if c is not an inner class
            s.append(getClassName(c, full));
        } else {
            // c is an inner class, so include type params of outer.
            ClassSymbol encl = c.owner.enclClass();
            s.append(classToString(env, encl, full))
             .append('.')
             .append(c.name);
        }
        s.append(TypeMaker.typeParametersString(env, c, full));
        return s.toString();
    }

    /**
     * Is this class (or any enclosing class) generic?  That is, does
     * it have type parameters?
     */
    static boolean isGeneric(ClassSymbol c) {
        return c.type.allparams().nonEmpty();
    }

    /**
     * Return the formal type parameters of this class or interface.
     * Return an empty array if there are none.
     */
    public TypeVariable[] typeParameters() {
        if (env.legacyDoclet) {
            return new TypeVariable[0];
        }
        TypeVariable res[] = new TypeVariable[type.getTypeArguments().length()];
        TypeMaker.getTypes(env, type.getTypeArguments(), res);
        return res;
    }

    /**
     * Return the type parameter tags of this class or interface.
     */
    public ParamTag[] typeParamTags() {
        return (env.legacyDoclet)
            ? new ParamTag[0]
            : comment().typeParamTags();
    }

    /**
     * Return the modifier string for this class. If it's an interface
     * exclude 'abstract' keyword from the modifier string
     */
    @Override
    public String modifiers() {
        return Modifier.toString(modifierSpecifier());
    }

    @Override
    public int modifierSpecifier() {
        int modifiers = getModifiers();
        return (isInterface() || isAnnotationType())
                ? modifiers & ~Modifier.ABSTRACT
                : modifiers;
    }

    /**
     * Return the superclass of this class
     *
     * @return the ClassDocImpl for the superclass of this class, null
     * if there is no superclass.
     */
    public ClassDoc superclass() {
        if (isInterface() || isAnnotationType()) return null;
        if (tsym == env.syms.objectType.tsym) return null;
        ClassSymbol c = (ClassSymbol)env.types.supertype(type).tsym;
        if (c == null || c == tsym) c = (ClassSymbol)env.syms.objectType.tsym;
        return env.getClassDoc(c);
    }

    /**
     * Return the superclass of this class.  Return null if this is an
     * interface.  A superclass is represented by either a
     * <code>ClassDoc</code> or a <code>ParameterizedType</code>.
     */
    public com.sun.javadoc.Type superclassType() {
        if (isInterface() || isAnnotationType() ||
                (tsym == env.syms.objectType.tsym))
            return null;
        Type sup = env.types.supertype(type);
        return TypeMaker.getType(env,
                                 (sup != type) ? sup : env.syms.objectType);
    }

    /**
     * Test whether this class is a subclass of the specified class.
     *
     * @param cd the candidate superclass.
     * @return true if cd is a superclass of this class.
     */
    public boolean subclassOf(ClassDoc cd) {
        return tsym.isSubClass(((ClassDocImpl)cd).tsym, env.types);
    }

    /**
     * Return interfaces implemented by this class or interfaces
     * extended by this interface.
     *
     * @return An array of ClassDocImpl representing the interfaces.
     * Return an empty array if there are no interfaces.
     */
    public ClassDoc[] interfaces() {
        ListBuffer<ClassDocImpl> ta = new ListBuffer<ClassDocImpl>();
        for (Type t : env.types.interfaces(type)) {
            ta.append(env.getClassDoc((ClassSymbol)t.tsym));
        }
        //### Cache ta here?
        return ta.toArray(new ClassDocImpl[ta.length()]);
    }

    /**
     * Return interfaces implemented by this class or interfaces extended
     * by this interface. Includes only directly-declared interfaces, not
     * inherited interfaces.
     * Return an empty array if there are no interfaces.
     */
    public com.sun.javadoc.Type[] interfaceTypes() {
        //### Cache result here?
        return TypeMaker.getTypes(env, env.types.interfaces(type));
    }

    /**
     * Return fields in class.
     * @param filter include only the included fields if filter==true
     */
    public FieldDoc[] fields(boolean filter) {
        return fields(filter, false);
    }

    /**
     * Return included fields in class.
     */
    public FieldDoc[] fields() {
        return fields(true, false);
    }

    /**
     * Return the enum constants if this is an enum type.
     */
    public FieldDoc[] enumConstants() {
        return fields(false, true);
    }

    /**
     * Return fields in class.
     * @param filter  if true, return only the included fields
     * @param enumConstants  if true, return the enum constants instead
     */
    private FieldDoc[] fields(boolean filter, boolean enumConstants) {
        List<FieldDocImpl> fields = List.nil();
        for (Scope.Entry e = tsym.members().elems; e != null; e = e.sibling) {
            if (e.sym != null && e.sym.kind == VAR) {
                VarSymbol s = (VarSymbol)e.sym;
                boolean isEnum = ((s.flags() & Flags.ENUM) != 0) &&
                                 !env.legacyDoclet;
                if (isEnum == enumConstants &&
                        (!filter || env.shouldDocument(s))) {
                    fields = fields.prepend(env.getFieldDoc(s));
                }
            }
        }
        return fields.toArray(new FieldDocImpl[fields.length()]);
    }

    /**
     * Return methods in class.
     * This method is overridden by AnnotationTypeDocImpl.
     *
     * @param filter include only the included methods if filter==true
     * @return an array of MethodDocImpl for representing the visible
     * methods in this class.  Does not include constructors.
     */
    public MethodDoc[] methods(boolean filter) {
        Names names = tsym.name.table.names;
        List<MethodDocImpl> methods = List.nil();
        for (Scope.Entry e = tsym.members().elems; e != null; e = e.sibling) {
            if (e.sym != null &&
                e.sym.kind == Kinds.MTH && e.sym.name != names.init) {
                MethodSymbol s = (MethodSymbol)e.sym;
                if (!filter || env.shouldDocument(s)) {
                    methods = methods.prepend(env.getMethodDoc(s));
                }
            }
        }
        //### Cache methods here?
        return methods.toArray(new MethodDocImpl[methods.length()]);
    }

    /**
     * Return included methods in class.
     *
     * @return an array of MethodDocImpl for representing the visible
     * methods in this class.  Does not include constructors.
     */
    public MethodDoc[] methods() {
        return methods(true);
    }

    /**
     * Return constructors in class.
     *
     * @param filter include only the included constructors if filter==true
     * @return an array of ConstructorDocImpl for representing the visible
     * constructors in this class.
     */
    public ConstructorDoc[] constructors(boolean filter) {
        Names names = tsym.name.table.names;
        List<ConstructorDocImpl> constructors = List.nil();
        for (Scope.Entry e = tsym.members().elems; e != null; e = e.sibling) {
            if (e.sym != null &&
                e.sym.kind == Kinds.MTH && e.sym.name == names.init) {
                MethodSymbol s = (MethodSymbol)e.sym;
                if (!filter || env.shouldDocument(s)) {
                    constructors = constructors.prepend(env.getConstructorDoc(s));
                }
            }
        }
        //### Cache constructors here?
        return constructors.toArray(new ConstructorDocImpl[constructors.length()]);
    }

    /**
     * Return included constructors in class.
     *
     * @return an array of ConstructorDocImpl for representing the visible
     * constructors in this class.
     */
    public ConstructorDoc[] constructors() {
        return constructors(true);
    }

    /**
     * Adds all inner classes of this class, and their
     * inner classes recursively, to the list l.
     */
    void addAllClasses(ListBuffer<ClassDocImpl> l, boolean filtered) {
        try {
            if (isSynthetic()) return;
            // sometimes synthetic classes are not marked synthetic
            if (!JavadocTool.isValidClassName(tsym.name.toString())) return;
            if (filtered && !env.shouldDocument(tsym)) return;
            if (l.contains(this)) return;
            l.append(this);
            List<ClassDocImpl> more = List.nil();
            for (Scope.Entry e = tsym.members().elems; e != null;
                 e = e.sibling) {
                if (e.sym != null && e.sym.kind == Kinds.TYP) {
                    ClassSymbol s = (ClassSymbol)e.sym;
                    ClassDocImpl c = env.getClassDoc(s);
                    if (c.isSynthetic()) continue;
                    if (c != null) more = more.prepend(c);
                }
            }
            // this extra step preserves the ordering from oldjavadoc
            for (; more.nonEmpty(); more=more.tail) {
                more.head.addAllClasses(l, filtered);
            }
        } catch (CompletionFailure e) {
            // quietly ignore completion failures
        }
    }

    /**
     * Return inner classes within this class.
     *
     * @param filter include only the included inner classes if filter==true.
     * @return an array of ClassDocImpl for representing the visible
     * classes defined in this class. Anonymous and local classes
     * are not included.
     */
    public ClassDoc[] innerClasses(boolean filter) {
        ListBuffer<ClassDocImpl> innerClasses = new ListBuffer<ClassDocImpl>();
        for (Scope.Entry e = tsym.members().elems; e != null; e = e.sibling) {
            if (e.sym != null && e.sym.kind == Kinds.TYP) {
                ClassSymbol s = (ClassSymbol)e.sym;
                if ((s.flags_field & Flags.SYNTHETIC) != 0) continue;
                if (!filter || env.isVisible(s)) {
                    innerClasses.prepend(env.getClassDoc(s));
                }
            }
        }
        //### Cache classes here?
        return innerClasses.toArray(new ClassDocImpl[innerClasses.length()]);
    }

    /**
     * Return included inner classes within this class.
     *
     * @return an array of ClassDocImpl for representing the visible
     * classes defined in this class. Anonymous and local classes
     * are not included.
     */
    public ClassDoc[] innerClasses() {
        return innerClasses(true);
    }

    /**
     * Find a class within the context of this class.
     * Search order: qualified name, in this class (inner),
     * in this package, in the class imports, in the package
     * imports.
     * Return the ClassDocImpl if found, null if not found.
     */
    //### The specified search order is not the normal rule the
    //### compiler would use.  Leave as specified or change it?
    public ClassDoc findClass(String className) {
        ClassDoc searchResult = searchClass(className);
        if (searchResult == null) {
            ClassDocImpl enclosingClass = (ClassDocImpl)containingClass();
            //Expand search space to include enclosing class.
            while (enclosingClass != null && enclosingClass.containingClass() != null) {
                enclosingClass = (ClassDocImpl)enclosingClass.containingClass();
            }
            searchResult = enclosingClass == null ?
                null : enclosingClass.searchClass(className);
        }
        return searchResult;
    }

    private ClassDoc searchClass(String className) {
        Names names = tsym.name.table.names;

        // search by qualified name first
        ClassDoc cd = env.lookupClass(className);
        if (cd != null) {
            return cd;
        }

        // search inner classes
        //### Add private entry point to avoid creating array?
        //### Replicate code in innerClasses here to avoid consing?
        for (ClassDoc icd : innerClasses()) {
            if (icd.name().equals(className) ||
                    //### This is from original javadoc but it looks suspicious to me...
                    //### I believe it is attempting to compensate for the confused
                    //### convention of including the nested class qualifiers in the
                    //### 'name' of the inner class, rather than the true simple name.
                    icd.name().endsWith("." + className)) {
                return icd;
            } else {
                ClassDoc innercd = ((ClassDocImpl) icd).searchClass(className);
                if (innercd != null) {
                    return innercd;
                }
            }
        }

        // check in this package
        cd = containingPackage().findClass(className);
        if (cd != null) {
            return cd;
        }

        // make sure that this symbol has been completed
        if (tsym.completer != null) {
            tsym.complete();
        }

        // search imports

        if (tsym.sourcefile != null) {

            //### This information is available only for source classes.

            Env<AttrContext> compenv = env.enter.getEnv(tsym);
            if (compenv == null) return null;

            Scope s = compenv.toplevel.namedImportScope;
            for (Scope.Entry e = s.lookup(names.fromString(className)); e.scope != null; e = e.next()) {
                if (e.sym.kind == Kinds.TYP) {
                    ClassDoc c = env.getClassDoc((ClassSymbol)e.sym);
                    return c;
                }
            }

            s = compenv.toplevel.starImportScope;
            for (Scope.Entry e = s.lookup(names.fromString(className)); e.scope != null; e = e.next()) {
                if (e.sym.kind == Kinds.TYP) {
                    ClassDoc c = env.getClassDoc((ClassSymbol)e.sym);
                    return c;
                }
            }
        }

        return null; // not found
    }


    private boolean hasParameterTypes(MethodSymbol method, String[] argTypes) {

        if (argTypes == null) {
            // wildcard
            return true;
        }

        int i = 0;
        List<Type> types = method.type.getParameterTypes();

        if (argTypes.length != types.length()) {
            return false;
        }

        for (Type t : types) {
            String argType = argTypes[i++];
            // For vararg method, "T..." matches type T[].
            if (i == argTypes.length) {
                argType = argType.replace("...", "[]");
            }
            if (!hasTypeName(env.types.erasure(t), argType)) {  //###(gj)
                return false;
            }
        }
        return true;
    }
    // where
    private boolean hasTypeName(Type t, String name) {
        return
            name.equals(TypeMaker.getTypeName(t, true))
            ||
            name.equals(TypeMaker.getTypeName(t, false))
            ||
            (qualifiedName() + "." + name).equals(TypeMaker.getTypeName(t, true));
    }



    /**
     * Find a method in this class scope.
     * Search order: this class, interfaces, superclasses, outerclasses.
     * Note that this is not necessarily what the compiler would do!
     *
     * @param methodName the unqualified name to search for.
     * @param paramTypeArray the array of Strings for method parameter types.
     * @return the first MethodDocImpl which matches, null if not found.
     */
    public MethodDocImpl findMethod(String methodName, String[] paramTypes) {
        // Use hash table 'searched' to avoid searching same class twice.
        //### It is not clear how this could happen.
        return searchMethod(methodName, paramTypes, new HashSet<ClassDocImpl>());
    }

    private MethodDocImpl searchMethod(String methodName,
                                       String[] paramTypes, Set<ClassDocImpl> searched) {
        //### Note that this search is not necessarily what the compiler would do!

        Names names = tsym.name.table.names;
        // do not match constructors
        if (names.init.contentEquals(methodName)) {
            return null;
        }

        ClassDocImpl cdi;
        MethodDocImpl mdi;

        if (searched.contains(this)) {
            return null;
        }
        searched.add(this);

        //DEBUG
        /*---------------------------------*
         System.out.print("searching " + this + " for " + methodName);
         if (paramTypes == null) {
         System.out.println("()");
         } else {
         System.out.print("(");
         for (int k=0; k < paramTypes.length; k++) {
         System.out.print(paramTypes[k]);
         if ((k + 1) < paramTypes.length) {
         System.out.print(", ");
         }
         }
         System.out.println(")");
         }
         *---------------------------------*/

        // search current class
        Scope.Entry e = tsym.members().lookup(names.fromString(methodName));

        //### Using modifier filter here isn't really correct,
        //### but emulates the old behavior.  Instead, we should
        //### apply the normal rules of visibility and inheritance.

        if (paramTypes == null) {
            // If no parameters specified, we are allowed to return
            // any method with a matching name.  In practice, the old
            // code returned the first method, which is now the last!
            // In order to provide textually identical results, we
            // attempt to emulate the old behavior.
            MethodSymbol lastFound = null;
            for (; e.scope != null; e = e.next()) {
                if (e.sym.kind == Kinds.MTH) {
                    //### Should intern methodName as Name.
                    if (e.sym.name.toString().equals(methodName)) {
                        lastFound = (MethodSymbol)e.sym;
                    }
                }
            }
            if (lastFound != null) {
                return env.getMethodDoc(lastFound);
            }
        } else {
            for (; e.scope != null; e = e.next()) {
                if (e.sym != null &&
                    e.sym.kind == Kinds.MTH) {
                    //### Should intern methodName as Name.
                    if (hasParameterTypes((MethodSymbol)e.sym, paramTypes)) {
                        return env.getMethodDoc((MethodSymbol)e.sym);
                    }
                }
            }
        }

        //### If we found a MethodDoc above, but which did not pass
        //### the modifier filter, we should return failure here!

        // search superclass
        cdi = (ClassDocImpl)superclass();
        if (cdi != null) {
            mdi = cdi.searchMethod(methodName, paramTypes, searched);
            if (mdi != null) {
                return mdi;
            }
        }

        // search interfaces
        ClassDoc intf[] = interfaces();
        for (int i = 0; i < intf.length; i++) {
            cdi = (ClassDocImpl)intf[i];
            mdi = cdi.searchMethod(methodName, paramTypes, searched);
            if (mdi != null) {
                return mdi;
            }
        }

        // search enclosing class
        cdi = (ClassDocImpl)containingClass();
        if (cdi != null) {
            mdi = cdi.searchMethod(methodName, paramTypes, searched);
            if (mdi != null) {
                return mdi;
            }
        }

        //###(gj) As a temporary measure until type variables are better
        //### handled, try again without the parameter types.
        //### This should most often find the right method, and occassionally
        //### find the wrong one.
        //if (paramTypes != null) {
        //    return findMethod(methodName, null);
        //}

        return null;
    }

    /**
     * Find constructor in this class.
     *
     * @param constrName the unqualified name to search for.
     * @param paramTypeArray the array of Strings for constructor parameters.
     * @return the first ConstructorDocImpl which matches, null if not found.
     */
    public ConstructorDoc findConstructor(String constrName,
                                          String[] paramTypes) {
        Names names = tsym.name.table.names;
        for (Scope.Entry e = tsym.members().lookup(names.fromString("<init>")); e.scope != null; e = e.next()) {
            if (e.sym.kind == Kinds.MTH) {
                if (hasParameterTypes((MethodSymbol)e.sym, paramTypes)) {
                    return env.getConstructorDoc((MethodSymbol)e.sym);
                }
            }
        }

        //###(gj) As a temporary measure until type variables are better
        //### handled, try again without the parameter types.
        //### This will often find the right constructor, and occassionally
        //### find the wrong one.
        //if (paramTypes != null) {
        //    return findConstructor(constrName, null);
        //}

        return null;
    }

    /**
     * Find a field in this class scope.
     * Search order: this class, outerclasses, interfaces,
     * superclasses. IMP: If see tag is defined in an inner class,
     * which extends a super class and if outerclass and the super
     * class have a visible field in common then Java compiler cribs
     * about the ambiguity, but the following code will search in the
     * above given search order.
     *
     * @param fieldName the unqualified name to search for.
     * @return the first FieldDocImpl which matches, null if not found.
     */
    public FieldDoc findField(String fieldName) {
        return searchField(fieldName, new HashSet<ClassDocImpl>());
    }

    private FieldDocImpl searchField(String fieldName, Set<ClassDocImpl> searched) {
        Names names = tsym.name.table.names;
        if (searched.contains(this)) {
            return null;
        }
        searched.add(this);

        for (Scope.Entry e = tsym.members().lookup(names.fromString(fieldName)); e.scope != null; e = e.next()) {
            if (e.sym.kind == Kinds.VAR) {
                //### Should intern fieldName as Name.
                return env.getFieldDoc((VarSymbol)e.sym);
            }
        }

        //### If we found a FieldDoc above, but which did not pass
        //### the modifier filter, we should return failure here!

        ClassDocImpl cdi = (ClassDocImpl)containingClass();
        if (cdi != null) {
            FieldDocImpl fdi = cdi.searchField(fieldName, searched);
            if (fdi != null) {
                return fdi;
            }
        }

        // search superclass
        cdi = (ClassDocImpl)superclass();
        if (cdi != null) {
            FieldDocImpl fdi = cdi.searchField(fieldName, searched);
            if (fdi != null) {
                return fdi;
            }
        }

        // search interfaces
        ClassDoc intf[] = interfaces();
        for (int i = 0; i < intf.length; i++) {
            cdi = (ClassDocImpl)intf[i];
            FieldDocImpl fdi = cdi.searchField(fieldName, searched);
            if (fdi != null) {
                return fdi;
            }
        }

        return null;
    }

    /**
     * Get the list of classes declared as imported.
     * These are called "single-type-import declarations" in the JLS.
     * This method is deprecated in the ClassDoc interface.
     *
     * @return an array of ClassDocImpl representing the imported classes.
     *
     * @deprecated  Import declarations are implementation details that
     *          should not be exposed here.  In addition, not all imported
     *          classes are imported through single-type-import declarations.
     */
    @Deprecated
    public ClassDoc[] importedClasses() {
        // information is not available for binary classfiles
        if (tsym.sourcefile == null) return new ClassDoc[0];

        ListBuffer<ClassDocImpl> importedClasses = new ListBuffer<ClassDocImpl>();

        Env<AttrContext> compenv = env.enter.getEnv(tsym);
        if (compenv == null) return new ClassDocImpl[0];

        Name asterisk = tsym.name.table.names.asterisk;
        for (JCTree t : compenv.toplevel.defs) {
            if (t.getTag() == JCTree.IMPORT) {
                JCTree imp = ((JCImport) t).qualid;
                if ((TreeInfo.name(imp) != asterisk) &&
                        (imp.type.tsym.kind & Kinds.TYP) != 0) {
                    importedClasses.append(
                            env.getClassDoc((ClassSymbol)imp.type.tsym));
                }
            }
        }

        return importedClasses.toArray(new ClassDocImpl[importedClasses.length()]);
    }

    /**
     * Get the list of packages declared as imported.
     * These are called "type-import-on-demand declarations" in the JLS.
     * This method is deprecated in the ClassDoc interface.
     *
     * @return an array of PackageDocImpl representing the imported packages.
     *
     * ###NOTE: the syntax supports importing all inner classes from a class as well.
     * @deprecated  Import declarations are implementation details that
     *          should not be exposed here.  In addition, this method's
     *          return type does not allow for all type-import-on-demand
     *          declarations to be returned.
     */
    @Deprecated
    public PackageDoc[] importedPackages() {
        // information is not available for binary classfiles
        if (tsym.sourcefile == null) return new PackageDoc[0];

        ListBuffer<PackageDocImpl> importedPackages = new ListBuffer<PackageDocImpl>();

        //### Add the implicit "import java.lang.*" to the result
        Names names = tsym.name.table.names;
        importedPackages.append(env.getPackageDoc(env.reader.enterPackage(names.java_lang)));

        Env<AttrContext> compenv = env.enter.getEnv(tsym);
        if (compenv == null) return new PackageDocImpl[0];

        for (JCTree t : compenv.toplevel.defs) {
            if (t.getTag() == JCTree.IMPORT) {
                JCTree imp = ((JCImport) t).qualid;
                if (TreeInfo.name(imp) == names.asterisk) {
                    JCFieldAccess sel = (JCFieldAccess)imp;
                    Symbol s = sel.selected.type.tsym;
                    PackageDocImpl pdoc = env.getPackageDoc(s.packge());
                    if (!importedPackages.contains(pdoc))
                        importedPackages.append(pdoc);
                }
            }
        }

        return importedPackages.toArray(new PackageDocImpl[importedPackages.length()]);
    }

    /**
     * Return the type's dimension information.
     * Always return "", as this is not an array type.
     */
    public String dimension() {
        return "";
    }

    /**
     * Return this type as a class, which it already is.
     */
    public ClassDoc asClassDoc() {
        return this;
    }

    /**
     * Return null (unless overridden), as this is not an annotation type.
     */
    public AnnotationTypeDoc asAnnotationTypeDoc() {
        return null;
    }

    /**
     * Return null, as this is not a class instantiation.
     */
    public ParameterizedType asParameterizedType() {
        return null;
    }

    /**
     * Return null, as this is not a type variable.
     */
    public TypeVariable asTypeVariable() {
        return null;
    }

    /**
     * Return null, as this is not a wildcard type.
     */
    public WildcardType asWildcardType() {
        return null;
    }

    /**
     * Return false, as this is not a primitive type.
     */
    public boolean isPrimitive() {
        return false;
    }

    //--- Serialization ---

    //### These methods ignore modifier filter.

    /**
     * Return true if this class implements <code>java.io.Serializable</code>.
     *
     * Since <code>java.io.Externalizable</code> extends
     * <code>java.io.Serializable</code>,
     * Externalizable objects are also Serializable.
     */
    public boolean isSerializable() {
        try {
            return env.types.isSubtype(type, env.syms.serializableType);
        } catch (CompletionFailure ex) {
            // quietly ignore completion failures
            return false;
        }
    }

    /**
     * Return true if this class implements
     * <code>java.io.Externalizable</code>.
     */
    public boolean isExternalizable() {
        try {
            return env.types.isSubtype(type, env.externalizableSym.type);
        } catch (CompletionFailure ex) {
            // quietly ignore completion failures
            return false;
        }
    }

    /**
     * Return the serialization methods for this class.
     *
     * @return an array of <code>MethodDocImpl</code> that represents
     * the serialization methods for this class.
     */
    public MethodDoc[] serializationMethods() {
        if (serializedForm == null) {
            serializedForm = new SerializedForm(env, tsym, this);
        }
        //### Clone this?
        return serializedForm.methods();
    }

    /**
     * Return the Serializable fields of class.<p>
     *
     * Return either a list of default fields documented by
     * <code>serial</code> tag<br>
     * or return a single <code>FieldDoc</code> for
     * <code>serialPersistentField</code> member.
     * There should be a <code>serialField</code> tag for
     * each Serializable field defined by an <code>ObjectStreamField</code>
     * array component of <code>serialPersistentField</code>.
     *
     * @returns an array of <code>FieldDoc</code> for the Serializable fields
     * of this class.
     *
     * @see #definesSerializableFields()
     * @see SerialFieldTagImpl
     */
    public FieldDoc[] serializableFields() {
        if (serializedForm == null) {
            serializedForm = new SerializedForm(env, tsym, this);
        }
        //### Clone this?
        return serializedForm.fields();
    }

    /**
     * Return true if Serializable fields are explicitly defined with
     * the special class member <code>serialPersistentFields</code>.
     *
     * @see #serializableFields()
     * @see SerialFieldTagImpl
     */
    public boolean definesSerializableFields() {
        if (!isSerializable() || isExternalizable()) {
            return false;
        } else {
            if (serializedForm == null) {
                serializedForm = new SerializedForm(env, tsym, this);
            }
            //### Clone this?
            return serializedForm.definesSerializableFields();
        }
    }

    /**
     * Determine if a class is a RuntimeException.
     * <p>
     * Used only by ThrowsTagImpl.
     */
    boolean isRuntimeException() {
        return tsym.isSubClass(env.syms.runtimeExceptionType.tsym, env.types);
    }

    /**
     * Return the source position of the entity, or null if
     * no position is available.
     */
    @Override
    public SourcePosition position() {
        if (tsym.sourcefile == null) return null;
        return SourcePositionImpl.make(tsym.sourcefile,
                                       (tree==null) ? Position.NOPOS : tree.pos,
                                       lineMap);
    }
}
