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

import org.netbeans.modules.javascript2.jquery.PropertyNameDataItem;
import org.netbeans.modules.javascript2.jquery.SelectorItem;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.csl.api.CodeCompletionContext;
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.spi.ParserResult;
import org.netbeans.modules.css.indexing.api.CssIndex;
import org.netbeans.modules.html.editor.lib.api.HtmlVersion;
import org.netbeans.modules.html.editor.lib.api.model.*;
import org.netbeans.modules.javascript2.editor.spi.CompletionContext;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.javascript2.editor.spi.CompletionProvider;
import org.netbeans.modules.javascript2.jquery.PropertyNameDataLoader;
import org.netbeans.modules.javascript2.jquery.model.JQueryUtils;
import org.netbeans.modules.javascript2.jquery.SelectorsLoader;
import org.netbeans.modules.javascript2.jquery.editor.JQueryCompletionItem.DocSimpleElement;
import org.openide.filesystems.FileObject;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.Exceptions;

/**
 *
 * @author Petr Pisl
 */
@CompletionProvider.Registration(priority=10)
public class JQueryCodeCompletion implements CompletionProvider {

    private static final Logger LOGGER = Logger.getLogger(JQueryCodeCompletion.class.getName());

    public static final String HELP_LOCATION = "docs/jquery-api.xml"; //NOI18N
    private static File jQueryApiFile;
    
    private static final String PROPERTY_NAME_FILE_LOCATION = "docs/jquery-propertyNames.xml"; //NOI18N
    private static File propertyNameFile;

    private static Collection<HtmlTagAttribute> allAttributes;

    private int lastTsOffset = 0;
    
    public List<CompletionProposal> complete(CodeCompletionContext ccContext, CompletionContext jsCompletionContext, String prefix) {
        long start = System.currentTimeMillis();
        List<CompletionProposal> result = new ArrayList<CompletionProposal>();
        ParserResult parserResult = ccContext.getParserResult();
        int offset = ccContext.getCaretOffset();
        lastTsOffset = ccContext.getParserResult().getSnapshot().getEmbeddedOffset(offset);
        switch (jsCompletionContext) {
            case IN_STRING:
                TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(parserResult.getSnapshot().getTokenHierarchy(), offset);
                if (ts != null) {
                    ts.move(lastTsOffset);
                    if (ts.moveNext()) {
                        if (ts.token().id() == JsTokenId.STRING_END) {
                            ts.movePrevious();
                        }
                        if (ts.token().id() == JsTokenId.STRING) {
                            prefix = ts.token().text().toString().substring(0, lastTsOffset - ts.offset());
                        }
                    }
                }
            case GLOBAL:
            case EXPRESSION:
            case OBJECT_PROPERTY:
                if (JQueryUtils.isInJQuerySelector(parserResult, lastTsOffset)) {
                    addSelectors(result, parserResult, prefix, lastTsOffset, ccContext);
                }
                break;
            case OBJECT_PROPERTY_NAME:
                completeObjectPropertyName(ccContext, result, prefix);
                break;
            default:
                break;
        }
        long end = System.currentTimeMillis();
        LOGGER.log(Level.FINE, "Counting jQuery CC took {0}ms ", (end - start));
        return result;
    }

    private int findParamIndex(ParserResult parserResult, int offset) {
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(parserResult.getSnapshot().getTokenHierarchy(), offset);
        if (ts == null) {
            return -1;
        }
        ts.move(offset);
        if (!(ts.moveNext() && ts.movePrevious())) {
            return -1;
        }
        Token<? extends JsTokenId> token = LexUtilities.findNext(ts, Arrays.asList(JsTokenId.WHITESPACE));
        // count index of parameters
        int paramIndex = 0;
        while(token.id() != JsTokenId.EOL && token.id() != JsTokenId.BRACKET_LEFT_PAREN) {
            if (token.id() == JsTokenId.OPERATOR_COMMA) {
                paramIndex ++;
            } else if (token.id() == JsTokenId.OPERATOR_DOT) {
                // we are not inside ()
                return -1;
            }
            token = LexUtilities.findNext(ts, Arrays.asList(JsTokenId.WHITESPACE));
        }
        if (token.id() == JsTokenId.BRACKET_LEFT_PAREN) {
            return paramIndex;
        }
        return -1;
    }
    
