/*
 * 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.completion;

import com.sun.el.parser.AstAssign;
import com.sun.el.parser.AstBracketSuffix;
import com.sun.el.parser.AstDeferredExpression;
import com.sun.el.parser.AstDotSuffix;
import com.sun.el.parser.AstDynamicExpression;
import com.sun.el.parser.AstFunction;
import com.sun.el.parser.AstIdentifier;
import com.sun.el.parser.AstInteger;
import com.sun.el.parser.AstLambdaExpression;
import com.sun.el.parser.AstListData;
import com.sun.el.parser.AstMapData;
import com.sun.el.parser.AstMethodArguments;
import com.sun.el.parser.AstSemiColon;
import com.sun.el.parser.AstString;
import com.sun.el.parser.Node;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.modules.csl.api.CodeCompletionContext;
import org.netbeans.modules.csl.api.CodeCompletionHandler;
import org.netbeans.modules.csl.api.CodeCompletionHandler2;
import org.netbeans.modules.csl.api.CodeCompletionResult;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.Documentation;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ParameterInfo;
import org.netbeans.modules.csl.spi.DefaultCompletionResult;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.el.lexer.api.ELTokenId;
import org.netbeans.modules.web.el.AstPath;
import org.netbeans.modules.web.el.CompilationContext;
import org.netbeans.modules.web.el.ELElement;
import org.netbeans.modules.web.el.ELParserResult;
import org.netbeans.modules.web.el.ELTypeUtilities;
import org.netbeans.modules.web.el.ELVariableResolvers;
import org.netbeans.modules.web.el.NodeUtil;
import org.netbeans.modules.web.el.ResourceBundles;
import org.netbeans.modules.web.el.refactoring.RefactoringUtil;
import org.netbeans.modules.web.el.spi.ELPlugin;
import org.netbeans.modules.web.el.spi.ELVariableResolver.VariableInfo;
import org.netbeans.modules.web.el.spi.Function;
import org.netbeans.modules.web.el.spi.ResourceBundle;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;

/**
 * Code completer for Expression Language.
 *
 * @author Erno Mononen
 */
public final class ELCodeCompletionHandler implements CodeCompletionHandler2 {

    private static Set<String> keywordFixedTexts = null;

    /**
     * Gets Set of {@link ELTokenId#fixedText()} values with {@link ELTokenId.ELTokenCategories.KEYWORDS} category.<br/>
     * Result is resolved and stored on first method call into static field {@link #keywordFixedTexts}.
     * On next call is returned the same Set instance.
     * @return Set of {@link ELTokenId#fixedText()} values with {@link ELTokenId.ELTokenCategories.KEYWORDS} category.
     */
    private static synchronized Set<String> getKeywordFixedTexts() {
        if (keywordFixedTexts == null) {
            keywordFixedTexts = new HashSet<>();
            for (ELTokenId elTokenId : ELTokenId.values()) {
                if (ELTokenId.ELTokenCategories.KEYWORDS.hasCategory(elTokenId)) {
                    keywordFixedTexts.add(elTokenId.fixedText());
                }
            }
        }
        return keywordFixedTexts;
    }

