package bsh.engine;

import bsh.*;
import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
	Adopted from http://ikayzo.org/svn/beanshell/BeanShell/engine/src/bsh/engine/BshScriptEngine.java
	Notes
	This engine supports open-ended pluggable scriptcontexts
*/

public class BshScriptEngine extends AbstractScriptEngine implements Compilable, Invocable {
	// The BeanShell global namespace for the interpreter is stored in the
	// engine scope map under this key.
	static final String engineNameSpaceKey = "org_beanshell_engine_namespace";

	private BshScriptEngineFactory factory;
	private bsh.Interpreter interpreter;


	public BshScriptEngine() {
		this(null);
	}


	public BshScriptEngine(BshScriptEngineFactory factory) {
		this.factory = factory;
		getInterpreter(); // go ahead and prime the interpreter now
	}


	protected Interpreter getInterpreter() {
		if (interpreter == null) {
			this.interpreter = new bsh.Interpreter();
			interpreter.setNameSpace(null); // should always be set by context
		}

		return interpreter;
	}


	public Object eval(String script, ScriptContext scriptContext) throws ScriptException {
		return evalSource(script, scriptContext);
	}


	public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException {
		return evalSource(reader, scriptContext);
	}

	/*
		This is the primary implementation method.
		We respect the String/Reader difference here in BeanShell because
		BeanShell will do a few extra things in the string case... e.g.
		tack on a trailing ";" semicolon if necessary.
	*/


	private Object evalSource(Object source, ScriptContext scriptContext) throws ScriptException {
		bsh.NameSpace contextNameSpace = getEngineNameSpace(scriptContext);
		Interpreter bsh = getInterpreter();
		bsh.setNameSpace(contextNameSpace);

		bsh.setOut(toPrintStream(scriptContext.getWriter()));
		bsh.setErr(toPrintStream(scriptContext.getErrorWriter()));

		try {
			if (source instanceof Reader) {
				return bsh.eval((Reader) source);
			} else {
				return bsh.eval((String) source);
			}
		} catch (ParseException e) {
			// explicit parsing error
			throw new ScriptException(e.toString(), e.getErrorSourceFile(), e.getErrorLineNumber());
		} catch (TargetError e) {
			// The script threw an application level exception
			// set it as the cause ?
			ScriptException se = new ScriptException(e.toString(), e.getErrorSourceFile(), e.getErrorLineNumber());
			se.initCause(e.getTarget());
			throw se;
		} catch (EvalError e) {
			// The script couldn't be evaluated properly
			throw new ScriptException(e.toString(), e.getErrorSourceFile(), e.getErrorLineNumber());
		} catch (InterpreterError e) {
			// The interpreter had a fatal problem
			throw new ScriptException(e.toString());
		}
	}


	private PrintStream toPrintStream(final Writer writer) {
		// This is a big hack, convert writer to PrintStream
		return new PrintStream(new WriterOutputStream(writer));
	}


	/*
		Check the context for an existing global namespace embedded
		in the script context engine scope.  If none exists, ininitialize the
		context with one.
	*/


	private static NameSpace getEngineNameSpace(ScriptContext scriptContext) {
		NameSpace ns = (NameSpace) scriptContext.getAttribute(engineNameSpaceKey, ScriptContext.ENGINE_SCOPE);

		if (ns == null) {
			// Create a global namespace for the interpreter
			Map<String, Object> engineView = new ScriptContextEngineView(scriptContext);
			ns = new ExternalNameSpace(null/*parent*/, "javax_script_context", engineView);

			scriptContext.setAttribute(engineNameSpaceKey, ns, ScriptContext.ENGINE_SCOPE);
		}

		return ns;
	}


	public Bindings createBindings() {
		return new SimpleBindings();
	}


	public ScriptEngineFactory getFactory() {
		if (factory == null) {
			factory = new BshScriptEngineFactory();
		}
		return factory;
	}


	/**
	 * Compiles the script (source represented as a {@code String}) for later
	 * execution.
	 *
	 * @param script The source of the script, represented as a {@code String}.
	 * @return An subclass of {@code CompiledScript} to be executed later
	 *         using one of the {@code eval} methods of {@code CompiledScript}.
	 * @throws ScriptException	  if compilation fails.
	 * @throws NullPointerException if the argument is null.
	 */

	public CompiledScript compile(String script) throws ScriptException {
		try {
			final PreparsedScript preparsed = new PreparsedScript(script);
			return new CompiledScript() {

				@Override
				public Object eval(ScriptContext context) throws ScriptException {
					final HashMap<String, Object> map = new HashMap<String, Object>();
					final List<Integer> scopes = new ArrayList<Integer>(context.getScopes());
					Collections.sort(scopes); // lowest scope at first pos
					Collections.reverse(scopes); // highest scope at first pos
					for (final Integer scope : scopes) {
						map.putAll(context.getBindings(scope));
					}
					preparsed.setOut(toPrintStream(context.getWriter()));
					preparsed.setErr(toPrintStream(context.getErrorWriter()));
					try {
						return preparsed.invoke(map);
					} catch (final EvalError e) {
						throw constructScriptException(e);
					}
				}


				@Override
				public ScriptEngine getEngine() {
					return BshScriptEngine.this;
				}

			};
		} catch (final EvalError e) {
			throw constructScriptException(e);
		}
	}


	private ScriptException constructScriptException(final EvalError e) {
		return new ScriptException(e.getMessage(), e.getErrorSourceFile(), e.getErrorLineNumber());
	}


	private static String convertToString(Reader reader) throws IOException {
		final StringBuffer buffer = new StringBuffer(64);
		char[] cb = new char[64];
		int len;
		while ((len = reader.read(cb)) != -1) {
			buffer.append(cb, 0, len);
		}
		return buffer.toString();
	}