    private String findFunctionName(ParserResult parserResult, int offset) {
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(parserResult.getSnapshot().getTokenHierarchy(), offset);
        if (ts == null) {
            return null;
        }
        ts.move(offset);
        if (!(ts.moveNext() && ts.movePrevious())) {
            return null;
        }
        Token<? extends JsTokenId> token = LexUtilities.findNext(ts, Arrays.asList(JsTokenId.WHITESPACE));
        while(token.id() != JsTokenId.EOL && token.id() != JsTokenId.BRACKET_LEFT_PAREN) {
            if (token.id() == JsTokenId.OPERATOR_DOT) {
                // we are not inside ()
                return null;
            }
            token = LexUtilities.findNext(ts, Arrays.asList(JsTokenId.WHITESPACE));
        }
        if (token.id() == JsTokenId.BRACKET_LEFT_PAREN && ts.movePrevious()) {
            token = LexUtilities.findNext(ts, Arrays.asList(JsTokenId.WHITESPACE));
            if (token.id() == JsTokenId.IDENTIFIER){
                return token.text().toString();
            }
        }
        return null;
    }
    
    

    @Override
    public String getHelpDocumentation(ParserResult info, ElementHandle element) {
        if (element != null && element instanceof DocSimpleElement) {
            return ((DocSimpleElement)element).getDocumentation();
        }
        if (element != null && element.getKind() == ElementKind.CALL) {
            String name = element.getName();
            name = name.substring(1); // remove :
            int index = name.indexOf('(');
            if (index > -1) {
                name = name.substring(0, index);
            }
            return SelectorsLoader.getDocumentation(getJQueryAPIFile(), name);
        } else if (element != null &&  element.getKind() == ElementKind.METHOD) {
            if (JQueryUtils.isJQuery(info, lastTsOffset)) {
                return SelectorsLoader.getMethodDocumentation(getJQueryAPIFile(), element.getName());
            }
        }
        return null;
    }

    private void completeObjectPropertyName(CodeCompletionContext ccContext, List<CompletionProposal> result, String prefix) {
        
        // find the object that can be configured
        TokenHierarchy<?> th = ccContext.getParserResult().getSnapshot().getTokenHierarchy();
        if (th == null) {
            return;
        }
        int carretOffset  = ccContext.getCaretOffset();
        int eOffset = ccContext.getParserResult().getSnapshot().getEmbeddedOffset(carretOffset);
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(th, eOffset);
        if (ts == null) {
            return;
        }
        
        ts.move(eOffset);
        
        if (!ts.moveNext() && !ts.movePrevious()){
            return;
        }
        
        Token<? extends JsTokenId> token = null;
        JsTokenId tokenId;
        //find the begining of the object literal
        int balance = 1;
        while (ts.movePrevious() && balance > 0) {
            token = ts.token();
            tokenId = token.id();
            if (tokenId == JsTokenId.BRACKET_RIGHT_CURLY) {
                balance++;
            } else if (tokenId == JsTokenId.BRACKET_LEFT_CURLY) {
                balance--;
            }
        }
        if (token == null || balance != 0) {
            return;
        }
        
        // now we should be at the beginning of the object literal. 
        token = LexUtilities.findPreviousToken(ts, Arrays.asList(JsTokenId.IDENTIFIER));
        tokenId = token.id();
        StringBuilder sb = new StringBuilder(token.text());
        while ((tokenId == JsTokenId.IDENTIFIER || tokenId == JsTokenId.OPERATOR_DOT) && ts.movePrevious()) {
            token = ts.token(); tokenId = token.id();
            if (tokenId == JsTokenId.OPERATOR_DOT) {
                sb.insert(0, '.'); // NOI18N
            } else if (tokenId == JsTokenId.IDENTIFIER) {
                sb.insert(0, token.text());
            }
        }
        
        String fqn = sb.toString();
        Map<String, Collection<PropertyNameDataItem>> data = getPropertyNameData();
        if (fqn.startsWith(JQueryUtils.JQUERY$)) { // NOI18N
            fqn = fqn.replace(JQueryUtils.JQUERY$, JQueryUtils.JQUERY); //NOI18N
        }
        Collection<PropertyNameDataItem> items = data.get(fqn);
        int anchorOffset = ccContext.getParserResult().getSnapshot().getOriginalOffset(eOffset) - ccContext.getPrefix().length();
        if (items != null) {
            boolean addComma = addComma(ts, eOffset);
            for (PropertyNameDataItem item : items) {
                if (item.getName().startsWith(prefix)) {
                    result.add(JQueryCompletionItem.createPropertyNameItem(item, anchorOffset, addComma));
                }
            }
        }
    }

