/*******************************************************************************
 * Copyright (c) 2008-2013 LuaJ. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/
package org.luaj.vm2.script;

import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import javax.script.*;

import org.luaj.vm2.*;
import org.luaj.vm2.libs.ThreeArgFunction;
import org.luaj.vm2.libs.TwoArgFunction;
import org.luaj.vm2.libs.jse.CoerceJavaToLua;
import org.luaj.vm2.libs.jse.CoerceLuaToJava;

/**
 * Implementation of the ScriptEngine interface which can compile and execute
 * scripts using luaj.
 *
 * <p>
 * This engine requires the types of the Bindings and ScriptContext to be 
 * compatible with the engine.  For creating new client context use
 * ScriptEngine.createContext() which will return {@link LuajContext}, 
 * and for client bindings use the default engine scoped bindings or
 * construct a {@link LuajBindings} directly.
 */
public class LuaScriptEngine extends AbstractScriptEngine implements ScriptEngine, Compilable, Invocable {

	private static final String __ENGINE_VERSION__   = Lua._VERSION;
	private static final String __NAME__             = "Luaj";
	private static final String __SHORT_NAME__       = "Luaj";
	private static final String __LANGUAGE__         = "lua";
	private static final String __LANGUAGE_VERSION__ = "5.5";
	private static final String __ARGV__             = "arg";
	private static final String __FILENAME__         = "?";

	private static final ScriptEngineFactory myFactory = new LuaScriptEngineFactory();

	private LuajContext context;

	public LuaScriptEngine() {
		// set up context
		context = new LuajContext();
		context.setBindings(createBindings(), ScriptContext.ENGINE_SCOPE);
		setContext(context);

		// set special values
		put(LANGUAGE_VERSION, __LANGUAGE_VERSION__);
		put(LANGUAGE, __LANGUAGE__);
		put(ENGINE, __NAME__);
		put(ENGINE_VERSION, __ENGINE_VERSION__);
		put(ARGV, __ARGV__);
		put(FILENAME, __FILENAME__);
		put(NAME, __SHORT_NAME__);
		put("THREADING", null);
	}

	@Override
	public CompiledScript compile(String script) throws ScriptException {
		return compile(new StringReader(script));
	}

	@Override
	public CompiledScript compile(Reader script) throws ScriptException {
		try {
			InputStream is = new Utf8Encoder(script);
			try {
				final Globals g = context.globals;
				final LuaFunction f = g.load(script, "script").checkfunction();
				return new LuajCompiledScript(f, g);
			} catch ( LuaError lee ) {
				throw new ScriptException(lee.getMessage() );
			} finally {
				is.close();
			}
		} catch ( Exception e ) {
			throw new ScriptException("eval threw "+e.toString());
		}
	}

	@Override
	public Object eval(Reader reader, Bindings bindings) throws ScriptException {
		return ((LuajCompiledScript) compile(reader)).eval(context.globals, bindings);
	}

	@Override
	public Object eval(String script, Bindings bindings) throws ScriptException {
		return eval(new StringReader(script), bindings);
	}

	@Override
	protected ScriptContext getScriptContext(Bindings nn) {
		throw new IllegalStateException("LuajScriptEngine should not be allocating contexts.");
	}

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

	@Override
	public Object eval(String script, ScriptContext context)
			throws ScriptException {
		return eval(new StringReader(script), context);
	}

	@Override
	public Object eval(Reader reader, ScriptContext context)
			throws ScriptException {
		return compile(reader).eval(context);
	}

	@Override
	public ScriptEngineFactory getFactory() {
		return myFactory;
	}

