/*
 * 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.camel.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Helper for Camel OGNL (Object-Graph Navigation Language) expressions.
 */
public final class OgnlHelper {

    private static final Pattern INDEX_PATTERN = Pattern.compile("^(.*)\\[(.*)\\]$");

    private OgnlHelper() {
    }

    /**
     * Tests whether or not the given String is a Camel OGNL expression.
     * <p/>
     * An expression is considered an OGNL expression when it contains either one of the following chars: . or [
     *
     * @param  expression the String
     * @return            <tt>true</tt> if a Camel OGNL expression, otherwise <tt>false</tt>.
     */
    public static boolean isValidOgnlExpression(String expression) {
        if (expression == null || expression.isEmpty()) {
            return false;
        }

        // the brackets should come in a pair
        int bracketBegin = StringHelper.countChar(expression, '[');
        int bracketEnd = StringHelper.countChar(expression, ']');
        if (bracketBegin > 0 && bracketEnd > 0) {
            return bracketBegin == bracketEnd;
        }

        return expression.contains(".");
    }

    public static boolean isInvalidValidOgnlExpression(String expression) {
        if (expression == null) {
            return false;
        }

        if (expression.indexOf('.') == -1 && expression.indexOf('[') == -1 && expression.indexOf(']') == -1) {
            return false;
        }

        // the brackets should come in pair
        int bracketBegin = StringHelper.countChar(expression, '[');
        int bracketEnd = StringHelper.countChar(expression, ']');
        if (bracketBegin > 0 || bracketEnd > 0) {
            return bracketBegin != bracketEnd;
        }

        // check for double dots
        if (expression.contains("..")) {
            return true;
        }

        return false;
    }

    /**
     * Validates whether the method name is using valid java identifiers in the name Will throw
     * {@link IllegalArgumentException} if the method name is invalid.
     */
    public static void validateMethodName(String method) {
        for (int i = 0; i < method.length(); i++) {
            char ch = method.charAt(i);
            if (i == 0 && '.' == ch) {
                // its a dot before a method name
                continue;
            }
            if (ch == '(' || ch == '[' || ch == '.' || ch == '?') {
                // break when method name ends and sub method or arguments begin
                break;
            }
            if (i == 0 && !Character.isJavaIdentifierStart(ch)) {
                throw new IllegalArgumentException(
                        "Method name must start with a valid java identifier at position: 0 in method: " + method);
            } else if (!Character.isJavaIdentifierPart(ch)) {
                throw new IllegalArgumentException(
                        "Method name must be valid java identifier at position: " + i + " in method: " + method);
            }
        }
    }

    /**
     * Tests whether or not the given Camel OGNL expression is using the null safe operator or not.
     *
     * @param  ognlExpression the Camel OGNL expression
     * @return                <tt>true</tt> if the null safe operator is used, otherwise <tt>false</tt>.
     */
    public static boolean isNullSafeOperator(String ognlExpression) {
        return ognlExpression.startsWith("?");
    }

    /**
     * Removes any leading operators from the Camel OGNL expression.
     * <p/>
     * Will remove any leading of the following chars: ? or .
     *
     * @param  ognlExpression the Camel OGNL expression
     * @return                the Camel OGNL expression without any leading operators.
     */
    public static String removeLeadingOperators(String ognlExpression) {
        if (ognlExpression.startsWith("?")) {
            ognlExpression = ognlExpression.substring(1);
        }
        if (ognlExpression.startsWith(".")) {
            ognlExpression = ognlExpression.substring(1);
        }
        return ognlExpression;
    }

    /**
     * Removes any trailing operators from the Camel OGNL expression.
     *
     * @param  ognlExpression the Camel OGNL expression
     * @return                the Camel OGNL expression without any trailing operators.
     */
    public static String removeTrailingOperators(String ognlExpression) {
        int pos = ognlExpression.indexOf('[');
        if (pos != -1) {
            return ognlExpression.substring(0, pos);
        }
        return ognlExpression;
    }

    public static String removeOperators(String ognlExpression) {
        return removeLeadingOperators(removeTrailingOperators(ognlExpression));
    }

    public static KeyValueHolder<String, String> isOgnlIndex(String ognlExpression) {
        Matcher matcher = INDEX_PATTERN.matcher(ognlExpression);
        if (matcher.matches()) {

            // to avoid empty strings as we want key/value to be null in such cases
            String key = matcher.group(1);
            if (key != null && key.isEmpty()) {
                key = null;
            }

            // to avoid empty strings as we want key/value to be null in such cases
            String value = matcher.group(2);
            if (value != null && value.isEmpty()) {
                value = null;
            }

            return new KeyValueHolder<>(key, value);
        }

        return null;
    }

