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

import java.io.StringReader;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;

import jakarta.el.ELContext;
import jakarta.el.ELException;
import jakarta.el.FunctionMapper;
import jakarta.el.MethodExpression;
import jakarta.el.ValueExpression;
import jakarta.el.VariableMapper;

import org.apache.el.MethodExpressionImpl;
import org.apache.el.MethodExpressionLiteral;
import org.apache.el.ValueExpressionImpl;
import org.apache.el.parser.AstDeferredExpression;
import org.apache.el.parser.AstDynamicExpression;
import org.apache.el.parser.AstFunction;
import org.apache.el.parser.AstIdentifier;
import org.apache.el.parser.AstLiteralExpression;
import org.apache.el.parser.AstValue;
import org.apache.el.parser.ELParser;
import org.apache.el.parser.Node;
import org.apache.el.parser.NodeVisitor;
import org.apache.el.util.ConcurrentCache;
import org.apache.el.util.ExceptionUtils;
import org.apache.el.util.MessageFactory;

/**
 * @author Jacob Hookom [jacob@hookom.net]
 */
public final class ExpressionBuilder implements NodeVisitor {

    private static final SynchronizedStack<ELParser> parserCache = new SynchronizedStack<>();

    private static final int CACHE_SIZE;
    private static final String CACHE_SIZE_PROP = "org.apache.el.ExpressionBuilder.CACHE_SIZE";

    static {
        String cacheSizeStr;
        if (System.getSecurityManager() == null) {
            cacheSizeStr = System.getProperty(CACHE_SIZE_PROP, "5000");
        } else {
            cacheSizeStr = AccessController
                    .doPrivileged((PrivilegedAction<String>) () -> System.getProperty(CACHE_SIZE_PROP, "5000"));
        }
        CACHE_SIZE = Integer.parseInt(cacheSizeStr);
    }

    private static final ConcurrentCache<String,Node> expressionCache = new ConcurrentCache<>(CACHE_SIZE);

    private FunctionMapper fnMapper;

    private VariableMapper varMapper;

    private final String expression;

    public ExpressionBuilder(String expression, ELContext ctx) throws ELException {
        this.expression = expression;

        FunctionMapper ctxFn = ctx.getFunctionMapper();
        VariableMapper ctxVar = ctx.getVariableMapper();

        if (ctxFn != null) {
            this.fnMapper = new FunctionMapperFactory(ctxFn);
        }
        if (ctxVar != null) {
            this.varMapper = new VariableMapperFactory(ctxVar);
        }
    }

    public static Node createNode(String expr) throws ELException {
        Node n = createNodeInternal(expr);
        return n;
    }

    private static Node createNodeInternal(String expr) throws ELException {
        if (expr == null) {
            throw new ELException(MessageFactory.get("error.null"));
        }

        Node n = expressionCache.get(expr);
        if (n == null) {
            ELParser parser = parserCache.pop();
            try {
                if (parser == null) {
                    parser = new ELParser(new StringReader(expr));
                } else {
                    parser.ReInit(new StringReader(expr));
                }
                n = parser.CompositeExpression();

                // validate composite expression
                int numChildren = n.jjtGetNumChildren();
                if (numChildren == 1) {
                    n = n.jjtGetChild(0);
                } else {
                    Class<?> type = null;
                    Node child = null;
                    for (int i = 0; i < numChildren; i++) {
                        child = n.jjtGetChild(i);
                        if (child instanceof AstLiteralExpression) {
                            continue;
                        }
                        if (type == null) {
                            type = child.getClass();
                        } else {
                            if (!type.equals(child.getClass())) {
                                throw new ELException(MessageFactory.get("error.mixed", expr));
                            }
                        }
                    }
                }

                if (n instanceof AstDeferredExpression || n instanceof AstDynamicExpression) {
                    n = n.jjtGetChild(0);
                }
                expressionCache.put(expr, n);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                throw new ELException(MessageFactory.get("error.parseFail", expr), t);
            } finally {
                if (parser != null) {
                    parserCache.push(parser);
                }
            }
        }
        return n;
    }

    private void prepare(Node node) throws ELException {
        try {
            node.accept(this);
        } catch (Exception e) {
            if (e instanceof ELException) {
                throw (ELException) e;
            } else {
                throw (new ELException(e));
            }
        }
        if (this.fnMapper instanceof FunctionMapperFactory) {
            this.fnMapper = ((FunctionMapperFactory) this.fnMapper).create();
        }
        if (this.varMapper instanceof VariableMapperFactory) {
            this.varMapper = ((VariableMapperFactory) this.varMapper).create();
        }
    }

