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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
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 java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
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.editor.BaseDocument;
import org.netbeans.modules.csl.api.CodeCompletionContext;
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.css.indexing.api.CssIndex;
import org.netbeans.modules.html.editor.lib.api.HtmlVersion;
import org.netbeans.modules.html.editor.lib.api.model.HtmlModel;
import org.netbeans.modules.html.editor.lib.api.model.HtmlModelFactory;
import org.netbeans.modules.html.editor.lib.api.model.HtmlTag;
import org.netbeans.modules.html.editor.lib.api.model.HtmlTagAttribute;
import org.netbeans.modules.html.editor.lib.api.model.HtmlTagType;
import static org.netbeans.modules.javascript2.jade.editor.JadeCompletionContext.ATTRIBUTE;
import static org.netbeans.modules.javascript2.jade.editor.JadeCompletionContext.TAG_AND_KEYWORD;
import org.netbeans.modules.javascript2.jade.editor.lexer.JadeTokenId;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;

/**
 *
 * @author Petr Pisl
 */
public class JadeCodeCompletion implements CodeCompletionHandler2 {

    private static final Logger LOGGER = Logger.getLogger(JadeCodeCompletion.class.getName());
    
    private boolean caseSensitive;
    
    protected final static String CSS_ID_PREFIX = "#";
    protected final static String CSS_CLASS_PREFIX = ".";
    
    @Override
    public CodeCompletionResult complete(CodeCompletionContext context) {
        ParserResult info = context.getParserResult();
        int carretOffset = context.getParserResult().getSnapshot().getEmbeddedOffset(context.getCaretOffset());
        String prefix = context.getPrefix();
        this.caseSensitive = context.isCaseSensitive();
        
        JadeCompletionContext jadeContext = JadeCompletionContext.findCompletionContext(info, carretOffset);
        final List<CompletionProposal> resultList = new ArrayList<>();
        JadeCompletionItem.CompletionRequest request = new JadeCompletionItem.CompletionRequest( info, 
                prefix == null ? carretOffset : carretOffset - prefix.length(), prefix);
        LOGGER.log(Level.FINEST, "Jade Completion Context {0}, prefix: {1}, anchorOffset: {2}", new Object[] {jadeContext, request.prefix, request.anchor}); //NOI18N
        switch (jadeContext) {
            case TAG_AND_KEYWORD :
                completeKewords(request, resultList);
            case TAG:
                completeTags(request, resultList);
                break;
            case ATTRIBUTE:
                completeAttributes(request, resultList);
                break;
            case CSS_ID:
                completeTagIds(request, resultList);
                break;
            case CSS_CLASS:
                completeCSSClasses(request, resultList);
                break;  
        }
        if (!resultList.isEmpty()) {
            return new DefaultCompletionResult(resultList, false);
        }
        return CodeCompletionResult.NONE;
    }

    @Override
    public String document(ParserResult info, ElementHandle element) {
        return null;
    }

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

    @Override
    public String getPrefix(ParserResult info, int caretOffset, boolean upToOffset) {
        String prefix = null;

        BaseDocument doc = (BaseDocument) info.getSnapshot().getSource().getDocument(false);
        if (doc == null) {
            return null;
        }

        //caretOffset = info.getSnapshot().getEmbeddedOffset(caretOffset);
        TokenSequence<? extends JadeTokenId> ts = info.getSnapshot().getTokenHierarchy().tokenSequence(JadeTokenId.jadeLanguage());
        if (ts == null) {
            return null;
        }

        int offset = info.getSnapshot().getEmbeddedOffset(caretOffset);
        ts.move(offset);

        if (!ts.moveNext() && !ts.movePrevious()) {
            return null;
        }
        
        if (ts.offset() == offset) {
            // We're looking at the offset to the RIGHT of the caret
            // and here I care about what's on the left
            ts.movePrevious();
        }

        Token<? extends JadeTokenId> token = ts.token();
        if (token.id() == JadeTokenId.TAG || token.id().isKeyword()) {
            prefix = token.text().toString();
            if (upToOffset) {
                if (offset - ts.offset() >= 0) {
                    prefix = prefix.substring(0, offset - ts.offset());
                }
            }
        }
        return prefix;
    }

    @Override
    public QueryType getAutoQuery(JTextComponent component, String typedText) {
        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 null;
    }

