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

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.ImageIcon;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.CodeCompletionResult;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.html.editor.lib.api.model.HtmlTagAttribute;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.javascript2.model.api.IndexedElement;
import org.netbeans.modules.javascript2.model.api.JsElement;
import org.netbeans.modules.javascript2.model.api.JsFunction;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.types.api.Type;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.javascript2.model.api.ModelUtils;
import org.netbeans.modules.javascript2.editor.options.OptionsUtils;
import org.netbeans.modules.javascript2.editor.parser.JsParserResult;
import org.netbeans.modules.javascript2.editor.spi.CompletionContext;
import org.netbeans.modules.javascript2.model.api.Index;
import org.netbeans.modules.javascript2.model.api.Model;
import org.netbeans.modules.parsing.api.Snapshot;
import org.openide.filesystems.FileObject;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle.Messages;

/**
 *
 * @author Petr Pisl
 */
public class JsCompletionItem implements CompletionProposal {
    
    protected final CompletionRequest request;
    protected final ElementHandle element;
    
    JsCompletionItem(ElementHandle element, CompletionRequest request) {
        this.element = element;
        this.request = request;
    }
    
    @Override
    public int getAnchorOffset() {
        return LexUtilities.getLexerOffset((JsParserResult)request.info, request.anchor);
    }

    @Override
    public ElementHandle getElement() {
        return element;
    }

    @Override
    public String getName() {
        return element.getName();
    }

    @Override
    public String getInsertPrefix() {
        return element.getName();
    }

    @Override
    public String getSortText() {
        StringBuilder sb = new StringBuilder();
        if (element != null) {
            FileObject sourceFo = request.result.getSnapshot().getSource().getFileObject();
            FileObject elementFo = element.getFileObject();
            if (elementFo != null && sourceFo != null && sourceFo.equals(elementFo)) {
                sb.append("1");     //NOI18N
            } else {
                if (OffsetRange.NONE.equals(element.getOffsetRange(request.result))) {
                    sb.append("8");
                } else {
                    sb.append("9");     //NOI18N
                }
            }
        }
        sb.append(getName());    
        return sb.toString();
    }
    
    protected boolean isDeprecated() {
        return element.getModifiers().contains(Modifier.DEPRECATED);
    }
    
    @Override
    public String getLhsHtml(HtmlFormatter formatter) {
        formatName(formatter);
        return formatter.getText();
    }

    protected void formatName(HtmlFormatter formatter) {
        if (isDeprecated()) {
            formatter.deprecated(true);
            formatter.appendText(getName());
            formatter.deprecated(false);
        } else {
            formatter.appendText(getName());
        }
    }
    
    @Messages("JsCompletionItem.lbl.js.platform=JS Platform")
    @Override
    public String getRhsHtml(HtmlFormatter formatter) {
        String location = null;
        if (element instanceof JsElement) {
            JsElement jsElement = (JsElement) element;
            if (jsElement.isPlatform()) {
                location = Bundle.JsCompletionItem_lbl_js_platform();
            } else if (jsElement.getSourceLabel() != null) {
                location = jsElement.getSourceLabel();
            }
        }
        if (location == null) {
            location = getFileNameURL();
        }
        if (location == null) {
            return null;
        }

        formatter.reset();
        boolean isgues = OffsetRange.NONE.equals(element.getOffsetRange(request.result));
        if (isgues) {
            formatter.appendHtml("<font color=#999999>");
        }
        formatter.appendText(location);
        if (isgues) {
            formatter.appendHtml("</font>");
        }
        return formatter.getText();
    }

    @Override
    public ElementKind getKind() {
        return element.getKind();
    }

    @Override
    public ImageIcon getIcon() {
        return null;
    }

