/*
 *  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.apache.groovy.parser.antlr4;

import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenStream;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ModifierNode;

import java.util.BitSet;
import java.util.regex.Pattern;

import static org.apache.groovy.parser.antlr4.GroovyParser.ASSIGN;
import static org.apache.groovy.parser.antlr4.GroovyParser.AT;
import static org.apache.groovy.parser.antlr4.GroovyParser.BuiltInPrimitiveType;
import static org.apache.groovy.parser.antlr4.GroovyParser.CapitalizedIdentifier;
import static org.apache.groovy.parser.antlr4.GroovyParser.DO;
import static org.apache.groovy.parser.antlr4.GroovyParser.DOT;
import static org.apache.groovy.parser.antlr4.GroovyParser.ExpressionContext;
import static org.apache.groovy.parser.antlr4.GroovyParser.FOR;
import static org.apache.groovy.parser.antlr4.GroovyParser.Identifier;
import static org.apache.groovy.parser.antlr4.GroovyParser.LBRACK;
import static org.apache.groovy.parser.antlr4.GroovyParser.LPAREN;
import static org.apache.groovy.parser.antlr4.GroovyParser.LT;
import static org.apache.groovy.parser.antlr4.GroovyParser.PostfixExprAltContext;
import static org.apache.groovy.parser.antlr4.GroovyParser.RPAREN;
import static org.apache.groovy.parser.antlr4.GroovyParser.StringLiteral;
import static org.apache.groovy.parser.antlr4.GroovyParser.WHILE;
import static org.apache.groovy.parser.antlr4.GroovyParser.YIELD;
import static org.apache.groovy.parser.antlr4.util.StringUtils.matches;

/**
 * Some semantic predicates for altering the behaviour of the lexer and parser
 */
public class SemanticPredicates {
    private static final Pattern NONSPACES_PATTERN = Pattern.compile("\\S+?");
    private static final Pattern LETTER_AND_LEFTCURLY_PATTERN = Pattern.compile("[a-zA-Z_{]");
    private static final Pattern NONSURROGATE_PATTERN = Pattern.compile("[^\u0000-\u007F\uD800-\uDBFF]");
    private static final Pattern SURROGATE_PAIR1_PATTERN = Pattern.compile("[\uD800-\uDBFF]");
    private static final Pattern SURROGATE_PAIR2_PATTERN = Pattern.compile("[\uDC00-\uDFFF]");
    private static final int PATH_EXPRESSION_ARGUMENTS = 2;
    private static final int PATH_EXPRESSION_CLOSURE_OR_LAMBDA = 3;

