/*
 * Copyright (c) 1994, 2003, 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 sun.tools.java;

import java.util.Hashtable;
import java.util.Vector;
import java.util.Enumeration;
import java.util.List;
import java.util.Collections;
import java.io.IOException;

/**
 * This class describes the classes and packages imported
 * from a source file. A Hashtable called bindings is maintained
 * to quickly map symbol names to classes. This table is flushed
 * everytime a new import is added.
 *
 * A class name is resolved as follows:
 *  - if it is a qualified name then return the corresponding class
 *  - if the name corresponds to an individually imported class then return that class
 *  - check if the class is defined in any of the imported packages,
 *    if it is then return it, make sure it is defined in only one package
 *  - assume that the class is defined in the current package
 *
 * WARNING: The contents of this source file are not part of any
 * supported API.  Code that depends on them does so at its own risk:
 * they are subject to change or removal without notice.
 */

public
class Imports implements Constants {
    /**
     * The current package, which is implicitly imported,
     * and has precedence over other imported packages.
     */
    Identifier currentPackage = idNull;

    /**
     * A location for the current package declaration.  Used to
     * report errors against the current package.
     */
    long currentPackageWhere = 0;

    /**
     * The imported classes, including memoized imports from packages.
     */
    Hashtable<Identifier, Identifier> classes = new Hashtable<>();

    /**
     * The imported package identifiers.  This will not contain duplicate
     * imports for the same package.  It will also not contain the
     * current package.
     */
    Vector<IdentifierToken> packages = new Vector<>();

    /**
     * The (originally) imported classes.
     * A vector of IdentifierToken.
     */
    Vector<IdentifierToken> singles = new Vector<>();

    /**
     * Are the import names checked yet?
     */
    protected int checked;

    /**
     * Constructor, always import java.lang.
     */
    public Imports(Environment env) {
        addPackage(idJavaLang);
    }

    /**
     * Check the names of the imports.
     */
    public synchronized void resolve(Environment env) {
        if (checked != 0) {
            return;
        }
        checked = -1;

        // After all class information has been read, now we can
        // safely inspect import information for errors.
        // If we did this before all parsing was finished,
        // we could get vicious circularities, since files can
        // import each others' classes.

        // A note: the resolution of the package java.lang takes place
        // in the sun.tools.javac.BatchEnvironment#setExemptPackages().

        // Make sure that the current package's name does not collide
        // with the name of an existing class. (bug 4101529)
        //
        // This change has been backed out because, on WIN32, it
        // failed to distinguish between java.awt.event and
        // java.awt.Event when looking for a directory.  We will
        // add this back in later.
        //
        // if (currentPackage != idNull) {
        //    Identifier resolvedName =
        //      env.resolvePackageQualifiedName(currentPackage);
        //
        //   Identifier className = resolvedName.getTopName();
        //
        //   if (importable(className, env)) {
        //      // The name of the current package is also the name
        //      // of a class.
        //      env.error(currentPackageWhere, "package.class.conflict",
        //                currentPackage, className);
        //     }
        // }

        Vector<IdentifierToken> resolvedPackages = new Vector<>();
        for (Enumeration<IdentifierToken> e = packages.elements() ; e.hasMoreElements() ;) {
            IdentifierToken t = e.nextElement();
            Identifier nm = t.getName();
            long where = t.getWhere();

            // Check to see if this package is exempt from the "exists"
            // check.  See the note in
            // sun.tools.javac.BatchEnvironment#setExemptPackages()
            // for more information.
            if (env.isExemptPackage(nm)) {
                resolvedPackages.addElement(t);
                continue;
            }

            // (Note: This code is moved from BatchParser.importPackage().)
            try {
                Identifier rnm = env.resolvePackageQualifiedName(nm);
                if (importable(rnm, env)) {
                    // This name is a real class; better not be a package too.
                    if (env.getPackage(rnm.getTopName()).exists()) {
                        env.error(where, "class.and.package",
                                  rnm.getTopName());
                    }
                    // Pass an "inner" name to the imports.
                    if (!rnm.isInner())
                        rnm = Identifier.lookupInner(rnm, idNull);
                    nm = rnm;
                } else if (!env.getPackage(nm).exists()) {
                    env.error(where, "package.not.found", nm, "import");
                } else if (rnm.isInner()) {
                    // nm exists, and rnm.getTopName() is a parent package
                    env.error(where, "class.and.package", rnm.getTopName());
                }
                resolvedPackages.addElement(new IdentifierToken(where, nm));
            } catch (IOException ee) {
                env.error(where, "io.exception", "import");
            }
        }
        packages = resolvedPackages;

        for (Enumeration<IdentifierToken> e = singles.elements() ; e.hasMoreElements() ;) {
            IdentifierToken t = e.nextElement();
            Identifier nm = t.getName();
            long where = t.getWhere();
            Identifier pkg = nm.getQualifier();

            // (Note: This code is moved from BatchParser.importClass().)
            nm = env.resolvePackageQualifiedName(nm);
            if (!env.classExists(nm.getTopName())) {
                env.error(where, "class.not.found", nm, "import");
            }

            // (Note: This code is moved from Imports.addClass().)
            Identifier snm = nm.getFlatName().getName();

            // make sure it isn't already imported explicitly
            Identifier className = classes.get(snm);
            if (className != null) {
                Identifier f1 = Identifier.lookup(className.getQualifier(),
                                                  className.getFlatName());
                Identifier f2 = Identifier.lookup(nm.getQualifier(),
                                                  nm.getFlatName());
                if (!f1.equals(f2)) {
                    env.error(where, "ambig.class", nm, className);
                }
            }
            classes.put(snm, nm);


            // The code here needs to check to see, if we
            // are importing an inner class, that all of its
            // enclosing classes are visible to us.  To check this,
            // we need to construct a definition for the class.
            // The code here used to call...
            //
            //     ClassDefinition def = env.getClassDefinition(nm);
            //
            // ...but that interfered with the basicCheck()'ing of
            // interfaces in certain cases (bug no. 4086139).  Never
            // fear.  Instead we load the class with a call to the
            // new getClassDefinitionNoCheck() which does no basicCheck() and
            // lets us answer the questions we are interested in w/o
            // interfering with the demand-driven nature of basicCheck().

            try {
                // Get a declaration
                ClassDeclaration decl = env.getClassDeclaration(nm);

                // Get the definition (no env argument)
                ClassDefinition def = decl.getClassDefinitionNoCheck(env);

                // Get the true name of the package containing this class.
                // `pkg' from above is insufficient.  It includes the
                // names of our enclosing classes.  Fix for 4086815.
                Identifier importedPackage = def.getName().getQualifier();

                // Walk out the outerClass chain, ensuring that each level
                // is visible from our perspective.
                for (; def != null; def = def.getOuterClass()) {
                    if (def.isPrivate()
                        || !(def.isPublic()
                             || importedPackage.equals(currentPackage))) {
                        env.error(where, "cant.access.class", def);
                        break;
                    }
                }
            } catch (AmbiguousClass ee) {
                env.error(where, "ambig.class", ee.name1, ee.name2);
            } catch (ClassNotFound ee) {
                env.error(where, "class.not.found", ee.name, "import");
            }
        }
        checked = 1;
    }

