/*
 * 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 jakarta.el;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;

/**
 * Provides a simple API for evaluating EL expressions in a standalone environment. This class wraps an
 * {@link ELManager} and provides methods for evaluating expressions, defining beans and functions, and managing
 * variables. It is the primary entry point for applications that use the EL API outside of a Jakarta EE container.
 *
 * @since EL 3.0
 */
public class ELProcessor {

    private static final Set<String> PRIMITIVES = new HashSet<>();
    static {
        PRIMITIVES.add("boolean");
        PRIMITIVES.add("byte");
        PRIMITIVES.add("char");
        PRIMITIVES.add("double");
        PRIMITIVES.add("float");
        PRIMITIVES.add("int");
        PRIMITIVES.add("long");
        PRIMITIVES.add("short");
    }

    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    private final ELManager manager = new ELManager();
    private final ELContext context = manager.getELContext();
    private final ExpressionFactory factory = ELManager.getExpressionFactory();

    /**
     * Constructs an ELProcessor with a default {@link ELManager} and {@link StandardELContext}.
     * The processor is ready to evaluate expressions immediately after construction.
     */
    public ELProcessor() {
    }

    /**
     * Returns the {@link ELManager} that manages the EL context, resolvers, functions, and
     * variables for this processor.
     *
     * @return the ELManager instance
     */
    public ELManager getELManager() {
        return manager;
    }

    /**
     * Evaluates the given EL expression and returns the result as the requested type.
     * The expression is automatically wrapped in {@literal ${}} delimiters before evaluation.
     *
     * @param <T>        the expected result type
     * @param expression the EL expression to evaluate
     *
     * @return the result of evaluating the expression, coerced to the requested type
     *
     * @throws ELException if the expression cannot be evaluated
     */
    public <T> T eval(String expression) {
        @SuppressWarnings("unchecked")
        T result = (T) getValue(expression, Object.class);
        return result;
    }

    /**
     * Evaluates the given EL expression and returns the result coerced to the expected type.
     * The expression is automatically wrapped in {@literal ${}} delimiters before evaluation.
     *
     * @param <T>            the expected result type
     * @param expression     the EL expression to evaluate
     * @param expectedType   the class of the expected result type
     *
     * @return the result of evaluating the expression, coerced to the expected type
     *
     * @throws ELException if the expression cannot be evaluated
     */
    public <T> T getValue(String expression, Class<T> expectedType) {
        ValueExpression ve = factory.createValueExpression(context, bracket(expression), expectedType);
        return ve.getValue(context);
    }

    /**
     * Evaluates the given EL expression and sets the result to the specified value.
     * This is used for EL assignment expressions. The expression is automatically wrapped
     * in {@literal ${}} delimiters before evaluation.
     *
     * @param expression the EL expression to evaluate
     * @param value      the value to assign
     *
     * @throws ELException if the expression cannot be evaluated or the target is not writable
     */
    public void setValue(String expression, Object value) {
        ValueExpression ve = factory.createValueExpression(context, bracket(expression), Object.class);
        ve.setValue(context, value);
    }

    /**
     * Registers a variable in the EL context. The variable is associated with the result
     * of evaluating the given expression. Passing a {@code null} expression removes the
     * variable from the context.
     *
     * @param variable   the variable name
     * @param expression the EL expression to associate with the variable, or {@code null} to remove
     *
     * @throws ELException if the expression cannot be evaluated
     */
    public void setVariable(String variable, String expression) {
        if (expression == null) {
            manager.setVariable(variable, null);
        } else {
            ValueExpression ve = factory.createValueExpression(context, bracket(expression), Object.class);
            manager.setVariable(variable, ve);
        }
    }

