/*
 * 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
 *
 *      https://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.commons.jexl3.internal;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.JexlFeatures;
import org.apache.commons.jexl3.JexlInfo;
import org.apache.commons.jexl3.JexlOptions;
import org.apache.commons.jexl3.JexlScript;
import org.apache.commons.jexl3.parser.ASTJexlScript;

/**
 * <p>A JexlScript implementation.</p>
 *
 * @since 1.1
 */
public class Script implements JexlScript, JexlExpression {

    /**
     * Implements the Future and Callable interfaces to help delegation.
     */
    public class Callable implements java.util.concurrent.Callable<Object> {

        /** The actual interpreter. */
        protected final Interpreter interpreter;

        /** Use interpreter as marker for not having run. */
        protected volatile Object result;

        /**
         * The base constructor.
         *
         * @param intrprtr the interpreter to use
         */
        protected Callable(final Interpreter intrprtr) {
            this.interpreter = intrprtr;
            this.result = intrprtr;
        }

        @Override
        public Object call() throws Exception {
            synchronized(this) {
                if (result == interpreter) {
                    checkCacheVersion();
                    result = interpret();
                }
                return result;
            }
        }

        /**
         * Soft cancel the execution.
         *
         * @return true if cancel was successful, false otherwise
         */
        public boolean cancel() {
            return interpreter.cancel();
        }

        /**
         * Run the interpreter.
         *
         * @return the evaluation result
         */
        protected Object interpret() {
            return interpreter.interpret(script);
        }

        /**
         * @return true if interruption will throw a JexlException.Cancel, false otherwise
         */
        public boolean isCancellable() {
            return interpreter.isCancellable();
        }

        /**
         * @return true if evaluation was canceled, false otherwise
         */
        public boolean isCancelled() {
            return interpreter.isCancelled();
        }
    }

    /**
     * The engine for this expression.
     */
    protected final Engine jexl;

    /**
     * Original expression stripped from leading and trailing spaces.
     */
    protected final String source;

    /**
     * The resulting AST we can interpret.
     */
    protected final ASTJexlScript script;

    /**
     * The engine version (as class loader change count) that last evaluated this script.
     */
    protected int version;

    /**
     * Do not let this be generally instantiated with a 'new'.
     *
     * @param engine the interpreter to evaluate the expression
     * @param expr   the expression source.
     * @param ref    the parsed expression.
     */
    protected Script(final Engine engine, final String expr, final ASTJexlScript ref) {
        jexl = engine;
        source = expr;
        script = ref;
        version = jexl.getUberspect().getVersion();
    }

    /**
     * Creates a Callable from this script.
     * <p>This allows submitting it to an executor pool and provides support for asynchronous calls.</p>
     * <p>The interpreter will handle interruption/cancellation gracefully if needed.</p>
     *
     * @param context the context
     * @return the callable
     */
    @Override
    public Callable callable(final JexlContext context) {
        return callable(context, (Object[]) null);
    }

    /**
     * Creates a Callable from this script.
     * <p>This allows submitting it to an executor pool and provides support for asynchronous calls.</p>
     * <p>The interpreter will handle interruption/cancellation gracefully if needed.</p>
     *
     * @param context the context
     * @param args    the script arguments
     * @return the callable
     */
    @Override
    public Callable callable(final JexlContext context, final Object... args) {
        return new Callable(createInterpreter(context, script.createFrame(args)));
    }

    /**
     * Checks that this script cached methods (wrt introspection) matches the engine version.
     * <p>
     * If the engine class loader has changed since we last evaluated this script, the script local cache
     * is invalidated to drop references to obsolete methods. It is not strictly necessary since the tryExecute
     * will fail because the class won't match but it seems cleaner nevertheless.
     * </p>
     */
    protected void checkCacheVersion() {
        final int uberVersion = jexl.getUberspect().getVersion();
        if (version != uberVersion) {
            // version 0 of the uberSpect is an illusion due to order of construction; no need to clear cache
            if (version > 0) {
                script.clearCache();
            }
            version = uberVersion;
        }
    }

