/*
 * 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.util.Arrays;
import java.util.List;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.jade.editor.lexer.JadeTokenId;

/**
 *
 * @author Petr Pisl
 */
public enum JadeCompletionContext {
    NONE, // There shouldn't be any code completion
    TAG,  // offer only html tags
    TAG_AND_KEYWORD, // tags and keywords
    ATTRIBUTE,    // html attributes
    CSS_ID, 
    CSS_CLASS;
    
      
    private static final List<Object[]> KEYWORD_POSITION = Arrays.asList(
        new Object[]{JadeTokenId.EOL},    
        new Object[]{JadeTokenId.EOL, JadeTokenId.WHITESPACE},
        new Object[]{JadeTokenId.EOL, JadeTokenId.TAG},
        new Object[]{JadeTokenId.EOL, JadeTokenId.WHITESPACE, JadeTokenId.TAG}
    );
    
    private static final List<Object[]> TAG_POSITION = Arrays.asList(
        new Object[]{JadeTokenId.TAG},
        new Object[]{JadeTokenId.TAG, JadeTokenId.OPERATOR_COLON, JadeTokenId.WHITESPACE}    
//        new Object[]{JadeTokenId.BRACKET_LEFT_PAREN},
//        new Object[]{JadeTokenId.EOL, JadeTokenId.EOL},
//        new Object[]{JadeTokenId.EOL, JadeTokenId.TAG},
//        new Object[]{JadeTokenId.EOL, JadeTokenId.WHITESPACE},    
//        new Object[]{JadeTokenId.EOL, JadeTokenId.WHITESPACE, JadeTokenId.TAG},    
//        new Object[]{JadeTokenId.TAG, JadeTokenId.OPERATOR_COLON},    
//        new Object[]{JadeTokenId.TAG, JadeTokenId.OPERATOR_COLON},
//        new Object[]{JadeTokenId.TAG, JadeTokenId.WHITESPACE},
//        new Object[]{JadeTokenId.TAG, JadeTokenId.EOL},
//        new Object[]{JadeTokenId.TAG, JadeTokenId.OPERATOR_COLON, JadeTokenId.TAG},
//        
//        new Object[]{JadeTokenId.TAG, JadeTokenId.OPERATOR_COLON, JadeTokenId.WHITESPACE, JadeTokenId.TAG}
    );
    
    private static final List<JadeTokenId> WHITESPACES = Arrays.asList(JadeTokenId.WHITESPACE, JadeTokenId.EOL);
    
    private static final List<Object[]> ATTRIBUTE_POSITION = Arrays.asList(
        new Object[]{JadeTokenId.ATTRIBUTE},
        new Object[]{JadeTokenId.BRACKET_LEFT_PAREN},
        new Object[]{JadeTokenId.BRACKET_LEFT_PAREN, JadeTokenId.EOL},
        new Object[]{JadeTokenId.BRACKET_LEFT_PAREN, JadeTokenId.WHITESPACE}, 
        new Object[]{JadeTokenId.BRACKET_LEFT_PAREN, JadeTokenId.EOL, JadeTokenId.WHITESPACE},
        new Object[]{JadeTokenId.JAVASCRIPT, JadeTokenId.OPERATOR_COMMA},
        new Object[]{JadeTokenId.JAVASCRIPT, JadeTokenId.OPERATOR_COMMA, JadeTokenId.WHITESPACE},
        new Object[]{JadeTokenId.ATTRIBUTE, JadeTokenId.OPERATOR_ASSIGNMENT, JadeTokenId.JAVASCRIPT, JadeTokenId.WHITESPACE},
        new Object[]{JadeTokenId.ATTRIBUTE, JadeTokenId.OPERATOR_ASSIGNMENT, JadeTokenId.JAVASCRIPT}
    );
    
    private static final List<Object[]> CSS_ID_POSITION = Arrays.asList(
        new Object[]{JadeTokenId.CSS_ID},
        new Object[]{JadeTokenId.TAG, JadeTokenId.TEXT},
        new Object[]{JadeTokenId.CSS_CLASS, JadeTokenId.TEXT}
    );
    