    /**
     * Registers a function in the EL context by looking up the static method in the given class.
     * The method name may include a return type and parameter signature in the format
     * {@code returnType methodName(paramTypes)}. The function is then callable in EL expressions
     * as {@code prefix:function(args)}.
     *
     * @param prefix     the namespace prefix for the function
     * @param function   the local function name, or empty to use the method name
     * @param className  the fully qualified name of the class containing the static method
     * @param methodName the method name, optionally with return type and parameter signature
     *
     * @throws ClassNotFoundException  if the class cannot be found or is not public
     * @throws NoSuchMethodException   if the method cannot be found or is not a public static method
     */
    public void defineFunction(String prefix, String function, String className, String methodName)
            throws ClassNotFoundException, NoSuchMethodException {

        if (prefix == null || function == null || className == null || methodName == null) {
            throw new NullPointerException(Util.message(context, "elProcessor.defineFunctionNullParams"));
        }

        // Check the imports
        Class<?> clazz = context.getImportHandler().resolveClass(className);

        if (clazz == null) {
            clazz = Class.forName(className, true, Util.getContextClassLoader());
        }

        if (!Modifier.isPublic(clazz.getModifiers())) {
            throw new ClassNotFoundException(
                    Util.message(context, "elProcessor.defineFunctionInvalidClass", className));
        }

        MethodSignature sig = new MethodSignature(context, methodName, className);

        if (function.length() == 0) {
            function = sig.getName();
        }

        // Only returns public methods. Module access is checked below.
        Method methods[] = clazz.getMethods();

        for (Method method : methods) {
            if (!Modifier.isStatic(method.getModifiers())) {
                continue;
            }
            if (!Util.canAccess(null, method)) {
                continue;
            }
            if (method.getName().equals(sig.getName())) {
                if (sig.getParamTypeNames() == null) {
                    // Only a name provided, no signature so map the first
                    // method declared
                    manager.mapFunction(prefix, function, method);
                    return;
                }
                if (sig.getParamTypeNames().length != method.getParameterTypes().length) {
                    continue;
                }
                if (sig.getParamTypeNames().length == 0) {
                    manager.mapFunction(prefix, function, method);
                    return;
                } else {
                    Class<?>[] types = method.getParameterTypes();
                    String[] typeNames = sig.getParamTypeNames();
                    if (types.length == typeNames.length) {
                        boolean match = true;
                        for (int i = 0; i < types.length; i++) {
                            if (i == types.length - 1 && method.isVarArgs()) {
                                String typeName = typeNames[i];
                                if (typeName.endsWith("...")) {
                                    typeName = typeName.substring(0, typeName.length() - 3);
                                    if (!typeName.equals(types[i].getName())) {
                                        match = false;
                                    }
                                } else {
                                    match = false;
                                }
                            } else if (!types[i].getName().equals(typeNames[i])) {
                                match = false;
                                break;
                            }
                        }
                        if (match) {
                            manager.mapFunction(prefix, function, method);
                            return;
                        }
                    }
                }
            }
        }

        throw new NoSuchMethodException(
                Util.message(context, "elProcessor.defineFunctionNoMethod", methodName, className));
    }


    /**
     * Map a method to a function name.
     *
     * @param prefix   Function prefix
     * @param function Function name
     * @param method   Method
     *
     * @throws NullPointerException  If any of the arguments are null
     * @throws NoSuchMethodException If the method is not static
     */
    public void defineFunction(String prefix, String function, Method method) throws NoSuchMethodException {

        if (prefix == null || function == null || method == null) {
            throw new NullPointerException(Util.message(context, "elProcessor.defineFunctionNullParams"));
        }

        int modifiers = method.getModifiers();

        // Check for static, public method and module access
        if (!Modifier.isStatic(modifiers) || !Util.canAccess(null, method)) {
            throw new NoSuchMethodException(Util.message(context, "elProcessor.defineFunctionInvalidMethod",
                    method.getName(), method.getDeclaringClass().getName()));
        }

        manager.mapFunction(prefix, function, method);
    }


    /**
     * Defines or removes a bean accessible by name in EL expressions. When a non-null bean
     * is provided, it is stored under the given name. When {@code null} is provided, the
     * bean with the given name is removed.
     *
     * @param name the bean name
     * @param bean the bean object to define, or {@code null} to remove the bean
     */
    public void defineBean(String name, Object bean) {
        manager.defineBean(name, bean);
    }


    private static String bracket(String expression) {
        return "${" + expression + "}";
    }

    private static class MethodSignature {

        private final String name;
        private final String[] parameterTypeNames;

