/*******************************************************************************
* Copyright (c) 2009 Luaj.org. 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.libs.jse;


import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.compiler.LuaC;
import org.luaj.vm2.libs.LibFunction;
import org.luaj.vm2.libs.VarArgFunction;

/**
 * Subclass of {@link LibFunction} which implements the features of the luajava package.
 * <p>
 * Luajava is an approach to mixing lua and java using simple functions that bind
 * java classes and methods to lua dynamically.  The API is documented on the
 * <a href="http://www.keplerproject.org/luajava/">luajava</a> documentation pages.
 * 
 * <p>
 * Typically, this library is included as part of a call to
 * {@link JsePlatform#standardGlobals()}
 * <pre> {@code
 * Globals globals = JsePlatform.standardGlobals();
 * System.out.println( globals.get("luajava").get("bindClass").call( LuaValue.valueOf("java.lang.System") ).invokeMethod("currentTimeMillis") );
 * } </pre>
 * <p>
 * To instantiate and use it directly,
 * link it into your globals table via {@link Globals#load} using code such as:
 * <pre> {@code
 * Globals globals = new Globals();
 * globals.load(new JseBaseLib());
 * globals.load(new PackageLib());
 * globals.load(new LuajavaLib());
 * globals.load(
 *      "sys = luajava.bindClass('java.lang.System')\n"+
 *      "print ( sys:currentTimeMillis() )\n", "main.lua" ).call();
 * } </pre>
 * <p>
 * 
 * The {@code luajava} library is available
 * on all JSE platforms via the call to {@link JsePlatform#standardGlobals()}
 * and the luajava api's are simply invoked from lua.
 * Because it makes extensive use of Java's reflection API, it is not available
 * on JME, but can be used in Android applications.
 * <p>
 * This has been implemented to match as closely as possible the behavior in the corresponding library in C.
 * 
 * @see LibFunction
 * @see JsePlatform
 * @see org.luaj.vm2.libs.jme.JmePlatform
 * @see LuaC
 * @see CoerceJavaToLua
 * @see CoerceLuaToJava
 * @see <a href="http://www.keplerproject.org/luajava/manual.html#luareference">http://www.keplerproject.org/luajava/manual.html#luareference</a>
 */
public class LuajavaLib extends VarArgFunction {

	public interface VisibilityPolicy {
		boolean isClassVisible(Class clazz);
		boolean isInnerClassVisible(Class owner, Class innerClass);
		boolean isFieldVisible(Class owner, Field field);
		boolean isMethodVisible(Class owner, Method method);
		boolean isConstructorVisible(Class owner, Constructor constructor);
	}

	private static final VisibilityPolicy PUBLIC_MEMBERS_POLICY = new VisibilityPolicy() {
		public boolean isClassVisible(Class clazz) {
			return true;
		}

		public boolean isInnerClassVisible(Class owner, Class innerClass) {
			return true;
		}

		public boolean isFieldVisible(Class owner, Field field) {
			return true;
		}

		public boolean isMethodVisible(Class owner, Method method) {
			return true;
		}

		public boolean isConstructorVisible(Class owner, Constructor constructor) {
			return true;
		}
	};

	private static volatile VisibilityPolicy visibilityPolicy = PUBLIC_MEMBERS_POLICY;

	static final int INIT           = 0;
	static final int BINDCLASS      = 1;
	static final int NEWINSTANCE	= 2;
	static final int NEW			= 3;
	static final int CREATEPROXY	= 4;
	static final int LOADLIB		= 5;

	static final String[] NAMES = {
		"bindClass",
		"newInstance",
		"new",
		"createProxy",
		"loadLib",
	};
	
	static final int METHOD_MODIFIERS_VARARGS = 0x80;

	public LuajavaLib() {
	}

	public static void setVisibilityPolicy(VisibilityPolicy policy) {
		visibilityPolicy = policy != null ? policy : PUBLIC_MEMBERS_POLICY;
		JavaClass.flushClassCache();
	}

	public static VisibilityPolicy getVisibilityPolicy() {
		return visibilityPolicy;
	}

	static boolean isClassVisible(Class clazz) {
		return visibilityPolicy.isClassVisible(clazz);
	}

	static boolean isInnerClassVisible(Class owner, Class innerClass) {
		return visibilityPolicy.isInnerClassVisible(owner, innerClass);
	}

	static boolean isFieldVisible(Class owner, Field field) {
		return visibilityPolicy.isFieldVisible(owner, field);
	}

	static boolean isMethodVisible(Class owner, Method method) {
		return visibilityPolicy.isMethodVisible(owner, method);
	}