    /**
     * Regular expression with repeating groups is a pain to get right and then nobody understands the reg exp
     * afterwards. So we use a bit ugly/low-level Java code to split the OGNL into methods.
     *
     * @param  ognl                     the ognl expression
     * @return                          a list of methods, will return an empty list, if ognl expression has no methods
     * @throws IllegalArgumentException if the last method has a missing ending parenthesis
     */
    public static List<String> splitOgnl(String ognl) {
        // return an empty list if ognl is empty
        if (ognl == null || ognl.isEmpty() || ognl.trim().isEmpty()) {
            return Collections.EMPTY_LIST;
        }

        List<String> methods = new ArrayList<>(4);
        StringBuilder sb = new StringBuilder();

        int j = 0; // j is used as counter per method
        int squareBracketCnt = 0; // special to keep track if and how deep we are inside a square bracket block, eg: [foo]
        int parenthesisBracketCnt = 0; // special to keep track if and how deep we are inside a parenthesis block, eg: bar(${body}, ${header.foo})

        for (int i = 0; i < ognl.length(); i++) {
            char ch = ognl.charAt(i);
            // special for starting a new method
            if (j == 0 || j == 1 && ognl.charAt(i - 1) == '?'
                    || ch != '.' && ch != '?' && ch != ']') {
                sb.append(ch);
                // special if we are doing square bracket
                if (ch == '[' && parenthesisBracketCnt == 0) {
                    squareBracketCnt++;
                } else if (ch == '(') {
                    parenthesisBracketCnt++;
                } else if (ch == ')') {
                    parenthesisBracketCnt--;
                }
                j++; // advance
            } else {
                if (ch == '.' && squareBracketCnt == 0 && parenthesisBracketCnt == 0) {
                    // only treat dot as a method separator if not inside a square bracket block
                    // as dots can be used in key names when accessing maps

                    // a dit denotes end of this method and a new method is to be invoked
                    String s = sb.toString();

                    // reset sb
                    sb.setLength(0);

                    // pass over ? to the new method
                    if (s.endsWith("?")) {
                        sb.append("?");
                        s = s.substring(0, s.length() - 1);
                    }

                    // add the method
                    methods.add(s);

                    // reset j to begin a new method
                    j = 0;
                } else if (ch == ']' && parenthesisBracketCnt == 0) {
                    // append ending ] to method name
                    sb.append(ch);
                    String s = sb.toString();

                    // reset sb
                    sb.setLength(0);

                    // add the method
                    methods.add(s);

                    // reset j to begin a new method
                    j = 0;

                    // no more square bracket
                    squareBracketCnt--;
                }

                // and don't lose the char if its not an ] end marker (as we already added that)
                if (ch != ']' || parenthesisBracketCnt > 0) {
                    sb.append(ch);
                }

                // only advance if already begun on the new method
                if (j > 0) {
                    j++;
                }
            }
        }

        // add remainder in buffer when reached end of data
        if (sb.length() > 0) {
            methods.add(sb.toString());
        }

        String last = methods.isEmpty() ? null : methods.get(methods.size() - 1);
        if (parenthesisBracketCnt > 0 && last != null) {
            // there is an unclosed parenthesis bracket on the last method, so it should end with a parenthesis
            if (last.contains("(") && !last.endsWith(")")) {
                throw new IllegalArgumentException("Method should end with parenthesis, was " + last);
            }
        }

        return methods;
    }

    public static String methodAsDoubleQuotes(String ognl) {
        StringBuilder sb = new StringBuilder();

        int singleBracketCnt = 0;
        int doubleBracketCnt = 0;
        for (int i = 0; i < ognl.length(); i++) {
            char ch = ognl.charAt(i);
            char next = i < ognl.length() - 1 ? ognl.charAt(i + 1) : 0;

            if (ch == '\\' && next == '\'') {
                if (singleBracketCnt % 2 != 0) {
                    // its an escaped single quote inside an existing quote
                    // then unescape it
                    sb.append('\'');
                    // and skip over to next
                    i++;
                    continue;
                }
            }

            if (doubleBracketCnt % 2 == 0 && ch == '\'') {
                singleBracketCnt++;
                sb.append('"');
            } else if (singleBracketCnt % 2 == 0 && ch == '"') {
                doubleBracketCnt++;
                sb.append('"');
            } else {
                sb.append(ch);
            }
        }

        return sb.toString();
    }

}