	@Override
	public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
		LuaValue function = getInvocableValue(currentGlobals(), name);
		return invokeValue(function, toLuaVarargs(args));
	}

	@Override
	public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {
		LuaValue target = thiz instanceof LuaValue ? (LuaValue) thiz : toLua(thiz);
		LuaValue function = target.get(name);
		if (!function.isfunction()) {
			throw new NoSuchMethodException(name + " on " + target.typename());
		}
		return invokeValue(function, prependSelf(target, args));
	}

	@Override
	public <T> T getInterface(Class<T> clasz) {
		return getInterface((Object) currentGlobals(), clasz);
	}

	@Override
	public <T> T getInterface(Object thiz, Class<T> clasz) {
		if (thiz == null || clasz == null || !clasz.isInterface()) {
			return null;
		}
		LuaValue target = thiz instanceof LuaValue ? (LuaValue) thiz : toLua(thiz);
		Globals globals = currentGlobals();
		return hasAllInterfaceMethods(target, clasz, thiz == globals) ? createInterfaceProxy(target, clasz, thiz != globals) : null;
	}

	private LuaValue getInvocableValue(Globals globals, String name) throws NoSuchMethodException {
		prepareBindings(globals, getContext().getBindings(ScriptContext.ENGINE_SCOPE));
		LuaValue function = globals.get(name);
		if (!function.isfunction()) {
			throw new NoSuchMethodException(name);
		}
		return function;
	}

	private Object invokeValue(LuaValue function, Varargs args) throws ScriptException {
		try {
			return toJava(function.invoke(args));
		} catch (LuaError e) {
			throw scriptException(e);
		}
	}

	private static ScriptException scriptException(LuaError e) {
		ScriptException se = new ScriptException(e.getMessage());
		se.initCause(e);
		return se;
	}

	private void prepareBindings(Globals globals, Bindings bindings) {
		globals.setmetatable(new BindingsMetatable(bindings));
	}

	private Globals currentGlobals() {
		return ((LuajContext) getContext()).globals;
	}

	private static LuaValue[] toLuaArgs(Object[] args) {
		if (args == null || args.length == 0) {
			return new LuaValue[0];
		}
		LuaValue[] values = new LuaValue[args.length];
		for (int i = 0; i < args.length; ++i) {
			values[i] = toLua(args[i]);
		}
		return values;
	}

	private static Varargs toLuaVarargs(Object[] args) {
		return LuaValue.varargsOf(toLuaArgs(args));
	}

	private static Varargs prependSelf(LuaValue target, Object[] args) {
		return LuaValue.varargsOf(target, toLuaVarargs(args));
	}

	private boolean hasAllInterfaceMethods(LuaValue target, Class<?> clasz, boolean globalTarget) {
		if (globalTarget) {
			prepareBindings(currentGlobals(), getContext().getBindings(ScriptContext.ENGINE_SCOPE));
		}
		Method[] methods = clasz.getMethods();
		for (int i = 0; i < methods.length; ++i) {
			Method method = methods[i];
			if (method.getDeclaringClass() == Object.class) {
				continue;
			}
			if (!target.get(method.getName()).isfunction()) {
				return false;
			}
		}
		return true;
	}

	private <T> T createInterfaceProxy(final LuaValue target, final Class<T> clasz, final boolean methodStyle) {
		return clasz.cast(Proxy.newProxyInstance(
				clasz.getClassLoader(),
				new Class[] { clasz },
				new InvocationHandler() {
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						if (method.getDeclaringClass() == Object.class) {
							String name = method.getName();
							if ("toString".equals(name)) {
								return "LuajInterfaceProxy(" + clasz.getName() + ")";
							}
							if ("hashCode".equals(name)) {
								return Integer.valueOf(System.identityHashCode(proxy));
							}
							if ("equals".equals(name)) {
								return Boolean.valueOf(proxy == args[0]);
							}
						}
						LuaValue function = target.get(method.getName());
						if (!function.isfunction()) {
							throw new NoSuchMethodException(method.getName());
						}
						Varargs result = methodStyle ? target.invokemethod(method.getName(), toLuaVarargs(args)) : function.invoke(toLuaVarargs(args));
						LuaValue value = result.arg1();
						return CoerceLuaToJava.coerce(value, method.getReturnType());
					}
				}));
	}


	class LuajCompiledScript extends CompiledScript {
		final LuaFunction function;
		final Globals compiling_globals;
		LuajCompiledScript(LuaFunction function, Globals compiling_globals) {
			this.function = function;
			this.compiling_globals = compiling_globals;
		}

		public ScriptEngine getEngine() {
			return LuaScriptEngine.this;
		}

		public Object eval() throws ScriptException {
			return eval(getContext());
		}

		public Object eval(Bindings bindings) throws ScriptException {
			return eval(((LuajContext) getContext()).globals, bindings);
		}

		public Object eval(ScriptContext context) throws ScriptException {
			return eval(((LuajContext) context).globals, context.getBindings(ScriptContext.ENGINE_SCOPE));
		}

		Object eval(Globals g, Bindings b) throws ScriptException {
			prepareBindings(g, b);
			LuaFunction f = function;
			if (f.isclosure())
				f = new LuaClosure(f.checkclosure().p, g);
			else {
				try {
					f = f.getClass().newInstance();
				} catch (Exception e) {
					throw new ScriptException(e);
				}
				f.initupvalue1(g);
			}
			return toJava(f.invoke(LuaValue.NONE));
		}
	}

	// ------ convert char stream to byte stream for lua compiler ----- 

	private final class Utf8Encoder extends InputStream {
		private final Reader r;
		private final int[] buf = new int[2];
		private int n;

		private Utf8Encoder(Reader r) {
			this.r = r;
		}

		public int read() throws IOException {
			if ( n > 0 )
				return buf[--n];
			int c = r.read();
			if ( c < 0x80 )
				return c;
			n = 0;
			if ( c < 0x800 ) {
				buf[n++] = (0x80 | ( c      & 0x3f));
				return     (0xC0 | ((c>>6)  & 0x1f));
			} else {
				buf[n++] = (0x80 | ( c      & 0x3f));
				buf[n++] = (0x80 | ((c>>6)  & 0x3f));
				return     (0xE0 | ((c>>12) & 0x0f));
			}
		}
	}

	static class BindingsMetatable extends LuaTable {

		BindingsMetatable(final Bindings bindings) {
			this.rawset(LuaValue.INDEX, new TwoArgFunction() {
				public LuaValue call(LuaValue table, LuaValue key) {
					if (key.isstring())
						return toLua(bindings.get(key.tojstring()));
					else
						return this.rawget(key);
				}
			});
			this.rawset(LuaValue.NEWINDEX, new ThreeArgFunction() {
				public LuaValue call(LuaValue table, LuaValue key, LuaValue value) {
					if (key.isstring()) {
						final String k = key.tojstring();
						final Object v = toJava(value);
						if (v == null)
							bindings.remove(k);
						else
							bindings.put(k, v);
					} else {
						this.rawset(key, value);
					}
					return LuaValue.NONE;
				}
			});
		}
	}

	static private LuaValue toLua(Object javaValue) {
		return javaValue == null? LuaValue.NIL:
				javaValue instanceof LuaValue? (LuaValue) javaValue:
						CoerceJavaToLua.coerce(javaValue);
	}

	static private Object toJava(LuaValue luajValue) {
		switch ( luajValue.type() ) {
			case LuaValue.TNIL: return null;
			case LuaValue.TSTRING: return luajValue.tojstring();
			case LuaValue.TUSERDATA: return luajValue.checkuserdata(Object.class);
			case LuaValue.TNUMBER: return luajValue.isinttype()?
					(Object) Long.valueOf(luajValue.tolong()):
					(Object) Double.valueOf(luajValue.todouble());
			default: return luajValue;
		}
	}

	static private Object toJava(Varargs v) {
		final int n = v.narg();
		switch (n) {
			case 0: return null;
			case 1: return toJava(v.arg1());
			default:
				Object[] o = new Object[n];
				for (int i=0; i<n; ++i)
					o[i] = toJava(v.arg(i+1));
				return o;
		}
	}

}