    @Override
    public CodeCompletionResult complete(final CodeCompletionContext context) {
        final List<CompletionProposal> proposals = new ArrayList<>(50);
        CodeCompletionResult result = new DefaultCompletionResult(proposals, false);
        final ELElement element = getElementAt(context.getParserResult(), context.getCaretOffset());
        if (element == null || !element.isValid()) {
            return CodeCompletionResult.NONE;
        }
        final Node target = getTargetNode(element, context.getCaretOffset());
        if(target == null) {
            //completion called outside of the EL content, resp. inside the
            //delimiters #{ or } area
            return CodeCompletionResult.NONE;
        }
        AstPath path = new AstPath(element.getNode());
        final List<Node> rootToNode = path.rootToNode(target);
        if (rootToNode.isEmpty()) {
            return result;
        }
        final PrefixMatcher prefixMatcher = PrefixMatcher.create(target, context);
        if (prefixMatcher == null) {
            return CodeCompletionResult.NONE;
        }
        // see if it is bundle key in the array notation and if so complete them
        if (target instanceof AstString) {
            ResourceBundles bundle = ResourceBundles.get(getFileObject(context));
            String bundleIdentifier = bundle.findResourceBundleIdentifier(path);
            if (bundleIdentifier != null) {
                proposeBundleKeysInArrayNotation(context, prefixMatcher, element, bundleIdentifier, (AstString) target, proposals);
                return proposals.isEmpty() ? CodeCompletionResult.NONE : result;
            }
        }

        final Node nodeToResolve = getNodeToResolve(target, rootToNode);
        final Map<AstIdentifier, Node> assignments = getAssignments(context.getParserResult(), context.getCaretOffset());
        final FileObject file = context.getParserResult().getSnapshot().getSource().getFileObject();
        JavaSource jsource = JavaSource.create(ELTypeUtilities.getElimplExtendedCPI(file));
        try {
            jsource.runUserActionTask(new Task<CompilationController>() {

                @Override
                public void run(CompilationController info) throws Exception {
                    info.toPhase(JavaSource.Phase.RESOLVED);
                    CompilationContext ccontext = CompilationContext.create(file, info);

                    // assignments to resolve
                    Node node = nodeToResolve instanceof AstIdentifier && assignments.containsKey((AstIdentifier) nodeToResolve) ?
                            assignments.get((AstIdentifier) nodeToResolve) : nodeToResolve;

                    // fetch information from the JSF editor for resolving beans if necessary for cc:interface elements
                    List<VariableInfo> attrsObjects = Collections.<VariableInfo>emptyList();
                    if (ELTypeUtilities.isRawObjectReference(ccontext, node, false)) {
                        attrsObjects = ELVariableResolvers.getRawObjectProperties(ccontext, "attrs", context.getParserResult().getSnapshot()); //NOI18N
                    }

                    Element resolved = null;
                    if (!isInLambda(rootToNode)) {
                        // resolve the element
                        resolved = ELTypeUtilities.resolveElement(ccontext, element, nodeToResolve, assignments, attrsObjects);
                    }

                    if (ELTypeUtilities.isStaticIterableElement(ccontext, node)) {
                        proposeStream(ccontext, context, prefixMatcher, proposals);
                    } else if (ELTypeUtilities.isRawObjectReference(ccontext, node, true)) {
                        proposeRawObjectProperties(ccontext, context, prefixMatcher, node, proposals);
                    } else if (ELTypeUtilities.isScopeObject(ccontext, node)) {
                        // seems to be something like "sessionScope.^", so complete beans from the scope
                        proposeBeansFromScope(ccontext, context, prefixMatcher, element, node, proposals);
                    } else if (ELTypeUtilities.isResourceBundleVar(ccontext, node)) {
                        proposeBundleKeysInDotNotation(context, prefixMatcher, element, node, proposals);
                    } else if (resolved == null) {
                        if (target instanceof AstDotSuffix == false && node instanceof AstFunction == false) {
                            proposeFunctions(ccontext, context, prefixMatcher, element, proposals);
                            proposeManagedBeans(ccontext, context, prefixMatcher, element, proposals);
                            proposeBundles(ccontext, context, prefixMatcher, element, proposals);
                            proposeVariables(ccontext, context, prefixMatcher, element, proposals);
                            proposeImpicitObjects(ccontext, context, prefixMatcher, proposals);
                            proposeKeywords(context, prefixMatcher, proposals);
                            proposeAssignements(context, prefixMatcher, assignments, proposals);
                        }
                        if (ELStreamCompletionItem.STREAM_METHOD.equals(node.getImage())) {
                            proposeOperators(ccontext, context, prefixMatcher, element, proposals, rootToNode, isBracketProperty(target, rootToNode));
                        }
                        ELJavaCompletion.propose(ccontext, context, element, target, proposals);
                    } else {
                        proposeMethods(ccontext, context, resolved, prefixMatcher, element, proposals, rootToNode, isBracketProperty(target, rootToNode));
                        if (ELTypeUtilities.isIterableElement(ccontext, resolved)) {
                            proposeStream(ccontext, context, prefixMatcher, proposals);
                        }
                    }
                }
            }, true);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        
        

        return proposals.isEmpty() ? CodeCompletionResult.NONE : result;
    }

    private static boolean isBracketProperty(Node target, List<Node> rootToNode) {
        Node previous = rootToNode.get(rootToNode.size() - 1);
        return (target instanceof AstString && previous instanceof AstBracketSuffix);
    }

    private static Node getNodeToResolve(Node target, List<Node> rootToNode) {
        Node previous = rootToNode.get(rootToNode.size() - 1);
        // due to the ast structure in the case of identifiers we need to try to resolve the type of the identifier,
        // otherwise the type of the preceding node.
        if (target instanceof AstString && previous instanceof AstBracketSuffix) {
            return rootToNode.get(rootToNode.size() - 2);
        } else if (target instanceof AstIdentifier
                && ((previous instanceof AstIdentifier || previous instanceof AstDotSuffix || NodeUtil.isMethodCall(previous))
                    || target.jjtGetParent() instanceof AstSemiColon)) {
            return target;
        } else {
            for (int i = rootToNode.size() - 1; i >= 0; i--) {
                Node node = rootToNode.get(i);
                if (isArrayIndexCall(rootToNode, i)) {
                    // array item call - bean.myArray[0].
                    return node;
                } else if (node instanceof AstMethodArguments) {
                    // prvious node was method call
                    return rootToNode.get(i - 1);
                } else if (node instanceof AstListData || node instanceof AstMapData) {
                    return node;
                } else if (node.jjtGetParent() instanceof AstSemiColon) {
                    return previous;
                } else if (node instanceof AstIdentifier || node instanceof AstDotSuffix) {
                    return node;
                }
            }
            return previous;
        }
    }

    private static boolean isArrayIndexCall(List<Node> rootToNode, int nodeIndex) {
        for (int i = nodeIndex; i >= 0; i--) {
            if (rootToNode.get(nodeIndex) instanceof AstInteger && rootToNode.get(i) instanceof AstBracketSuffix) {
                return true;
            }
        }
        return false;
    }

    private static ELElement getElementAt(ParserResult parserResult, int offset) {
        ELParserResult elParserResult = (ELParserResult) parserResult;
        ELElement result = elParserResult.getElementAt(offset);
        if (result == null || result.isValid()) {
            return result;
        } 
        // try to sanitize
        ELSanitizer sanitizer = new ELSanitizer(result, offset - result.getOriginalOffset().getStart());
        return sanitizer.sanitized();
    }

    private Map<AstIdentifier, Node> getAssignments(ParserResult parserResult, int offset) {
        Map<AstIdentifier, Node> result = new HashMap<>();
        ELParserResult elParserResult = (ELParserResult) parserResult;
        for (ELElement elElement : elParserResult.getElementsTo(offset)) {
            if (elElement.getError() != null) {
                elElement = getElementAt(parserResult, offset);
            }
            if (elElement.getNode() == null) {
                continue;
            }
            AstPath astPath = new AstPath(elElement.getNode());
            for (Node node : astPath.rootToLeaf()) {
                if (node instanceof AstAssign) {
                    Node leftSide = node.jjtGetChild(0);
                    Node leaf = getLastAssigneableLeaf(elElement.getNode());
                    Node targetNode = getTargetNode(elElement, elElement.getOriginalOffset().getStart() + leaf.endOffset());
                    Node rightSide = getNodeToResolve(targetNode, astPath.rootToNode(targetNode, true));
                    if (leftSide instanceof AstIdentifier && rightSide instanceof Node) {
                        result.put((AstIdentifier) leftSide, rightSide);
                    }
                }
            }
        }
        return result;
    }

    private static boolean isInLambda(List<Node> rootToNode) {
        boolean inLambda = false;
        for (int i = rootToNode.size() - 1; i >= 0; i--) {
            Node node = rootToNode.get(i);
            if (node instanceof AstDotSuffix) {
                break;
            } else if (node instanceof AstLambdaExpression) {
                inLambda = true;
            }
        }
        return inLambda;
    }

    private static Node getLastAssigneableLeaf(Node root) {
        AstPath astPath = new AstPath(root);
        for (Node node : astPath.rootToLeaf()) {
            if (node instanceof AstSemiColon) {
                for (int i = 0; i < node.jjtGetNumChildren(); i++) {
                    Node childNode = node.jjtGetChild(i);
                    AstPath path = new AstPath(childNode);
                    return getNodeToResolve(childNode, path.rootToLeaf());
                }
            }
        }
        return getNodeToResolve(root, astPath.rootToLeaf());
    }

    /**
     * @param offset swing <b>document</b> offset
     */
    private Node getTargetNode(ELElement element, int offset) {
        Node result = element.findNodeAt(offset);
        // in EL AST for example #{foo.bar^} the caret is at a deferred expression node, whereas
        // for code completion we need the "bar" property node; the code below tries to accomplish
        // that
        if (result instanceof AstDeferredExpression || result instanceof AstDynamicExpression) {
            //ensure we do not jump out from the expression && do not offer EL stuff in delimiters
            int relativeOffset = offset - element.getOriginalOffset().getStart();
            assert relativeOffset >= 0;
            
            //do the workaround only if the completion called at or after 
            //the opening EL delimiter #{ or ${, not before or inside it
            if(relativeOffset >= 2) { 
                Node realTarget = element.findNodeAt(offset - 1);
                if (realTarget != null) {
                    result = realTarget;
                }
            } else {
                //ensure no EL completion before or inside the delimiters
                return null; 
            }
        }
        return result;
    }

    private FileObject getFileObject(CodeCompletionContext context) {
        return context.getParserResult().getSnapshot().getSource().getFileObject();
    }

    private void proposeOperators(CompilationContext ccontext, CodeCompletionContext context,
            PrefixMatcher prefixMatcher, ELElement element, List<CompletionProposal> proposals, List<Node> rootToNode,
            boolean isBracketProperty) {
        TypeElement streamElement = ccontext.info().getElements().getTypeElement("com.sun.el.stream.Stream"); //NOI18N
        if (streamElement != null) {
            proposeJavaMethodsForElements(ccontext, context, prefixMatcher, element,
                    Arrays.<Element>asList(streamElement), isBracketProperty, proposals);
        }
    }

    private void proposeMethods(CompilationContext info, CodeCompletionContext context, Element resolved,
            PrefixMatcher prefix, ELElement elElement,List<CompletionProposal> proposals, List<Node> rootToNode,
            boolean isBracketProperty) {
        List<Element> allTypes = ELTypeUtilities.getSuperTypesFor(info, resolved, elElement, rootToNode);
        proposeJavaMethodsForElements(info, context, prefix, elElement, allTypes, isBracketProperty, proposals);
    }

    private void proposeJavaMethodsForElements(CompilationContext info, CodeCompletionContext context,
            PrefixMatcher prefix, ELElement elElement, List<Element> elements, boolean isBracketCall,
            List<CompletionProposal> proposals) {
        for(Element element : elements) {
            for (ExecutableElement enclosed : ElementFilter.methodsIn(element.getEnclosedElements())) {
                //do not propose Object's members
                if(element.getSimpleName().contentEquals("Object")) { //NOI18N
                    //XXX hmm, not an ideal non-fqn check
                    continue;
                }

                if (!enclosed.getModifiers().contains(Modifier.PUBLIC) ||
                        enclosed.getModifiers().contains(Modifier.STATIC)) {
                    continue;
                }
                boolean hasParameters = !enclosed.getParameters().isEmpty();

                String methodName = enclosed.getSimpleName().toString();
                String propertyName = RefactoringUtil.getPropertyName(methodName, enclosed.getReturnType(), true);
                if (hasParameters) {
                    propertyName = methodName;
                }
                if (!prefix.matches(propertyName)) {
                    continue;
                }

                // Now check methodName or propertyName is EL keyword.
                if (getKeywordFixedTexts().contains(propertyName) ||
                        getKeywordFixedTexts().contains(methodName) ) {
                    continue;
                }
                if (ELPlugin.Query.isValidProperty(enclosed, context.getParserResult().getSnapshot().getSource(), info, context)) {
                    ELVariableCompletionItem completionItem = new ELVariableCompletionItem(propertyName, ELTypeUtilities.getTypeNameFor(info, enclosed));
                    completionItem.setSmart(true);
                    completionItem.setAnchorOffset(context.getCaretOffset() - prefix.length());

                    if (!contains(proposals, propertyName)) {
                        proposals.add(completionItem);
                    }
                } else {
                    ELJavaCompletionItem completionItem;

                    if (!contains(proposals, propertyName)) {
                        if (!hasParameters) {
                            completionItem = new ELJavaCompletionItem(info, enclosed, elElement, isBracketCall); //
                        } else {
                            completionItem = new ELJavaCompletionItem(info, enclosed, methodName, elElement, isBracketCall);
                        }

                        completionItem.setSmart(false);
                        completionItem.setAnchorOffset(context.getCaretOffset() - prefix.length());

                        proposals.add(completionItem);
                    }
                }
            }
        }
    }

    private void proposeStream(CompilationContext info, CodeCompletionContext context, PrefixMatcher prefix, List<CompletionProposal> proposals) {
        if (prefix.matches(ELStreamCompletionItem.STREAM_METHOD)) {
            proposals.add(new ELStreamCompletionItem(context.getCaretOffset() - prefix.length()));
        }
    }

    private boolean contains(List<CompletionProposal> completionProposals, String proposalName) {
        if (proposalName == null || proposalName.isEmpty()) {
            return true;
        }
        for (CompletionProposal completionProposal : completionProposals) {
            if (proposalName.equals(completionProposal.getName())) {
                return true;
            }
        }
        return false;
    }

    private void proposeImpicitObjects(CompilationContext info, CodeCompletionContext context,
            PrefixMatcher prefix, List<CompletionProposal> proposals) {

        for (org.netbeans.modules.web.el.spi.ImplicitObject implicitObject : ELTypeUtilities.getImplicitObjects(info)) {
            if (prefix.matches(implicitObject.getName())) {
                ELImplictObjectCompletionItem item = new ELImplictObjectCompletionItem(implicitObject.getName(), implicitObject.getClazz());
                item.setAnchorOffset(context.getCaretOffset() - prefix.length());
                item.setSmart(true);
                proposals.add(item);
            }
        }
    }

    private void proposeKeywords(CodeCompletionContext context,
            PrefixMatcher prefix, List<CompletionProposal> proposals) {

        for (ELTokenId elToken : ELTokenId.values()) {
            if (!ELTokenId.ELTokenCategories.KEYWORDS.hasCategory(elToken)) {
                continue;
            }
            if (elToken.fixedText() == null) {
                continue;
            }
            if (prefix.matches(elToken.fixedText())) {
                ELKeywordCompletionItem item = new ELKeywordCompletionItem(elToken.fixedText());
                item.setAnchorOffset(context.getCaretOffset() - prefix.length());
                proposals.add(item);
            }

        }
    }

    private void proposeAssignements(CodeCompletionContext context, PrefixMatcher prefix,
            Map<AstIdentifier, Node> assignments, List<CompletionProposal> proposals) {

        for (Map.Entry<AstIdentifier, Node> entry : assignments.entrySet()) {
            AstIdentifier variable = entry.getKey();
            if (prefix.matches(variable.getImage())) {
                ELAssignedVariableCompletionItem item = new ELAssignedVariableCompletionItem(variable.getImage());
                item.setAnchorOffset(context.getCaretOffset() - prefix.length());
                proposals.add(item);
            }
        }
    }

    private void proposeManagedBeans(CompilationContext info, CodeCompletionContext context,
            PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {

        for (VariableInfo bean : ELVariableResolvers.getManagedBeans(info, getFileObject(context))) {
            if (!prefix.matches(bean.name)) {
                continue;
            }
            Element element = ELTypeUtilities.getElementForType(info, bean.clazz);
            if(element == null) {
                continue; //unresolvable bean class name
            }
            ELJavaCompletionItem item = new ELJavaCompletionItem(info, element, bean.name, elElement);
            item.setAnchorOffset(context.getCaretOffset() - prefix.length());
            item.setSmart(true);
            if (!contains(proposals, bean.name)) {
                proposals.add(item);
            }
        }
    }

    private void proposeBeansFromScope(CompilationContext info, CodeCompletionContext context,
            PrefixMatcher prefix, ELElement elElement, Node scopeNode, List<CompletionProposal> proposals) {

        String scope = scopeNode.getImage();
        // this is ugly, but in the JSF model beans
        // are stored to "session", "application" etc (instead of "sessionScope").
        // see org.netbeans.modules.web.jsf.api.facesmodel.ManagedBean.Scope
        final String scopeString = "Scope";//NOI18N
        if (scope.endsWith(scopeString)) {
            scope = scope.substring(0, scope.length() - scopeString.length());
        }

        for (VariableInfo bean : ELVariableResolvers.getBeansInScope(info, scope, context.getParserResult().getSnapshot())) {
            if (!prefix.matches(bean.name)) {
                continue;
            }
            Element element = ELTypeUtilities.getElementForType(info, bean.clazz);
            ELJavaCompletionItem item = new ELJavaCompletionItem(info, element, elElement);
            item.setAnchorOffset(context.getCaretOffset() - prefix.length());
            item.setSmart(true);
            proposals.add(item);
        }
    }

    private void proposeRawObjectProperties(CompilationContext info, CodeCompletionContext context,
            PrefixMatcher prefix, Node scopeNode, List<CompletionProposal> proposals) {

        for (VariableInfo property : ELVariableResolvers.getRawObjectProperties(info, scopeNode.getImage(), context.getParserResult().getSnapshot())) {
            if (!prefix.matches(property.name)) {
                continue;
            }
            ELRawObjectPropertyCompletionItem item = new ELRawObjectPropertyCompletionItem(property.name);
            item.setAnchorOffset(context.getCaretOffset() - prefix.length());
            item.setSmart(true);
            proposals.add(item);
        }
    }

    private void proposeVariables(CompilationContext info, CodeCompletionContext context,
            PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {

        for (VariableInfo bean : ELVariableResolvers.getVariables(info, context.getParserResult().getSnapshot(), context.getCaretOffset())) {
            if (!prefix.matches(bean.name)) {
                continue;
            }
            if(bean.clazz == null) {
                //probably a refered (w/o type) variable, just show it in the completion w/o type
                ELVariableCompletionItem item = new ELVariableCompletionItem(bean.name, bean.expression);
                item.setAnchorOffset(context.getCaretOffset() - prefix.length());
                item.setSmart(true);
                proposals.add(item);

            } else {
                //resolved variable
                Element element = ELTypeUtilities.getElementForType(info, bean.clazz);
                if (element == null) {
                    continue;
                }
                ELJavaCompletionItem item = new ELJavaCompletionItem(info, element, elElement);
                item.setAnchorOffset(context.getCaretOffset() - prefix.length());
                item.setSmart(true);
                proposals.add(item);
            }
        }
    }

    private void proposeBundles(CompilationContext ccontext, CodeCompletionContext context,
            PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {

        ResourceBundles resourceBundles = ResourceBundles.get(getFileObject(context));
        if (!resourceBundles.canHaveBundles()) {
            return;
        }
        for (ResourceBundle bundle : resourceBundles.getBundles(ccontext.context())) {
            if (!prefix.matches(bundle.getVar())) {
                continue;
            }
            ELResourceBundleCompletionItem item = new ELResourceBundleCompletionItem(ccontext.file(), bundle, resourceBundles);
            item.setAnchorOffset(context.getCaretOffset() - prefix.length());
            proposals.add(item);
        }

    }

    private void proposeBundleKeysInArrayNotation(CodeCompletionContext context,
            PrefixMatcher prefix, ELElement elElement, String bundleKey, AstString target, List<CompletionProposal> proposals) {

        if (target.getImage().isEmpty()
                || elElement.getOriginalOffset(target).getStart() >= context.getCaretOffset()) {
            return;
        }
        ResourceBundles resourceBundles = ResourceBundles.get(getFileObject(context));
        if (!resourceBundles.canHaveBundles()) {
            return;
        }
        for (Map.Entry<String, String> entry : resourceBundles.getEntries(bundleKey).entrySet()) {
            if (!prefix.matches(entry.getKey())) {
                continue;
            }
            ELResourceBundleKeyCompletionItem item = new ELResourceBundleKeyCompletionItem(entry.getKey(), entry.getValue(), elElement);
            item.setSmart(true);
            item.setAnchorOffset(context.getCaretOffset() - prefix.length());
            proposals.add(item);
        }
    }
    
    // "msg.key" notation
    private void proposeBundleKeysInDotNotation(CodeCompletionContext context,
            PrefixMatcher prefix, 
            ELElement elElement, 
            Node baseObjectNode,
            List<CompletionProposal> proposals) {
        
        String bundleKey = baseObjectNode.getImage();
        ResourceBundles resourceBundles = ResourceBundles.get(getFileObject(context));
        if (!resourceBundles.canHaveBundles()) {
            return;
        }
        for (Map.Entry<String, String> entry : resourceBundles.getEntries(bundleKey).entrySet()) {
            if (!prefix.matches(entry.getKey())) {
                continue;
            }
            ELResourceBundleKeyCompletionItem item = new ELResourceBundleKeyCompletionItem(entry.getKey(), entry.getValue(), elElement);
            item.setSmart(true);
            item.setAnchorOffset(context.getCaretOffset() - prefix.length());
            proposals.add(item);
        }
    }

    private void proposeFunctions(CompilationContext info, CodeCompletionContext context,
            PrefixMatcher prefix, ELElement elElement, List<CompletionProposal> proposals) {

        for (Function function : ELTypeUtilities.getELFunctions(info)) {
            if (!prefix.matches(function.getName())) {
                continue;
            }
            ELFunctionCompletionItem item = new ELFunctionCompletionItem(
                    function.getName(),
                    function.getReturnType(),
                    function.getParameters(),
                    function.getDescription());
            item.setAnchorOffset(context.getCaretOffset() - prefix.length());
            proposals.add(item);
        }
    }

    @Override
    public String document(ParserResult info, final ElementHandle element) {
        Documentation doc = documentElement(info, element, new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return false;
            }
        });
        if (doc != null) {
            return doc.getContent();
        }
        return null;
    }

    @Override
    public ElementHandle resolveLink(String link, ElementHandle originalHandle) {
        return null;
    }

    @Override
    public String getPrefix(ParserResult info, int caretOffset, boolean upToOffset) {
        ELElement element = getElementAt(info, caretOffset);
        if (element == null) {
            return null;
        }
        Node node = element.findNodeAt(caretOffset);
        // get the prefix for bundle keys
        if (node instanceof AstString) {
            int startOffset = element.getOriginalOffset(node).getStart();
            int end = caretOffset - startOffset;
            String image = node.getImage();
            if (end > 0 && !image.isEmpty()) {
                // index 0 in AstString#image is either ' or ",
                // so start from 1
                return image.substring(1, end);
            }
        }
        // use the default CSL behavior for getting the prefix
        return null;
    }

    @Override
    public QueryType getAutoQuery(JTextComponent component, String typedText) {
        assert typedText.length() > 0;
        char last = typedText.charAt(typedText.length() - 1);
        switch(last) {
            case '.':
                return QueryType.COMPLETION;
        }
        return QueryType.NONE;
    }

    @Override
    public String resolveTemplateVariable(String variable, ParserResult info, int caretOffset, String name, Map parameters) {
        return null;
    }

    @Override
    public Set<String> getApplicableTemplates(Document doc, int selectionBegin, int selectionEnd) {
        return Collections.emptySet();
    }

    @Override
    public ParameterInfo parameters(ParserResult info, int caretOffset, CompletionProposal proposal) {
        return ParameterInfo.NONE;
    }

    @Override
    public Documentation documentElement(ParserResult info, ElementHandle element, Callable<Boolean> cancel) {
        if (!(element instanceof ELElementHandle)) {
            return null;
        }
        return ((ELElementHandle) element).document(info, cancel);
    }

    /*package*/ static class PrefixMatcher {

        private final String prefix;
        private final boolean exact;
        private final boolean caseSensitive;

        private PrefixMatcher(String value, boolean exact, boolean caseSensitive) {
            this.prefix = value;
            this.exact = exact;
            this.caseSensitive = caseSensitive;
        }

        static PrefixMatcher create(Node target, CodeCompletionContext context) {
            String prefix = context.getPrefix() != null ? context.getPrefix() : "";
            boolean isDoc = context.getQueryType() == QueryType.DOCUMENTATION;
            if (isDoc) {
                prefix = getPrefixForDocumentation(target);
            }
            // for documentation we need full prefix
            if (isDoc && prefix.isEmpty()) {
                return null;
            }
            return new PrefixMatcher(prefix, isDoc, context.isCaseSensitive());
        }

        static PrefixMatcher create(String prefix, CodeCompletionContext context) {
            return new PrefixMatcher(prefix, false, context.isCaseSensitive());
        }

        private static String getPrefixForDocumentation(Node target) {
            if (target instanceof AstString) {
                return ((AstString) target).getString();
            }
            return target.getImage() == null ? "" : target.getImage();
        }

        boolean matches(String str) {
            if (exact) {
                return prefix.equals(str);
            } else if (caseSensitive) {
                return str.startsWith(prefix);
            } else {
                return str.toLowerCase().startsWith(prefix.toLowerCase());
            }
        }

        int length() {
            return prefix.length();
        }

        public String getPrefix() {
            return prefix;
        }
    }
}
