/*
 * Copyright (c) 2015, 2017, 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 jdk.javadoc.internal.doclets.toolkit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.FileObject;
import javax.tools.JavaFileManager.Location;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TreePath;
import com.sun.tools.doclint.DocLint;
import com.sun.tools.javac.api.BasicJavacTask;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Scope;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.ModuleSymbol;
import com.sun.tools.javac.code.Symbol.PackageSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.comp.AttrContext;
import com.sun.tools.javac.comp.Env;
import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.model.JavacTypes;
import com.sun.tools.javac.util.Names;

import jdk.javadoc.internal.doclets.toolkit.util.Utils;
import jdk.javadoc.internal.tool.ToolEnvironment;
import jdk.javadoc.internal.tool.DocEnvImpl;

import static com.sun.tools.javac.code.Kinds.Kind.*;
import static com.sun.tools.javac.code.Scope.LookupKind.NON_RECURSIVE;

import static javax.lang.model.element.ElementKind.*;

/**
 * A quarantine class to isolate all the workarounds and bridges to
 * a locality. This class should eventually disappear once all the
 * standard APIs support the needed interfaces.
 *
 *
 *  <p><b>This is NOT part of any supported API.
 *  If you write code that depends on this, you do so at your own risk.
 *  This code and its internal interfaces are subject to change or
 *  deletion without notice.</b>
 */
public class WorkArounds {

    public final Configuration configuration;
    public final ToolEnvironment toolEnv;
    public final Utils utils;

    private DocLint doclint;

    public WorkArounds(Configuration configuration) {
        this.configuration = configuration;
        this.utils = this.configuration.utils;
        this.toolEnv = ((DocEnvImpl)this.configuration.docEnv).toolEnv;
    }

    Map<CompilationUnitTree, Boolean> shouldCheck = new HashMap<>();
    // TODO: fix this up correctly
    public void runDocLint(TreePath path) {
        CompilationUnitTree unit = path.getCompilationUnit();
        if (doclint != null && shouldCheck.computeIfAbsent(unit, doclint::shouldCheck)) {
            doclint.scan(path);
        }
    }

    // TODO: fix this up correctly
    public void initDocLint(Collection<String> opts, Collection<String> customTagNames, String htmlVersion) {
        ArrayList<String> doclintOpts = new ArrayList<>();
        boolean msgOptionSeen = false;

        for (String opt : opts) {
            if (opt.startsWith(DocLint.XMSGS_OPTION)) {
                if (opt.equals(DocLint.XMSGS_CUSTOM_PREFIX + "none"))
                    return;
                msgOptionSeen = true;
            }
            doclintOpts.add(opt);
        }

        if (!msgOptionSeen) {
            doclintOpts.add(DocLint.XMSGS_OPTION);
        }

        String sep = "";
        StringBuilder customTags = new StringBuilder();
        for (String customTag : customTagNames) {
            customTags.append(sep);
            customTags.append(customTag);
            sep = DocLint.SEPARATOR;
        }
        doclintOpts.add(DocLint.XCUSTOM_TAGS_PREFIX + customTags.toString());
        doclintOpts.add(DocLint.XHTML_VERSION_PREFIX + htmlVersion);

        JavacTask t = BasicJavacTask.instance(toolEnv.context);
        doclint = new DocLint();
        // standard doclet normally generates H1, H2
        doclintOpts.add(DocLint.XIMPLICIT_HEADERS + "2");
        doclint.init(t, doclintOpts.toArray(new String[doclintOpts.size()]), false);
    }

    // TODO: fix this up correctly
    public boolean haveDocLint() {
        return (doclint == null);
    }