	/**
	 * Compiles the script (source read from {@code Reader}) for later
	 * execution.  Functionality is identical to {@code compile(String)} other
	 * than the way in which the source is passed.
	 *
	 * @param script The reader from which the script source is obtained.
	 * @return An implementation of {@code CompiledScript} to be executed
	 *         later using one of its {@code eval} methods of
	 *         {@code CompiledScript}.
	 * @throws ScriptException	  if compilation fails.
	 * @throws NullPointerException if argument is null.
	 */
	public CompiledScript compile(Reader script) throws ScriptException {
		try {
			return compile(convertToString(script));
		} catch (IOException e) {
			throw new ScriptException(e);
		}
	}


	/**
	 * Calls a procedure compiled during a previous script execution, which is
	 * retained in the state of the {@code ScriptEngine{@code .
	 *
	 * @param name The name of the procedure to be called.
	 * @param thiz If the procedure is a member  of a class defined in the script
	 *             and thiz is an instance of that class returned by a previous execution or
	 *             invocation, the named method is called through that instance. If classes are
	 *             not supported in the scripting language or if the procedure is not a member
	 *             function of any class, the argument must be {@code null}.
	 * @param args Arguments to pass to the procedure.  The rules for converting
	 *             the arguments to scripting variables are implementation-specific.
	 * @return The value returned by the procedure.  The rules for converting the
	 *         scripting variable returned by the procedure to a Java Object are
	 *         implementation-specific.
	 * @throws javax.script.ScriptException if an error occurrs during invocation
	 *                                      of the method.
	 * @throws NoSuchMethodException		if method with given name or matching argument
	 *                                      types cannot be found.
	 * @throws NullPointerException		 if method name is null.
	 */
	public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {
		if (!(thiz instanceof bsh.This)) {
			throw new ScriptException("Illegal objec type: " + thiz.getClass());
		}

		bsh.This bshObject = (bsh.This) thiz;

		try {
			return bshObject.invokeMethod(name, args);
		} catch (ParseException e) {
			// explicit parsing error
			throw new ScriptException(e.toString(), e.getErrorSourceFile(), e.getErrorLineNumber());
		} catch (TargetError e) {
			// The script threw an application level exception
			// set it as the cause ?
			ScriptException se = new ScriptException(e.toString(), e.getErrorSourceFile(), e.getErrorLineNumber());
			se.initCause(e.getTarget());
			throw se;
		} catch (EvalError e) {
			// The script couldn't be evaluated properly
			throw new ScriptException(e.toString(), e.getErrorSourceFile(), e.getErrorLineNumber());
		} catch (InterpreterError e) {
			// The interpreter had a fatal problem
			throw new ScriptException(e.toString());
		}
	}


	/**
	 * Same as invoke(Object, String, Object...) with {@code null} as the
	 * first argument.  Used to call top-level procedures defined in scripts.
	 *
	 * @param args Arguments to pass to the procedure
	 * @return The value returned by the procedure
	 * @throws javax.script.ScriptException if an error occurrs during invocation
	 *                                      of the method.
	 * @throws NoSuchMethodException		if method with given name or matching
	 *                                      argument types cannot be found.
	 * @throws NullPointerException		 if method name is null.
	 */
	public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
		return invokeMethod(getGlobal(), name, args);
	}


	/**
	 * Returns an implementation of an interface using procedures compiled in the
	 * interpreter. The methods of the interface may be implemented using the
	 * {@code invoke} method.
	 *
	 * @param clasz The {@code Class} object of the interface to return.
	 * @return An instance of requested interface - null if the requested interface
	 *         is unavailable, i. e. if compiled methods in the
	 *         {@code ScriptEngine} cannot be found matching the ones in the
	 *         requested interface.
	 * @throws IllegalArgumentException if the specified {@code Class} object
	 *                                  does not exist or is not an interface.
	 */
	public <T> T getInterface(Class<T> clasz) {
		return clasz.cast(getGlobal().getInterface(clasz));
	}


	/**
	 * Returns an implementation of an interface using member functions of a
	 * scripting object compiled in the interpreter. The methods of the interface
	 * may be implemented using invoke(Object, String, Object...) method.
	 *
	 * @param thiz  The scripting object whose member functions are used to
	 *              implement the methods of the interface.
	 * @param clasz The {@code Class} object of the interface to return.
	 * @return An instance of requested interface - null if the requested
	 *         interface is unavailable, i. e. if compiled methods in the
	 *         {@code ScriptEngine} cannot be found matching the ones in the
	 *         requested interface.
	 * @throws IllegalArgumentException if the specified {@code Class} object
	 *                                  does not exist or is not an interface, or if the specified Object is null
	 *                                  or does not represent a scripting object.
	 */
	public <T> T getInterface(Object thiz, Class<T> clasz) {
		if (!(thiz instanceof bsh.This)) {
			throw new IllegalArgumentException("invalid object type: " + thiz.getClass());
		}

		bsh.This bshThis = (bsh.This) thiz;
		return clasz.cast(bshThis.getInterface(clasz));
	}


	private bsh.This getGlobal() {
		// requires 2.0b5 to make getThis() public
		return getEngineNameSpace(getContext()).getThis(getInterpreter());
	}

	/*
		This is a total hack.  We need to introduce a writer to the
		Interpreter.
	*/

	class WriterOutputStream extends OutputStream {

		Writer writer;


		WriterOutputStream(Writer writer) {
			this.writer = writer;
		}


		public void write(int b) throws IOException {
			writer.write(b);
		}


		public void flush() throws IOException {
			writer.flush();
		}


		public void close() throws IOException {
			writer.close();
		}
	}

}
