/*
 * 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.java.hints.declarative;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.lexer.Token;
import org.netbeans.spi.lexer.Lexer;
import org.netbeans.spi.lexer.LexerInput;
import org.netbeans.spi.lexer.LexerRestartInfo;
import org.netbeans.spi.lexer.TokenFactory;

/**
 *
 * @author Jan Lahoda
 */
class DeclarativeHintLexer implements Lexer<DeclarativeHintTokenId> {

    private final LexerInput input;
    private final TokenFactory<DeclarativeHintTokenId> fact;

    public DeclarativeHintLexer(LexerRestartInfo<DeclarativeHintTokenId> info) {
        input = info.input();
        fact  = info.tokenFactory();
    }

    @Override
    public Token<DeclarativeHintTokenId> nextToken() {
        int read = input.read();

        if (read == LexerInput.EOF) {
            return null;
        }

        int whitespaceLength = 0;
        
        if (Character.isWhitespace(read)) {
            while ((read != LexerInput.EOF) && Character.isWhitespace((char) read)) {
                read = input.read();
            }

            if (read == LexerInput.EOF) {
                return fact.createToken(DeclarativeHintTokenId.WHITESPACE);
            }

            whitespaceLength = input.readLength() - 1;
        }

        while (read != LexerInput.EOF) {
            Matcher variableMatcher = VARIABLE_RE.matcher(input.readText());

            if (variableMatcher.find()) {
                int start = variableMatcher.start();

                if (start == 0) {
                    Matcher m;

                    while ((read = input.read()) != LexerInput.EOF && (m = VARIABLE_RE.matcher(input.readText())).find()) {
                        if (m.end() < input.readLength())
                            break;
                    }

                    if (read != LexerInput.EOF) {
                        input.backup(1);
                    }

                    return fact.createToken(DeclarativeHintTokenId.VARIABLE);
                }

                if (whitespaceLength == start) {
                    input.backup(input.readLength() - whitespaceLength);
                    return fact.createToken(DeclarativeHintTokenId.WHITESPACE);
                }

                input.backup(input.readLength() - start);

                return fact.createToken(DeclarativeHintTokenId.JAVA_SNIPPET);
            }

            if (input.readLength() > 1) {
                String inputString = input.readText().toString();
                
                for (Entry<String, DeclarativeHintTokenId> e : BLOCK_TOKEN_START.entrySet()) {
                    if (!inputString.substring(0, inputString.length() - 1).endsWith(e.getKey()))
                        continue;

                    input.backup(1);
                    
                    Token<DeclarativeHintTokenId> preread = resolvePrereadText(e.getKey().length(), whitespaceLength);

                    if (preread != null) {
                        return preread;
                    }

                    return readBlockToken(e.getValue(), BLOCK_TOKEN_END.get(e.getValue()));
                }
                
                Token<DeclarativeHintTokenId> t = testToken(inputString, whitespaceLength, false);

                if (t == null && inputString.endsWith("::")) {
                    //need to disambiguate :: as a member ref separator; and as a snippet-conditions separator
                    int len = input.readLength();
                    if (len > 2) {
                        boolean colonColon = false;
                        StringBuilder ident = new StringBuilder();
                        boolean identFinished = false;
                        OUTER: while (true) {
                            int read2 = input.read();
                            if (Character.isWhitespace(read2)) {
                                if (ident.length() > 0) {
                                    identFinished = true;
                                }
                                continue;
                            }
                            if (identFinished || !Character.isJavaIdentifierPart((char) read2)) {
                                identFinished = true;
                                switch (read2) {
                                    case '(': case '&': case 'i': case '!':
                                        //condition
                                        colonColon = true;
                                        break OUTER;
                                    case ';':
                                        if (input.read() == ';') {
                                            input.backup(2);
                                            if ("otherwise".contentEquals(ident)) {
                                                //condition
                                                colonColon = true;
                                                break OUTER;
                                            }
                                        }
                                    default:
                                        //member ref
                                        break OUTER;
                                }
                            } else {
                                ident.append((char) read2);
                            }
                        }
                        if (colonColon) {
                            t = resolvePrereadText(input.readLength() - len + 2, whitespaceLength);
                        }
                    } else {
                        t = fact.createToken(DeclarativeHintTokenId.DOUBLE_COLON);
                    }
                }
                if (t != null) {
                    return t;
                }

            }

            read = input.read();
        }

        Token<DeclarativeHintTokenId> t = testToken(input.readText().toString(), whitespaceLength, true);

        if (t != null) {
            return t;
        }

        return fact.createToken(DeclarativeHintTokenId.JAVA_SNIPPET);
    }

    @Override
    public Object state() {
        return null;
    }

    @Override
    public void release() {}

