/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.web.el;

import com.sun.el.parser.*;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.el.ELException;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.ElementFilter;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatformManager;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.TypeUtilities.TypeNameOptions;
import org.netbeans.modules.web.el.completion.ELStreamCompletionItem;
import org.netbeans.modules.web.el.spi.ELVariableResolver.VariableInfo;
import org.netbeans.modules.web.el.spi.ImplicitObject;
import org.netbeans.modules.web.el.refactoring.RefactoringUtil;
import org.netbeans.modules.web.el.spi.ELPlugin;
import org.netbeans.modules.web.el.spi.ELVariableResolver;
import org.netbeans.modules.web.el.spi.Function;
import org.netbeans.modules.web.el.spi.ImplicitObjectType;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;

/**
 * Utility class for resolving elements/types for EL expressions.
 *
 * @author Erno Mononen
 */
public final class ELTypeUtilities {

    private static final Logger LOG = Logger.getLogger(ELTypeUtilities.class.getName());

    private static final String FACES_CONTEXT_CLASS = "javax.faces.context.FacesContext";   //NOI18N
    private static final String UI_COMPONENT_CLASS = "javax.faces.component.UIComponent";   //NOI18N
    private static final String STREAM_CLASS = "com.sun.el.stream.Stream";                  //NOI18N

    private static final FileObject EL_IMPL_JAR_FO = FileUtil.getArchiveRoot(FileUtil.toFileObject(
            InstalledFileLocator.getDefault().locate("modules/ext/el-impl.jar", "org.netbeans.modules.libs.elimpl", false))); //NOI18N

    private static final Map<Class<? extends Node>, Set<TypeKind>> TYPES = new HashMap<>();

    static {
        put(AstFloatingPoint.class, TypeKind.FLOAT, TypeKind.DOUBLE);
        put(AstTrue.class, TypeKind.BOOLEAN);
        put(AstFalse.class, TypeKind.BOOLEAN);
        put(AstInteger.class, TypeKind.INT, TypeKind.SHORT, TypeKind.LONG);
    }

    private static void put(Class<? extends Node> node, TypeKind... kinds) {
        Set<TypeKind> kindSet = EnumSet.noneOf(TypeKind.class);
        kindSet.addAll(Arrays.asList(kinds));
        TYPES.put(node, kindSet);
    }

    private ELTypeUtilities() {
        //do not create instancies
    }

    public static String getTypeNameFor(CompilationContext info, Element element) {
        final TypeMirror tm = getTypeMirrorFor(info, element);
        return info.info().getTypeUtilities().getTypeName(tm).toString();
    }

    public static Element getTypeFor(CompilationContext info, Element element) {
        TypeMirror tm = getTypeMirrorFor(info, element);
        return info.info().getTypes().asElement(tm);
    }

    public static List<Element> getSuperTypesFor(CompilationContext info, Element element) {
        return getSuperTypesFor(info, element, null, null);
    }
    
    /**
     * 
     * @param element
     * @return a list of Element-s representing all the superclasses of the element. 
     * The list starts with the given element itself and ends with java.lang.Object
     */
    public static List<Element> getSuperTypesFor(CompilationContext info, Element element, ELElement elElement, List<Node> rootToNode) {
        final TypeMirror tm = getTypeMirrorFor(info, element, elElement, rootToNode);
        List<Element> types = new ArrayList<>();

        Deque<TypeMirror> deque = new ArrayDeque<>();
        deque.add(tm);
        while (!deque.isEmpty()) {
            TypeMirror mirror = deque.pop();
            if (mirror.getKind() == TypeKind.DECLARED) {
                Element el = info.info().getTypes().asElement(mirror);
                types.add(el);

                if (el.getKind() == ElementKind.CLASS) {
                    TypeElement tel = (TypeElement) el;
                    TypeMirror superclass = tel.getSuperclass();
                    deque.add(superclass);
                } else if (el.getKind() == ElementKind.INTERFACE) {
                    TypeElement tel = (TypeElement) el;
                    for (TypeMirror ifaceMirror : tel.getInterfaces()) {
                        deque.add(ifaceMirror);
                    }
                }
            }
        }

        return types;
    }

    /**
     * Resolves the element for the given {@code target}.
     * @param elem
     * @param target
     * @return the element or {@code null}.
     */
    public static Element resolveElement(CompilationContext info, final ELElement elem, final Node target) {
        return resolveElement(info, elem, target, Collections.<AstIdentifier, Node>emptyMap(), Collections.<VariableInfo>emptyList());
    }