    // TODO: jx.l.m directSuperTypes don't work for things like Enum,
    // so we use javac directly, investigate why jx.l.m is not cutting it.
    public List<TypeMirror> interfaceTypesOf(TypeMirror type) {
        com.sun.tools.javac.util.List<com.sun.tools.javac.code.Type> interfaces =
                ((DocEnvImpl)configuration.docEnv).toolEnv.getTypes().interfaces((com.sun.tools.javac.code.Type)type);
        if (interfaces.isEmpty()) {
            return Collections.emptyList();
        }
        List<TypeMirror> list = new ArrayList<>(interfaces.size());
        for (com.sun.tools.javac.code.Type t : interfaces) {
            list.add((TypeMirror)t);
        }
        return list;
    }

    /*
     * TODO: This method exists because of a bug in javac which does not
     * handle "@deprecated tag in package-info.java", when this issue
     * is fixed this method and its uses must be jettisoned.
     */
    public boolean isDeprecated0(Element e) {
        if (!utils.getDeprecatedTrees(e).isEmpty()) {
            return true;
        }
        JavacTypes jctypes = ((DocEnvImpl)configuration.docEnv).toolEnv.typeutils;
        TypeMirror deprecatedType = utils.getDeprecatedType();
        for (AnnotationMirror anno : e.getAnnotationMirrors()) {
            if (jctypes.isSameType(anno.getAnnotationType().asElement().asType(), deprecatedType))
                return true;
        }
        return false;
    }

    // TODO: fix jx.l.m add this method.
    public boolean isSynthesized(AnnotationMirror aDesc) {
        return ((Attribute)aDesc).isSynthesized();
    }

    // TODO: fix the caller
    public Object getConstValue(VariableElement ve) {
        return ((VarSymbol)ve).getConstValue();
    }

    // TODO: DocTrees: Trees.getPath(Element e) is slow a factor 4-5 times.
    public Map<Element, TreePath> getElementToTreePath() {
        return toolEnv.elementToTreePath;
    }

    // TODO: we need ElementUtils.getPackage to cope with input strings
    // to return the proper unnamedPackage for all supported releases.
    PackageElement getUnnamedPackage() {
        return (toolEnv.source.allowModules())
                ? toolEnv.syms.unnamedModule.unnamedPackage
                : toolEnv.syms.noModule.unnamedPackage;
    }

    // TODO: implement in either jx.l.m API (preferred) or DocletEnvironment.
    FileObject getJavaFileObject(PackageElement packageElement) {
        return ((PackageSymbol)packageElement).sourcefile;
    }

    // TODO: needs to ported to jx.l.m.
    public TypeElement searchClass(TypeElement klass, String className) {
        // search by qualified name first
        TypeElement te = configuration.docEnv.getElementUtils().getTypeElement(className);
        if (te != null) {
            return te;
        }

        // search inner classes
        for (TypeElement ite : utils.getClasses(klass)) {
            TypeElement innerClass = searchClass(ite, className);
            if (innerClass != null) {
                return innerClass;
            }
        }

        // check in this package
        te = utils.findClassInPackageElement(utils.containingPackage(klass), className);
        if (te != null) {
            return te;
        }

        ClassSymbol tsym = (ClassSymbol)klass;
        // make sure that this symbol has been completed
        // TODO: do we need this anymore ?
        if (tsym.completer != null) {
            tsym.complete();
        }

        // search imports
        if (tsym.sourcefile != null) {

            //### This information is available only for source classes.
            Env<AttrContext> compenv = toolEnv.getEnv(tsym);
            if (compenv == null) {
                return null;
            }
            Names names = tsym.name.table.names;
            Scope s = compenv.toplevel.namedImportScope;
            for (Symbol sym : s.getSymbolsByName(names.fromString(className))) {
                if (sym.kind == TYP) {
                    return (TypeElement)sym;
                }
            }

            s = compenv.toplevel.starImportScope;
            for (Symbol sym : s.getSymbolsByName(names.fromString(className))) {
                if (sym.kind == TYP) {
                    return (TypeElement)sym;
                }
            }
        }

        return null; // not found
    }