	static boolean isConstructorVisible(Class owner, Constructor constructor) {
		return visibilityPolicy.isConstructorVisible(owner, constructor);
	}

	public Varargs invoke(Varargs args) {
		try {
			switch ( opcode ) {
			case INIT: {
				// LuaValue modname = args.arg1();
				LuaValue env = args.arg(2);
				LuaTable t = new LuaTable();
				bind( t, this.getClass(), NAMES, BINDCLASS );
				env.set("luajava", t);
				if (!env.get("package").isnil()) env.get("package").get("loaded").set("luajava", t);
				return t;
			}
			case BINDCLASS: {
				final Class clazz = classForName(args.checkjstring(1));
				if (!isClassVisible(clazz))
					throw new LuaError("class is not visible: " + clazz.getName());
				return JavaClass.forClass(clazz);
			}
			case NEWINSTANCE:
			case NEW: {
				// get constructor
				final LuaValue c = args.checkvalue(1);
				final Class clazz = (opcode==NEWINSTANCE? classForName(c.tojstring()): (Class) c.checkuserdata(Class.class));
				if (!isClassVisible(clazz))
					throw new LuaError("class is not visible: " + clazz.getName());
				final Varargs consargs = args.subargs(2);
				return JavaClass.forClass(clazz).getConstructor().invoke(consargs);
			}
				
			case CREATEPROXY: {
				final int niface = args.narg()-1;
				if ( niface <= 0 )
					throw new LuaError("no interfaces");
				final LuaValue lobj = args.checktable(niface+1);
				
				// get the interfaces
				final Class[] ifaces = new Class[niface];
				for ( int i=0; i<niface; i++ )
					ifaces[i] = classForName(args.checkjstring(i+1));
				
				// create the invocation handler
				InvocationHandler handler = new ProxyInvocationHandler(lobj);
				
				// create the proxy object
				Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(), ifaces, handler);
				
				// return the proxy
				return LuaValue.userdataOf( proxy );
			}
			case LOADLIB: {
				// get constructor
				String classname = args.checkjstring(1);
				String methodname = args.checkjstring(2);
				Class clazz = classForName(classname);
				Method method = clazz.getMethod(methodname, new Class[] {});
				Object result = method.invoke(clazz, new Object[] {});
				if ( result instanceof LuaValue ) {
					return (LuaValue) result;
				} else {
					return NIL;
				}
			}
			default:
				throw new LuaError("not yet supported: "+this);
			}
		} catch (LuaError e) {
			throw e;
		} catch (InvocationTargetException ite) {
			throw new LuaError(ite.getTargetException());
		} catch (Exception e) {
			throw new LuaError(e);
		}
	}

	// load classes using app loader to allow luaj to be used as an extension
	protected Class classForName(String name) throws ClassNotFoundException {
		Class clazz = null;
		ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
		if (contextLoader != null) {
			try {
				clazz = Class.forName(name, true, contextLoader);
			} catch (ClassNotFoundException ignored) {
			}
		}
		if (clazz == null) {
			ClassLoader libraryLoader = getClass().getClassLoader();
			if (libraryLoader != null) {
				try {
					clazz = Class.forName(name, true, libraryLoader);
				} catch (ClassNotFoundException ignored) {
				}
			}
		}
		if (clazz == null) {
			clazz = Class.forName(name, true, ClassLoader.getSystemClassLoader());
		}
		if (!isClassVisible(clazz))
			throw new ClassNotFoundException("class is not visible: " + name);
		return clazz;
	}
	
	private static final class ProxyInvocationHandler implements InvocationHandler {
		private final LuaValue lobj;

		private ProxyInvocationHandler(LuaValue lobj) {
			this.lobj = lobj;
		}

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			String name = method.getName();
			LuaValue func = lobj.get(name);
			if ( func.isnil() )
				return null;
			boolean isvarargs = ((method.getModifiers() & METHOD_MODIFIERS_VARARGS) != 0);
			int n = args!=null? args.length: 0;
			LuaValue[] v;
			if ( isvarargs ) {
				Object o = args[--n];
				int m = Array.getLength( o );
				v = new LuaValue[n+m];
				for ( int i=0; i<n; i++ )
					v[i] = CoerceJavaToLua.coerce(args[i]);
				for ( int i=0; i<m; i++ )
					v[i+n] = CoerceJavaToLua.coerce(Array.get(o,i));
			} else {
				v = new LuaValue[n];
				for ( int i=0; i<n; i++ )
					v[i] = CoerceJavaToLua.coerce(args[i]);
			}
			LuaValue result = func.invoke(v).arg1();
			return CoerceLuaToJava.coerce(result, method.getReturnType());
		}
	}
	
}