    /**
     * Check whether the next characters are only white spaces until the end of line or end of file.
     */
    public static boolean isFollowedByWhiteSpaces(CharStream cs) {
        for (int index = 1, c = cs.LA(index); !('\r' == c || '\n' == c || CharStream.EOF == c); index++, c = cs.LA(index)) {
            if (matches(String.valueOf((char) c), NONSPACES_PATTERN)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check whether the next character is one of the specified characters.
     */
    public static boolean isFollowedBy(CharStream cs, char... chars) {
        int c1 = cs.LA(1);

        for (char c : chars) {
            if (c1 == c) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check whether the next character is a valid Java identifier part or a left curly brace (for GString expressions).
     */
    public static boolean isFollowedByJavaLetterInGString(CharStream cs) {
        int c1 = cs.LA(1);

        if ('$' == c1) { // single $ is not a valid identifier
            return false;
        }

        String str1 = String.valueOf((char) c1);

        if (matches(str1, LETTER_AND_LEFTCURLY_PATTERN)) {
            return true;
        }

        if (matches(str1, NONSURROGATE_PATTERN)
                && Character.isJavaIdentifierPart(c1)) {
            return true;
        }

        int c2 = cs.LA(2);
        String str2 = String.valueOf((char) c2);

        if (matches(str1, SURROGATE_PAIR1_PATTERN)
                && matches(str2, SURROGATE_PAIR2_PATTERN)
                && Character.isJavaIdentifierPart(Character.toCodePoint((char) c1, (char) c2))) {

            return true;
        }

        return false;
    }

    /**
     * Check whether following a method name of command expression.
     * Method name should not end with "2: arguments" and "3: closure"
     *
     * @param context the preceding expression
     */
    public static boolean isFollowingArgumentsOrClosure(ExpressionContext context) {
        if (!(context instanceof PostfixExprAltContext ctx)) return false;

        try {
            int pathExpressionType = ctx.postfixExpression().pathExpression().t;
            return PATH_EXPRESSION_ARGUMENTS == pathExpressionType || PATH_EXPRESSION_CLOSURE_OR_LAMBDA == pathExpressionType;
        } catch (RuntimeException e) {
            throw new GroovyBugError("Unexpected structure of expression context: " + context, e);
        }
    }

    /**
     * Distinguish between method declaration and method call/constructor declaration
     */
    public static boolean isInvalidMethodDeclaration(TokenStream ts) {
        int tokenType = ts.LT(1).getType();

        return (Identifier == tokenType || CapitalizedIdentifier == tokenType || StringLiteral == tokenType || YIELD == tokenType)
                && LPAREN == (ts.LT(2).getType());
    }

    private static final BitSet MODIFIER_TYPES = new BitSet();
    static {
        for (Integer tokenType : ModifierNode.MODIFIER_OPCODE_MAP.keySet()) {
            if (tokenType != null && tokenType >= 0) {
                MODIFIER_TYPES.set(tokenType);
            }
        }
    }

    /**
     * O(1) modifier-token membership. Negative types (e.g. {@link Token#EOF}) are never
     * modifiers
     */
    private static boolean isModifierType(final int tokenType) {
        return tokenType >= 0 && MODIFIER_TYPES.get(tokenType);
    }

    /**
     * Distinguish between local variable declaration and method call, e.g. `a b`
     */
    public static boolean isInvalidLocalVariableDeclaration(TokenStream ts) {
        int index = 2;
        Token token;
        int tokenType;
        int tokenType2 = ts.LT(index).getType();
        int tokenType3;

        if (DOT == tokenType2) {
            int tokeTypeN;

            do {
                index = index + 2;
                tokeTypeN = ts.LT(index).getType();
            } while (DOT == tokeTypeN);

            if (LT == tokeTypeN || LBRACK == tokeTypeN) {
                return false;
            }

            index = index - 1;
            tokenType2 = ts.LT(index + 1).getType();
        } else {
            index = 1;
        }

        token = ts.LT(index);
        tokenType = token.getType();
        tokenType3 = ts.LT(index + 2).getType();
        int nextCodePoint = token.getText().codePointAt(0);

        return // VOID == tokenType ||
                !(BuiltInPrimitiveType == tokenType || isModifierType(tokenType))
                        && !Character.isUpperCase(nextCodePoint)
                        && nextCodePoint != '@'
                        && !(ASSIGN == tokenType3 || (LT == tokenType2 || LBRACK == tokenType2))
                || (nextCodePoint == '@' && isAnnotatedLoopStatement(ts));

    }

    /**
     * When the input starts with one or more annotations, scan past them and check whether
     * the first non-annotation token is a loop keyword ({@code for}, {@code while}, {@code do}).
     * If so, the construct is an annotated loop statement, NOT a local variable declaration.
     *
     * @param ts the token stream positioned at the first annotation {@code @} token
     * @return {@code true} if annotations are followed by a loop keyword
     */
    static boolean isAnnotatedLoopStatement(TokenStream ts) {
        int idx = 1; // ts.LT(1) is '@'
        while (ts.LT(idx).getType() == AT) {
            idx += 2; // skip AT and annotation name
            // skip qualifier parts of a fully-qualified annotation name, e.g. @java.lang.Deprecated
            while (ts.LT(idx).getType() == DOT) {
                idx += 2; // skip DOT and next name element
            }
            // skip annotation arguments (parenthesised), handling nesting
            if (ts.LT(idx).getType() == LPAREN) {
                idx++;
                int depth = 1;
                while (depth > 0 && ts.LT(idx).getType() != Token.EOF) {
                    int t = ts.LT(idx++).getType();
                    if (t == LPAREN) depth++;
                    else if (t == RPAREN) depth--;
                }
            }
        }
        int afterAnnotations = ts.LT(idx).getType();
        return afterAnnotations == FOR || afterAnnotations == WHILE || afterAnnotations == DO;
    }
}