    // TODO:  need to re-implement this using j.l.m. correctly!, this has
    // implications on testInterface, the note here is that javac's supertype
    // does the right thing returning Parameters in scope.
    /**
     * Return the type containing the method that this method overrides.
     * It may be a <code>TypeElement</code> or a <code>TypeParameterElement</code>.
     * @param method target
     * @return a type
     */
    public TypeMirror overriddenType(ExecutableElement method) {
        if (utils.isStatic(method)) {
            return null;
        }
        MethodSymbol sym = (MethodSymbol)method;
        ClassSymbol origin = (ClassSymbol) sym.owner;
        for (com.sun.tools.javac.code.Type t = toolEnv.getTypes().supertype(origin.type);
                t.hasTag(com.sun.tools.javac.code.TypeTag.CLASS);
                t = toolEnv.getTypes().supertype(t)) {
            ClassSymbol c = (ClassSymbol) t.tsym;
            for (com.sun.tools.javac.code.Symbol sym2 : c.members().getSymbolsByName(sym.name)) {
                if (sym.overrides(sym2, origin, toolEnv.getTypes(), true)) {
                    return t;
                }
            }
        }
        return null;
    }

    // TODO: the method jx.l.m.Elements::overrides does not check
    // the return type, see JDK-8174840 until that is resolved,
    // use a  copy of the same method, with a return type check.

    // Note: the rider.overrides call in this method *must* be consistent
    // with the call in overrideType(....), the method above.
    public boolean overrides(ExecutableElement e1, ExecutableElement e2, TypeElement cls) {
        MethodSymbol rider = (MethodSymbol)e1;
        MethodSymbol ridee = (MethodSymbol)e2;
        ClassSymbol origin = (ClassSymbol)cls;

        return rider.name == ridee.name &&

               // not reflexive as per JLS
               rider != ridee &&

               // we don't care if ridee is static, though that wouldn't
               // compile
               !rider.isStatic() &&

               // Symbol.overrides assumes the following
               ridee.isMemberOf(origin, toolEnv.getTypes()) &&

               // check access, signatures and check return types
               rider.overrides(ridee, origin, toolEnv.getTypes(), true);
    }

    // TODO: jx.l.m ?
    public Location getLocationForModule(ModuleElement mdle) {
        ModuleSymbol msym = (ModuleSymbol)mdle;
        return msym.sourceLocation != null
                ? msym.sourceLocation
                : msym.classLocation;
    }

    //------------------Start of Serializable Implementation---------------------//
    private final static Map<TypeElement, NewSerializedForm> serializedForms = new HashMap<>();

    public SortedSet<VariableElement> getSerializableFields(Utils utils, TypeElement klass) {
        NewSerializedForm sf = serializedForms.get(klass);
        if (sf == null) {
            sf = new NewSerializedForm(utils, configuration.docEnv.getElementUtils(), klass);
            serializedForms.put(klass, sf);
        }
        return sf.fields;
    }

    public SortedSet<ExecutableElement>  getSerializationMethods(Utils utils, TypeElement klass) {
        NewSerializedForm sf = serializedForms.get(klass);
        if (sf == null) {
            sf = new NewSerializedForm(utils, configuration.docEnv.getElementUtils(), klass);
            serializedForms.put(klass, sf);
        }
        return sf.methods;
    }

    public boolean definesSerializableFields(Utils utils, TypeElement klass) {
        if (!utils.isSerializable(klass) || utils.isExternalizable(klass)) {
            return false;
        } else {
            NewSerializedForm sf = serializedForms.get(klass);
            if (sf == null) {
                sf = new NewSerializedForm(utils, configuration.docEnv.getElementUtils(), klass);
                serializedForms.put(klass, sf);
            }
            return sf.definesSerializableFields;
        }
    }