    /**
     * Resolves the element for the given {@code target}.
     * @param elem
     * @param target
     * @return the element or {@code null}.
     */
    public static Element resolveElement(CompilationContext info, final ELElement elem, final Node target, Map<AstIdentifier, Node> assignments, List<VariableInfo> variableInfos) {
        TypeResolverVisitor typeResolver = new TypeResolverVisitor(info, elem, target, assignments, variableInfos);
        elem.getNode().accept(typeResolver);
        return typeResolver.getResult();
    }

    public static TypeMirror getReturnType(CompilationContext info, final ExecutableElement method) {
        return getReturnType(info, method, null, null);
    }    
    
    /**
     * Gets the return type of the given {@code method}.
     * @param method
     * @return
     */
    public static TypeMirror getReturnType(CompilationContext info, final ExecutableElement method, ELElement elElement, List<Node> rootToNode) {
        TypeKind returnTypeKind = method.getReturnType().getKind();
        if (returnTypeKind.isPrimitive()) {
            return info.info().getTypes().getPrimitiveType(returnTypeKind);
        } else if (returnTypeKind == TypeKind.VOID) {
            return info.info().getTypes().getNoType(returnTypeKind);
        } else if (returnTypeKind == TypeKind.TYPEVAR && rootToNode != null && elElement != null) {
            return getReturnTypeForGenericClass(info, method, elElement, rootToNode);
        } else {
            return method.getReturnType();
        }
    }
    
    public static TypeMirror getReturnTypeForGenericClass(CompilationContext info, final ExecutableElement method, ELElement elElement, List<Node> rootToNode) {
        Node node = null;
        for (int i = rootToNode.size() - 1; i > 0; i--) {
            node = rootToNode.get(i);
            if (node instanceof AstIdentifier) {
                break;
            }
        }
        if (node != null) {
            Element element = ELTypeUtilities.resolveElement(info, elElement, node);
            if (element == null) {
                return method.getReturnType();
            }

            TypeMirror type = element.asType();
            // interfaces are at the end of the List - first parameter has to be superclass
            TypeMirror directSupertype = info.info().getTypes().directSupertypes(type).get(0);
            if (directSupertype instanceof DeclaredType) {
                DeclaredType declaredType = (DeclaredType) directSupertype;
                // index of involved type argument
                int indexOfTypeArgument = -1;
                // list of all type arguments
                List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();

                // search for the same method in the generic class
                for (Element enclosedElement : declaredType.asElement().getEnclosedElements()) {
                    if (method.equals(enclosedElement)) {
                        TypeMirror returnType = ((ExecutableElement) enclosedElement).getReturnType();
                        // get index of type argument which is returned by involved method
                        indexOfTypeArgument = info.info().getElementUtilities().enclosingTypeElement(method).
                                getTypeParameters().indexOf(((TypeVariable) returnType).asElement());
                        break;
                    }
                }
                if (indexOfTypeArgument != -1 && indexOfTypeArgument < typeArguments.size()) {
                    return typeArguments.get(indexOfTypeArgument);
                }
            }
        }

        return method.getReturnType();
    }

    
    private static List<Node> getMethodParameters(Node methodNode) {
        assert NodeUtil.isMethodCall(methodNode);
        
        if (methodNode.jjtGetNumChildren() == 0) {
            return Collections.emptyList();
        }

        Node methodArgs = methodNode.jjtGetChild(0);
        for (int i = 0; i < methodNode.jjtGetNumChildren(); i++) {
            Node currentNode = methodNode.jjtGetChild(i);
            if (currentNode instanceof AstMethodArguments) {
                methodArgs = currentNode;
                break;
            }
        }

        if (!(methodArgs instanceof AstMethodArguments)) {
            return Collections.emptyList();
        }

        List<Node> parameters = new ArrayList<>();
        for (int i = 0; i < methodArgs.jjtGetNumChildren(); i++) {
            parameters.add(methodArgs.jjtGetChild(i));
        }
        return parameters;
    }

    /**
     * Says whether the given method node and the element's method correspond.
     * @param info context
     * @param methodNode EL's method node
     * @param method method element
     * @return {@code true} if the method node correspond (param, naming) to the method element, {@code false} otherwise
     */
    public static boolean isSameMethod(CompilationContext info, Node methodNode, ExecutableElement method) {
        return isSameMethod(info, methodNode, method, false);
    }

