/*
 * Copyright (c) 1999, 2016, 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.util.*;
import java.util.regex.Pattern;

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

/**
 * A data structure that encapsulates the visible members of a particular
 * type for a given class tree.  To use this data structor, you must specify
 * the type of member you are interested in (nested class, field, constructor
 * or method) and the leaf of the class tree.  The data structure will map
 * all visible members in the leaf and classes above the leaf in the tree.
 *
 *  <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>
 *
 * @author Atul M Dambalkar
 * @author Jamie Ho (rewrite)
 */
@Deprecated
public class VisibleMemberMap {

    private boolean noVisibleMembers = true;

    public static final int INNERCLASSES    = 0;
    public static final int ENUM_CONSTANTS  = 1;
    public static final int FIELDS          = 2;
    public static final int CONSTRUCTORS    = 3;
    public static final int METHODS         = 4;
    public static final int ANNOTATION_TYPE_FIELDS = 5;
    public static final int ANNOTATION_TYPE_MEMBER_OPTIONAL = 6;
    public static final int ANNOTATION_TYPE_MEMBER_REQUIRED = 7;
    public static final int PROPERTIES      = 8;

    /**
     * The total number of member types is {@value}.
     */
    public static final int NUM_MEMBER_TYPES = 9;

    public static final String STARTLEVEL = "start";

    /**
     * List of ClassDoc objects for which ClassMembers objects are built.
     */
    private final List<ClassDoc> visibleClasses = new ArrayList<>();

    /**
     * Map for each member name on to a map which contains members with same
     * name-signature. The mapped map will contain mapping for each MemberDoc
     * onto it's respecive level string.
     */
    private final Map<Object,Map<ProgramElementDoc,String>> memberNameMap = new HashMap<>();

    /**
     * Map of class and it's ClassMembers object.
     */
    private final Map<ClassDoc,ClassMembers> classMap = new HashMap<>();

    /**
     * Type whose visible members are requested.  This is the leaf of
     * the class tree being mapped.
     */
    private final ClassDoc classdoc;

    /**
     * Member kind: InnerClasses/Fields/Methods?
     */
    private final int kind;

    /**
     * The configuration this VisibleMemberMap was created with.
     */
    private final Configuration configuration;
    private final Utils utils;

    private static final Map<ClassDoc, ProgramElementDoc[]> propertiesCache = new HashMap<>();
    private static final Map<ProgramElementDoc, ProgramElementDoc> classPropertiesMap = new HashMap<>();
    private static final Map<ProgramElementDoc, GetterSetter> getterSetterMap = new HashMap<>();

    /**
     * Construct a VisibleMemberMap of the given type for the given
     * class.
     *
     * @param classdoc the class whose members are being mapped.
     * @param kind the kind of member that is being mapped.
     * @param configuration the configuration to use to construct this
     * VisibleMemberMap. If the field configuration.nodeprecated is true the
     * deprecated members are excluded from the map. If the field
     * configuration.javafx is true the JavaFX features are used.
     */
    public VisibleMemberMap(ClassDoc classdoc,
                            int kind,
                            Configuration configuration) {
        this.classdoc = classdoc;
        this.kind = kind;
        this.configuration = configuration;
        this.utils = configuration.utils;
        new ClassMembers(classdoc, STARTLEVEL).build();
    }

    /**
     * Return the list of visible classes in this map.
     *
     * @return the list of visible classes in this map.
     */
    public List<ClassDoc> getVisibleClassesList() {
        sort(visibleClasses);
        return visibleClasses;
    }

    /**
     * Returns the property field documentation belonging to the given member.
     * @param ped the member for which the property documentation is needed.
     * @return the property field documentation, null if there is none.
     */
    public ProgramElementDoc getPropertyMemberDoc(ProgramElementDoc ped) {
        return classPropertiesMap.get(ped);
    }

    /**
     * Returns the getter documentation belonging to the given property method.
     * @param propertyMethod the method for which the getter is needed.
     * @return the getter documentation, null if there is none.
     */
    public ProgramElementDoc getGetterForProperty(ProgramElementDoc propertyMethod) {
        return getterSetterMap.get(propertyMethod).getGetter();
    }