    private Node build() throws ELException {
        Node n = createNodeInternal(this.expression);
        this.prepare(n);
        if (n instanceof AstDeferredExpression || n instanceof AstDynamicExpression) {
            n = n.jjtGetChild(0);
        }
        return n;
    }

    @Override
    public void visit(Node node) throws ELException {
        if (node instanceof AstFunction) {

            AstFunction funcNode = (AstFunction) node;

            Method m = null;

            if (this.fnMapper != null) {
                m = fnMapper.resolveFunction(funcNode.getPrefix(), funcNode.getLocalName());
            }

            // References to variables that refer to lambda expressions will be
            // parsed as functions. This is handled at runtime but at this point
            // need to treat it as a variable rather than a function.
            if (m == null && this.varMapper != null && funcNode.getPrefix().length() == 0) {
                this.varMapper.resolveVariable(funcNode.getLocalName());
                return;
            }

            if (this.fnMapper == null) {
                throw new ELException(MessageFactory.get("error.fnMapper.null"));
            }

            if (m == null) {
                throw new ELException(MessageFactory.get("error.fnMapper.method", funcNode.getOutputName()));
            }

            int methodParameterCount = m.getParameterTypes().length;
            // AstFunction->MethodParameters->Parameters()
            int inputParameterCount = node.jjtGetChild(0).jjtGetNumChildren();
            if (m.isVarArgs() && inputParameterCount < methodParameterCount - 1 ||
                    !m.isVarArgs() && inputParameterCount != methodParameterCount) {
                throw new ELException(MessageFactory.get("error.fnMapper.paramcount", funcNode.getOutputName(),
                        "" + methodParameterCount, "" + node.jjtGetChild(0).jjtGetNumChildren()));
            }
        } else if (node instanceof AstIdentifier && this.varMapper != null) {
            String variable = node.getImage();

            // simply capture it
            this.varMapper.resolveVariable(variable);
        }
    }

    public ValueExpression createValueExpression(Class<?> expectedType) throws ELException {
        Node n = this.build();
        return new ValueExpressionImpl(this.expression, n, this.fnMapper, this.varMapper, expectedType);
    }

    public MethodExpression createMethodExpression(Class<?> expectedReturnType, Class<?>[] expectedParamTypes)
            throws ELException {
        Node n = this.build();
        if (!n.isParametersProvided() && expectedParamTypes == null) {
            throw new NullPointerException(MessageFactory.get("error.method.nullParms"));
        }
        if (n instanceof AstValue || n instanceof AstIdentifier) {
            return new MethodExpressionImpl(expression, n, this.fnMapper, this.varMapper, expectedReturnType,
                    expectedParamTypes);
        } else if (n instanceof AstLiteralExpression) {
            return new MethodExpressionLiteral(expression, expectedReturnType, expectedParamTypes);
        } else {
            throw new ELException(MessageFactory.get("error.invalidMethodExpression", expression));
        }
    }

    /*
     * Copied from org.apache.tomcat.util.collections.SynchronizedStack since we don't want the EL implementation to
     * depend on the JAR where that class resides.
     */
    private static class SynchronizedStack<T> {

        public static final int DEFAULT_SIZE = 128;
        private static final int DEFAULT_LIMIT = -1;

        private int size;
        private final int limit;

        /*
         * Points to the next available object in the stack
         */
        private int index = -1;

        private Object[] stack;


        SynchronizedStack() {
            this(DEFAULT_SIZE, DEFAULT_LIMIT);
        }

        SynchronizedStack(int size, int limit) {
            this.size = size;
            this.limit = limit;
            stack = new Object[size];
        }


        public synchronized boolean push(T obj) {
            index++;
            if (index == size) {
                if (limit == -1 || size < limit) {
                    expand();
                } else {
                    index--;
                    return false;
                }
            }
            stack[index] = obj;
            return true;
        }

        @SuppressWarnings("unchecked")
        public synchronized T pop() {
            if (index == -1) {
                return null;
            }
            T result = (T) stack[index];
            stack[index--] = null;
            return result;
        }

        private void expand() {
            int newSize = size * 2;
            if (limit != -1 && newSize > limit) {
                newSize = limit;
            }
            Object[] newStack = new Object[newSize];
            System.arraycopy(stack, 0, newStack, 0, size);
            // This is the only point where garbage is created by throwing away the
            // old array. Note it is only the array, not the contents, that becomes
            // garbage.
            stack = newStack;
            size = newSize;
        }
    }

}