    /* TODO we need a clean port to jx.l.m
     * The serialized form is the specification of a class' serialization state.
     * <p>
     *
     * It consists of the following information:
     * <p>
     *
     * <pre>
     * 1. Whether class is Serializable or Externalizable.
     * 2. Javadoc for serialization methods.
     *    a. For Serializable, the optional readObject, writeObject,
     *       readResolve and writeReplace.
     *       serialData tag describes, in prose, the sequence and type
     *       of optional data written by writeObject.
     *    b. For Externalizable, writeExternal and readExternal.
     *       serialData tag describes, in prose, the sequence and type
     *       of optional data written by writeExternal.
     * 3. Javadoc for serialization data layout.
     *    a. For Serializable, the name,type and description
     *       of each Serializable fields.
     *    b. For Externalizable, data layout is described by 2(b).
     * </pre>
     *
     */
    static class NewSerializedForm {

        final Utils utils;
        final Elements elements;

        final SortedSet<ExecutableElement> methods;

        /* List of FieldDocImpl - Serializable fields.
         * Singleton list if class defines Serializable fields explicitly.
         * Otherwise, list of default serializable fields.
         * 0 length list for Externalizable.
         */
        final SortedSet<VariableElement> fields;

        /* True if class specifies serializable fields explicitly.
         * using special static member, serialPersistentFields.
         */
        boolean definesSerializableFields = false;

        // Specially treated field/method names defined by Serialization.
        private static final String SERIALIZABLE_FIELDS = "serialPersistentFields";
        private static final String READOBJECT = "readObject";
        private static final String WRITEOBJECT = "writeObject";
        private static final String READRESOLVE = "readResolve";
        private static final String WRITEREPLACE = "writeReplace";
        private static final String READOBJECTNODATA = "readObjectNoData";

        NewSerializedForm(Utils utils, Elements elements, TypeElement te) {
            this.utils = utils;
            this.elements = elements;
            methods = new TreeSet<>(utils.makeGeneralPurposeComparator());
            fields = new TreeSet<>(utils.makeGeneralPurposeComparator());
            if (utils.isExternalizable(te)) {
                /* look up required public accessible methods,
                 *   writeExternal and readExternal.
                 */
                String[] readExternalParamArr = {"java.io.ObjectInput"};
                String[] writeExternalParamArr = {"java.io.ObjectOutput"};

                ExecutableElement md = findMethod(te, "readExternal", Arrays.asList(readExternalParamArr));
                if (md != null) {
                    methods.add(md);
                }
                md = findMethod((ClassSymbol) te, "writeExternal", Arrays.asList(writeExternalParamArr));
                if (md != null) {
                    methods.add(md);
                }
            } else if (utils.isSerializable(te)) {
                VarSymbol dsf = getDefinedSerializableFields((ClassSymbol) te);
                if (dsf != null) {
                    /* Define serializable fields with array of ObjectStreamField.
                     * Each ObjectStreamField should be documented by a
                     * serialField tag.
                     */
                    definesSerializableFields = true;
                    fields.add((VariableElement) dsf);
                } else {

                    /* Calculate default Serializable fields as all
                     * non-transient, non-static fields.
                     * Fields should be documented by serial tag.
                     */
                    computeDefaultSerializableFields((ClassSymbol) te);
                }

                /* Check for optional customized readObject, writeObject,
                 * readResolve and writeReplace, which can all contain
                 * the serialData tag.        */
                addMethodIfExist((ClassSymbol) te, READOBJECT);
                addMethodIfExist((ClassSymbol) te, WRITEOBJECT);
                addMethodIfExist((ClassSymbol) te, READRESOLVE);
                addMethodIfExist((ClassSymbol) te, WRITEREPLACE);
                addMethodIfExist((ClassSymbol) te, READOBJECTNODATA);
            }
        }

        private VarSymbol getDefinedSerializableFields(ClassSymbol def) {
            Names names = def.name.table.names;

            /* SERIALIZABLE_FIELDS can be private,
             */
            for (Symbol sym : def.members().getSymbolsByName(names.fromString(SERIALIZABLE_FIELDS))) {
                if (sym.kind == VAR) {
                    VarSymbol f = (VarSymbol) sym;
                    if ((f.flags() & Flags.STATIC) != 0
                            && (f.flags() & Flags.PRIVATE) != 0) {
                        return f;
                    }
                }
            }
            return null;
        }