    /**
     * Says whether the given method node and the element's method correspond.
     * @param info context
     * @param methodNode EL's method node
     * @param method method element
     * @param includeSetter whether setters method should be considered as correct - <b>not precise, do not call it in refactoring</b>
     * @return {@code true} if the method node correspond with the method element, {@code false} otherwise
     */
    public static boolean isSameMethod(CompilationContext info, Node methodNode, ExecutableElement method, boolean includeSetter) {
        String image = getMethodName(methodNode);
        String methodName = method.getSimpleName().toString();
        TypeMirror methodReturnType = method.getReturnType();
        if (image == null) {
            return false;
        }
        int methodParams = method.getParameters().size();
        if (NodeUtil.isMethodCall(methodNode) &&
                (methodName.equals(image) || RefactoringUtil.getPropertyName(methodName, methodReturnType).equals(image))) {
            //now we are in AstDotSuffix or AstBracketSuffix

            //lets check if the parameters are equal
            List<Node> parameters = getMethodParameters(methodNode);
            int methodNodeParams = parameters.size();
            if (method.isVarArgs()) {
                return methodParams == 1 ? true : methodNodeParams >= methodParams;
            }
            return (method.getParameters().size() == methodNodeParams && haveSameParameters(info, methodNode, method))
                    || methodNodeParams == 0 && hasActionEventArgument(method);
        }

        if (methodNode instanceof AstDotSuffix) {
            if (methodName.equals(image) || RefactoringUtil.getPropertyName(methodName, methodReturnType).equals(image)) {
                if (methodNode.jjtGetNumChildren() > 0) {
                    for (int i = 0; i < method.getParameters().size(); i++) {
                        final VariableElement methodParameter = method.getParameters().get(i);
                        final Node methodNodeParameter = methodNode.jjtGetChild(i);

                        if (!isSameType(info, methodNodeParameter, methodParameter)) {
                            return false;
                        }
                    }
                }

                if (image.equals(methodName)) {
                    return true;
                }

                return method.isVarArgs()
                        ? method.getParameters().size() == 1
                        : method.getParameters().isEmpty();
            } else if (includeSetter && RefactoringUtil.getPropertyName(methodName, methodReturnType, true).equals(image)) {
                // issue #225849 - we don't have additional information from the Facelet,
                // believe the naming conventions. This is not used for refactoring actions.
                return true;
            }
        }
        return false;
    }

    public static TypeElement getElementForType(CompilationContext info, final String clazz) {
        return info.info().getElements().getTypeElement(clazz);
    }

    public static List<String> getParameterNames(CompilationContext info, final ExecutableElement method) {
        List<String> result = new ArrayList<>();
        for (VariableElement param : method.getParameters()) {
            result.add(param.getSimpleName().toString());
        }
        return result;
    }

    public static String getParametersAsString(CompilationContext info, ExecutableElement method) {
        StringBuilder result = new StringBuilder();
        for (VariableElement param : method.getParameters()) {
            if (result.length() > 0) {
                result.append(",");
            }
            String type = info.info().getTypeUtilities().getTypeName(param.asType()).toString();
            result.append(type);
            result.append(" ");
            result.append(param.getSimpleName().toString());
        }

        if (result.length() > 0) {
            result.insert(0, "(");
            result.append(")");
        }
        return result.toString();
    }

    public static Collection<ImplicitObject> getImplicitObjects(CompilationContext info) {
        return ELPlugin.Query.getImplicitObjects(info.file());
    }

    public static Collection<Function> getELFunctions(CompilationContext info) {
        return ELPlugin.Query.getFunctions(info.file());
    }

    public static boolean isScopeObject(CompilationContext info, Node target) {
        if (!(target instanceof AstIdentifier)) {
            return false;
        }
        for (ImplicitObject each : getImplicitObjects(info)) {
            if (each.getType() == ImplicitObjectType.SCOPE_TYPE
                    && each.getName().equals(target.getImage())) {
                return true;
            }
        }
        return false;
    }

    public static boolean isRawObjectReference(CompilationContext info, Node target, boolean directly) {
//        Parse tree for #{cc.attrs.muj} expression
//
//        CompositeExpression
//            DeferredExpression
//                Value
//                    Identifier[cc]
//                    PropertySuffix[attrs]
//                    PropertySuffix[muj]

        return isImplicitObjectReference(info, target, Arrays.asList(ImplicitObjectType.RAW), directly);
    }