    /**
     * Lookup a class, given the current set of imports,
     * AmbiguousClass exception is thrown if the name can be
     * resolved in more than one way. A ClassNotFound exception
     * is thrown if the class is not found in the imported classes
     * and packages.
     */
    public synchronized Identifier resolve(Environment env, Identifier nm) throws ClassNotFound {
        if (tracing) env.dtEnter("Imports.resolve: " + nm);

        // If the class has the special ambiguous prefix, then we will
        // get the original AmbiguousClass exception by removing the
        // prefix and proceeding in the normal fashion.
        // (part of solution for 4059855)
        if (nm.hasAmbigPrefix()) {
            nm = nm.removeAmbigPrefix();
        }

        if (nm.isQualified()) {
            // Don't bother it is already qualified
            if (tracing) env.dtExit("Imports.resolve: QUALIFIED " + nm);
            return nm;
        }

        if (checked <= 0) {
            checked = 0;
            resolve(env);
        }

        // Check if it was imported before
        Identifier className = classes.get(nm);
        if (className != null) {
            if (tracing) env.dtExit("Imports.resolve: PREVIOUSLY IMPORTED " + nm);
            return className;
        }

        // Note: the section below has changed a bit during the fix
        // for bug 4093217.  The current package is no longer grouped
        // with the rest of the import-on-demands; it is now checked
        // separately.  Also, the list of import-on-demands is now
        // guarranteed to be duplicate-free, so the code below can afford
        // to be a bit simpler.

        // First we look in the current package.  The current package
        // is given precedence over the rest of the import-on-demands,
        // which means, among other things, that a class in the current
        // package cannot be ambiguous.
        Identifier id = Identifier.lookup(currentPackage, nm);
        if (importable(id, env)) {
            className = id;
        } else {
            // If it isn't in the current package, try to find it in
            // our import-on-demands.
            Enumeration<IdentifierToken> e = packages.elements();
            while (e.hasMoreElements()) {
                IdentifierToken t = e.nextElement();
                id = Identifier.lookup(t.getName(), nm);

                if (importable(id, env)) {
                    if (className == null) {
                        // We haven't found any other matching classes yet.
                        // Set className to what we've found and continue
                        // looking for an ambiguity.
                        className = id;
                    } else {
                        if (tracing)
                            env.dtExit("Imports.resolve: AMBIGUOUS " + nm);

                        // We've found an ambiguity.
                        throw new AmbiguousClass(className, id);
                    }
                }
            }
        }

        // Make sure a class was found
        if (className == null) {
            if (tracing) env.dtExit("Imports.resolve: NOT FOUND " + nm);
            throw new ClassNotFound(nm);
        }

        // Remember the binding
        classes.put(nm, className);
        if (tracing) env.dtExit("Imports.resolve: FIRST IMPORT " + nm);
        return className;
    }

