/*
 * $Id: DefaultJavaReflector.java 173 2013-07-28 20:46:07Z andre@naef.com $
 * See LICENSE.txt for license terms.
 */

package com.naef.jnlua;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Default implementation of the <code>JavaReflector</code> interface.
 */
public class DefaultJavaReflector implements JavaReflector {
	// -- Static
	private static final DefaultJavaReflector INSTANCE = new DefaultJavaReflector();
	private static final Object JAVA_FUNCTION_TYPE = new Object();
	private static final Object[] EMPTY_ARGUMENTS = new Object[0];

	// -- State
	private Map<Class<?>, Map<String, Accessor>> accessors = new HashMap<Class<?>, Map<String, Accessor>>();
	private ReadWriteLock accessorLock = new ReentrantReadWriteLock();
	private Map<LuaCallSignature, Invocable> invocableDispatches = new HashMap<LuaCallSignature, Invocable>();
	private ReadWriteLock invocableDispatchLock = new ReentrantReadWriteLock();
	private JavaFunction index = new Index();
	private JavaFunction newIndex = new NewIndex();
	private JavaFunction equal = new Equal();
	private JavaFunction length = new Length();
	private JavaFunction lessThan = new LessThan();
	private JavaFunction lessThanOrEqual = new LessThanOrEqual();
	private JavaFunction toString = new ToString();
	private JavaFunction pairs = new Pairs();
	private JavaFunction ipairs = new IPairs();
	private JavaFunction javaFields = new AccessorPairs(FieldAccessor.class);
	private JavaFunction javaMethods = new AccessorPairs(
			InvocableAccessor.class);
	private JavaFunction javaProperties = new AccessorPairs(
			PropertyAccessor.class);

	// -- Static methods
	/**
	 * Returns the instance of this class.
	 * 
	 * @return the instance
	 */
	public static DefaultJavaReflector getInstance() {
		return INSTANCE;
	}

	// -- Construction
	/**
	 * Creates a new instances;
	 */
	private DefaultJavaReflector() {
	}

	// -- JavaReflector methods
	@Override
	public JavaFunction getMetamethod(Metamethod metamethod) {
		switch (metamethod) {
		case INDEX:
			return index;
		case NEWINDEX:
			return newIndex;
		case LEN:
			return length;
		case EQ:
			return equal;
		case LT:
			return lessThan;
		case LE:
			return lessThanOrEqual;
		case TOSTRING:
			return toString;
		case PAIRS:
			return pairs;
		case IPAIRS:
			return ipairs;
		case JAVAFIELDS:
			return javaFields;
		case JAVAMETHODS:
			return javaMethods;
		case JAVAPROPERTIES:
			return javaProperties;
		default:
			return null;
		}
	}

	// -- Private methods
	/**
	 * Returns the accessors of an object.
	 */
	private Map<String, Accessor> getObjectAccessors(Object object) {
		// Check cache
		Class<?> clazz = getObjectClass(object);
		accessorLock.readLock().lock();
		try {
			Map<String, Accessor> result = accessors.get(clazz);
			if (result != null) {
				return result;
			}
		} finally {
			accessorLock.readLock().unlock();
		}

		// Fill in
		Map<String, Accessor> result = createClassAccessors(clazz);
		accessorLock.writeLock().lock();
		try {
			if (!accessors.containsKey(clazz)) {
				accessors.put(clazz, result);
			} else {
				result = accessors.get(clazz);
			}
		} finally {
			accessorLock.writeLock().unlock();
		}
		return result;
	}

	/**
	 * Creates the accessors of a class.
	 */
	private Map<String, Accessor> createClassAccessors(Class<?> clazz) {
		Map<String, Accessor> result = new HashMap<String, Accessor>();

		// Fields
		Field[] fields = clazz.getFields();
		for (int i = 0; i < fields.length; i++) {
			result.put(fields[i].getName(), new FieldAccessor(fields[i]));
		}

		// Methods
		Map<String, Map<List<Class<?>>, Invocable>> accessibleMethods = new HashMap<String, Map<List<Class<?>>, Invocable>>();
		Method[] methods = clazz.getMethods();
		for (int i = 0; i < methods.length; i++) {
			// Do not overwrite fields
			Method method = methods[i];
			if (result.containsKey(method.getName())) {
				continue;
			}

			// Attempt to find the method in a public class if the declaring
			// class is not public
			if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
				method = getPublicClassMethod(clazz, method.getName(),
						method.getParameterTypes());
				if (method == null) {
					continue;
				}
			}

			// For each method name and parameter type list, keep
			// only the method declared by the most specific class
			Map<List<Class<?>>, Invocable> overloaded = accessibleMethods
					.get(method.getName());
			if (overloaded == null) {
				overloaded = new HashMap<List<Class<?>>, Invocable>();
				accessibleMethods.put(method.getName(), overloaded);
			}
			List<Class<?>> parameterTypes = Arrays.asList(method
					.getParameterTypes());
			Invocable currentInvocable = overloaded.get(parameterTypes);
			if (currentInvocable != null
					&& method.getDeclaringClass().isAssignableFrom(
							currentInvocable.getDeclaringClass())) {
				continue;
			}
			overloaded.put(parameterTypes, new InvocableMethod(method));
		}
		for (Map.Entry<String, Map<List<Class<?>>, Invocable>> entry : accessibleMethods
				.entrySet()) {
			result.put(entry.getKey(), new InvocableAccessor(clazz, entry
					.getValue().values()));
		}