    public static String getRawObjectName(Node root) {
        List<Node> path = new AstPath(root).rootToLeaf();
        for (int i = 0; i < root.jjtGetNumChildren(); i++) {
            if ("cc".equals(path.get(i).getImage())) { //NOI18N
                if ((i + 2) < root.jjtGetNumChildren()
                        && "attrs".equals(path.get(i + 1).getImage())) { //NOI18N
                    return path.get(i + 2).getImage();
                }
            }
        }
        return null;
    }

    public static boolean isImplicitObjectReference(CompilationContext info, Node target, List<ImplicitObjectType> types, boolean directly) {
        int repeation = directly ? 2 : Integer.MAX_VALUE;
        while (target != null && repeation > 0) {
            if (target instanceof AstIdentifier) {
                for (ImplicitObject each : getImplicitObjects(info)) {
                    if (types.contains(each.getType()) && each.getName().equals(target.getImage())) {
                        return true;
                    }
                }
            }

            target = NodeUtil.getSiblingBefore(target);
            repeation--;
        }

        return false;
    }
    
    public static boolean isResourceBundleVar(CompilationContext info, Node target) {
        if (!(target instanceof AstIdentifier)) {
            return false;
        }
        ResourceBundles resourceBundles = ResourceBundles.get(info.file());
        if (!resourceBundles.canHaveBundles()) {
            return false;
        }
        String bundleVar = target.getImage();
        return resourceBundles.isResourceBundleIdentifier(bundleVar, info.context());
    }

    /**
     * Returns name of the method which is called by bracket call.
     * @param bracketSuffixNode node of the type AstBracketSuffix
     * @return method name without any quotes or apostrophes
     */
    public static String getBracketMethodName(Node bracketSuffixNode) {
        String nameInclQuot = bracketSuffixNode.jjtGetChild(0).getImage();
        if (nameInclQuot.length() > 2) {
            return nameInclQuot.substring(1, nameInclQuot.length() - 1);
        } else {
            return ""; //NOI18N
        }
    }

    private static String getMethodName(Node methodNode) {
        if (methodNode instanceof AstBracketSuffix) {
            // method call in the brackets like bean['myCall']()
            return getBracketMethodName(methodNode);
        } else {
            // method call after the dot like bean.myCall()
            return methodNode.getImage();
        }
    }

    private static TypeMirror getTypeMirrorFor(CompilationContext info, Element element) {
        return getTypeMirrorFor(info, element, null, null);
    }

    private static TypeMirror getTypeMirrorFor(CompilationContext info, Element element, ELElement elElement, List<Node> rootToNode) {
        if (element.getKind() == ElementKind.METHOD) {
            return getReturnType(info, (ExecutableElement) element, elElement, rootToNode);
        }
        return element.asType();
    }

    private static boolean isValidatorMethod(CompilationContext info, ExecutableElement method) {
        if (method.getParameters().size() != 3) {
            return false;
        }
        VariableElement param1 = method.getParameters().get(0);
        VariableElement param2 = method.getParameters().get(1);
        CharSequence param1Type = getTypeName(info, param1.asType());
        CharSequence param2Type = getTypeName(info, param2.asType());
        return FACES_CONTEXT_CLASS.equals(param1Type) && UI_COMPONENT_CLASS.equals(param2Type);
    }

    private static CharSequence getTypeName(CompilationContext info, TypeMirror type) {
        return info.info().getTypeUtilities().getTypeName(type, TypeNameOptions.PRINT_FQN);
    }

    private static boolean haveSameParameters(CompilationContext info, Node methodNode, ExecutableElement method) {
        List<Node> methodNodeParameters = getMethodParameters(methodNode);
        for (int i = 0; i < methodNodeParameters.size(); i++) {
            Node paramNode = methodNodeParameters.get(i);
            if (!isSameType(info, paramNode, method.getParameters().get(i))) {
                return false;
            }
        }
        return true;
    }

    private static boolean hasActionEventArgument(ExecutableElement method) {
        List<? extends VariableElement> parameters = method.getParameters();

        // check one parameter ActionEvent method
        if (parameters.size() != 1) {
            return false;
        }

        return "javax.faces.event.ActionEvent".equals(parameters.get(0).asType().toString()); //NOI18N
    }