    /**
     * Check to see if 'id' names an importable class in `env'.
     * This method was made public and static for utility.
     */
    static public boolean importable(Identifier id, Environment env) {
        if (!id.isInner()) {
            return env.classExists(id);
        } else if (!env.classExists(id.getTopName())) {
            return false;
        } else {
            // load the top class and look inside it
            try {
                // There used to be a call to...
                //    env.getClassDeclaration(id.getTopName());
                // ...here.  It has been replaced with the
                // two statements below.  These should be functionally
                // the same except for the fact that
                // getClassDefinitionNoCheck() does not call
                // basicCheck().  This allows us to avoid a circular
                // need to do basicChecking that can arise with
                // certain patterns of importing and inheritance.
                // This is a fix for a variant of bug 4086139.
                //
                // Note: the special case code in env.getClassDefinition()
                // which handles inner class names is not replicated below.
                // This should be okay, as we are looking up id.getTopName(),
                // not id.
                ClassDeclaration decl =
                    env.getClassDeclaration(id.getTopName());
                ClassDefinition c =
                    decl.getClassDefinitionNoCheck(env);

                return c.innerClassExists(id.getFlatName().getTail());
            } catch (ClassNotFound ee) {
                return false;
            }
        }
    }

    /**
     * Suppose a resolve() call has failed.
     * This routine can be used silently to give a reasonable
     * default qualification (the current package) to the identifier.
     * This decision is recorded for future reference.
     */
    public synchronized Identifier forceResolve(Environment env, Identifier nm) {
        if (nm.isQualified())
            return nm;

        Identifier className = classes.get(nm);
        if (className != null) {
            return className;
        }

        className = Identifier.lookup(currentPackage, nm);

        classes.put(nm, className);
        return className;
    }

    /**
     * Add a class import
     */
    public synchronized void addClass(IdentifierToken t) {
        singles.addElement(t);
    }
    // for compatibility
    public void addClass(Identifier nm) throws AmbiguousClass {
        addClass(new IdentifierToken(nm));
    }

    /**
     * Add a package import, or perhaps an inner class scope.
     * Ignore any duplicate imports.
     */
    public synchronized void addPackage(IdentifierToken t) {
        final Identifier name = t.getName();

        // If this is a duplicate import for the current package,
        // ignore it.
        if (name == currentPackage) {
            return;
        }

        // If this is a duplicate of a package which has already been
        // added to the list, ignore it.
        final int size = packages.size();
        for (int i = 0; i < size; i++) {
            if (name == (packages.elementAt(i)).getName()) {
                return;
            }
        }

        // Add the package to the list.
        packages.addElement(t);
    }
    // for compatibility
    public void addPackage(Identifier id) {
        addPackage(new IdentifierToken(id));
    }

    /**
     * Specify the current package with an IdentifierToken.
     */
    public synchronized void setCurrentPackage(IdentifierToken t) {
        currentPackage = t.getName();
        currentPackageWhere = t.getWhere();
    }

    /**
     * Specify the current package
     */
    public synchronized void setCurrentPackage(Identifier id) {
        currentPackage = id;
    }

    /**
     * Report the current package
     */
    public Identifier getCurrentPackage() {
        return currentPackage;
    }

    /**
     * Return an unmodifiable list of IdentifierToken representing
     * packages specified as imports.
     */
    public List<IdentifierToken> getImportedPackages() {
        return Collections.unmodifiableList(packages);
    }

    /**
     * Return an unmodifiable list of IdentifierToken representing
     * classes specified as imports.
     */
    public List<IdentifierToken> getImportedClasses() {
        return Collections.unmodifiableList(singles);
    }

    /**
     * Extend an environment with my resolve() method.
     */
    public Environment newEnvironment(Environment env) {
        return new ImportEnvironment(env, this);
    }
}

final
class ImportEnvironment extends Environment {
    Imports imports;

    ImportEnvironment(Environment env, Imports imports) {
        super(env, env.getSource());
        this.imports = imports;
    }

    public Identifier resolve(Identifier nm) throws ClassNotFound {
        return imports.resolve(this, nm);
    }

    public Imports getImports() {
        return imports;
    }
}
