package bsh;

import bsh.classpath.ClassManagerImpl;

import java.io.*;
import java.util.Map;

/**
 * With this class the script source is only parsed once and the resulting AST is used for
 * {@link #invoke(java.util.Map) every invocation}. This class is designed to be thread-safe.  
 */
public class PreparsedScript {

	private final BshMethod _method;
	private final Interpreter _interpreter;


	public PreparsedScript(final String source) throws EvalError {
		this(source, getDefaultClassLoader());
	}


	private static ClassLoader getDefaultClassLoader() {
		ClassLoader cl = null;
		try {
			cl = Thread.currentThread().getContextClassLoader();
		} catch (final SecurityException e) {
			// ignore
		}
		if (cl == null) {
			cl = PreparsedScript.class.getClassLoader();
		}
		if (cl != null) {
			return cl;
		}
		return ClassLoader.getSystemClassLoader();
	}


	public PreparsedScript(final String source, final ClassLoader classLoader) throws EvalError {
		final ClassManagerImpl classManager = new ClassManagerImpl();
		classManager.setClassLoader(classLoader);
		final NameSpace nameSpace = new NameSpace(classManager, "global");
		_interpreter = new Interpreter(new StringReader(""), System.out, System.err, false, nameSpace, null, null);
		try {
			final This callable = (This) _interpreter.eval("__execute() { " + source + "\n" + "}\n" + "return this;");
			_method = callable.getNameSpace().getMethod("__execute", new Class[0], false);
		} catch (final UtilEvalError e) {
			throw new IllegalStateException(e);
		}
	}


	public Object invoke(final Map<String, ?> context) throws EvalError {
		final NameSpace nameSpace = new NameSpace(_interpreter.getClassManager(), "BeanshellExecutable");
		nameSpace.setParent(_interpreter.getNameSpace());
		final BshMethod method = new BshMethod(_method.getName(), _method.getReturnType(), _method.getParameterNames(), _method.getParameterTypes(), _method.methodBody, nameSpace, _method.getModifiers());
		for (final Map.Entry<String, ?> entry : context.entrySet()) {
			try {
				final Object value = entry.getValue();
				nameSpace.setVariable(entry.getKey(), value != null ? value : Primitive.NULL, false);
			} catch (final UtilEvalError e) {
				throw new EvalError("cannot set variable '" + entry.getKey() + '\'', null, null, e);
			}
		}
		final Object result = method.invoke(new Object[0], _interpreter);
		if (result instanceof Primitive) {
			if (( (Primitive) result).getType() == Void.TYPE) {
				return null;
			}
			return ( (Primitive) result).getValue();
		}
		return result;
	}


	public void setOut(final PrintStream value) {
		_interpreter.setOut(value);
	}


	public void setErr(final PrintStream value) {
		_interpreter.setErr(value);
	}

}