    private static boolean isSameType(CompilationContext info, final Node paramNode, final VariableElement param) {
        TypeKind paramKind = param.asType().getKind();
        if (!paramKind.isPrimitive()) {
            // try unboxing
            try {
                PrimitiveType unboxedType = info.info().getTypes().unboxedType(param.asType());
                paramKind = unboxedType.getKind();
            } catch (IllegalArgumentException iae) {
                // not unboxable (isn't there a way to check this before trying to unbox??)
            }

        }
        if (TYPES.containsKey(paramNode.getClass())) {
            return TYPES.get(paramNode.getClass()).contains(paramKind);
        }
        if (paramNode instanceof AstString) {
            CharSequence typeName = info.info().getTypeUtilities().getTypeName(param.asType(), TypeNameOptions.PRINT_FQN);
            if (String.class.getName().contentEquals(typeName)) {
                return true;
            }
            if (paramKind == TypeKind.DECLARED) {
                return isSubtypeOf(info, param.asType(), "java.lang.Enum"); // NOI18N
            }
            return false;
        }
        // the ast param is an object whose real type we don't know
        // would need to further type inference for more exact matching
        return true;

    }

    /**
     * Gets the element matching the given name from the given enclosing class.
     * @param name the name of the element to find.
     * @param enclosing
     * @return
     */
    private static ExecutableElement getElementForProperty(CompilationContext info, Node property, Element enclosing) {
        for (Element element : getSuperTypesFor(info, enclosing)) {
            for (ExecutableElement each : ElementFilter.methodsIn(element.getEnclosedElements())) {
                // we're only interested in public methods
                if (!each.getModifiers().contains(Modifier.PUBLIC)) {
                    continue;
                }
                if (isSameMethod(info, property, each, true)) {
                    return each;
                }
            }
        }
        return null;
    }

    private static Element getIdentifierType(CompilationContext info, AstIdentifier identifier, ELElement element) {
        if (info.file() == null) {
            // Strange case - file was deleted? Try to find out whether it's invalid.
            FileObject fileObject = element.getSnapshot().getSource().getFileObject();
            LOG.log(Level.WARNING, "FileObject to resolve doesn''t exist: {0}, isValid: {1}",
                    new Object[]{fileObject, fileObject != null ? fileObject.isValid() : "null"});
            return null;
        }
        String tempClass = null;
        // try implicit objects first
        for (ImplicitObject implicitObject : getImplicitObjects(info)) {
            if (implicitObject.getName().equals(identifier.getImage())) {
                if (implicitObject.getClazz() == null || implicitObject.getClazz().isEmpty()) {
                    // the identiefier represents an implicit object whose type we don't know
//                    tempClass = Object.class.getName();
                } else {
                    tempClass = implicitObject.getClazz();
                }
                break;
            }
        }
        if (tempClass == null) {
            // managed beans
            tempClass = ELVariableResolvers.findBeanClass(info, identifier.getImage(), element.getSnapshot().getSource().getFileObject());
        }
        if (tempClass != null) {
            return info.info().getElements().getTypeElement(tempClass);
        }

        // probably a variable
        int offset = element.getOriginalOffset().getStart() + identifier.startOffset();

        Collection<ELVariableResolver.VariableInfo> vis = ELVariableResolvers.getVariables(info, element.getSnapshot(), offset);
        for (ELVariableResolver.VariableInfo vi : vis) {
            if (identifier.getImage().equals(vi.name)) {
                try {
                    ELPreprocessor elp = new ELPreprocessor(vi.expression, ELPreprocessor.XML_ENTITY_REFS_CONVERSION_TABLE);
                    Node expressionNode = ELParser.parse(elp);
                    if (expressionNode != null) {
                        return getReferredType(info, expressionNode, element.getSnapshot().getSource().getFileObject());
                    }
                } catch (ELException e) {
                    //invalid expression
                }
            }
        }

        return null;

    }

    /**
     * Resolves the given variable type
     * @param vi the variable to be resolved
     * @return source Element representing the variable
     */
    public static Element getReferredType(CompilationContext info, VariableInfo vi, FileObject context) {
        //resolved variable
        if (vi.clazz != null) {
            return info.info().getElements().getTypeElement(vi.clazz);
        }

        //unresolved variable
        assert vi.expression != null;
        try {
            ELPreprocessor elp = new ELPreprocessor(vi.expression, ELPreprocessor.XML_ENTITY_REFS_CONVERSION_TABLE);
            Node expressionNode = ELParser.parse(elp);
            if (expressionNode != null) {
                return getReferredType(info, expressionNode, context);
            }
        } catch (ELException e) {
            //invalid expression
        }

        return null;
    }