    private static final List<Object[]> CSS_CLASS_POSITION = Arrays.asList(
        new Object[]{JadeTokenId.CSS_CLASS},
        new Object[]{JadeTokenId.TAG, JadeTokenId.PLAIN_TEXT_DELIMITER},
        new Object[]{JadeTokenId.CSS_ID, JadeTokenId.PLAIN_TEXT_DELIMITER},
        new Object[]{JadeTokenId.CSS_CLASS, JadeTokenId.PLAIN_TEXT_DELIMITER}
    );
    
    @NonNull
    public static JadeCompletionContext findCompletionContext(ParserResult info, int offset){
        TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy();
        boolean isEOF = false;
        if (th == null) {
            return NONE;
        }
        TokenSequence<JadeTokenId> ts = th.tokenSequence(JadeTokenId.jadeLanguage());
        if (ts == null) {
            return NONE;
        }
        
        ts.move(offset);
        
        if (!ts.movePrevious()) {
            return TAG_AND_KEYWORD;
        } else if (ts.token().id() == JadeTokenId.TAG) {
            // just check, whether we are on the first tag in the file. see issue #251160
            if (!ts.movePrevious()) {
                return TAG_AND_KEYWORD;
            }
            ts.moveNext();
        }
        if (!ts.moveNext()) {
            if (ts.token() != null && ts.token().id() == JadeTokenId.EOL) {
                while ((ts.token().id() == JadeTokenId.WHITESPACE || ts.token().id() == JadeTokenId.EOL) && ts.movePrevious()) {
                }
                if (acceptTokenChains(ts, ATTRIBUTE_POSITION, false)) {
                    return ATTRIBUTE;
                }
                return TAG_AND_KEYWORD;
            }
            isEOF = true;
        }
        
        Token<JadeTokenId> token = ts.token();
        JadeTokenId id = token.id();
        String text = null;
        switch (id) {
            case ATTRIBUTE: return ATTRIBUTE;
            case TAG: 
                if (acceptTokenChains(ts, KEYWORD_POSITION, false)) {
                    return TAG_AND_KEYWORD;
                }
                return TAG;
            case CSS_ID: return CSS_ID;
            case CSS_CLASS: return CSS_CLASS;
            case TEXT: 
                text = token.text().toString();
                if (JadeCodeCompletion.CSS_ID_PREFIX.equals(text) && (acceptTokenChains(ts, TAG_POSITION, true) || isEOF)) {
                    return CSS_ID;
                }
                break;
            case PLAIN_TEXT_DELIMITER:
                if (acceptTokenChains(ts, TAG_POSITION, true)) {
                    return CSS_CLASS;
                }
                if (isEOF) {
                    return CSS_CLASS;
                }
                break;
            case COMMENT:
                String commentText = token.text().toString();
                int index = offset -  ts.offset() - 1;
                int spaces = 0;
                if (index > -1 && index < commentText.length()) {
                    
                    while (index > -1 && (commentText.charAt(index) == ' ' || commentText.charAt(index) == '\t')) {
                        spaces++;
                        index--;
                    }
                    if (index > -1) {
                        char ch = commentText.charAt(index);
                        if (ch == '\n') {
                            if (spaces == 0) {
                                return TAG_AND_KEYWORD;
                            } else {
                                if (ts.movePrevious() && ts.token().id() == JadeTokenId.COMMENT_DELIMITER && ts.movePrevious()) {
                                    
                                    token = ts.token();
                                    id = token.id();
                                    if (id == JadeTokenId.WHITESPACE && token.length() >= spaces) {
                                        return TAG_AND_KEYWORD;
                                    }
                                }
                            }
                        }
                    }
                }
                return NONE;
        }
        if (id.isKeyword()) {
            return TAG_AND_KEYWORD;
        }
        
        int helpIndex = ts.index();
        while ((id == JadeTokenId.WHITESPACE || id == JadeTokenId.EOL) && ts.movePrevious()) {
            id = ts.token().id();
        }
        if (acceptTokenChains(ts, ATTRIBUTE_POSITION, helpIndex == ts.index())) {
            return ATTRIBUTE;
        }
        if (helpIndex != ts.index()) {
            ts.moveIndex(helpIndex);
            ts.moveNext();
            id = ts.token().id();
        }
        
        if (acceptTokenChains(ts, KEYWORD_POSITION, !isEOF)) {
            return TAG_AND_KEYWORD;
        }
        
        // check tag: ^ position
        if (acceptTokenChains(ts, TAG_POSITION, true)) {
            return TAG;
        }
        
        if (acceptTokenChains(ts, CSS_CLASS_POSITION, true)) {
            return CSS_CLASS;
        }
        if (acceptTokenChains(ts, CSS_ID_POSITION, true)) {
            return CSS_ID;
        }

        boolean isBeginOfLine = false;
        if (ts.movePrevious()) {
            token = ts.token();
            id = token.id();
            if (id == JadeTokenId.WHITESPACE && ts.movePrevious()) {
                token = ts.token();
                id = token.id();
            }
            switch (id) {
                case EOL:
                    isBeginOfLine = true;
                    if (text != null) {
                        if (JadeCodeCompletion.CSS_CLASS_PREFIX.equals(text)) {
                            return CSS_CLASS;
                        }
                        if (JadeCodeCompletion.CSS_ID_PREFIX.equals(text)) {
                            return CSS_ID;
                        }
                    }
                    break;
                case COMMENT: 
                    return TAG_AND_KEYWORD;
                case TEXT:
                    text = token.text().toString();
                    if (JadeCodeCompletion.CSS_ID_PREFIX.equals(text) && acceptTokenChains(ts, KEYWORD_POSITION, true)) {
                        return CSS_ID;
                    }
                    break;
                case PLAIN_TEXT_DELIMITER:
                    if (acceptTokenChains(ts, KEYWORD_POSITION, true)) {
                        return CSS_CLASS;
                    }
                    break;
            }
            
            while (ts.movePrevious()) {
                token = ts.token();
                id = token.id(); 
                if (id == JadeTokenId.TAG) {
                    if (isBeginOfLine) {
                        return TAG_AND_KEYWORD;
                    } else {
                        return NONE;
                    }
                }
                if (id == JadeTokenId.ATTRIBUTE || id == JadeTokenId.BRACKET_LEFT_PAREN) {
                    return ATTRIBUTE;
                }
                
                if (id != JadeTokenId.EOL && id != JadeTokenId.WHITESPACE && id != JadeTokenId.JAVASCRIPT) {
                    if (isBeginOfLine) {
                        return TAG_AND_KEYWORD;
                    }
                    return NONE;
                }
            }  
        }         
        return NONE;
    }
    
    private static boolean acceptTokenChains(TokenSequence tokenSequence, List<Object[]> tokenIdChains, boolean movePrevious) {
        for (Object[] tokenIDChain : tokenIdChains){
            if (acceptTokenChain(tokenSequence, tokenIDChain, movePrevious)){
                return true;
            }
        }

        return false;
    }
    
    private static boolean acceptTokenChain(TokenSequence tokenSequence, Object[] tokenIdChain, boolean movePrevious) {
        int orgTokenSequencePos = tokenSequence.offset();
        boolean accept = true;
        boolean moreTokens = movePrevious ? tokenSequence.movePrevious() : true;

        for (int i = tokenIdChain.length - 1; i >= 0; i --){
            Object tokenID = tokenIdChain[i];

            if (!moreTokens){
                accept = false;
                break;
            }

           if (tokenID instanceof JadeTokenId) {
                if (tokenSequence.token().id() == tokenID){
                    moreTokens = tokenSequence.movePrevious();
                } else {
                    // NO MATCH
                    accept = false;
                    break;
                }
            } else {
                assert false : "Unsupported token type: " + tokenID.getClass().getName();
            }
        }

        tokenSequence.move(orgTokenSequencePos);
        tokenSequence.moveNext();
       return accept;
    }
}