    private synchronized static Map<String, Collection<PropertyNameDataItem>> getPropertyNameData() {
        return PropertyNameDataLoader.getData(getPropertyNameDataFile());
    }
    
    private static synchronized File getPropertyNameDataFile() {
        if (propertyNameFile == null) {
            propertyNameFile = InstalledFileLocator.getDefault().locate(PROPERTY_NAME_FILE_LOCATION, "org.netbeans.modules.javascript2.jquery", false); //NOI18N
        }
        return propertyNameFile;
    }

    private boolean addComma(TokenSequence<? extends JsTokenId> ts, int eOffset) {
        // we know that we are at the position, where the name of property is entered
        ts.move(eOffset);
        if (ts.moveNext()) {
            // we are looking for ',' -> don't add comma, is already there
            // ':' or an identifier -> need to add comma, next expression is new property definition
            // '}' -> end of object literal object definition -> no need comma there
            Token<? extends JsTokenId>token = LexUtilities.findNext(ts, Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.EOL, JsTokenId.BLOCK_COMMENT, JsTokenId.LINE_COMMENT));
            if (token.id() == JsTokenId.IDENTIFIER || token.id() == JsTokenId.STRING_BEGIN) {
                return true;
            } 
        }
        return false;
    }
    
    private enum SelectorKind {
        TAG, TAG_ATTRIBUTE, CLASS, ID, TAG_ATTRIBUTE_COMPARATION, AFTER_COLON
    }
    
    private static class SelectorContext {
        String prefix;
        Collection<SelectorKind> kinds;
        int prefixIndex;

        public SelectorContext(String prefix, int prefixIndex, Collection<SelectorKind> kinds) {
            this.prefix = prefix;
            this.kinds = kinds;
            this.prefixIndex = prefixIndex;
        }
        
    }
    
    private static HashMap<String, List<SelectorKind>> contextMap = new HashMap<String, List<SelectorKind>>();    
    private static Collection< SelectorItem> afterColonList = Collections.emptyList();
    
    private void fillContextMap() {
        contextMap.put(" (", Arrays.asList(SelectorKind.TAG, SelectorKind.ID, SelectorKind.CLASS, SelectorKind.AFTER_COLON));
        contextMap.put("#", Arrays.asList(SelectorKind.ID));
        contextMap.put(".", Arrays.asList(SelectorKind.CLASS));
        contextMap.put("[", Arrays.asList(SelectorKind.TAG_ATTRIBUTE));
        contextMap.put(":", Arrays.asList(SelectorKind.AFTER_COLON));
    }
    
    private void fillAfterColonList() {
        if(getJQueryAPIFile() != null) {
            afterColonList = SelectorsLoader.getSelectors(getJQueryAPIFile());
        }
    }

    private synchronized File getJQueryAPIFile() {
        if (jQueryApiFile == null) {
            jQueryApiFile = InstalledFileLocator.getDefault().locate(HELP_LOCATION, "org.netbeans.modules.javascript2.jquery", false); //NOI18N
        }
        return jQueryApiFile;
    }
  
    private SelectorContext findSelectorContext(String text) {
        int index = text.length() - 1;
        StringBuilder prefix = new StringBuilder();
        while (index > -1) {
            char c = text.charAt(index);
            switch (c) {
                case ' ':
                case '(':
                case ',':
                    return new SelectorContext(prefix.toString(), index, Arrays.asList(SelectorKind.TAG, SelectorKind.ID, SelectorKind.CLASS));
                case '#':
                    return new SelectorContext(prefix.toString(), index, Arrays.asList(SelectorKind.ID));
                case '.':
                    return new SelectorContext(prefix.toString(), index, Arrays.asList(SelectorKind.CLASS));
                case '[':
                    return new SelectorContext(prefix.toString(), index, Arrays.asList(SelectorKind.TAG_ATTRIBUTE));
                case ':':
                    return new SelectorContext(prefix.toString(), index, Arrays.asList(SelectorKind.AFTER_COLON));
            }
            prefix.insert(0, c);
            index--;
        }
        if (index < 0) {
            return new SelectorContext(prefix.toString(), 0, Arrays.asList(SelectorKind.TAG, SelectorKind.ID, SelectorKind.CLASS, SelectorKind.AFTER_COLON));
        }
        return null;
    }
    
    private void addSelectors(final List<CompletionProposal> result, final ParserResult parserResult, final String prefix, final int offset, CodeCompletionContext ccContex) {
        /*
         * basic selectors: 
         * $(document); // Activate jQuery for object
         * $('#mydiv') // Element with ID "mydiv" 
         * $('p.first') // P tags with class first. 
         * $('p[title="Hello"]') // P tags with title "Hello"
         * $('p[title^="H"]') // P tags title starting with H
         */
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(parserResult.getSnapshot().getTokenHierarchy(), offset);
        if (ts == null) {
            return;
        }
        ts.move(offset);
        if (!(ts.moveNext() && ts.movePrevious())) {
            return;
        }
        String wrapup = ""; //NOI18N
        String prefixText = prefix;
        int anchorOffsetDelta = prefix.length() - ccContex.getPrefix().length();
        if (!prefixText.isEmpty() && anchorOffsetDelta > 0) {
            char ch = prefixText.charAt(anchorOffsetDelta - 1);
            if (ch == '#' || ch == '.' || ch == ':') {
                anchorOffsetDelta--;
            }
        }
//        if (!(ts.token().id() == JsTokenId.STRING || ts.token().id() == JsTokenId.STRING_END || ts.token().id() == JsTokenId.STRING_BEGIN)) {
//            wrapup = "'"; //NOI18N
//            if (ts.token().id() == JsTokenId.IDENTIFIER) {
//                ts.movePrevious();
//            }
//            if(ts.token().id() == JsTokenId.OPERATOR_COLON) {
//                prefixText = ":" + prefixText; //NOI18N
//                anchorOffsetDelta = prefix.isEmpty() ? 0 : -1;
//            } else if (ts.token().id() == JsTokenId.OPERATOR_DOT) {
//                prefixText = "." + prefixText; //NOI18N
//                anchorOffsetDelta = prefix.isEmpty() ? 0 : -1;
//            } else {
//                anchorOffsetDelta = 0;
//            }
////            if (prefix.isEmpty()) {
////                anchorOffsetDelta = 1;
////            }
//
//
//        }
        
        
        if(contextMap.isEmpty()) {
            fillContextMap();
        }
        
        SelectorContext context = findSelectorContext(prefixText);
        
        if (context != null) {
            int docOffset = parserResult.getSnapshot().getOriginalOffset(offset) - prefixText.length();
            int anchorOffset = docOffset + anchorOffsetDelta;
            for (SelectorKind selectorKind : context.kinds) {
                switch (selectorKind) {
                    case TAG:
                        Collection<HtmlTag> tags = getHtmlTags(context.prefix);
                        for (HtmlTag htmlTag : tags) {
                            result.add(JQueryCompletionItem.create(htmlTag, anchorOffset, wrapup));
                        }
                        break;
                    case TAG_ATTRIBUTE:
                        // provide attributes
                        int index = prefix.lastIndexOf('[');
                        String tagName = "";
                        if (index > 0) {
                            tagName = prefix.substring(0, index);
                        }
                        
                        if (!tagName.isEmpty()) {
                            index = tagName.lastIndexOf(' ');
                            if (index > -1) {
                                tagName = tagName.substring(index + 1);
                            }
                            index = tagName.indexOf('.');
                            if (index > -1) {
                                tagName = tagName.substring(0, index);
                            }
                            index = tagName.indexOf('#');
                            if (index > -1) {
                                tagName = tagName.substring(0, index);
                            }
                            index = tagName.lastIndexOf('(');
                            if (index > -1) {
                                tagName = tagName.substring(index + 1);
                            }
                        }
                        
                        
                        
                        if (!tagName.isEmpty() && (tagName.charAt(0) == '.' || tagName.charAt(0) == '#' || tagName.charAt(0) == '(')) {
                            if (ts.token().id() == JsTokenId.STRING_BEGIN) {
                                ts.moveNext();
                            }
                            if (ts.token().id() == JsTokenId.STRING) {
                                String value = ts.token().text().toString();
                                index = value.indexOf(prefix);
                                if (index > -1) {
                                    tagName = value.substring(0, index);
                                    index--;
                                    while (index > -1) {
                                        char ch = tagName.charAt(index);
                                        if (ch == '.' || ch == '#' || ch == ',' || ch == '=' 
                                                || ch == '"' || ch == '\'' || ch == '[' 
                                                || ch == ']' || ch == '(' || ch == ')' 
                                                || ch == ':') {
                                            break;
                                        }
                                        index --;
                                    }
                                    if (index > -1) {
                                        tagName = tagName.substring(index + 1);
                                    }
                                } else {
                                    tagName = "";
                                }
                            } else {
                                tagName = "";
                            }
                        }
                        String attributePrefix = prefix.substring(context.prefixIndex + 1);
                        anchorOffset = docOffset + prefix.length() - context.prefix.length();
                        Collection<HtmlTagAttribute> attributes = getHtmlAttributes(tagName, attributePrefix);
                        for (HtmlTagAttribute htmlTagAttribute : attributes) {
                            result.add(JQueryCompletionItem.create(htmlTagAttribute, anchorOffset, ""));
                        }
                        break;
                    case ID:
                        Collection<String> tagIds = getTagIds(context.prefix, parserResult);
                        for (String tagId : tagIds) {
                            result.add(JQueryCompletionItem.createCSSItem("#" + tagId, anchorOffset, wrapup));
                        }
                        break;
                    case CLASS:
                        Collection<String> classes = getCSSClasses(context.prefix, parserResult);
                        for (String cl : classes) {
                            result.add(JQueryCompletionItem.createCSSItem("." + cl, anchorOffset, wrapup));
                        }
                        break;
                    case AFTER_COLON:
                        if(afterColonList.isEmpty()) {
                            fillAfterColonList();
                        }
                        for (SelectorItem selector : afterColonList) {
                            if (selector.getDisplayText().startsWith(context.prefix)) {
                                result.add(JQueryCompletionItem.createJQueryItem(":" + selector.getDisplayText(), anchorOffset, wrapup, selector.getInsertTemplate()));
                            }
                        }
                        break;
                }
            }
        }
    }

    private Collection<String> getTagIds(String tagIdPrefix, ParserResult parserResult) {
        FileObject fo = parserResult.getSnapshot().getSource().getFileObject();
        if (fo == null) {
            return Collections.emptyList();
        }
        Project project = FileOwnerQuery.getOwner(fo);
        HashSet<String> unique = new HashSet<String>();
        try {
            CssIndex cssIndex = CssIndex.create(project);
            Map<FileObject, Collection<String>> findIdsByPrefix = cssIndex.findIdsByPrefix(tagIdPrefix);

            for (Collection<String> ids : findIdsByPrefix.values()) {
                for (String id : ids) {
                    unique.add(id);
                }
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return unique;
    }

    private Collection<String> getCSSClasses(String classPrefix, ParserResult parserResult) {
        FileObject fo = parserResult.getSnapshot().getSource().getFileObject();
        if(fo == null) {
            return Collections.emptyList();
        }
        Project project = FileOwnerQuery.getOwner(fo);
        HashSet<String> unique = new HashSet<String>();
        try {
            CssIndex cssIndex = CssIndex.create(project);
            Map<FileObject, Collection<String>> findIdsByPrefix = cssIndex.findClassesByPrefix(classPrefix);

            for (Collection<String> ids : findIdsByPrefix.values()) {
                for (String id : ids) {
                    unique.add(id);
                }
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return unique;

    }

    private Collection<HtmlTagAttribute> getHtmlAttributes(final String tagName, final String prefix) {
        Collection<HtmlTagAttribute> result = Collections.<HtmlTagAttribute>emptyList();
        HtmlModel htmlModel = HtmlModelFactory.getModel(HtmlVersion.HTML5);
        HtmlTag htmlTag = htmlModel.getTag(tagName);
        if (htmlTag != null) {
            if (prefix.isEmpty()) {
                if (tagName.isEmpty()) {
                    result = getAllAttributes(htmlModel);
                } else {
                    result = htmlTag.getAttributes();
                }
            } else {
                Collection<HtmlTagAttribute> attributes = htmlTag.getAttributes();
                if (tagName.isEmpty() || htmlTag.getTagClass() == HtmlTagType.UNKNOWN) {
                    attributes = getAllAttributes(htmlModel);
                }
                result = new ArrayList<HtmlTagAttribute>();
                for (HtmlTagAttribute htmlTagAttribute : attributes) {
                    if(htmlTagAttribute.getName().startsWith(prefix)) {
                        result.add(htmlTagAttribute);
                    }
                }
            }
        }
        return result;
    }

    private Collection<HtmlTag> getHtmlTags(String prefix) {
        Collection<HtmlTag> result = Collections.emptyList();
        HtmlModel htmlModel = HtmlModelFactory.getModel(HtmlVersion.HTML5);
        Collection<HtmlTag> allTags = htmlModel.getAllTags();
        if (prefix.isEmpty()) {
            result = allTags;
        } else {
            result = new ArrayList<HtmlTag>();
            for (HtmlTag htmlTag : allTags) {
                if (htmlTag.getName().startsWith(prefix)) {
                    result.add(htmlTag);
                }
            }
        }
        return result;
    }

    private synchronized Collection<HtmlTagAttribute> getAllAttributes(HtmlModel htmlModel) {
        if (allAttributes == null) {
            initAllAttributes(htmlModel);
        }
        return allAttributes;
    }

    private synchronized void initAllAttributes(HtmlModel htmlModel) {
        assert allAttributes == null;
        Map<String, HtmlTagAttribute> result = new HashMap<String, HtmlTagAttribute>();
        for (HtmlTag htmlTag : htmlModel.getAllTags()) {
            for (HtmlTagAttribute htmlTagAttribute : htmlTag.getAttributes()) {
                // attributes can probably differ per tag so we can just offer some of them,
                // at least for the CC purposes it should be complete list of attributes for unknown tag
                if (!result.containsKey(htmlTagAttribute.getName())) {
                    result.put(htmlTagAttribute.getName(), htmlTagAttribute);
                }
            }
        }
        allAttributes = result.values();
    }
}