    @Override
    public Set<Modifier> getModifiers() {
        Set<Modifier> modifiers = (getElement() == null || getElement().getModifiers().isEmpty() ? Collections.EMPTY_SET : EnumSet.copyOf(getElement().getModifiers()));
        if (modifiers.contains(Modifier.PRIVATE) && (modifiers.contains(Modifier.PUBLIC) || modifiers.contains(Modifier.PROTECTED))) {
            modifiers.remove(Modifier.PUBLIC);
            modifiers.remove(Modifier.PROTECTED);
        }
        return modifiers;
    }

    @Override
    public boolean isSmart() {
        // TODO implemented properly
        return false;
    }

    @Override
    public int getSortPrioOverride() {
        int order = 100;
        if (element != null && element instanceof JsElement) {
            if (((JsElement)element).isPlatform()) {
                if (ModelUtils.PROTOTYPE.equals(element.getName())) { //NOI18N
                    order = 1;
                } else {
                    order = 0;
                }
            }
            if (OffsetRange.NONE.equals(element.getOffsetRange(request.result))) {
                order = 120;
            }
        }
        return order;
    }

    @Override
    public String getCustomInsertTemplate() {
        return null;
    }

    @CheckForNull
    public final String getFileNameURL() {
        ElementHandle elem = getElement();
        if (elem == null) {
            return null;
        }
        FileObject fo = elem.getFileObject();
        if (fo != null) {
            return fo.getNameExt();
        }
        return getName();
     }
    
    public static class CompletionRequest {
        public int anchor;
        public JsParserResult result;
        public ParserResult info;
        public String prefix;
        public CompletionContext completionContext;
        public boolean addHtmlTagAttributes;
        public CancelSupport cancelSupport;
    }
    
    private static ImageIcon priviligedIcon = null;
    private static ImageIcon publicGenerator = null;
    private static ImageIcon privateGenerator = null;
    private static ImageIcon priviligedGenerator = null;
    
    public static class JsFunctionCompletionItem extends JsCompletionItem {
        
        private final Set<String> returnTypes;
        private final Map<String, Set<String>> parametersTypes;
        JsFunctionCompletionItem(ElementHandle element, CompletionRequest request, Set<String> resolvedReturnTypes, Map<String, Set<String>> parametersTypes) {
            super(element, request);
            this.returnTypes = resolvedReturnTypes != null ? resolvedReturnTypes : Collections.EMPTY_SET;
            this.parametersTypes = parametersTypes != null ? parametersTypes : Collections.EMPTY_MAP;
        }

        @Override
        public String getLhsHtml(HtmlFormatter formatter) {
            formatter.setMaxLength(OptionsUtils.forLanguage(JsTokenId.javascriptLanguage()).getCodeCompletionItemSignatureWidth());
            formatter.emphasis(true);
            formatName(formatter);
            formatter.emphasis(false);
            if (!asObject()) {
                formatter.appendText("(");  //NOI18N
                appendParamsStr(formatter);
                formatter.appendText(")");  //NOI18N
                appendReturnTypes(formatter);
            }
            return formatter.getText();
        }

        private void appendParamsStr(HtmlFormatter formatter){
            for (Iterator<Map.Entry<String, Set<String>>> it = parametersTypes.entrySet().iterator(); it.hasNext();) {
                Map.Entry<String, Set<String>> entry = it.next();
                formatter.parameters(true);
                formatter.appendText(entry.getKey());
                formatter.parameters(false);
                Collection<String> types = entry.getValue();
                if (!types.isEmpty()) {
                    formatter.type(true);
                    formatter.appendText(": ");  //NOI18N
                    for (Iterator<String> itTypes = types.iterator(); itTypes.hasNext();) {
                        formatter.appendText(itTypes.next());
                        if (itTypes.hasNext()) {
                            formatter.appendText("|");   //NOI18N
                        }
                    }
                    formatter.type(false);
                }
                if (it.hasNext()) {
                    formatter.appendText(", ");  //NOI18N
                }    
            }
        }