    /**
     * @return the element for the type that that given {@code expression} refers to, i.e.
     * the return type of the last method in the expression.
     * 
     * The method can ONLY be used for resolved expressions, i.e. the base object must be a known bean,
     * not a variable!
     */
    public static Element getReferredType(final CompilationContext info, Node expression, final FileObject context) {

        final Element[] result = new Element[1];
        expression.accept(new NodeVisitor() {

            @Override
            public void visit(Node node) throws ELException {
                if (node instanceof AstIdentifier) {
                    Node parent = node.jjtGetParent();
                    String beanClass = ELVariableResolvers.findBeanClass(info, node.getImage(), context);
                    if (beanClass == null) {
                        return;
                    }
                    Element enclosing = info.info().getElements().getTypeElement(beanClass);
                    if (enclosing == null) {
                        //no such class on the classpath
                        return;
                    }
                    ExecutableElement method, lastResolved = null;
                    for (int i = 0; i < parent.jjtGetNumChildren(); i++) {
                        Node current = parent.jjtGetChild(i);
                        if (enclosing != null && (current instanceof AstDotSuffix || NodeUtil.isMethodCall(current))) {
                            method = getElementForProperty(info, current, enclosing);
                            if (method == null) {
                                continue;
                            } else if (ELStreamCompletionItem.STREAM_METHOD.equals(method.getSimpleName().toString())
                                    && isIterableElement(info, enclosing)) {
                                // method is resolved - in case of JDK8 and stream used over iterable
                                break;
                            } else {
                                // issue #243833 - use last resolved method if any
                                lastResolved = method;
                            }
                            enclosing = info.info().getTypes().asElement(getReturnType(info, method));
                        }
                    }
                    if (lastResolved == null) {
                        return;
                    }
                    TypeMirror returnType = getReturnType(info, lastResolved);
                    //XXX: works just for generic collections, i.e. the assumption is
                    // that variables refer to collections, which is not always the case

                    if (returnType.getKind() == TypeKind.DECLARED) {
                        if (isSubtypeOf(info, returnType, "java.lang.Iterable")) { //NOI18N
                            List<? extends TypeMirror> typeArguments = ((DeclaredType) returnType).getTypeArguments();
                            for (TypeMirror arg : typeArguments) {
                                result[0] = info.info().getTypes().asElement(arg);
                                return;
                            }
                            //use the returned type itself
                            result[0] = info.info().getTypes().asElement(returnType);
                        }
                    } else if (returnType.getKind() == TypeKind.ARRAY) {
                        TypeMirror componentType = ((ArrayType) returnType).getComponentType();
                        result[0] = info.info().getTypes().asElement(componentType);
                    }
                }
            }
        });

        return result[0];
    }

    /**
     * Whether the given node represents static {@link Iterable} field where can be used operators.
     * @param ccontext compilation context
     * @param node node to examine
     * @return {@code true} if the object is static {@link Iterable} field, {@code false} otherwise
     * @since 1.26
     */
    public static boolean isStaticIterableElement(CompilationContext ccontext, Node node) {
        return (node instanceof AstListData || node instanceof AstMapData);
    }

    /**
     * Whether the given node represents access to the collection or array.
     * @param ccontext compilation context
     * @param node node to examine
     * @return {@code true} if the node means access to collection or array, {@code false} otherwise
     * @since 1.34
     */
    public static boolean isAccessIntoCollection(CompilationContext ccontext, Node node) {
        return (node instanceof AstInteger);
    }

    /**
     * Whether the given node represents static {@link Iterable} field where can be used operators.
     * @param ccontext compilation context
     * @param element element to examine
     * @return {@code true} if the object is static {@link Iterable} field, {@code false} otherwise
     * @since 1.26
     */
    public static boolean isIterableElement(CompilationContext ccontext, Element element) {
        if (element.getKind() == ElementKind.CLASS) {
            return isSubtypeOf(ccontext, element.asType(), "java.lang.Iterable"); //NOI18N
        } else if (element.getKind() == ElementKind.METHOD) {
            TypeMirror returnType = ELTypeUtilities.getReturnType(ccontext, (ExecutableElement) element);
            if (returnType.getKind() == TypeKind.ARRAY
                    || isSubtypeOf(ccontext, returnType, "java.lang.Iterable")) { //NOI18N
                return true;
            }
        } else if (element.getKind() == ElementKind.INTERFACE) {
            return isSubtypeOf(ccontext, element.asType(), "java.lang.Iterable"); //NOI18N
        }
        return false;
    }