        /*
         * Catalog Serializable method if it exists in current ClassSymbol.
         * Do not look for method in superclasses.
         *
         * Serialization requires these methods to be non-static.
         *
         * @param method should be an unqualified Serializable method
         *               name either READOBJECT, WRITEOBJECT, READRESOLVE
         *               or WRITEREPLACE.
         * @param visibility the visibility flag for the given method.
         */
        private void addMethodIfExist(ClassSymbol def, String methodName) {
            Names names = def.name.table.names;

            for (Symbol sym : def.members().getSymbolsByName(names.fromString(methodName))) {
                if (sym.kind == MTH) {
                    MethodSymbol md = (MethodSymbol) sym;
                    if ((md.flags() & Flags.STATIC) == 0) {
                        /*
                         * WARNING: not robust if unqualifiedMethodName is overloaded
                         *          method. Signature checking could make more robust.
                         * READOBJECT takes a single parameter, java.io.ObjectInputStream.
                         * WRITEOBJECT takes a single parameter, java.io.ObjectOutputStream.
                         */
                        methods.add(md);
                    }
                }
            }
        }

        /*
         * Compute default Serializable fields from all members of ClassSymbol.
         *
         * must walk over all members of ClassSymbol.
         */
        private void computeDefaultSerializableFields(ClassSymbol te) {
            for (Symbol sym : te.members().getSymbols(NON_RECURSIVE)) {
                if (sym != null && sym.kind == VAR) {
                    VarSymbol f = (VarSymbol) sym;
                    if ((f.flags() & Flags.STATIC) == 0
                            && (f.flags() & Flags.TRANSIENT) == 0) {
                        //### No modifier filtering applied here.
                        //### Add to beginning.
                        //### Preserve order used by old 'javadoc'.
                        fields.add(f);
                    }
                }
            }
        }

        /**
         * 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 paramTypes the array of Strings for method parameter types.
         * @return the first MethodDocImpl which matches, null if not found.
         */
        public ExecutableElement findMethod(TypeElement te, String methodName,
                List<String> paramTypes) {
            List<? extends Element> allMembers = this.elements.getAllMembers(te);
            loop:
            for (Element e : allMembers) {
                if (e.getKind() != METHOD) {
                    continue;
                }
                ExecutableElement ee = (ExecutableElement) e;
                if (!ee.getSimpleName().contentEquals(methodName)) {
                    continue;
                }
                List<? extends VariableElement> parameters = ee.getParameters();
                if (paramTypes.size() != parameters.size()) {
                    continue;
                }
                for (int i = 0; i < parameters.size(); i++) {
                    VariableElement ve = parameters.get(i);
                    if (!ve.asType().toString().equals(paramTypes.get(i))) {
                        break loop;
                    }
                }
                return ee;
            }
            TypeElement encl = utils.getEnclosingTypeElement(te);
            if (encl == null) {
                return null;
            }
            return findMethod(encl, methodName, paramTypes);
        }
    }

    // TODO: we need to eliminate this, as it is hacky.
    /**
     * Returns a representation of the package truncated to two levels.
     * For instance if the given package represents foo.bar.baz will return
     * a representation of foo.bar
     * @param pkg the PackageElement
     * @return an abbreviated PackageElement
     */
    public PackageElement getAbbreviatedPackageElement(PackageElement pkg) {
        String parsedPackageName = utils.parsePackageName(pkg);
        ModuleElement encl = (ModuleElement) pkg.getEnclosingElement();
        PackageElement abbrevPkg = encl == null
                ? utils.elementUtils.getPackageElement(parsedPackageName)
                : ((JavacElements) utils.elementUtils).getPackageElement(encl, parsedPackageName);
        return abbrevPkg;
    }
}