        MethodSignature(ELContext context, String methodName, String className) throws NoSuchMethodException {

            int paramIndex = methodName.indexOf('(');

            if (paramIndex == -1) {
                name = methodName.trim();
                parameterTypeNames = null;
            } else {
                String returnTypeAndName = methodName.substring(0, paramIndex).trim();
                // Assume that the return type and the name are separated by
                // whitespace. Given the use of trim() above, there should only
                // be one sequence of whitespace characters.
                int wsPos = -1;
                for (int i = 0; i < returnTypeAndName.length(); i++) {
                    if (Character.isWhitespace(returnTypeAndName.charAt(i))) {
                        wsPos = i;
                        break;
                    }
                }
                if (wsPos == -1) {
                    throw new NoSuchMethodException();
                }
                name = returnTypeAndName.substring(wsPos).trim();

                String paramString = methodName.substring(paramIndex).trim();
                // We know the params start with '(', check they end with ')'
                if (!paramString.endsWith(")")) {
                    throw new NoSuchMethodException(Util.message(context,
                            "elProcessor.defineFunctionInvalidParameterList", paramString, methodName, className));
                }
                // Trim '(' and ')'
                paramString = paramString.substring(1, paramString.length() - 1).trim();
                if (paramString.length() == 0) {
                    parameterTypeNames = EMPTY_STRING_ARRAY;
                } else {
                    parameterTypeNames = paramString.split(",");
                    ImportHandler importHandler = context.getImportHandler();
                    for (int i = 0; i < parameterTypeNames.length; i++) {
                        String parameterTypeName = parameterTypeNames[i].trim();
                        int dimension = 0;
                        int bracketPos = parameterTypeName.indexOf('[');
                        if (bracketPos > -1) {
                            String parameterTypeNameOnly = parameterTypeName.substring(0, bracketPos).trim();
                            while (bracketPos > -1) {
                                dimension++;
                                bracketPos = parameterTypeName.indexOf('[', bracketPos + 1);
                            }
                            parameterTypeName = parameterTypeNameOnly;
                        }
                        boolean varArgs = false;
                        if (parameterTypeName.endsWith("...")) {
                            varArgs = true;
                            dimension = 1;
                            parameterTypeName = parameterTypeName.substring(0, parameterTypeName.length() - 3).trim();
                        }
                        boolean isPrimitive = PRIMITIVES.contains(parameterTypeName);
                        if (isPrimitive && dimension > 0) {
                            // When in an array, class name changes for primitive
                            switch (parameterTypeName) {
                                case "boolean":
                                    parameterTypeName = "Z";
                                    break;
                                case "byte":
                                    parameterTypeName = "B";
                                    break;
                                case "char":
                                    parameterTypeName = "C";
                                    break;
                                case "double":
                                    parameterTypeName = "D";
                                    break;
                                case "float":
                                    parameterTypeName = "F";
                                    break;
                                case "int":
                                    parameterTypeName = "I";
                                    break;
                                case "long":
                                    parameterTypeName = "J";
                                    break;
                                case "short":
                                    parameterTypeName = "S";
                                    break;
                                default:
                                    // Should never happen
                                    break;
                            }
                        } else if (!isPrimitive && !parameterTypeName.contains(".")) {
                            Class<?> clazz = importHandler.resolveClass(parameterTypeName);
                            if (clazz == null) {
                                throw new NoSuchMethodException(
                                        Util.message(context, "elProcessor.defineFunctionInvalidParameterTypeName",
                                                parameterTypeNames[i], methodName, className));
                            }
                            parameterTypeName = clazz.getName();
                        }
                        if (dimension > 0) {
                            // Convert to array form of class name
                            StringBuilder sb = new StringBuilder();
                            for (int j = 0; j < dimension; j++) {
                                sb.append('[');
                            }
                            if (!isPrimitive) {
                                sb.append('L');
                            }
                            sb.append(parameterTypeName);
                            if (!isPrimitive) {
                                sb.append(';');
                            }
                            parameterTypeName = sb.toString();
                        }
                        if (varArgs) {
                            parameterTypeName += "...";
                        }
                        parameterTypeNames[i] = parameterTypeName;
                    }
                }
            }

        }

        public String getName() {
            return name;
        }

        /**
         * @return <code>null</code> if just the method name was specified, an empty List if an empty parameter list was
         *             specified - i.e. () - otherwise an ordered list of parameter type names
         */
        public String[] getParamTypeNames() {
            return parameterTypeNames;
        }
    }
}