    private Token<DeclarativeHintTokenId> testToken(String toTest, int whitespaceLength, boolean eof) {
        if (toTest.length() < 2 && !eof) return null;

        DeclarativeHintTokenId id = null;
        boolean exact = false;
        int backup = (-1);
        int add = eof ? 0 : 1;
        String snip = toTest.substring(0, toTest.length() - add);
        String lastImage = "";
        
        for (Entry<String, DeclarativeHintTokenId> e : TOKENS.entrySet()) {
            int i = snip.indexOf(e.getKey());
            if (i != (-1) && (snip.length() - i + 1 > backup || e.getKey().startsWith(lastImage)) && (e.getValue() != DeclarativeHintTokenId.COLON || !snip.substring(i).startsWith("::"))) {
                id = e.getValue();
                backup = snip.length() - i + 1;
                exact = i == 0;
                lastImage = e.getKey();
            }

            if (!eof) {
                for (int c = 1; c <= e.getKey().length(); c++) {
                    if (toTest.endsWith(e.getKey().substring(e.getKey().length() - c))) {
                        return null;
                    }
                }
            }
        }

        if (id == null) {
            return null;
        }

        if (exact) {
            if (input.readLength() - lastImage.length() + (eof ? 1 : 0) > 0)
                input.backup(input.readLength() - lastImage.length() + (eof ? 1 : 0));
            return fact.createToken(id);
        }
        
        Token<DeclarativeHintTokenId> t = resolvePrereadText(backup, whitespaceLength);

        if (t != null) {
            return t;
        } else {
            if (!eof)
                input.backup(1);
            
            return fact.createToken(id);
        }
    }

    private Token<DeclarativeHintTokenId> resolvePrereadText(int backupLength, int whitespaceLength) {
        if (whitespaceLength > 0) {
            if (input.readLengthEOF() == whitespaceLength + backupLength) {
                input.backup(input.readLengthEOF() - whitespaceLength);

                return fact.createToken(DeclarativeHintTokenId.WHITESPACE);
            } else {
                input.backup(backupLength);

                return fact.createToken(DeclarativeHintTokenId.JAVA_SNIPPET);
            }
        } else {
            if (input.readLengthEOF() == backupLength) {
                return null;
            } else {
                input.backup(backupLength);

                return fact.createToken(DeclarativeHintTokenId.JAVA_SNIPPET);
            }
        }
    }

    private Token<DeclarativeHintTokenId> readBlockToken(DeclarativeHintTokenId tokenId, String tokenEnd) {
        while (input.read() != LexerInput.EOF && !input.readText().toString().endsWith(tokenEnd))
            ;

        return fact.createToken(tokenId);
    }

    private static final Pattern DISPLAY_NAME_RE = Pattern.compile("'[^']*':");
    private static final Pattern VARIABLE_RE = Pattern.compile("\\$[A-Za-z0-9_$]+");

    private static final Map<String, DeclarativeHintTokenId> TOKENS;
    private static final Map<String, DeclarativeHintTokenId> BLOCK_TOKEN_START;
    private static final Map<DeclarativeHintTokenId, String> BLOCK_TOKEN_END;

    static {
        Map<String, DeclarativeHintTokenId> map = new HashMap<>();

        map.put("=>", DeclarativeHintTokenId.LEADS_TO);
        map.put("&&", DeclarativeHintTokenId.AND);
        map.put("!", DeclarativeHintTokenId.NOT);
        map.put(";;", DeclarativeHintTokenId.DOUBLE_SEMICOLON);
        map.put("%%", DeclarativeHintTokenId.DOUBLE_PERCENT);
        map.put("instanceof", DeclarativeHintTokenId.INSTANCEOF);
        map.put("otherwise", DeclarativeHintTokenId.OTHERWISE);
        map.put(":", DeclarativeHintTokenId.COLON);

        TOKENS = Collections.unmodifiableMap(map);

        Map<String, DeclarativeHintTokenId> blockStartMap = new HashMap<>();

        blockStartMap.put("/*", DeclarativeHintTokenId.BLOCK_COMMENT);
        blockStartMap.put("//", DeclarativeHintTokenId.LINE_COMMENT);
        blockStartMap.put("<?", DeclarativeHintTokenId.JAVA_BLOCK);
        blockStartMap.put("<!", DeclarativeHintTokenId.OPTIONS);
        blockStartMap.put("'", DeclarativeHintTokenId.CHAR_LITERAL);
        blockStartMap.put("\"", DeclarativeHintTokenId.STRING_LITERAL);

        BLOCK_TOKEN_START = Collections.unmodifiableMap(blockStartMap);

        Map<DeclarativeHintTokenId, String> blockEndMap = new HashMap<>();

        blockEndMap.put(DeclarativeHintTokenId.BLOCK_COMMENT, "*/");
        blockEndMap.put(DeclarativeHintTokenId.LINE_COMMENT, "\n");
        blockEndMap.put(DeclarativeHintTokenId.JAVA_BLOCK, "?>");
        blockEndMap.put(DeclarativeHintTokenId.OPTIONS, ">");
        blockEndMap.put(DeclarativeHintTokenId.CHAR_LITERAL, "'");
        blockEndMap.put(DeclarativeHintTokenId.STRING_LITERAL, "\"");

        BLOCK_TOKEN_END = Collections.unmodifiableMap(blockEndMap);
    }
}