		// Constructors
		Constructor<?>[] constructors = clazz.getConstructors();
		List<Invocable> accessibleConstructors = new ArrayList<Invocable>(
				constructors.length);
		for (int i = 0; i < constructors.length; i++) {
			// Ignore constructor if the declaring class is not public
			if (!Modifier.isPublic(constructors[i].getDeclaringClass()
					.getModifiers())) {
				continue;
			}
			accessibleConstructors
					.add(new InvocableConstructor(constructors[i]));
		}
		if (clazz.isInterface()) {
			accessibleConstructors.add(new InvocableProxy(clazz));
		}
		if (!accessibleConstructors.isEmpty()) {
			result.put("new", new InvocableAccessor(clazz,
					accessibleConstructors));
		}

		// Properties
		BeanInfo beanInfo;
		try {
			beanInfo = Introspector.getBeanInfo(clazz);
		} catch (IntrospectionException e) {
			throw new RuntimeException(e);
		}
		PropertyDescriptor[] propertyDescriptors = beanInfo
				.getPropertyDescriptors();
		for (int i = 0; i < propertyDescriptors.length; i++) {
			// Do not overwrite fields or methods
			if (result.containsKey(propertyDescriptors[i].getName())) {
				continue;
			}

			// Attempt to find the read/write methods in a public class if the
			// declaring class is not public
			Method method = propertyDescriptors[i].getReadMethod();
			if (method != null
					&& !Modifier.isPublic(method.getDeclaringClass()
							.getModifiers())) {
				method = getPublicClassMethod(clazz, method.getName(),
						method.getParameterTypes());
				try {
					propertyDescriptors[i].setReadMethod(method);
				} catch (IntrospectionException e) {
				}
			}
			method = propertyDescriptors[i].getWriteMethod();
			if (method != null
					&& !Modifier.isPublic(method.getDeclaringClass()
							.getModifiers())) {
				method = getPublicClassMethod(clazz, method.getName(),
						method.getParameterTypes());
				try {
					propertyDescriptors[i].setWriteMethod(method);
				} catch (IntrospectionException e) {
				}
			}

			// Do not process properties without a read and a write method
			if (propertyDescriptors[i].getReadMethod() == null
					&& propertyDescriptors[i].getWriteMethod() == null) {
				continue;
			}
			result.put(propertyDescriptors[i].getName(), new PropertyAccessor(
					clazz, propertyDescriptors[i]));
		}
		return result;
	}

	/**
	 * Returns a public class method matching a method name and parameter list.
	 * The public class can be a superclass or interface.
	 */
	private Method getPublicClassMethod(Class<?> clazz, String methodName,
			Class<?>[] parameterTypes) {
		Method method = getPublicSuperclassMethod(clazz, methodName,
				parameterTypes);
		if (method != null) {
			return method;
		}
		return getInterfaceMethod(clazz, methodName, parameterTypes);
	}

	/**
	 * Returns a public superclass method matching a method name and parameter
	 * list.
	 */
	private Method getPublicSuperclassMethod(Class<?> clazz, String methodName,
			Class<?>[] parameterTypes) {
		Class<?> superclass = clazz.getSuperclass();
		while (superclass != null) {
			// Process public superclasses only
			if (Modifier.isPublic(superclass.getModifiers())) {
				// Find method in superclass
				try {
					Method method = superclass.getDeclaredMethod(methodName,
							parameterTypes);
					if (Modifier.isPublic(method.getModifiers())) {
						return method;
					}
				} catch (NoSuchMethodException e) {
					// Not found
				}
			}

			// Check superclass
			superclass = superclass.getSuperclass();
		}

		// Not found
		return null;
	}

	/**
	 * Returns an interface method matching a method name and parameter list.
	 */
	private Method getInterfaceMethod(Class<?> clazz, String methodName,
			Class<?>[] parameterTypes) {
		do {
			// Get interfaces
			Class<?>[] interfaces = clazz.getInterfaces();
			for (int i = 0; i < interfaces.length; i++) {
				// Ignore non-public interfaces
				if (!Modifier.isPublic(interfaces[i].getModifiers())) {
					continue;
				}

				// Find method in the current interface
				try {
					return interfaces[i].getDeclaredMethod(methodName,
							parameterTypes);
				} catch (NoSuchMethodException e) {
					// Not found
				}

				// Check superinterfaces
				Method method = getInterfaceMethod(interfaces[i], methodName,
						parameterTypes);
				if (method != null) {
					return method;
				}
			}

			// Check superclass
			clazz = clazz.getSuperclass();
		} while (clazz != null);

		// Not found
		return null;
	}

	/**
	 * Returns the class of an object, or the class itself if the object is
	 * already a class.
	 */
	private Class<?> getObjectClass(Object object) {
		return object instanceof Class<?> ? (Class<?>) object : object
				.getClass();
	}

	// -- Nested types
	/**
	 * <code>__index</code> metamethod implementation.
	 */
	private class Index implements JavaFunction {
		public int invoke(LuaState luaState) {
			// Get object and class
			Object object = luaState.toJavaObject(1, Object.class);
			Class<?> objectClass = getObjectClass(object);

			// Handle arrays
			if (objectClass.isArray()) {
				if (!luaState.isNumber(2)) {
					throw new LuaRuntimeException(String.format(
							"attempt to read array with %s accessor",
							luaState.typeName(2)));
				}
				int index = luaState.toInteger(2);
				int length = Array.getLength(object);
				if (index < 1 || index > length) {
					throw new LuaRuntimeException(String.format(
							"attempt to read array of length %d at index %d",
							length, index));
				}
				luaState.pushJavaObject(Array.get(object, index - 1));
				return 1;
			}

			// Handle objects
			Map<String, Accessor> objectAccessors = getObjectAccessors(object);
			String key = luaState.toString(-1);
			if (key == null) {
				throw new LuaRuntimeException(String.format(
						"attempt to read class %s with %s accessor", object
								.getClass().getCanonicalName(), luaState
								.typeName(-1)));
			}
			Accessor accessor = objectAccessors.get(key);
			if (accessor == null) {
				throw new LuaRuntimeException(
						String.format(
								"attempt to read class %s with accessor '%s' (undefined)",
								objectClass.getCanonicalName(), key));
			}
			accessor.read(luaState, object);
			return 1;
		}
	}

	/**
	 * <code>__newindex</code> metamethod implementation.
	 */
	private class NewIndex implements JavaFunction {
		public int invoke(LuaState luaState) {
			// Get object and class
			Object object = luaState.toJavaObject(1, Object.class);
			Class<?> objectClass = getObjectClass(object);

			// Handle arrays
			if (objectClass.isArray()) {
				if (!luaState.isNumber(2)) {
					throw new LuaRuntimeException(String.format(
							"attempt to write array with %s accessor",
							luaState.typeName(2)));
				}
				int index = luaState.toInteger(2);
				int length = Array.getLength(object);
				if (index < 1 || index > length) {
					throw new LuaRuntimeException(String.format(
							"attempt to write array of length %d at index %d",
							length, index));
				}
				Class<?> componentType = objectClass.getComponentType();
				if (!luaState.isJavaObject(3, componentType)) {
					throw new LuaRuntimeException(
							String.format(
									"attempt to write array of %s at index %d with %s value",
									componentType.getCanonicalName(),
									luaState.typeName(3)));
				}
				Object value = luaState.toJavaObject(3, componentType);
				Array.set(object, index - 1, value);
				return 0;
			}

			// Handle objects
			Map<String, Accessor> objectAccessors = getObjectAccessors(object);
			String key = luaState.toString(2);
			if (key == null) {
				throw new LuaRuntimeException(String.format(
						"attempt to write class %s with %s accessor", object
								.getClass().getCanonicalName(), luaState
								.typeName(2)));
			}
			Accessor accessor = objectAccessors.get(key);
			if (accessor == null) {
				throw new LuaRuntimeException(
						String.format(
								"attempt to write class %s with accessor '%s' (undefined)",
								objectClass.getCanonicalName(), key));
			}
			accessor.write(luaState, object);
			return 0;
		}
	}

	/**
	 * <code>__len</code> metamethod implementation.
	 */
	private class Length implements JavaFunction {
		@Override
		public int invoke(LuaState luaState) {
			Object object = luaState.toJavaObject(1, Object.class);
			if (object.getClass().isArray()) {
				luaState.pushInteger(Array.getLength(object));
				return 1;
			}
			luaState.pushInteger(0);
			return 1;
		}
	}

	/**
	 * <code>__eq</code> metamethod implementation.
	 */
	private class Equal implements JavaFunction {
		@Override
		public int invoke(LuaState luaState) {
			Object object1 = luaState.toJavaObject(1, Object.class);
			Object object2 = luaState.toJavaObject(2, Object.class);
			luaState.pushBoolean(object1 == object2 || object1 != null
					&& object1.equals(object2));
			return 1;
		}
	}

	/**
	 * <code>__lt</code> metamethod implementation.
	 */
	private class LessThan implements JavaFunction {
		@SuppressWarnings("unchecked")
		@Override
		public int invoke(LuaState luaState) {
			if (!luaState.isJavaObject(1, Comparable.class)) {
				throw new LuaRuntimeException(String.format(
						"class %s does not implement Comparable",
						luaState.typeName(1)));
			}
			Comparable<Object> comparable = luaState.toJavaObject(1,
					Comparable.class);
			Object object = luaState.toJavaObject(2, Object.class);
			luaState.pushBoolean(comparable.compareTo(object) < 0);
			return 1;
		}
	}

	/**
	 * <code>__le</code> metamethod implementation.
	 */
	private class LessThanOrEqual implements JavaFunction {
		@SuppressWarnings("unchecked")
		@Override
		public int invoke(LuaState luaState) {
			if (!luaState.isJavaObject(1, Comparable.class)) {
				throw new LuaRuntimeException(String.format(
						"class %s does not implement Comparable",
						luaState.typeName(1)));
			}
			Comparable<Object> comparable = luaState.toJavaObject(1,
					Comparable.class);
			Object object = luaState.toJavaObject(2, Object.class);
			luaState.pushBoolean(comparable.compareTo(object) <= 0);
			return 1;
		}
	}

	/**
	 * <code>__tostring</code> metamethod implementation.
	 */
	private class ToString implements JavaFunction {
		@Override
		public int invoke(LuaState luaState) {
			Object object = luaState.toJavaObject(1, Object.class);
			luaState.pushString(object != null ? object.toString() : "null");
			return 1;
		}
	}

	/**
	 * Provides an iterator for maps. For <code>NavigableMap</code> objects, the
	 * function returns a stateless iterator which allows concurrent
	 * modifications to the map. For other maps, the function returns an
	 * iterator based on <code>Iterator</code> which does not support concurrent
	 * modifications.
	 */
	private static class Pairs implements NamedJavaFunction {
		// -- Static
		private final JavaFunction navigableMapNext = new NavigableMapNext();

		// -- JavaFunction methods
		@SuppressWarnings("unchecked")
		@Override
		public int invoke(LuaState luaState) {
			Map<Object, Object> map = luaState.checkJavaObject(1, Map.class);
			luaState.checkArg(1, map != null,
					String.format("expected map, got %s", luaState.typeName(1)));
			if (map instanceof NavigableMap) {
				luaState.pushJavaFunction(navigableMapNext);
			} else {
				luaState.pushJavaFunction(new MapNext(map.entrySet().iterator()));
			}
			luaState.pushJavaObject(map);
			luaState.pushNil();
			return 3;
		}

		@Override
		public String getName() {
			return "pairs";
		}

		/**
		 * Provides a stateful iterator function for maps.
		 */
		private static class MapNext implements JavaFunction {
			// -- State
			private Iterator<Map.Entry<Object, Object>> iterator;

			// -- Construction
			/**
			 * Creates a new instance.
			 */
			public MapNext(Iterator<Map.Entry<Object, Object>> iterator) {
				this.iterator = iterator;
			}

			// -- JavaFunction methods
			public int invoke(LuaState luaState) {
				if (iterator.hasNext()) {
					Map.Entry<Object, Object> entry = iterator.next();
					luaState.pushJavaObject(entry.getKey());
					luaState.pushJavaObject(entry.getValue());
					return 2;
				} else {
					luaState.pushNil();
					return 1;
				}
			}
		}

		/**
		 * Provides a stateless iterator function for navigable maps.
		 */
		private static class NavigableMapNext implements JavaFunction {
			// -- JavaFunction methods
			@SuppressWarnings("unchecked")
			public int invoke(LuaState luaState) {
				NavigableMap<Object, Object> navigableMap = luaState
						.checkJavaObject(1, NavigableMap.class);
				Object key = luaState.checkJavaObject(2, Object.class);
				Map.Entry<Object, Object> entry;
				if (key != null) {
					entry = navigableMap.higherEntry(key);
				} else {
					entry = navigableMap.firstEntry();
				}
				if (entry != null) {
					luaState.pushJavaObject(entry.getKey());
					luaState.pushJavaObject(entry.getValue());
					return 2;
				} else {
					luaState.pushNil();
					return 1;
				}
			}
		}
	}

	/**
	 * Provides an iterator for lists and arrays.
	 */
	private static class IPairs implements NamedJavaFunction {
		// -- Static
		private final JavaFunction listNext = new ListNext();
		private final JavaFunction arrayNext = new ArrayNext();

		// -- JavaFunction methods
		@Override
		public int invoke(LuaState luaState) {
			Object object;
			if (luaState.isJavaObject(1, List.class)) {
				object = luaState.toJavaObject(1, List.class);
				luaState.pushJavaFunction(listNext);
			} else {
				object = luaState.checkJavaObject(1, Object.class);
				luaState.checkArg(1, object.getClass().isArray(), String
						.format("expected list or array, got %s",
								luaState.typeName(1)));
				luaState.pushJavaFunction(arrayNext);
			}
			luaState.pushJavaObject(object);
			luaState.pushInteger(0);
			return 3;
		}

		@Override
		public String getName() {
			return "ipairs";
		}

		/**
		 * Provides a stateless iterator function for lists.
		 */
		private static class ListNext implements JavaFunction {
			public int invoke(LuaState luaState) {
				List<?> list = luaState.checkJavaObject(1, List.class);
				int size = list.size();
				int index = luaState.checkInteger(2);
				index++;
				if (index >= 1 && index <= size) {
					luaState.pushInteger(index);
					luaState.pushJavaObject(list.get(index - 1));
					return 2;
				} else {
					luaState.pushNil();
					return 1;
				}
			}
		}

		/**
		 * Provides a stateless iterator function for arrays.
		 */
		private static class ArrayNext implements JavaFunction {
			public int invoke(LuaState luaState) {
				Object array = luaState.checkJavaObject(1, Object.class);
				int length = java.lang.reflect.Array.getLength(array);
				int index = luaState.checkInteger(2);
				index++;
				if (index >= 1 && index <= length) {
					luaState.pushInteger(index);
					luaState.pushJavaObject(Array.get(array, index - 1));
					return 2;
				} else {
					luaState.pushNil();
					return 1;
				}
			}
		}
	}

	/**
	 * Provides an iterator for accessors.
	 */
	private class AccessorPairs implements JavaFunction {
		// -- State
		private Class<?> accessorClass;

		// -- Construction
		/**
		 * Creates a new instance.
		 */
		public AccessorPairs(Class<?> accessorClass) {
			this.accessorClass = accessorClass;
		}

		// -- JavaFunction methods
		@Override
		public int invoke(LuaState luaState) {
			// Get object
			Object object = luaState.toJavaObject(1, Object.class);
			Class<?> objectClass = getObjectClass(object);

			// Create iterator
			Map<String, Accessor> objectAccessors = getObjectAccessors(object);
			Iterator<Entry<String, Accessor>> iterator = objectAccessors
					.entrySet().iterator();
			luaState.pushJavaObject(new AccessorNext(iterator,
					objectClass == object));
			luaState.pushJavaObject(object);
			luaState.pushNil();
			return 3;
		}

		// -- Member types
		/**
		 * Provides the next function for iterating accessors.
		 */
		private class AccessorNext implements JavaFunction {
			// -- State
			private Iterator<Entry<String, Accessor>> iterator;
			private boolean isStatic;

			// -- Construction
			/**
			 * Creates a new instance.
			 */
			public AccessorNext(Iterator<Entry<String, Accessor>> iterator,
					boolean isStatic) {
				this.iterator = iterator;
				this.isStatic = isStatic;
			}

			// -- JavaFunction methods
			@Override
			public int invoke(LuaState luaState) {
				while (iterator.hasNext()) {
					Entry<String, Accessor> entry = iterator.next();
					Accessor accessor = entry.getValue();

					// Filter by accessor class
					if (accessor.getClass() != accessorClass) {
						continue;
					}

					// Filter by non-static, static
					if (isStatic) {
						if (!accessor.isStatic()) {
							continue;
						}
					} else {
						if (!accessor.isNotStatic()) {
							continue;
						}
					}

					// Push match
					luaState.pushString(entry.getKey());
					Object object = luaState.toJavaObject(1, Object.class);
					accessor.read(luaState, object);
					return 2;
				}

				// End iteration
				return 0;
			}
		}
	}

	/**
	 * Provides access to class or object members.
	 */
	private interface Accessor {
		/**
		 * Reads the object member.
		 */
		void read(LuaState luaState, Object object);

		/**
		 * Writes the object member.
		 */
		void write(LuaState luaState, Object object);

		/**
		 * Returns whether this accessor is applicable in a non-static context.
		 */
		boolean isNotStatic();

		/**
		 * Returns whether this accessor is applicable in a static context.
		 */
		boolean isStatic();
	}

	/**
	 * Provides field access.
	 */
	private class FieldAccessor implements Accessor {
		// -- State
		private Field field;

		// -- Construction
		/**
		 * Creates a new instance.
		 */
		public FieldAccessor(Field field) {
			this.field = field;
		}

		// -- Accessor methods
		@Override
		public void read(LuaState luaState, Object object) {
			try {
				Class<?> objectClass = getObjectClass(object);
				if (objectClass == object) {
					object = null;
				}
				luaState.pushJavaObject(field.get(object));
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			}
		}

		@Override
		public void write(LuaState luaState, Object object) {
			try {
				Class<?> objectClass = getObjectClass(object);
				if (objectClass == object) {
					object = null;
				}
				Object value = luaState.checkJavaObject(-1, field.getType());
				field.set(object, value);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			}
		}

		@Override
		public boolean isNotStatic() {
			return !Modifier.isStatic(field.getModifiers());
		}

		@Override
		public boolean isStatic() {
			return Modifier.isStatic(field.getModifiers());
		}
	}

	/**
	 * Provides invocable access.
	 */
	private class InvocableAccessor implements Accessor, JavaFunction {
		// -- State
		private Class<?> clazz;
		private List<Invocable> invocables;

		// -- Construction
		/**
		 * Creates a new instance.
		 */
		public InvocableAccessor(Class<?> clazz,
				Collection<Invocable> invocables) {
			this.clazz = clazz;
			this.invocables = new ArrayList<Invocable>(invocables);
		}

		// -- Properties
		/**
		 * Returns the name of the invocable.
		 */
		public String getName() {
			return invocables.get(0).getName();
		}

		/**
		 * Returns what this invocable accessor is for.
		 */
		public String getWhat() {
			return invocables.get(0).getWhat();
		}

		// -- Accessor methods
		@Override
		public void read(LuaState luaState, Object object) {
			Class<?> objectClass = getObjectClass(object);
			if (objectClass == object) {
				object = null;
			}
			luaState.pushJavaFunction(this);
		}

		@Override
		public void write(LuaState luaState, Object object) {
			Class<?> objectClass = getObjectClass(object);
			throw new LuaRuntimeException(String.format(
					"attempt to write class %s with accessor '%s' (a %s)",
					objectClass.getCanonicalName(), getName(), getWhat()));
		}

		@Override
		public boolean isNotStatic() {
			for (Invocable invocable : invocables) {
				if (!Modifier.isStatic(invocable.getModifiers())) {
					return true;
				}
			}
			return false;
		}

		@Override
		public boolean isStatic() {
			for (Invocable invocable : invocables) {
				if (Modifier.isStatic(invocable.getModifiers())) {
					return true;
				}
			}
			return false;
		}

		// -- JavaFunction methods
		@Override
		public int invoke(LuaState luaState) {
			// Argument sanity checks
			Object object = luaState.checkJavaObject(1, Object.class);
			Class<?> objectClass = getObjectClass(object);
			luaState.checkArg(1, clazz.isAssignableFrom(objectClass), String
					.format("class %s is not a subclass of %s",
							objectClass.getCanonicalName(),
							clazz.getCanonicalName()));
			if (objectClass == object) {
				object = null;
			}

			// Invocable dispatch
			LuaCallSignature luaCallSignature = getLuaCallSignature(luaState);
			Invocable invocable;
			invocableDispatchLock.readLock().lock();
			try {
				invocable = invocableDispatches.get(luaCallSignature);
			} finally {
				invocableDispatchLock.readLock().unlock();
			}
			if (invocable == null) {
				invocable = dispatchInvocable(luaState, object == null);
				invocableDispatchLock.writeLock().lock();
				try {
					if (!invocableDispatches.containsKey(luaCallSignature)) {
						invocableDispatches.put(luaCallSignature, invocable);
					} else {
						invocable = invocableDispatches.get(luaCallSignature);
					}
				} finally {
					invocableDispatchLock.writeLock().unlock();
				}
			}

			// Prepare arguments
			int argCount = luaState.getTop() - 1;
			int parameterCount = invocable.getParameterCount();
			Object[] arguments = new Object[parameterCount];
			if (invocable.isVarArgs()) {
				for (int i = 0; i < parameterCount - 1; i++) {
					arguments[i] = luaState.toJavaObject(i + 2,
							invocable.getParameterType(i));
				}
				arguments[parameterCount - 1] = Array.newInstance(
						invocable.getParameterType(parameterCount - 1),
						argCount - (parameterCount - 1));
				for (int i = parameterCount - 1; i < argCount; i++) {
					Array.set(
							arguments[parameterCount - 1],
							i - (parameterCount - 1),
							luaState.toJavaObject(i + 2,
									invocable.getParameterType(i)));
				}
			} else {
				for (int i = 0; i < parameterCount; i++) {
					arguments[i] = luaState.toJavaObject(i + 2,
							invocable.getParameterType(i));
				}
			}

			// Invoke
			Object result;
			try {
				result = invocable.invoke(object, arguments);
			} catch (InstantiationException e) {
				throw new RuntimeException(e);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			} catch (InvocationTargetException e) {
				throw new RuntimeException(e.getTargetException());
			}

			// Return
			if (invocable.getReturnType() != Void.TYPE) {
				if (invocable.isRawReturn()) {
					luaState.pushJavaObjectRaw(result);
				} else {
					luaState.pushJavaObject(result);
				}
				return 1;
			} else {
				return 0;
			}
		}

		// -- Private methods
		/**
		 * Creates a Lua call signature.
		 */
		private LuaCallSignature getLuaCallSignature(LuaState luaState) {
			int argCount = luaState.getTop() - 1;
			Object[] types = new Object[argCount];
			for (int i = 0; i < argCount; i++) {
				LuaType type = luaState.type(i + 2);
				switch (type) {
				case FUNCTION:
					types[i] = luaState.isJavaFunction(i + 2) ? JAVA_FUNCTION_TYPE
							: LuaType.FUNCTION;
					break;
				case USERDATA:
					if (luaState.isJavaObjectRaw(i + 2)) {
						Object object = luaState.toJavaObjectRaw(i + 2);
						if (object instanceof TypedJavaObject) {
							types[i] = ((TypedJavaObject) object).getType();
						} else {
							types[i] = object.getClass();
						}
					} else {
						types[i] = LuaType.USERDATA;
					}
					break;
				default:
					types[i] = type;
				}
			}
			return new LuaCallSignature(clazz, getName(), types);
		}

		/**
		 * Dispatches an invocable.
		 */
		private Invocable dispatchInvocable(LuaState luaState,
				boolean staticDispatch) {
			// Begin with all candidates
			Set<Invocable> candidates = new HashSet<Invocable>(invocables);

			// Eliminate methods with an invalid static modifier
			for (Iterator<Invocable> i = candidates.iterator(); i.hasNext();) {
				Invocable invocable = i.next();
				if (Modifier.isStatic(invocable.getModifiers()) != staticDispatch) {
					i.remove();
				}
			}

			// Eliminate methods with an invalid parameter count
			int argCount = luaState.getTop() - 1;
			for (Iterator<Invocable> i = candidates.iterator(); i.hasNext();) {
				Invocable invocable = i.next();
				if (invocable.isVarArgs()) {
					if (argCount < invocable.getParameterCount() - 1) {
						i.remove();
					}
				} else {
					if (argCount != invocable.getParameterCount()) {
						i.remove();
					}
				}
			}

			// Eliminate methods that are not applicable
			Converter converter = luaState.getConverter();
			outer: for (Iterator<Invocable> i = candidates.iterator(); i
					.hasNext();) {
				Invocable invocable = i.next();
				for (int j = 0; j < argCount; j++) {
					int distance = converter.getTypeDistance(luaState, j + 2,
							invocable.getParameterType(j));
					if (distance == Integer.MAX_VALUE) {
						i.remove();
						continue outer;
					}
				}
			}

			// Eliminate variable arguments methods in the presence of fix
			// arguments methods
			boolean haveFixArgs = false;
			boolean haveVarArgs = false;
			for (Invocable invocable : candidates) {
				haveFixArgs = haveFixArgs || !invocable.isVarArgs();
				haveVarArgs = haveVarArgs || invocable.isVarArgs();
			}
			if (haveVarArgs && haveFixArgs) {
				for (Iterator<Invocable> i = candidates.iterator(); i.hasNext();) {
					Invocable invocable = i.next();
					if (invocable.isVarArgs()) {
						i.remove();
					}
				}
			}

			// Eliminate methods that are not closest
			outer: for (Iterator<Invocable> i = candidates.iterator(); i
					.hasNext();) {
				Invocable invocable = i.next();
				inner: for (Invocable other : candidates) {
					if (other == invocable) {
						continue;
					}
					int parameterCount = Math.min(
							argCount,
							Math.max(invocable.getParameterCount(),
									other.getParameterCount()));
					boolean delta = false;
					for (int j = 0; j < parameterCount; j++) {
						int distance = converter.getTypeDistance(luaState,
								j + 2, invocable.getParameterType(j));
						int otherDistance = converter.getTypeDistance(luaState,
								j + 2, other.getParameterType(j));
						if (otherDistance > distance) {
							// Other is not closer
							continue inner;
						}
						delta = delta || distance != otherDistance;
					}

					// If there is no delta, other is not closer
					if (!delta) {
						continue;
					}

					// Other is closer
					i.remove();
					continue outer;
				}
			}

			// Eliminate methods that are not most precise
			outer: for (Iterator<Invocable> i = candidates.iterator(); i
					.hasNext();) {
				Invocable invocable = i.next();
				inner: for (Invocable other : candidates) {
					if (other == invocable) {
						continue;
					}
					int parameterCount = Math.min(
							argCount,
							Math.max(invocable.getParameterCount(),
									other.getParameterCount()));
					boolean delta = false;
					for (int j = 0; j < parameterCount; j++) {
						Class<?> type = invocable.getParameterType(j);
						Class<?> otherType = other.getParameterType(j);
						if (!type.isAssignableFrom(otherType)) {
							// Other is not more specific
							continue inner;
						}
						delta = delta || type != otherType;
					}

					// If there is no delta, other is not more specific
					if (!delta) {
						continue;
					}

					// Other is more specific
					i.remove();
					continue outer;
				}
			}

			// Handle outcomes
			if (candidates.isEmpty()) {
				throw getSignatureMismatchException(luaState);
			}
			if (candidates.size() > 1) {
				throw getSignatureAmbivalenceException(luaState, candidates);
			}

			// Return
			return candidates.iterator().next();
		}

		/**
		 * Returns a Lua runtime exception indicating that no matching invocable
		 * has been found.
		 */
		private LuaRuntimeException getSignatureMismatchException(
				LuaState luaState) {
			return new LuaRuntimeException(String.format(
					"no %s of class %s matches '%s(%s)'", getWhat(),
					clazz.getCanonicalName(), getName(),
					getLuaSignatureString(luaState)));
		}

		/**
		 * Returns a Lua runtime exception indicating that an invocable is
		 * ambivalent.
		 */
		private LuaRuntimeException getSignatureAmbivalenceException(
				LuaState luaState, Set<Invocable> candidates) {
			StringBuffer sb = new StringBuffer();
			sb.append(String.format(
					"%s '%s(%s)' on class %s is ambivalent among ", getWhat(),
					getName(), getLuaSignatureString(luaState),
					clazz.getCanonicalName()));
			boolean first = true;
			for (Invocable invocable : candidates) {
				if (first) {
					first = false;
				} else {
					sb.append(", ");
				}
				sb.append(String.format("'%s(%s)'", getName(),
						getJavaSignatureString(invocable.getParameterTypes())));
			}
			return new LuaRuntimeException(sb.toString());
		}

		/**
		 * Returns a Lua value signature string for diagnostic messages.
		 */
		private String getLuaSignatureString(LuaState luaState) {
			int argCount = luaState.getTop() - 1;
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < argCount; i++) {
				if (i > 0) {
					sb.append(", ");
				}
				sb.append(luaState.typeName(i + 2));
			}
			return sb.toString();
		}

		/**
		 * Returns a Java type signature string for diagnostic messages.
		 */
		private String getJavaSignatureString(Class<?>[] types) {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < types.length; i++) {
				if (i > 0) {
					sb.append(", ");
				}
				sb.append(types[i].getCanonicalName());
			}
			return sb.toString();
		}
	}

	/**
	 * Provides property access.
	 */
	private class PropertyAccessor implements Accessor {
		// -- State
		private Class<?> clazz;
		private PropertyDescriptor propertyDescriptor;

		// -- Construction
		/**
		 * Creates a new instance.
		 */
		public PropertyAccessor(Class<?> clazz,
				PropertyDescriptor propertyDescriptor) {
			this.clazz = clazz;
			this.propertyDescriptor = propertyDescriptor;
		}

		// -- Accessor methods
		@Override
		public void read(LuaState luaState, Object object) {
			if (propertyDescriptor.getReadMethod() == null) {
				throw new LuaRuntimeException(
						String.format(
								"attempt to read class %s with accessor '%s' (a write-only property)",
								clazz.getCanonicalName(),
								propertyDescriptor.getName()));
			}
			try {
				luaState.pushJavaObject(propertyDescriptor.getReadMethod()
						.invoke(object, EMPTY_ARGUMENTS));
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			} catch (InvocationTargetException e) {
				throw new RuntimeException(e.getTargetException());
			}
		}

		@Override
		public void write(LuaState luaState, Object object) {
			if (propertyDescriptor.getWriteMethod() == null) {
				throw new LuaRuntimeException(
						String.format(
								"attempt to write class %s with acessor '%s' (a read-only property)",
								clazz.getCanonicalName(),
								propertyDescriptor.getName()));
			}
			try {
				Object value = luaState.checkJavaObject(-1,
						propertyDescriptor.getPropertyType());
				propertyDescriptor.getWriteMethod().invoke(object, value);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			} catch (InvocationTargetException e) {
				throw new RuntimeException(e.getTargetException());
			}
			luaState.pop(1);
		}

		@Override
		public boolean isNotStatic() {
			return true;
		}

		@Override
		public boolean isStatic() {
			return false;
		}
	}

	/**
	 * Virtual superinterface for methods and constructors.
	 */
	private interface Invocable {
		/**
		 * Returns what this invocable is, for use in diagnostic messages.
		 */
		public String getWhat();

		/**
		 * Returns the declaring class of this invocable.
		 */
		public Class<?> getDeclaringClass();

		/**
		 * Returns the modifiers of this invocable.
		 */
		public int getModifiers();

		/**
		 * Returns the name of this invocable.
		 */
		public String getName();

		/**
		 * Returns the return type of this invocable.
		 */
		public Class<?> getReturnType();

		/**
		 * Returns whether this invocable has a return value that must be pushed
		 * raw.
		 */
		public boolean isRawReturn();

		/**
		 * Returns the number of parameters.
		 */
		public int getParameterCount();

		/**
		 * Returns the parameter types of this invocable.
		 */
		public Class<?>[] getParameterTypes();

		/**
		 * Returns a parameter type, flattening variable arguments.
		 */
		public Class<?> getParameterType(int index);

		/**
		 * Returns whether this invocable has a variable number of arguments.
		 */
		public boolean isVarArgs();

		/**
		 * Invokes this invocable.
		 */
		public Object invoke(Object obj, Object... args)
				throws InstantiationException, IllegalAccessException,
				IllegalArgumentException, InvocationTargetException;
	}

	/**
	 * Invocable method.
	 */
	private static class InvocableMethod implements Invocable {
		private Method method;
		private Class<?>[] parameterTypes;

		/**
		 * Creates a new instance.
		 */
		public InvocableMethod(Method method) {
			this.method = method;
			this.parameterTypes = method.getParameterTypes();
		}

		@Override
		public String getWhat() {
			return "method";
		}

		@Override
		public Class<?> getDeclaringClass() {
			return method.getDeclaringClass();
		}

		@Override
		public int getModifiers() {
			return method.getModifiers();
		}

		@Override
		public String getName() {
			return method.getName();
		}

		@Override
		public Class<?> getReturnType() {
			return method.getReturnType();
		}

		@Override
		public boolean isRawReturn() {
			return false;
		}

		@Override
		public int getParameterCount() {
			return parameterTypes.length;
		}

		@Override
		public Class<?>[] getParameterTypes() {
			return parameterTypes;
		}

		@Override
		public Class<?> getParameterType(int index) {
			if (method.isVarArgs() && index >= parameterTypes.length - 1) {
				return parameterTypes[parameterTypes.length - 1]
						.getComponentType();
			} else {
				return parameterTypes[index];
			}
		}

		@Override
		public boolean isVarArgs() {
			return method.isVarArgs();
		}

		@Override
		public Object invoke(Object obj, Object... args)
				throws IllegalAccessException, IllegalArgumentException,
				InvocationTargetException {
			return method.invoke(obj, args);
		}

		@Override
		public String toString() {
			return method.toString();
		}
	}

	/**
	 * Invocable constructor.
	 */
	private static class InvocableConstructor implements Invocable {
		// -- State
		private Constructor<?> constructor;
		private Class<?>[] parameterTypes;

		/**
		 * Creates a new instance.
		 */
		public InvocableConstructor(Constructor<?> constructor) {
			this.constructor = constructor;
			this.parameterTypes = constructor.getParameterTypes();
		}

		@Override
		public String getWhat() {
			return "constructor";
		}

		@Override
		public Class<?> getDeclaringClass() {
			return constructor.getDeclaringClass();
		}

		@Override
		public int getModifiers() {
			return constructor.getModifiers() | Modifier.STATIC;
		}

		@Override
		public String getName() {
			return "new";
		}

		@Override
		public Class<?> getReturnType() {
			return constructor.getDeclaringClass();
		}

		@Override
		public boolean isRawReturn() {
			return false;
		}

		@Override
		public int getParameterCount() {
			return parameterTypes.length;
		}

		@Override
		public Class<?>[] getParameterTypes() {
			return parameterTypes;
		}

		@Override
		public Class<?> getParameterType(int index) {
			if (constructor.isVarArgs() && index >= parameterTypes.length - 1) {
				return parameterTypes[parameterTypes.length - 1]
						.getComponentType();
			} else {
				return parameterTypes[index];
			}
		}

		@Override
		public boolean isVarArgs() {
			return constructor.isVarArgs();
		}

		@Override
		public Object invoke(Object obj, Object... args)
				throws InstantiationException, IllegalAccessException,
				IllegalArgumentException, InvocationTargetException {
			return constructor.newInstance(args);
		}

		@Override
		public String toString() {
			return constructor.toString();
		}
	}

	/**
	 * Invocable proxy.
	 */
	private static class InvocableProxy implements Invocable {
		// -- Static
		private static final Class<?>[] PARAMETER_TYPES = new Class<?>[] { LuaValueProxy.class };

		// -- State
		private Class<?> interfaze;

		/**
		 * Creates a new instance.
		 */
		public InvocableProxy(Class<?> interfaze) {
			this.interfaze = interfaze;
		}

		@Override
		public String getWhat() {
			return "proxy";
		}

		@Override
		public Class<?> getDeclaringClass() {
			return interfaze;
		}

		@Override
		public int getModifiers() {
			return interfaze.getModifiers() | Modifier.STATIC;
		}

		@Override
		public String getName() {
			return "new";
		}

		@Override
		public Class<?> getReturnType() {
			return interfaze;
		}

		@Override
		public boolean isRawReturn() {
			return true;
		}

		@Override
		public int getParameterCount() {
			return 1;
		}

		@Override
		public Class<?>[] getParameterTypes() {
			return PARAMETER_TYPES;
		}

		@Override
		public Class<?> getParameterType(int index) {
			return PARAMETER_TYPES[0];
		}

		@Override
		public boolean isVarArgs() {
			return false;
		}

		@Override
		public Object invoke(Object obj, Object... args)
				throws InstantiationException, IllegalAccessException,
				IllegalArgumentException, InvocationTargetException {
			LuaValueProxy luaValueProxy = (LuaValueProxy) args[0];
			luaValueProxy.pushValue();
			Object proxy = luaValueProxy.getLuaState().getProxy(-1, interfaze);
			luaValueProxy.getLuaState().pop(1);
			return proxy;
		}

		@Override
		public String toString() {
			return interfaze.toString();
		}
	}

	/**
	 * Lua call signature.
	 */
	private static class LuaCallSignature {
		// -- State
		private Class<?> clazz;
		private String invocableName;
		private Object[] types;
		private int hashCode;

		// -- Construction
		/**
		 * Creates a new instance.
		 */
		public LuaCallSignature(Class<?> clazz, String invocableName,
				Object[] types) {
			this.clazz = clazz;
			this.invocableName = invocableName;
			this.types = types;
			hashCode = clazz.hashCode();
			hashCode = hashCode * 65599 + invocableName.hashCode();
			for (int i = 0; i < types.length; i++) {
				hashCode = hashCode * 65599 + types[i].hashCode();
			}
		}

		@Override
		public int hashCode() {
			return hashCode;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj == this) {
				return true;
			}
			if (!(obj instanceof LuaCallSignature)) {
				return false;
			}
			LuaCallSignature other = (LuaCallSignature) obj;
			if (clazz != other.clazz
					|| !invocableName.equals(other.invocableName)
					|| types.length != other.types.length) {
				return false;
			}
			for (int i = 0; i < types.length; i++) {
				if (types[i] != other.types[i]) {
					return false;
				}
			}
			return true;
		}

		@Override
		public String toString() {
			return clazz.getCanonicalName() + ": " + invocableName + "("
					+ Arrays.asList(types) + ")";
		}
	}
}