    /**
     * Whether the given node represents Map field.
     * @param ccontext compilation context
     * @param element element to examine
     * @return {@code true} if the element extends {@link Map} interface, {@code false} otherwise
     * @since 1.28
     */
    public static boolean isMapElement(CompilationContext ccontext, Element element) {
        return isSubtypeOf(ccontext, element.asType(), "java.util.Map"); //NOI18N
    }

    /**
     * Gets {@code ClasspathInfo} extended for the el-impl.jar. It guarantees to find Stream class on the classpath.
     * @param file file to be used for getting classpaths
     * @return extended classpath for the el-impl.jar
     * @since 1.39
     */
    public static ClasspathInfo getElimplExtendedCPI(FileObject file) {
        ClassPath bootPath = ClassPath.getClassPath(file, ClassPath.BOOT);
        if (bootPath == null) {
            bootPath = JavaPlatformManager.getDefault().getDefaultPlatform().getBootstrapLibraries();
        }
        ClassPath compilePath = ClassPath.getClassPath(file, ClassPath.COMPILE);
        if (compilePath == null) {
            compilePath = ClassPathSupport.createClassPath(new URL[0]);
        }
        ClassPath srcPath = ClassPath.getClassPath(file, ClassPath.SOURCE);
        return ClasspathInfo.create(
                bootPath,
                ClassPathSupport.createProxyClassPath(compilePath, ClassPathSupport.createClassPath(EL_IMPL_JAR_FO)),
                srcPath);
    }

    /**
     * Gets information whether given type is inherits particular type specified by FQN.
     * @param info compilation context
     * @param tm type to be check for inheritance from typeName
     * @param typeName parent class to be inherited from
     * @return {@code true} if tm inherits the given typeName, {@code false} otherwise
     */
    public static boolean isSubtypeOf(CompilationContext info, TypeMirror tm, CharSequence typeName) {
        Element element = info.info().getElements().getTypeElement(typeName);
        if (element == null) {
            return false;
        }
        TypeMirror type = element.asType(); //NOI18N
        TypeMirror erasedType = info.info().getTypes().erasure(type);
        TypeMirror tmErasure = info.info().getTypes().erasure(tm);

        return info.info().getTypes().isSubtype(tmErasure, erasedType);
    }

    private static TypeElement getTypeFor(CompilationContext info, String clazz) {
        return info.info().getElements().getTypeElement(clazz);
    }

    private static class TypeResolverVisitor implements NodeVisitor {

        private final ELElement elem;
        private final Node target;
        private final Map<AstIdentifier, Node> assignments;
        private final List<ELVariableResolver.VariableInfo> variableInfos;
        private Element result;
        private CompilationContext info;

        public TypeResolverVisitor(CompilationContext info, ELElement elem, Node target, Map<AstIdentifier, Node> assignments, List<ELVariableResolver.VariableInfo> variableInfos) {
            this.info = info;
            this.elem = elem;
            this.target = target;
            this.assignments = assignments;
            this.variableInfos = variableInfos;
        }

        public Element getResult() {
            return result;
        }

