/* 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.flowable.scripting.secure.impl;

import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Scriptable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Joram Barrez
 */
public class SecureScriptContextFactory extends ContextFactory {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecureScriptContextFactory.class);

    protected SecureScriptClassShutter classShutter;
    protected int observeInstructionCount = 10;
    protected long maxScriptExecutionTime = -1L;
    protected long maxMemoryUsed = -1L;
    protected int maxStackDepth = -1;
    protected int optimizationLevel = -1;
    protected SecureScriptThreadMxBeanWrapper threadMxBeanWrapper;
    protected boolean enableAccessToBeans = false;

    @Override
    protected Context makeContext() {
        SecureScriptContext context = new SecureScriptContext(this);

        // Setting this, as otherwise variables set seem to have the wrong type
        context.getWrapFactory().setJavaPrimitiveWrap(false);

        context.setOptimizationLevel(optimizationLevel);

        // Class white-listing
        if (classShutter != null) {
            context.setClassShutter(classShutter);
        }

        // Needed for both time and memory measurement
        if (maxScriptExecutionTime > 0L || maxMemoryUsed > 0L) {
            context.setGenerateObserverCount(true);
            context.setInstructionObserverThreshold(observeInstructionCount);
        }

        // Memory limit
        if (maxMemoryUsed > 0) {
            context.setThreadId(Thread.currentThread().getId());
        }

        // Max stack depth
        if (maxStackDepth > 0) {
            context.setOptimizationLevel(-1); // stack depth can only be set when no optimizations are applied
            context.setMaximumInterpreterStackDepth(maxStackDepth);
        }

        return context;
    }

    @Override
    protected void observeInstructionCount(Context cx, int instructionCount) {
        SecureScriptContext context = (SecureScriptContext) cx;

        // Time limit
        if (maxScriptExecutionTime > 0) {
            long currentTime = System.currentTimeMillis();
            if (currentTime - context.getStartTime() > maxScriptExecutionTime) {
                throw new Error("Maximum variableScope time of " + maxScriptExecutionTime + " ms exceeded");
            }
        }

        // Memory
        if (maxMemoryUsed > 0 && threadMxBeanWrapper != null) {

            if (context.getStartMemory() <= 0) {
                context.setStartMemory(threadMxBeanWrapper.getThreadAllocatedBytes(context.getThreadId()));
            } else {
                long currentAllocatedBytes = threadMxBeanWrapper.getThreadAllocatedBytes(context.getThreadId());
                if (currentAllocatedBytes - context.getStartMemory() >= maxMemoryUsed) {
                    throw new Error("Memory limit of " + maxMemoryUsed + " bytes reached");
                }
            }

        }
    }

    // Override {@link #doTopCall(Callable, Context, Scriptable, Scriptable, Object[])}
    @Override
    protected Object doTopCall(Callable callable, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        SecureScriptContext mcx = (SecureScriptContext) cx;
        mcx.setStartTime(System.currentTimeMillis());
        return super.doTopCall(callable, cx, scope, thisObj, args);
    }

    public int getOptimizationLevel() {
        return optimizationLevel;
    }

    public void setOptimizationLevel(int optimizationLevel) {
        this.optimizationLevel = optimizationLevel;
    }

    public SecureScriptClassShutter getClassShutter() {
        return classShutter;
    }

    public void setClassShutter(SecureScriptClassShutter classShutter) {
        this.classShutter = classShutter;
    }

    public int getObserveInstructionCount() {
        return observeInstructionCount;
    }

    public void setObserveInstructionCount(int observeInstructionCount) {
        this.observeInstructionCount = observeInstructionCount;
    }

    public long getMaxScriptExecutionTime() {
        return maxScriptExecutionTime;
    }

    public void setMaxScriptExecutionTime(long maxScriptExecutionTime) {
        this.maxScriptExecutionTime = maxScriptExecutionTime;
    }

    public long getMaxMemoryUsed() {
        return maxMemoryUsed;
    }

    public void setMaxMemoryUsed(long maxMemoryUsed) {
        this.maxMemoryUsed = maxMemoryUsed;
        if (maxMemoryUsed > 0) {
            try {
                Class clazz = Class.forName("com.sun.management.ThreadMXBean");
                if (clazz != null) {
                    this.threadMxBeanWrapper = new SecureScriptThreadMxBeanWrapper();
                }
            } catch (ClassNotFoundException cnfe) {
                LOGGER.warn("com.sun.management.ThreadMXBean was not found on the classpath. " +
                        "This means that the limiting the memory usage for a script will NOT work.");
            }
        }
    }

    public int getMaxStackDepth() {
        return maxStackDepth;
    }

    public void setMaxStackDepth(int maxStackDepth) {
        this.maxStackDepth = maxStackDepth;
    }

    public boolean isEnableAccessToBeans() {
        return enableAccessToBeans;
    }

    public void setEnableAccessToBeans(boolean enableAccessToBeans) {
        this.enableAccessToBeans = enableAccessToBeans;
    }
}