        private void appendReturnTypes(HtmlFormatter formatter) {
            if (!returnTypes.isEmpty()) {
                formatter.appendText(": "); //NOI18N
                formatter.type(true);
                for (Iterator<String> it = returnTypes.iterator(); it.hasNext();) {
                    formatter.appendText(it.next());
                    if (it.hasNext()) {
                        formatter.appendText("|"); //NOI18N
                    }
                }
                formatter.type(false);
            }
        }

        @Override
        public String getCustomInsertTemplate() {
            StringBuilder template = new StringBuilder();
            template.append(getName());
            if (!asObject()) {
                if (parametersTypes.isEmpty()) {
                    template.append("()${cursor}");     //NOI18N
                } else {
                    template.append("(${cursor})");     //NOI18N
                }
            } else {
                template.append("${cursor}");       //NOI18N
            }
            return template.toString();
        }

        @Override
        public ImageIcon getIcon() {
            if (getModifiers().contains(Modifier.PROTECTED)) {
                if(priviligedIcon == null) {
                    priviligedIcon = new ImageIcon(ImageUtilities.loadImage("org/netbeans/modules/javascript2/editor/resources/methodPriviliged.png")); //NOI18N
                }
                return priviligedIcon;
            }
            return super.getIcon(); //To change body of generated methods, choose Tools | Templates.
        }
        