    /**
     * Returns the setter documentation belonging to the given property method.
     * @param propertyMethod the method for which the setter is needed.
     * @return the setter documentation, null if there is none.
     */
    public ProgramElementDoc getSetterForProperty(ProgramElementDoc propertyMethod) {
        return getterSetterMap.get(propertyMethod).getSetter();
    }

    /**
     * Return the package private members inherited by the class.  Only return
     * if parent is package private and not documented.
     *
     * @param configuration the current configuration of the doclet.
     * @return the package private members inherited by the class.
     */
    private List<ProgramElementDoc> getInheritedPackagePrivateMethods(Configuration configuration) {
        List<ProgramElementDoc> results = new ArrayList<>();
        for (ClassDoc currentClass : visibleClasses) {
            if (currentClass != classdoc &&
                currentClass.isPackagePrivate() &&
                !utils.isLinkable(currentClass, configuration)) {
                // Document these members in the child class because
                // the parent is inaccessible.
                results.addAll(getMembersFor(currentClass));
            }
        }
        return results;
    }

    /**
     * Return the visible members of the class being mapped.  Also append at the
     * end of the list members that are inherited by inaccessible parents. We
     * document these members in the child because the parent is not documented.
     *
     * @param configuration the current configuration of the doclet.
     */
    public List<ProgramElementDoc> getLeafClassMembers(Configuration configuration) {
        List<ProgramElementDoc> result = getMembersFor(classdoc);
        result.addAll(getInheritedPackagePrivateMethods(configuration));
        return result;
    }

    /**
     * Retrn the list of members for the given class.
     *
     * @param cd the class to retrieve the list of visible members for.
     *
     * @return the list of members for the given class.
     */
    public List<ProgramElementDoc> getMembersFor(ClassDoc cd) {
        ClassMembers clmembers = classMap.get(cd);
        if (clmembers == null) {
            return new ArrayList<>();
        }
        return clmembers.getMembers();
    }

    /**
     * Sort the given mixed list of classes and interfaces to a list of
     * classes followed by interfaces traversed. Don't sort alphabetically.
     */
    private void sort(List<ClassDoc> list) {
        List<ClassDoc> classes = new ArrayList<>();
        List<ClassDoc> interfaces = new ArrayList<>();
        for (ClassDoc cd : list) {
            if (cd.isClass()) {
                classes.add(cd);
            }
            else {
                interfaces.add(cd);
            }
        }
        list.clear();
        list.addAll(classes);
        list.addAll(interfaces);
    }

    private void fillMemberLevelMap(List<ProgramElementDoc> list, String level) {
        for (ProgramElementDoc element : list) {
            Object key = getMemberKey(element);
            Map<ProgramElementDoc, String> memberLevelMap = memberNameMap.get(key);
            if (memberLevelMap == null) {
                memberLevelMap = new HashMap<>();
                memberNameMap.put(key, memberLevelMap);
            }
            memberLevelMap.put(element, level);
        }
    }

    private void purgeMemberLevelMap(List<ProgramElementDoc> list, String level) {
        for (ProgramElementDoc element : list) {
            Object key = getMemberKey(element);
            Map<ProgramElementDoc, String> memberLevelMap = memberNameMap.get(key);
            if (memberLevelMap != null && level.equals(memberLevelMap.get(element)))
                memberLevelMap.remove(element);
        }
    }

    /**
     * Represents a class member.  We should be able to just use a
     * ProgramElementDoc instead of this class, but that doesn't take
     * type variables in consideration when comparing.
     */
    private class ClassMember {
        private Set<ProgramElementDoc> members;

        public ClassMember(ProgramElementDoc programElementDoc) {
            members = new HashSet<>();
            members.add(programElementDoc);
        }

        public void addMember(ProgramElementDoc programElementDoc) {
            members.add(programElementDoc);
        }

