/*
 * Copyright 2017 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.jbpm.designer.expressioneditor.parser;

import java.text.MessageFormat;
import java.text.ParseException;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jbpm.designer.expressioneditor.model.Condition;
import org.jbpm.designer.expressioneditor.model.ConditionExpression;

public class ExpressionParser {

    private static final String VARIABLE_NAME_PARAM_REGEX = "[$_a-zA-Z][$_a-zA-Z0-9]*";

    public static final String KIE_FUNCTIONS = "KieFunctions.";

    private static Map<String, FunctionDef> functionsRegistry = new TreeMap<String, FunctionDef>();

    private int parseIndex = 0;

    private String expression;

    private String functionName = null;

    public static final String FUNCTION_NAME_NOT_RECOGNIZED_ERROR = "The function name \"{0}\" is not recognized by system.";

    public static final String FUNCTION_CALL_NOT_FOUND_ERROR = "Function call was not found, a token like \"" + KIE_FUNCTIONS + "functionName(variable, params)\" is expected.";

    public static final String VALID_FUNCTION_CALL_NOT_FOUND_ERROR = "The \"" + KIE_FUNCTIONS + "\" keyword must be followed by one of the following function names: \"{0}\"";

    public static final String FUNCTION_CALL_NOT_CLOSED_PROPERLY_ERROR = "Function call \"{0}\" is not closed properly, character \")\" is expected.";

    public static final String SENTENCE_NOT_CLOSED_PROPERLY_ERROR = "Script not closed properly, character \";\" is expected.";

    public static final String VARIABLE_NAME_EXPECTED_ERROR = "Variable name not found, a valid process variable name is expected.";

    public static final String PARAMETER_DELIMITER_EXPECTED_ERROR = "Parameter delimiter \",\" is expected.";

    public static final String STRING_PARAMETER_EXPECTED_ERROR = "String parameter value like \"some value\" is expected.";

    public static final String RETURN_SENTENCE_EXPECTED_ERROR = "Sentence \"{0}\" is expected.";

    public static final String BLANK_AFTER_RETURN_EXPECTED_ERROR = "Sentence \"{0}\" must be followed by a blank space or a line break.";

    private static String functionNames = null;

    static {

        //Operators for all types:

        FunctionDef isNull = new FunctionDef(Condition.IS_NULL);
        isNull.addParam("param1",
                        Object.class);
        functionsRegistry.put(isNull.getName(),
                              isNull);

        //Global operators:

        FunctionDef equalsTo = new FunctionDef(Condition.EQUALS_TO);
        equalsTo.addParam("param1",
                          Object.class);
        equalsTo.addParam("param2",
                          String.class);
        functionsRegistry.put(equalsTo.getName(),
                              equalsTo);

        //Operators for String type:

        FunctionDef isEmpty = new FunctionDef(Condition.IS_EMPTY);
        isEmpty.addParam("param1",
                         Object.class);
        functionsRegistry.put(isEmpty.getName(),
                              isEmpty);

        FunctionDef contains = new FunctionDef(Condition.CONTAINS);
        contains.addParam("param1",
                          Object.class);
        contains.addParam("param2",
                          String.class);
        functionsRegistry.put(contains.getName(),
                              contains);

        FunctionDef startsWith = new FunctionDef(Condition.STARTS_WITH);
        startsWith.addParam("param1",
                            Object.class);
        startsWith.addParam("param2",
                            String.class);
        functionsRegistry.put(startsWith.getName(),
                              startsWith);

        FunctionDef endsWith = new FunctionDef(Condition.ENDS_WITH);
        endsWith.addParam("param1",
                          Object.class);
        endsWith.addParam("param2",
                          String.class);
        functionsRegistry.put(endsWith.getName(),
                              endsWith);

        // Operators for Numeric types:

        FunctionDef greaterThan = new FunctionDef(Condition.GREATER_THAN);
        greaterThan.addParam("param1",
                             Object.class);
        greaterThan.addParam("param2",
                             String.class);
        functionsRegistry.put(greaterThan.getName(),
                              greaterThan);

        FunctionDef greaterOrEqualThan = new FunctionDef(Condition.GREATER_OR_EQUAL_THAN);
        greaterOrEqualThan.addParam("param1",
                                    Object.class);
        greaterOrEqualThan.addParam("param2",
                                    String.class);
        functionsRegistry.put(greaterOrEqualThan.getName(),
                              greaterOrEqualThan);

        FunctionDef lessThan = new FunctionDef(Condition.LESS_THAN);
        lessThan.addParam("param1",
                          Object.class);
        lessThan.addParam("param2",
                          String.class);
        functionsRegistry.put(lessThan.getName(),
                              lessThan);

        FunctionDef lessOrEqualThan = new FunctionDef(Condition.LESS_OR_EQUAL_THAN);
        lessOrEqualThan.addParam("param1",
                                 Object.class);
        lessOrEqualThan.addParam("param2",
                                 String.class);
        functionsRegistry.put(lessOrEqualThan.getName(),
                              lessOrEqualThan);

        FunctionDef between = new FunctionDef(Condition.BETWEEN);
        between.addParam("param1",
                         Object.class);
        between.addParam("param2",
                         String.class);
        between.addParam("param3",
                         String.class);
        functionsRegistry.put(between.getName(),
                              between);

        // Operators for Boolean type:

        FunctionDef isTrue = new FunctionDef(Condition.IS_TRUE);
        isTrue.addParam("param1",
                        Object.class);
        functionsRegistry.put(isTrue.getName(),
                              isTrue);

        FunctionDef isFalse = new FunctionDef(Condition.IS_FALSE);
        isFalse.addParam("param1",
                         Object.class);
        functionsRegistry.put(isFalse.getName(),
                              isFalse);

        StringBuilder functionNamesBuilder = new StringBuilder();
        functionNamesBuilder.append("{");
        boolean first = true;
        for (String functionName : functionsRegistry.keySet()) {
            if (!first) {
                functionNamesBuilder.append(", ");
            }
            functionNamesBuilder.append(functionName);
            first = false;
        }
        functionNamesBuilder.append("}");
        functionNames = functionNamesBuilder.toString();
    }

    public ExpressionParser(String expression) {
        this.expression = expression;
        this.parseIndex = expression != null ? 0 : -1;
    }

    public ConditionExpression parse() throws ParseException {

        ConditionExpression conditionExpression = new ConditionExpression();
        Condition condition = null;
        FunctionDef functionDef = null;

        parseReturnSentence();

        functionName = parseFunctionName();
        functionName = functionName.substring(KIE_FUNCTIONS.length(),
                                              functionName.length());
        functionDef = functionsRegistry.get(functionName);

        if (functionDef == null) {
            throw new ParseException(errorMessage(FUNCTION_NAME_NOT_RECOGNIZED_ERROR,
                                                  functionName),
                                     parseIndex);
        }

        conditionExpression.setOperator(ConditionExpression.AND_OPERATOR);
        condition = new Condition(functionName);
        conditionExpression.getConditions().add(condition);

        String param = null;
        boolean first = true;

        for (ParamDef paramDef : functionDef.getParams()) {
            if (first) {
                first = false;
            } else {
                parseParamDelimiter();
            }

            if (Object.class.getName().equals(paramDef.getType().getName())) {
                param = parseVariableName();
            } else {
                param = parseStringParameter();
            }
            condition.addParam(param);
        }

        //all parameters were consumed
        parseFunctionClose();
        parseSentenceClose();

        return conditionExpression;
    }

    private String parseReturnSentence() throws ParseException {

        int index = nextNonBlank();
        if (index < 0) {
            throw new ParseException(errorMessage(RETURN_SENTENCE_EXPECTED_ERROR,
                                                  "return"),
                                     parseIndex);
        }

        if (!expression.startsWith("return",
                                   index)) {
            //the expression does not start with return.
            throw new ParseException(errorMessage(RETURN_SENTENCE_EXPECTED_ERROR,
                                                  "return"),
                                     parseIndex);
        }

        parseIndex = index + "return".length();

        //next character after return must be a \n or a " "
        if (!isBlank(expression.charAt(parseIndex))) {
            throw new ParseException(errorMessage(BLANK_AFTER_RETURN_EXPECTED_ERROR,
                                                  "return"),
                                     parseIndex);
        }

        return "return";
    }

    private String parseFunctionName() throws ParseException {

        int index = nextNonBlank();
        if (index < 0) {
            throw new ParseException(errorMessage(FUNCTION_CALL_NOT_FOUND_ERROR),
                                     parseIndex);
        }
        String functionName = null;

        if (!expression.startsWith(KIE_FUNCTIONS,
                                   index)) {
            throw new ParseException(errorMessage(FUNCTION_CALL_NOT_FOUND_ERROR),
                                     parseIndex);
        }

        for (FunctionDef functionDef : functionsRegistry.values()) {
            if (expression.startsWith(KIE_FUNCTIONS + functionDef.getName() + "(",
                                      index)) {
                functionName = KIE_FUNCTIONS + functionDef.getName();
                break;
            }
        }

        if (functionName == null) {
            throw new ParseException(errorMessage(VALID_FUNCTION_CALL_NOT_FOUND_ERROR,
                                                  functionNames()),
                                     parseIndex);
        }

        parseIndex = index + functionName.length() + 1;

        return functionName;
    }

    private String parseFunctionClose() throws ParseException {
        int index = nextNonBlank();
        if (index < 0) {
            throw new ParseException(errorMessage(FUNCTION_CALL_NOT_CLOSED_PROPERLY_ERROR,
                                                  functionName),
                                     parseIndex);
        }

        if (expression.charAt(index) != ')') {
            throw new ParseException(errorMessage(FUNCTION_CALL_NOT_CLOSED_PROPERLY_ERROR,
                                                  functionName),
                                     parseIndex);
        }

        parseIndex = index + 1;
        return ")";
    }

    private String parseSentenceClose() throws ParseException {
        int index = nextNonBlank();
        if (index < 0) {
            throw new ParseException(errorMessage(SENTENCE_NOT_CLOSED_PROPERLY_ERROR),
                                     parseIndex);
        }

        if (expression.charAt(index) != ';') {
            throw new ParseException(errorMessage(SENTENCE_NOT_CLOSED_PROPERLY_ERROR),
                                     parseIndex);
        }

        parseIndex = index + 1;
        while (parseIndex < expression.length()) {
            if (!isBlank(expression.charAt(parseIndex))) {
                throw new ParseException(errorMessage(SENTENCE_NOT_CLOSED_PROPERLY_ERROR),
                                         parseIndex);
            }
            parseIndex++;
        }

        return ";";
    }

    private String parseVariableName() throws ParseException {
        int index = nextNonBlank();
        if (index < 0) {
            throw new ParseException(errorMessage(VARIABLE_NAME_EXPECTED_ERROR),
                                     parseIndex);
        }

        Pattern variableNameParam = Pattern.compile(VARIABLE_NAME_PARAM_REGEX);
        Matcher variableMatcher = variableNameParam.matcher(expression.substring(index,
                                                                                 expression.length()));

        if (!Pattern.matches(VARIABLE_NAME_PARAM_REGEX,
                             String.valueOf(expression.charAt(index)))) {
            throw new ParseException(errorMessage(VARIABLE_NAME_EXPECTED_ERROR),
                                     parseIndex);
        }

        String variableName = null;
        if (variableMatcher.find()) {
            variableName = variableMatcher.group();
        } else {
            throw new ParseException(errorMessage(VARIABLE_NAME_EXPECTED_ERROR),
                                     parseIndex);
        }

        parseIndex = index + variableName.length();

        return variableName;
    }

    private String parseParamDelimiter() throws ParseException {
        int index = nextNonBlank();
        if (index < 0) {
            throw new ParseException(errorMessage(PARAMETER_DELIMITER_EXPECTED_ERROR),
                                     parseIndex);
        }

        if (expression.charAt(index) != ',') {
            throw new ParseException(errorMessage(PARAMETER_DELIMITER_EXPECTED_ERROR),
                                     parseIndex);
        }

        parseIndex = index + 1;
        return ",";
    }

    private String parseStringParameter() throws ParseException {
        int index = nextNonBlank();
        if (index < 0) {
            throw new ParseException(STRING_PARAMETER_EXPECTED_ERROR,
                                     parseIndex);
        }

        if (expression.charAt(index) != '"') {
            throw new ParseException(STRING_PARAMETER_EXPECTED_ERROR,
                                     parseIndex);
        }

        int shift = 1;
        Character scapeChar = Character.valueOf('\\');
        Character last = null;
        boolean strReaded = false;
        StringBuilder param = new StringBuilder();
        for (int i = index + 1; i < expression.length(); i++) {
            if (expression.charAt(i) == '\\') {
                if (scapeChar.equals(last)) {
                    shift += 2;
                    param.append('\\');
                    last = null;
                } else {
                    last = expression.charAt(i);
                }
            } else if (expression.charAt(i) == '"') {
                if (scapeChar.equals(last)) {
                    shift += 2;
                    param.append('"');
                    last = null;
                } else {
                    shift++;
                    strReaded = true;
                    break;
                }
            } else if (expression.charAt(i) == 'n') {
                if (scapeChar.equals(last)) {
                    shift += 2;
                    param.append('\n');
                } else {
                    shift += 1;
                    param.append(expression.charAt(i));
                }
                last = null;
            } else {
                if (last != null) {
                    shift++;
                    param.append(last);
                }
                last = null;
                shift++;
                param.append(expression.charAt(i));
            }
        }

        if (!strReaded) {
            throw new ParseException(STRING_PARAMETER_EXPECTED_ERROR,
                                     parseIndex);
        }

        parseIndex = index + shift;
        return param.toString();
    }

    private int nextNonBlank() {
        if (parseIndex < 0) {
            return -1;
        }

        for (int i = parseIndex; i < expression.length(); i++) {
            if (!isBlank(expression.charAt(i))) {
                return i;
            }
        }

        return -1;
    }

    private int nextBlank() {
        if (parseIndex < 0) {
            return -1;
        }

        for (int i = parseIndex; i < expression.length(); i++) {
            if (isBlank(expression.charAt(i))) {
                return i;
            }
        }

        return -1;
    }

    private boolean isBlank(Character character) {
        return character != null && (character.equals('\n') || character.equals(' '));
    }

    private String errorMessage(String message,
                                Object... params) {
        return MessageFormat.format(message,
                                    params);
    }

    private String functionNames() {
        return functionNames;
    }
}