    @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 != null) {
            if (element instanceof JadeCompletionItem.SimpleElement) {
                return ((JadeCompletionItem.SimpleElement)element).getDocumentation();
            }
        }
        return null;
    }

    private void completeKewords(JadeCompletionItem.CompletionRequest request, List<CompletionProposal> resultList) {
        for (JadeTokenId id : JadeTokenId.values()) {
            if (id.isKeyword() && startsWith(id.getText(), request.prefix)) {
                resultList.add(new JadeCompletionItem.KeywordItem(id.getText(), request));
            }
        }
    }
    
    private void completeTags(JadeCompletionItem.CompletionRequest request, List<CompletionProposal> resultList) {
        Collection<HtmlTag> result;
        HtmlModel htmlModel = HtmlModelFactory.getModel(HtmlVersion.HTML5);
        Collection<HtmlTag> allTags = htmlModel.getAllTags();
        if (request.prefix == null || request.prefix.isEmpty()) {
            result = allTags;
        } else {
            result = new ArrayList<>();
            for (HtmlTag htmlTag : allTags) {
                if (startsWith(htmlTag.getName(), request.prefix)) {
                    result.add(htmlTag);
                }
            }
        }
        if (!result.isEmpty()) {
            for (HtmlTag tag : result) {
                resultList.add(JadeCompletionItem.create(request, tag));
            }
        }
    }
    
    private void completeAttributes(JadeCompletionItem.CompletionRequest request, List<CompletionProposal> resultList) {
        Collection<HtmlTagAttribute> resultAttrs;
        HtmlModel htmlModel = HtmlModelFactory.getModel(HtmlVersion.HTML5);
        String tagName = findTag(request);
        String prefix = request.prefix == null ? "" : request.prefix;
        HtmlTag htmlTag = tagName == null ? null : htmlModel.getTag(tagName);
        if (tagName != null && !tagName.isEmpty() && htmlTag != null) {
            if (prefix.isEmpty()) {
                if (tagName.isEmpty()) {
                    resultAttrs = getAllAttributes(htmlModel, prefix);
                } else {
                    resultAttrs = htmlTag.getAttributes();
                }
            } else {
                Collection<HtmlTagAttribute> attributes = htmlTag.getAttributes();
                if (tagName.isEmpty() || htmlTag.getTagClass() == HtmlTagType.UNKNOWN) {
                    attributes = getAllAttributes(htmlModel, prefix);
                }
                resultAttrs = new ArrayList<>();
                for (HtmlTagAttribute htmlTagAttribute : attributes) {
                    if(htmlTagAttribute.getName().startsWith(prefix)) {
                        resultAttrs.add(htmlTagAttribute);
                    }
                }
            }
        } else {
            resultAttrs = getAllAttributes(htmlModel, prefix);
        }
        for (HtmlTagAttribute attribute: resultAttrs) {
            resultList.add(JadeCompletionItem.create(request, attribute));
        }
    }
    
    private void completeTagIds(JadeCompletionItem.CompletionRequest request, List<CompletionProposal> resultList) {
        FileObject fo = request.parserResult.getSnapshot().getSource().getFileObject();
        if (fo == null) {
            return;
        }
        Project project = FileOwnerQuery.getOwner(fo);
        HashSet<String> unique = new HashSet<>();
        String prefix = request.prefix == null ? "" : request.prefix;
        try {
            CssIndex cssIndex = CssIndex.create(project);
            Map<FileObject, Collection<String>> findIdsByPrefix = cssIndex.findIdsByPrefix(prefix);

            for (Collection<String> ids : findIdsByPrefix.values()) {
                for (String id : ids) {
                    unique.add(id);
                }
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        for(String className: unique) {
            resultList.add(JadeCompletionItem.createCssItem(request, className, CSS_ID_PREFIX));
        }
    }
    
    private void completeCSSClasses(JadeCompletionItem.CompletionRequest request, List<CompletionProposal> resultList) {
        FileObject fo = request.parserResult.getSnapshot().getSource().getFileObject();
        if(fo == null) {
            return;
        }
        Project project = FileOwnerQuery.getOwner(fo);
        HashSet<String> unique = new HashSet<>();
        String prefix = request.prefix == null ? "" : request.prefix;
        try {
            CssIndex cssIndex = CssIndex.create(project);
            Map<FileObject, Collection<String>> findIdsByPrefix = cssIndex.findClassesByPrefix(prefix);

            for (Collection<String> ids : findIdsByPrefix.values()) {
                for (String id : ids) {
                    unique.add(id);
                }
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        
        for(String className: unique) {
            resultList.add(JadeCompletionItem.createCssItem(request, className, CSS_CLASS_PREFIX));
        }
    }
    
    private boolean startsWith(String theString, String prefix) {
        if (prefix == null || prefix.length() == 0) {
            return true;
        }

        return caseSensitive ? theString.startsWith(prefix)
                : theString.toLowerCase().startsWith(prefix.toLowerCase());
    }

    private String findTag(JadeCompletionItem.CompletionRequest request) {
        TokenHierarchy<?> th = request.parserResult.getSnapshot().getTokenHierarchy();
        if (th == null) {
            return null;
        }
        TokenSequence<JadeTokenId> ts = th.tokenSequence(JadeTokenId.jadeLanguage());
        if (ts == null) {
            return null;
        }
        
        ts.move(request.anchor);
        
        if (!ts.movePrevious()) {
            return null;
        }
        
        Token<JadeTokenId> token = LexerUtils.followsToken(ts, JadeTokenId.TAG, true, false);
        if (token == null) {
            return null;
        }
        
        JadeTokenId id = token.id();
        
        String tagName = null;
        if (id == JadeTokenId.TAG) {
            tagName = token.text().toString();
        }
        return tagName;
    }

    private Collection<HtmlTagAttribute> getAllAttributes(HtmlModel htmlModel, String prefix) {
        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()) && htmlTagAttribute.getName().startsWith(prefix)) {
                    result.put(htmlTagAttribute.getName(), htmlTagAttribute);
                }
            }
        }
        return result.values();
    }
}