        @Override
        public void visit(Node node) {
            Element enclosing = null;

            // look for possible assignments to the identifier
            Node evalNode;
            if (node instanceof AstIdentifier && assignments.containsKey((AstIdentifier) node)) {
                evalNode = assignments.get((AstIdentifier) node);
            } else {
                evalNode = node;
            }

            // traverses AST resolving types for each property starting from
            // an identifier until the target is found
            if (evalNode instanceof AstIdentifier) {
                enclosing = getIdentifierType(info, (AstIdentifier) evalNode, elem);
                if (enclosing != null) {
                    if (node.equals(target)) {
                        result = enclosing;
                        return;
                    }
                    Node parent = node.jjtGetParent();
                    for (int i = 0; i < parent.jjtGetNumChildren(); i++) {
                        Node child = parent.jjtGetChild(i);
                        if (child instanceof AstDotSuffix || NodeUtil.isMethodCall(child)) {
                            Element propertyType = getElementForProperty(info, child, enclosing);
                            if (propertyType == null) {
                                // special case handling for scope objects; their types don't help
                                // in resolving the beans they contain. The code below handles cases
                                // like sessionScope.myBean => sessionScope is in position parent.jjtGetChild(i - 1)
                                if (i > 0 && isScopeObject(info, parent.jjtGetChild(i - 1))) {
                                    final String clazz = ELVariableResolvers.findBeanClass(info, child.getImage(), elem.getSnapshot().getSource().getFileObject());
                                    if (clazz == null) {
                                        return;
                                    }
                                    // it's a managed bean in a scope
                                    propertyType = getTypeFor(info, clazz);
                                }

                                // cc.attrs.<xxx> resolving - XXX better way how to detect this special case?
                                if ("cc".equals(node.getImage())) {
                                    if ("attrs".equals(child.getImage())) {
                                        propertyType = info.info().getElements().getTypeElement("java.lang.Object"); //NOI18N
                                    } else {
                                        for (VariableInfo property : variableInfos) {
                                            if (child.getImage().equals(property.name)) {
                                                if (property.clazz == null) {
                                                    propertyType = info.info().getElements().getTypeElement("java.lang.Object"); //NOI18N
                                                } else {
                                                    propertyType = info.info().getElements().getTypeElement(property.clazz);
                                                }
                                            }
                                        }
                                    }
                                }

                                // maps
                                if (ELTypeUtilities.isMapElement(info, enclosing)) {
                                    result = info.info().getElements().getTypeElement("java.lang.Object"); //NOI18N
                                    return;
                                }

                                // stream method
                                if (ELTypeUtilities.isIterableElement(info, enclosing)) {
                                    propertyType = enclosing = info.info().getElements().getTypeElement(STREAM_CLASS);
                                }
                            } else {
                                // issue #244065 - in case of JDK8 and Collection, return the EL's Stream class
                                if (propertyType instanceof ExecutableElement) {
                                    String returnType = ((ExecutableElement) propertyType).getReturnType().toString();
                                    if ("java.util.stream.Stream<E>".equals(returnType)) {  //NOI18N
                                        propertyType = info.info().getElements().getTypeElement(STREAM_CLASS);
                                    }
                                }
                            }
                            if (propertyType == null) {
                                return;
                            }
                            if (child.equals(target)) {
                                result = propertyType;
                            } else if (propertyType.getKind() == ElementKind.METHOD) {
                                final ExecutableElement method = (ExecutableElement) propertyType;
                                TypeMirror returnType = getReturnType(info, method, elem, NodeUtil.getRootToNode(elem, target));
                                if (returnType.getKind() == TypeKind.ARRAY) {
                                    // for array try to look like Iterable (operators for array return type)
                                    enclosing = info.info().getElements().getTypeElement("java.lang.Iterable"); //NOI18N
                                } else {
                                    if (isAccessIntoCollection(info, target) && returnType instanceof DeclaredType) {
                                        List<? extends TypeMirror> typeArguments = ((DeclaredType) returnType).getTypeArguments();
                                        if (!typeArguments.isEmpty()) {
                                            enclosing = info.info().getTypes().asElement(typeArguments.get(0));
                                            result = enclosing;
                                        }
                                    } else {
                                        enclosing = info.info().getTypes().asElement(returnType);
                                    }
                                }
                                if (enclosing == null) {
                                    return;
                                }
                            } else {
                                enclosing = propertyType;
                            }
                        }
                    }
                }
            } else if (evalNode instanceof AstListData || evalNode instanceof AstMapData) {
                Node parent = node.jjtGetParent();
                for (int i = 0; i < parent.jjtGetNumChildren(); i++) {
                    Node child = parent.jjtGetChild(i);
                    if (child instanceof AstDotSuffix) {
                        if (ELStreamCompletionItem.STREAM_METHOD.equals(child.getImage())) {
                            enclosing = info.info().getElements().getTypeElement(STREAM_CLASS);
                        } else {
                            if (enclosing != null) {
                                ExecutableElement propertyType = getElementForProperty(info, child, enclosing);
                                if (target.getImage() != null && target.getImage().equals(child.getImage())) {
                                    if (propertyType != null) {
                                        result = propertyType;
                                    }
                                    return;
                                } else {
                                    if (propertyType != null) {
                                        enclosing = getTypeFor(info, propertyType.getReturnType().toString());
                                    }
                                }
                            }
                        }
                        // finish when the target matches property
                        if (target.getImage() != null && target.getImage().equals(child.getImage())) {
                            return;
                        }
                    }
                }
            }
        }
    }
}