        public boolean isEqual(MethodDoc member) {
            for (ProgramElementDoc element : members) {
                if (utils.executableMembersEqual(member, (MethodDoc) element)) {
                    members.add(member);
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * A data structure that represents the class members for
     * a visible class.
     */
    private class ClassMembers {

        /**
         * The mapping class, whose inherited members are put in the
         * {@link #members} list.
         */
        private ClassDoc mappingClass;

        /**
         * List of inherited members from the mapping class.
         */
        private List<ProgramElementDoc> members = new ArrayList<>();

        /**
         * Level/Depth of inheritance.
         */
        private String level;

        /**
         * Return list of inherited members from mapping class.
         *
         * @return List Inherited members.
         */
        public List<ProgramElementDoc> getMembers() {
            return members;
        }

        private ClassMembers(ClassDoc mappingClass, String level) {
            this.mappingClass = mappingClass;
            this.level = level;
            if (classMap.containsKey(mappingClass) &&
                        level.startsWith(classMap.get(mappingClass).level)) {
                //Remove lower level class so that it can be replaced with
                //same class found at higher level.
                purgeMemberLevelMap(getClassMembers(mappingClass, false),
                    classMap.get(mappingClass).level);
                classMap.remove(mappingClass);
                visibleClasses.remove(mappingClass);
            }
            if (!classMap.containsKey(mappingClass)) {
                classMap.put(mappingClass, this);
                visibleClasses.add(mappingClass);
            }

        }

        private void build() {
            if (kind == CONSTRUCTORS) {
                addMembers(mappingClass);
            } else {
                mapClass();
            }
        }

        private void mapClass() {
            addMembers(mappingClass);
            ClassDoc[] interfaces = mappingClass.interfaces();
            for (ClassDoc anInterface : interfaces) {
                String locallevel = level + 1;
                ClassMembers cm = new ClassMembers(anInterface, locallevel);
                cm.mapClass();
            }
            if (mappingClass.isClass()) {
                ClassDoc superclass = mappingClass.superclass();
                if (!(superclass == null || mappingClass.equals(superclass))) {
                    ClassMembers cm = new ClassMembers(superclass,
                                                       level + "c");
                    cm.mapClass();
                }
            }
        }

        /**
         * Get all the valid members from the mapping class. Get the list of
         * members for the class to be included into(ctii), also get the level
         * string for ctii. If mapping class member is not already in the
         * inherited member list and if it is visible in the ctii and not
         * overridden, put such a member in the inherited member list.
         * Adjust member-level-map, class-map.
         */
        private void addMembers(ClassDoc fromClass) {
            List<ProgramElementDoc> cdmembers = getClassMembers(fromClass, true);
            List<ProgramElementDoc> incllist = new ArrayList<>();
            for (ProgramElementDoc pgmelem : cdmembers) {
                if (!found(members, pgmelem) &&
                    memberIsVisible(pgmelem) &&
                    !isOverridden(pgmelem, level) &&
                    !isTreatedAsPrivate(pgmelem)) {
                    incllist.add(pgmelem);
                }
            }
            if (incllist.size() > 0) {
                noVisibleMembers = false;
            }
            members.addAll(incllist);
            fillMemberLevelMap(getClassMembers(fromClass, false), level);
        }

        private boolean isTreatedAsPrivate(ProgramElementDoc pgmelem) {
            if (!configuration.javafx) {
                return false;
            }

            Tag[] aspTags = pgmelem.tags("@treatAsPrivate");
            boolean result = (aspTags != null) && (aspTags.length > 0);
            return result;
        }

        /**
         * Is given doc item visible in given classdoc in terms fo inheritance?
         * The given doc item is visible in the given classdoc if it is public
         * or protected and if it is package-private if it's containing class
         * is in the same package as the given classdoc.
         */
        private boolean memberIsVisible(ProgramElementDoc pgmdoc) {
            if (pgmdoc.containingClass().equals(classdoc)) {
                //Member is in class that we are finding visible members for.
                //Of course it is visible.
                return true;
            } else if (pgmdoc.isPrivate()) {
                //Member is in super class or implemented interface.
                //Private, so not inherited.
                return false;
            } else if (pgmdoc.isPackagePrivate()) {
                //Member is package private.  Only return true if its class is in
                //same package.
                return pgmdoc.containingClass().containingPackage().equals(
                    classdoc.containingPackage());
            } else {
                //Public members are always inherited.
                return true;
            }
        }

        /**
         * Return all available class members.
         */
        private List<ProgramElementDoc> getClassMembers(ClassDoc cd, boolean filter) {
            if (cd.isEnum() && kind == CONSTRUCTORS) {
                //If any of these rules are hit, return empty array because
                //we don't document these members ever.
                return Arrays.asList(new ProgramElementDoc[] {});
            }
            ProgramElementDoc[] members = null;
            switch (kind) {
                case ANNOTATION_TYPE_FIELDS:
                    members = cd.fields(filter);
                    break;
                case ANNOTATION_TYPE_MEMBER_OPTIONAL:
                    members = cd.isAnnotationType() ?
                        filter((AnnotationTypeDoc) cd, false) :
                        new AnnotationTypeElementDoc[] {};
                    break;
                case ANNOTATION_TYPE_MEMBER_REQUIRED:
                    members = cd.isAnnotationType() ?
                        filter((AnnotationTypeDoc) cd, true) :
                        new AnnotationTypeElementDoc[] {};
                    break;
                case INNERCLASSES:
                    members = cd.innerClasses(filter);
                    break;
                case ENUM_CONSTANTS:
                    members = cd.enumConstants();
                    break;
                case FIELDS:
                    members = cd.fields(filter);
                    break;
                case CONSTRUCTORS:
                    members = cd.constructors();
                    break;
                case METHODS:
                    members = cd.methods(filter);
                    checkOnPropertiesTags((MethodDoc[])members);
                    break;
                case PROPERTIES:
                    members = properties(cd, filter);
                    break;
                default:
                    members = new ProgramElementDoc[0];
            }
            // Deprected members should be excluded or not?
            if (configuration.nodeprecated) {
                return utils.excludeDeprecatedMembersAsList(members);
            }
            return Arrays.asList(members);
        }

        /**
         * Filter the annotation type members and return either the required
         * members or the optional members, depending on the value of the
         * required parameter.
         *
         * @param doc The annotation type to process.
         * @param required
         * @return the annotation type members and return either the required
         * members or the optional members, depending on the value of the
         * required parameter.
         */
        private AnnotationTypeElementDoc[] filter(AnnotationTypeDoc doc,
            boolean required) {
            AnnotationTypeElementDoc[] members = doc.elements();
            List<AnnotationTypeElementDoc> targetMembers = new ArrayList<>();
            for (AnnotationTypeElementDoc member : members) {
                if ((required && member.defaultValue() == null) ||
                    ((!required) && member.defaultValue() != null)) {
                    targetMembers.add(member);
                }
            }
            return targetMembers.toArray(new AnnotationTypeElementDoc[]{});
        }

        private boolean found(List<ProgramElementDoc> list, ProgramElementDoc elem) {
            for (ProgramElementDoc pgmelem : list) {
                if (utils.matches(pgmelem, elem)) {
                    return true;
                }
            }
            return false;
        }


        /**
         * Is member overridden? The member is overridden if it is found in the
         * same level hierarchy e.g. member at level "11" overrides member at
         * level "111".
         */
        private boolean isOverridden(ProgramElementDoc pgmdoc, String level) {
            Map<?,String> memberLevelMap = (Map<?,String>) memberNameMap.get(getMemberKey(pgmdoc));
            if (memberLevelMap == null)
                return false;
            for (String mappedlevel : memberLevelMap.values()) {
                if (mappedlevel.equals(STARTLEVEL) ||
                    (level.startsWith(mappedlevel) &&
                     !level.equals(mappedlevel))) {
                    return true;
                }
            }
            return false;
        }

        private ProgramElementDoc[] properties(final ClassDoc cd, final boolean filter) {
            final MethodDoc[] allMethods = cd.methods(filter);
            final FieldDoc[] allFields = cd.fields(false);

            if (propertiesCache.containsKey(cd)) {
                return propertiesCache.get(cd);
            }

            final List<MethodDoc> result = new ArrayList<>();

            for (final MethodDoc propertyMethod : allMethods) {

                if (!isPropertyMethod(propertyMethod)) {
                    continue;
                }

                final MethodDoc getter = getterForField(allMethods, propertyMethod);
                final MethodDoc setter = setterForField(allMethods, propertyMethod);
                final FieldDoc field = fieldForProperty(allFields, propertyMethod);

                addToPropertiesMap(setter, getter, propertyMethod, field);
                getterSetterMap.put(propertyMethod, new GetterSetter(getter, setter));
                result.add(propertyMethod);
            }
            final ProgramElementDoc[] resultAray =
                    result.toArray(new ProgramElementDoc[result.size()]);
            propertiesCache.put(cd, resultAray);
            return resultAray;
        }

        private void addToPropertiesMap(MethodDoc setter,
                                        MethodDoc getter,
                                        MethodDoc propertyMethod,
                                        FieldDoc field) {
            if ((field == null)
                    || (field.getRawCommentText() == null)
                    || field.getRawCommentText().length() == 0) {
                addToPropertiesMap(setter, propertyMethod);
                addToPropertiesMap(getter, propertyMethod);
                addToPropertiesMap(propertyMethod, propertyMethod);
            } else {
                addToPropertiesMap(getter, field);
                addToPropertiesMap(setter, field);
                addToPropertiesMap(propertyMethod, field);
            }
        }

        private void addToPropertiesMap(ProgramElementDoc propertyMethod,
                                        ProgramElementDoc commentSource) {
            if (null == propertyMethod || null == commentSource) {
                return;
            }
            final String methodRawCommentText = propertyMethod.getRawCommentText();

            /* The second condition is required for the property buckets. In
             * this case the comment is at the property method (not at the field)
             * and it needs to be listed in the map.
             */
            if ((null == methodRawCommentText || 0 == methodRawCommentText.length())
                    || propertyMethod.equals(commentSource)) {
                classPropertiesMap.put(propertyMethod, commentSource);
            }
        }

        private MethodDoc getterForField(MethodDoc[] methods,
                                         MethodDoc propertyMethod) {
            final String propertyMethodName = propertyMethod.name();
            final String fieldName =
                    propertyMethodName.substring(0,
                            propertyMethodName.lastIndexOf("Property"));
            final String fieldNameUppercased =
                    "" + Character.toUpperCase(fieldName.charAt(0))
                                            + fieldName.substring(1);
            final String getterNamePattern;
            final String fieldTypeName = propertyMethod.returnType().toString();
            if ("boolean".equals(fieldTypeName)
                    || fieldTypeName.endsWith("BooleanProperty")) {
                getterNamePattern = "(is|get)" + fieldNameUppercased;
            } else {
                getterNamePattern = "get" + fieldNameUppercased;
            }

            for (MethodDoc methodDoc : methods) {
                if (Pattern.matches(getterNamePattern, methodDoc.name())) {
                    if (0 == methodDoc.parameters().length
                            && (methodDoc.isPublic() || methodDoc.isProtected())) {
                        return methodDoc;
                    }
                }
            }
            return null;
        }

        private MethodDoc setterForField(MethodDoc[] methods,
                                         MethodDoc propertyMethod) {
            final String propertyMethodName = propertyMethod.name();
            final String fieldName =
                    propertyMethodName.substring(0,
                            propertyMethodName.lastIndexOf("Property"));
            final String fieldNameUppercased =
                    "" + Character.toUpperCase(fieldName.charAt(0))
                                             + fieldName.substring(1);
            final String setter = "set" + fieldNameUppercased;

            for (MethodDoc methodDoc : methods) {
                if (setter.equals(methodDoc.name())) {
                    if (1 == methodDoc.parameters().length
                            && "void".equals(methodDoc.returnType().simpleTypeName())
                            && (methodDoc.isPublic() || methodDoc.isProtected())) {
                        return methodDoc;
                    }
                }
            }
            return null;
        }

        private FieldDoc fieldForProperty(FieldDoc[] fields, MethodDoc property) {

            for (FieldDoc field : fields) {
                final String fieldName = field.name();
                final String propertyName = fieldName + "Property";
                if (propertyName.equals(property.name())) {
                    return field;
                }
            }
            return null;
        }

        // properties aren't named setA* or getA*
        private final Pattern pattern = Pattern.compile("[sg]et\\p{Upper}.*");
        private boolean isPropertyMethod(MethodDoc method) {
            if (!configuration.javafx) {
               return false;
            }
            if (!method.name().endsWith("Property")) {
                return false;
            }

            if (! memberIsVisible(method)) {
                return false;
            }

            if (pattern.matcher(method.name()).matches()) {
                return false;
            }
            if (method.typeParameters().length > 0) {
                return false;
            }
            return 0 == method.parameters().length
                    && !"void".equals(method.returnType().simpleTypeName());
        }

        private void checkOnPropertiesTags(MethodDoc[] members) {
            for (MethodDoc methodDoc: members) {
                if (methodDoc.isIncluded()) {
                    for (Tag tag: methodDoc.tags()) {
                        String tagName = tag.name();
                        if (tagName.equals("@propertySetter")
                                || tagName.equals("@propertyGetter")
                                || tagName.equals("@propertyDescription")) {
                            if (!isPropertyGetterOrSetter(members, methodDoc)) {
                                configuration.message.warning(tag.position(),
                                        "doclet.javafx_tag_misuse");
                            }
                            break;
                        }
                    }
                }
            }
        }

        private boolean isPropertyGetterOrSetter(MethodDoc[] members,
                                                 MethodDoc methodDoc) {
            boolean found = false;
            String propertyName = utils.propertyNameFromMethodName(configuration, methodDoc.name());
            if (!propertyName.isEmpty()) {
                String propertyMethodName = propertyName + "Property";
                for (MethodDoc member: members) {
                    if (member.name().equals(propertyMethodName)) {
                        found = true;
                        break;
                    }
                }
            }
            return found;
        }
    }

    private class GetterSetter {
        private final ProgramElementDoc getter;
        private final ProgramElementDoc setter;

        public GetterSetter(ProgramElementDoc getter, ProgramElementDoc setter) {
            this.getter = getter;
            this.setter = setter;
        }

        public ProgramElementDoc getGetter() {
            return getter;
        }

        public ProgramElementDoc getSetter() {
            return setter;
        }
    }

    /**
     * Return true if this map has no visible members.
     *
     * @return true if this map has no visible members.
     */
    public boolean noVisibleMembers() {
        return noVisibleMembers;
    }

    private ClassMember getClassMember(MethodDoc member) {
        for (Object key : memberNameMap.keySet()) {
            if (key instanceof String) {
                continue;
            } else if (((ClassMember) key).isEqual(member)) {
                return (ClassMember) key;
            }
        }
        return new ClassMember(member);
    }

    /**
     * Return the key to the member map for the given member.
     */
    private Object getMemberKey(ProgramElementDoc doc) {
        if (doc.isConstructor()) {
            return doc.name() + ((ExecutableMemberDoc)doc).signature();
        } else if (doc.isMethod()) {
            return getClassMember((MethodDoc) doc);
        } else if (doc.isField() || doc.isEnumConstant() || doc.isAnnotationTypeElement()) {
            return doc.name();
        } else { // it's a class or interface
            String classOrIntName = doc.name();
            //Strip off the containing class name because we only want the member name.
            classOrIntName = classOrIntName.indexOf('.') != 0 ? classOrIntName.substring(classOrIntName.lastIndexOf('.'), classOrIntName.length()) : classOrIntName;
            return "clint" + classOrIntName;
        }
    }
}