        private boolean isAfterNewKeyword() {
            boolean isAfterNew = false;
            Snapshot snapshot = request.result.getSnapshot();
            int offset = request.anchor;
            TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(snapshot, snapshot.getOriginalOffset(offset));
            if (ts != null) {
                ts.move(offset);
                if (ts.moveNext()) {
                    Token<? extends JsTokenId> token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.IDENTIFIER, JsTokenId.OPERATOR_DOT, JsTokenId.BLOCK_COMMENT, JsTokenId.WHITESPACE, JsTokenId.LINE_COMMENT, JsTokenId.EOL));
                    if (token.id() == JsTokenId.KEYWORD_NEW) {
                        isAfterNew = true;
                    }
                }
            }
            return isAfterNew;
        }
        
        /**
         * 
         * @return true if the element should be treated as an object or function in the context
         */
        private boolean asObject() {
            boolean result = false;
            char firstChar = getName().charAt(0);
            JsElement.Kind jsKind = null;
            if (element instanceof JsElement) {
                jsKind = ((JsElement)element).getJSKind();
            }
            if ((jsKind != null && jsKind == JsElement.Kind.CONSTRUCTOR) || Character.isUpperCase(firstChar)) {
                boolean isAfterNew = isAfterNewKeyword();
                if (!isAfterNew) {
                    // check return types, whether it can be really constructor
                    for (String type : returnTypes) {
                        if (type.endsWith(element.getName())) {
                            return true;
                        }
                    }
                    if (returnTypes.isEmpty()) {
                        result = true;
                    } else if (returnTypes.size() == 1) {
                        String type = returnTypes.iterator().next(); 
                        firstChar = type.charAt(0);
                        if (Character.isUpperCase(firstChar) && !(Type.NUMBER.equals(type) || Type.BOOLEAN.equals(type) 
                                || Type.STRING.equals(type) || Type.ARRAY.equals(type))) {
                            result = true;
                        }
                    }
                }
            }
            return result;
        }
    }

    public static class JsGeneratorCompletionItem extends JsFunctionCompletionItem {

        public JsGeneratorCompletionItem(ElementHandle element, CompletionRequest request, Set<String> resolvedReturnTypes, Map<String, Set<String>> parametersTypes) {
            super(element, request, resolvedReturnTypes, parametersTypes);
        }
        
        @Override
        public ImageIcon getIcon() {
            if (getModifiers().contains(Modifier.PUBLIC)) {
                if (publicGenerator == null) {
                    publicGenerator = new ImageIcon(ImageUtilities.loadImage("org/netbeans/modules/javascript2/editor/resources/generatorPublic.png")); //NOI18N
                }
                return publicGenerator;
            } else if (getModifiers().contains(Modifier.PRIVATE)) {
                if (privateGenerator == null) {
                    privateGenerator = new ImageIcon(ImageUtilities.loadImage("org/netbeans/modules/javascript2/editor/resources/generatorPrivate.png")); //NOI18N
                }
                return privateGenerator;
            } else if (getModifiers().contains(Modifier.PROTECTED)) {
                if (priviligedGenerator == null) {
                    priviligedGenerator = new ImageIcon(ImageUtilities.loadImage("org/netbeans/modules/javascript2/editor/resources/generatorPriviliged.png")); //NOI18N
                }
                return priviligedGenerator;
            }
            return super.getIcon();
        }
        
    }
    
    public static class JsCallbackCompletionItem extends JsCompletionItem {
        private static ImageIcon callbackIcon = null;
        private IndexedElement.FunctionIndexedElement function;
                
        public JsCallbackCompletionItem(IndexedElement.FunctionIndexedElement element, CompletionRequest request) {
            super(element, request);
            function = element;
        }
        
        @Override
        public ImageIcon getIcon() {
            if (callbackIcon == null) {
                callbackIcon = new ImageIcon(ImageUtilities.loadImage("org/netbeans/modules/javascript2/editor/resources/methodCallback.png")); //NOI18N
            }
            return callbackIcon;
        }
        
        @Override
        public String getLhsHtml(HtmlFormatter formatter) {
            formatter.setMaxLength(OptionsUtils.forLanguage(JsTokenId.javascriptLanguage()).getCodeCompletionItemSignatureWidth());
            formatter.name(ElementKind.KEYWORD, true);
            formatter.appendText("function");   //NOI18N
            formatter.name(ElementKind.KEYWORD, false);
            formatter.appendText(" (");  //NOI18N
            appendParamsStr(formatter);
            formatter.appendText(")");  //NOI18N
            return formatter.getText();
        }

        @Override
        public int getSortPrioOverride() {
            return 90;      // display as first items?
        }
    
        
        private void appendParamsStr(HtmlFormatter formatter){
            for (Iterator<Map.Entry<String, Collection<String>>> it = function.getParameters().entrySet().iterator(); it.hasNext();) {
                Map.Entry<String, Collection<String>> entry = it.next();
                formatter.parameters(true);
                formatter.appendText(entry.getKey());
                formatter.parameters(false);
                Collection<String> types = entry.getValue();
                if (!types.isEmpty()) {
                    formatter.type(true);
                    formatter.appendText(": ");  //NOI18N
                    for (Iterator<String> itTypes = types.iterator(); itTypes.hasNext();) {
                        formatter.appendText(itTypes.next());
                        if (itTypes.hasNext()) {
                            formatter.appendText("|");   //NOI18N
                        }
                    }
                    formatter.type(false);
                }
                if (it.hasNext()) {
                    formatter.appendText(", ");  //NOI18N
                }    
            }
        }
        
        @Override
        public String getCustomInsertTemplate() {
            StringBuilder template = new StringBuilder();
            template.append(" \n /** ");    //NOI18N
            for (Iterator<Map.Entry<String, Collection<String>>> it = function.getParameters().entrySet().iterator(); it.hasNext();) {
                Map.Entry<String, Collection<String>> entry = it.next();
                Collection<String> types = entry.getValue();
                template.append("\n * @param {");//NOI18N
                if (!types.isEmpty()) {
                    for (Iterator<String> itTypes = types.iterator(); itTypes.hasNext();) {
                        template.append(itTypes.next());
                        if (itTypes.hasNext()) {
                            template.append("|");   //NOI18N
                        }
                    }
                } else {
                    template.append("Object");//NOI18N
                }
                template.append("} ");//NOI18N
                template.append(entry.getKey());
            }
            template.append("\n */");//NOI18N
            template.append("\nfunction (");//NOI18N
            for (Iterator<Map.Entry<String, Collection<String>>> it = function.getParameters().entrySet().iterator(); it.hasNext();) {
                Map.Entry<String, Collection<String>> entry = it.next();
                template.append(entry.getKey());
                if (it.hasNext()) {
                    template.append(", ");  //NOI18N
                }
            }
            template.append(") {\n ${cursor}\n}");//NOI18N
            return template.toString();
        }

        @Override
        public String getName() {
            return "function";
        }

    }
    
    static class KeywordItem extends JsCompletionItem {
        
        private static  ImageIcon keywordIcon = null;
        
        private final String keyword;
        
        private final JsKeywords.CompletionDescription description;

        public KeywordItem(String keyword, JsKeywords.CompletionDescription description, CompletionRequest request) {
            super(null, request);
            this.keyword = keyword;
            this.description = description;
        }

        @Override
        public String getName() {
            return keyword;
        }

        @Override
        public String getLhsHtml(HtmlFormatter formatter) {
            formatter.name(getKind(), true);
            formatter.appendText(getName());
            formatter.name(getKind(), false);
            return formatter.getText();
        }

        @Override
        public ElementKind getKind() {
            return ElementKind.KEYWORD;
        }

        @Override
        public String getRhsHtml(HtmlFormatter formatter) {
            JsVersion since = description.getVersion();
            if (since != null) {
                formatter.appendText(since.getDisplayName());
                return formatter.getText();
            }
            return null;
        }

        @Override
        public ImageIcon getIcon() {
            if (keywordIcon == null) {
                keywordIcon = new ImageIcon(ImageUtilities.loadImage("org/netbeans/modules/javascript2/editor/resources/javascript.png")); //NOI18N
            }
            return keywordIcon;
        }
        
        @Override
        public String getInsertPrefix() {
            return getName();
        }
        
        @Override
        public String getCustomInsertTemplate() {
            StringBuilder builder = new StringBuilder();
            
            JsKeywords.CompletionType type = description.getType();
            if (type == null) {
                return getName();
            }
            
            switch(type) {
                case SIMPLE:
                    builder.append(getName());
                    break;
                case ENDS_WITH_SPACE:
                    builder.append(getName());
                    builder.append(" ${cursor}"); //NOI18N
                    break;
                case CURSOR_INSIDE_BRACKETS:
                    builder.append(getName());
                    builder.append("(${cursor})"); //NOI18N
                    break;
                case ENDS_WITH_CURLY_BRACKETS:
                    builder.append(getName());
                    builder.append(" {${cursor}}"); //NOI18N
                    break;
                case ENDS_WITH_SEMICOLON:
                    builder.append(getName());
                    CharSequence text = request.info.getSnapshot().getText();
                    int index = request.anchor + request.prefix.length();
                    if (index == text.length() || ';' != text.charAt(index)) { //NOI18N
                        builder.append(";"); //NOI18N
                    }
                    break;
                case ENDS_WITH_COLON:
                    builder.append(getName());
                    builder.append(" ${cursor}:"); //NOI18N
                    break;
                case ENDS_WITH_DOT:
                    builder.append(getName());
                    builder.append(".${cursor}"); //NOI18N
                    break;
                default:
                    assert false : type.toString();
                    break;
            }
            return builder.toString();
        }

        @Override
        public int getSortPrioOverride() {
            return 130;
        }
    }

    static class CssCompletionItem extends JsCompletionItem {
        
        private static ImageIcon cssIcon = null;
        
        private final String name;
        
        public CssCompletionItem(String name, CompletionRequest request) {
            super(null, request);
            this.name = name;
        }
        
        @Override
        public ImageIcon getIcon() {
            if (cssIcon == null) {
                cssIcon = new ImageIcon(ImageUtilities.loadImage("org/netbeans/modules/javascript2/jquery/resources/style_sheet_16.png")); //NOI18N
            }
            return cssIcon;
        }
        
        @Override
        public String getName() {
            return name;
        }
        
        @Override
        public String getInsertPrefix() {
            return getName();
        }
        
        @Override
        public String getLhsHtml(HtmlFormatter formatter) {
            formatter.reset();
            formatter.appendText(getName());
            return formatter.getText();
        }

        @Override
        public ElementKind getKind() {
            return ElementKind.RULE;
        }

        @Override
        public String getRhsHtml(HtmlFormatter formatter) {
            return null;
        }
        
    }
    
    public static class JsHtmlAttributeItem extends JsCompletionItem {
        
        private final HtmlTagAttribute attr;
        
        public JsHtmlAttributeItem(HtmlTagAttribute attr, CompletionRequest request) {
            super(new HtmlAttrElement(attr), request);
            this.attr = attr;
        }
        
        @Override
        public String getName() {
            return attr.getName();
        }
        
        @Override
        public String getInsertPrefix() {
            return getName();
        }
        
        @Override
        public String getLhsHtml(HtmlFormatter formatter) {
            formatter.reset();
            formatter.appendText(getName());
            return formatter.getText();
        }

        @Override
        public ElementKind getKind() {
            return ElementKind.ATTRIBUTE;
        }

        @Messages("JsCompletionItem.lbl.html.attribute=HTML Attribute")
        @Override
        public String getRhsHtml(HtmlFormatter formatter) {
            formatter.reset();
            formatter.appendHtml("<font color=#999999>");
            formatter.appendText(Bundle.JsCompletionItem_lbl_html_attribute());
            formatter.appendHtml("</font>");
            return formatter.getText();
        }
        
        private static class HtmlAttrElement extends SimpleDocElement {
            private final HtmlTagAttribute attribute;
            
            public HtmlAttrElement(HtmlTagAttribute attribute) {
                super(attribute.getName(), ElementKind.ATTRIBUTE);
                this.attribute = attribute;
            }

            @Override
            public String getDocumentation() {
                String content = attribute.getHelp().getHelpContent();
                if (content == null) {
                    if (attribute.getHelp().getHelpResolver() != null && attribute.getHelp().getHelpURL() != null) {
                        content = attribute.getHelp().getHelpResolver().getHelpContent(attribute.getHelp().getHelpURL());
                    }
                }
                return content;
            }
        }
    }
    
    public static class JsPropertyCompletionItem extends JsCompletionItem {

        private final Set<String> resolvedTypes;
        
        JsPropertyCompletionItem(ElementHandle element, CompletionRequest request, Set<String> resolvedTypes) {
            super(element, request);
            this.resolvedTypes = resolvedTypes != null ? resolvedTypes : Collections.EMPTY_SET;
        }

        @Override
        public String getLhsHtml(HtmlFormatter formatter) {
            formatName(formatter);
            if (!resolvedTypes.isEmpty()) {
                formatter.type(true);
                formatter.appendText(": ");  //NOI18N
                for (Iterator<String> it = resolvedTypes.iterator(); it.hasNext();) {
                    formatter.appendText(it.next());
                    if (it.hasNext()) {
                        formatter.appendText("|");   //NOI18N
                    }
                }
                formatter.type(false);
            }
            return formatter.getText();
        }

        @Override
        public String getCustomInsertTemplate() {
            if (request.completionContext == CompletionContext.OBJECT_PROPERTY_NAME) {
                return getName() + ": ${cursor}"; // NOI18N
            }
            return super.getCustomInsertTemplate(); //To change body of generated methods, choose Tools | Templates.
        }
        
    }

    public static class Factory {
        
        public static void create( Map<String, List<JsElement>> items, CompletionRequest request, List<CompletionProposal> result) {
            CancelSupport cancelSupport = request.cancelSupport;
            if (cancelSupport.isCancelled()) {
                return;
            }
            // This maps unresolved types to the display name of the resolved type. 
            // It should save time to not resolve one type more times
            HashMap<String, Set<String>> resolvedTypes = new HashMap<String, Set<String>>();

            for (Map.Entry<String, List<JsElement>> entry: items.entrySet()) {

                // this helps to eleminate items that will look as the same items in the cc
                HashMap<String, JsCompletionItem> signatures = new HashMap<String, JsCompletionItem>();
                Index jsIndex = null;
                if (OptionsUtils.forLanguage(JsTokenId.javascriptLanguage()).autoCompletionTypeResolution()) {
                    jsIndex = Index.get(request.info.getSnapshot().getSource().getFileObject());
                }
                for (JsElement element : entry.getValue()) {
                    if (cancelSupport.isCancelled()) {
                        return;
                    }
                    switch (element.getJSKind()) {
                        case CONSTRUCTOR:
                        case FUNCTION:
                        case METHOD:
                        case GENERATOR:
                            Set<String> returnTypes = new HashSet<String>();
                            HashMap<String, Set<String>> allParameters = new LinkedHashMap<String, Set<String>>();
                            if (element instanceof JsFunction) {
                                // count return types
                                Collection<TypeUsage> resolveTypes = ModelUtils.resolveTypes(((JsFunction) element).getReturnTypes(),
                                        Model.getModel(request.info, false),
                                        jsIndex, false);
                                returnTypes.addAll(Utils.getDisplayNames(resolveTypes));
                                // count parameters type
                                for (JsObject jsObject : ((JsFunction) element).getParameters()) {
                                    Set<String> paramTypes = new HashSet<String>();
                                    for (TypeUsage type : jsObject.getAssignmentForOffset(jsObject.getOffset() + 1)) {
                                        Set<String> resolvedType = resolvedTypes.get(type.getType());
                                        if (resolvedType == null) {
                                            resolvedType = new HashSet(1);
                                            String displayName = ModelUtils.getDisplayName(type);
                                            if (!displayName.isEmpty()) {
                                                resolvedType.add(displayName);
                                            }
                                            resolvedTypes.put(type.getType(), resolvedType);
                                        }
                                        paramTypes.addAll(resolvedType);
                                    }
                                    allParameters.put(jsObject.getName(), paramTypes);
                                }
                            } else if (element instanceof IndexedElement.FunctionIndexedElement) {
                                // count return types
                                HashSet<TypeUsage> returnTypeUsages = new HashSet<TypeUsage>();
                                for (String type : ((IndexedElement.FunctionIndexedElement) element).getReturnTypes()) {
                                    returnTypeUsages.add(new TypeUsage(type, -1, false));
                                }
                                Collection<TypeUsage> resolveTypes = ModelUtils.resolveTypes(returnTypeUsages,
                                        Model.getModel(request.info, false),
                                        jsIndex, false);
                                returnTypes.addAll(Utils.getDisplayNames(resolveTypes));
                                // count parameters type
                                LinkedHashMap<String, Collection<String>> parameters = ((IndexedElement.FunctionIndexedElement) element).getParameters();
                                for (Map.Entry<String, Collection<String>> paramEntry : parameters.entrySet()) {
                                    Set<String> paramTypes = new HashSet<String>();
                                    for (String type : paramEntry.getValue()) {
                                        Set<String> resolvedType = resolvedTypes.get(type);
                                        if (resolvedType == null) {
                                            resolvedType = new HashSet(1);
                                            String displayName = ModelUtils.getDisplayName(type);
                                            if (!displayName.isEmpty()) {
                                                resolvedType.add(displayName);
                                            }
                                            resolvedTypes.put(type, resolvedType);
                                        }
                                        paramTypes.addAll(resolvedType);
                                    }
                                    allParameters.put(paramEntry.getKey(), paramTypes);
                                }
                            }
                            // create signature
                            String signature = createFnSignature(entry.getKey(), allParameters, returnTypes);
                            if (!signatures.containsKey(signature)) {
                                JsCompletionItem item = element.getJSKind() != JsElement.Kind.GENERATOR 
                                        ? new JsFunctionCompletionItem(element, request, returnTypes, allParameters)
                                        : new JsGeneratorCompletionItem(element, request, returnTypes, allParameters);
                                signatures.put(signature, item);
                            }
                            break;
                        case PARAMETER:
                        case PROPERTY:
                        case PROPERTY_GETTER:
                        case PROPERTY_SETTER:
                        case FIELD:
                        case VARIABLE:
                            Set<String> typesToDisplay = new HashSet<String>();
                            Collection<? extends TypeUsage> assignment = null;
                            if (element instanceof JsObject) {
                                JsObject jsObject = (JsObject) element;
                                assignment = jsObject.getAssignments();
                            } else if (element instanceof IndexedElement) {
                                IndexedElement iElement = (IndexedElement) element;
                                assignment = iElement.getAssignments();
                            }
                            if (assignment != null && !assignment.isEmpty()) {
                                HashSet<TypeUsage> toResolve = new HashSet<TypeUsage>();
                                for (TypeUsage type : assignment) {
                                    if (type.isResolved()) {
                                        if (!Type.UNDEFINED.equals(type.getType())) {
                                            typesToDisplay.add(ModelUtils.getDisplayName(type));
                                        }
                                    } else {
                                        Set<String> resolvedType = resolvedTypes.get(type.getType());
                                        if (resolvedType == null) {
                                            toResolve.clear();
                                            toResolve.add(type);
                                            resolvedType = new HashSet(1);
                                            Collection<TypeUsage> resolved = ModelUtils.resolveTypes(toResolve,
                                                    Model.getModel(request.result, false),
                                                    jsIndex, false);
                                            for (TypeUsage rType : resolved) {
                                                String displayName = ModelUtils.getDisplayName(rType);
                                                if (!displayName.isEmpty()) {
                                                    resolvedType.add(displayName);
                                                }
                                            }
                                            resolvedTypes.put(type.getType(), resolvedType);
                                        }
                                        typesToDisplay.addAll(resolvedType);
                                    }
                                }
                            }
                            // signatures
                            signature = element.getName() + ":" + createTypeSignature(typesToDisplay);
                            if (!signatures.containsKey(signature)) {
                                // add the item to the cc only if doesn't exist any similar
                                JsCompletionItem item = new JsPropertyCompletionItem(element, request, typesToDisplay);
                                signatures.put(signature, item);
                            }
                            break;
                        default:
                            signature = element.getName();
                            if (!signatures.containsKey(signature)) {
                                JsCompletionItem item = new JsCompletionItem(element, request);
                                signatures.put(signature, item);
                            }
                    }
                }
                for (JsCompletionItem item: signatures.values()) {
                    result.add(item);
                }
            }
        }
        
        private static String createFnSignature(String name, HashMap<String, Set<String>> params, Set<String> returnTypes) {
            StringBuilder sb = new StringBuilder();
            sb.append(name).append('(');
            for (Map.Entry<String, Set<String>> entry : params.entrySet()) {
                sb.append(entry.getKey()).append(':');
                sb.append(createTypeSignature(entry.getValue()));
                sb.append(',');
            }
            sb.append(')');
            sb.append(createTypeSignature(returnTypes));
            return sb.toString();
        }
        
        private static String createTypeSignature(Set<String> types) {
            StringBuilder sb = new StringBuilder();
            for(String name: types){
                sb.append(name).append('|');
            }
            return sb.toString();
        }
    }
    
    public abstract static class SimpleDocElement implements ElementHandle {

        private final String name;
        private final ElementKind kind;

        public SimpleDocElement(String name, ElementKind kind) {
            this.name = name;
            this.kind = kind;
        }
        
        
        @Override
        public FileObject getFileObject() {
            return null;
        }

        @Override
        public String getMimeType() {
            return "";
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public String getIn() {
            return "";
        }

        @Override
        public ElementKind getKind() {
            return kind;
        }

        @Override
        public Set<Modifier> getModifiers() {
            return Collections.<Modifier>emptySet();
        }

        @Override
        public boolean signatureEquals(ElementHandle handle) {
            return false;
        }

        @Override
        public OffsetRange getOffsetRange(ParserResult result) {
            return OffsetRange.NONE;
        }
        
        abstract public String getDocumentation();
    }
}