    /**
     * Creates this script frame for evaluation.
     *
     * @param args the arguments to bind to parameters
     * @return the frame (may be null)
     */
    protected Frame createFrame(final Object[] args) {
        return script.createFrame(args);
    }

    /**
     * Creates this script interpreter.
     *
     * @param context the context
     * @param frame the calling frame
     * @return  the interpreter
     */
    protected Interpreter createInterpreter(final JexlContext context, final Frame frame) {
        return createInterpreter(context, frame, null);
    }

    /**
     * Creates this script interpreter.
     *
     * @param context the context
     * @param frame the calling frame
     * @param options the interpreter options
     * @return  the interpreter
     */
    protected Interpreter createInterpreter(final JexlContext context, final Frame frame, final JexlOptions options) {
        return jexl.createInterpreter(context, frame, options != null ? options : jexl.evalOptions(script, context));
    }

    @Override
    public JexlScript curry(final Object... args) {
        final String[] parms = script.getParameters();
        if (parms == null || parms.length == 0) {
            return this;
        }
        return new Closure(this, args);
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Script other = (Script) obj;
        if (this.jexl != other.jexl) {
            return false;
        }
        if (!Objects.equals(this.source, other.source)) {
            return false;
        }
        return true;
    }

    @Override
    public Object evaluate(final JexlContext context) {
        return execute(context);
    }

    @Override
    public Object execute(final JexlContext context) {
        checkCacheVersion();
        final Frame frame = createFrame(null);
        final Interpreter interpreter = createInterpreter(context, frame);
        return interpreter.interpret(script);
    }

    @Override
    public Object execute(final JexlContext context, final Object... args) {
        checkCacheVersion();
        final Frame frame = createFrame(args != null && args.length > 0 ? args : null);
        final Interpreter interpreter = createInterpreter(context, frame);
        return interpreter.interpret(script);
    }

    /**
     * Gets this script captured variable, i.e. symbols captured from outer scopes.
     *
     * @return the captured variable names
     */
    public String[] getCapturedVariables() {
        return script.getCapturedVariables();
    }

    /**
     * @return the engine that created this script
     */
    public JexlEngine getEngine() {
        return jexl;
    }

    /**
     * @return the script features
     */
    public JexlFeatures getFeatures() {
        return script.getFeatures();
    }

    /**
     * @return the info
     */
    public JexlInfo getInfo() {
        return script.jexlInfo();
    }

    @Override
    public String[] getLocalVariables() {
        return script.getLocalVariables();
    }

    @Override
    public String[] getParameters() {
        return script.getParameters();
    }

    @Override
    public String getParsedText() {
        return getParsedText(2);
    }

    @Override
    public String getParsedText(final int indent) {
        final Debugger debug = new Debugger();
        debug.outputPragmas(true).indentation(indent).debug(script, false);
        return debug.toString();
    }

    /**
     * Gets this script pragmas
     * <p>Pragma keys are ant-ish variables, their values are scalar literals.
     *
     * @return the pragmas
     */
    @Override
    public Map<String, Object> getPragmas() {
        return script.getPragmas();
    }

    /**
     * @return the script AST
     */
    protected ASTJexlScript getScript() {
        return script;
    }

    @Override
    public String getSourceText() {
        return source;
    }

    @Override
    public String[] getUnboundParameters() {
        return getParameters();
    }

    /**
     * Gets this script variables.
     * <p>Note that since variables can be in an ant-ish form (ie foo.bar.quux), each variable is returned as
     * a list of strings where each entry is a fragment of the variable ({"foo", "bar", "quux"} in the example.</p>
     *
     * @return the variables or null
     */
    @Override
    public Set<List<String>> getVariables() {
        return jexl.getVariables(script);
    }

    @Override
    public int hashCode() {
        // CSOFF: Magic number
        int hash = 17;
        hash = 31 * hash + (this.jexl != null ? this.jexl.hashCode() : 0);
        hash = 31 * hash + (this.source != null ? this.source.hashCode() : 0);
        return hash;
        // CSON: Magic number
    }

    @Override
    public String toString() {
        CharSequence src = source;
        if (src == null) {
            final Debugger debug = new Debugger();
            debug.debug(script, false);
            src = debug.toString();
        }
        return src.toString();
    }
}
