/*
 * $Id: DefaultConverter.java 161 2012-10-06 13:53:02Z andre@naef.com $
 * See LICENSE.txt for license terms.
 */

package com.naef.jnlua;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.naef.jnlua.util.AbstractTableList;
import com.naef.jnlua.util.AbstractTableMap;

/**
 * Default implementation of the <code>Converter</code> interface.
 */
public class DefaultConverter implements Converter {
	// -- Static
	/**
	 * Raw byte array.
	 */
	private static final boolean RAW_BYTE_ARRAY = false; /* Boolean.parseBoolean(System
			.getProperty(DefaultConverter.class.getPackage().getName()
					+ ".rawByteArray")); */

	/**
	 * Static instance.
	 */
	private static final DefaultConverter INSTANCE = new DefaultConverter();

  public static boolean isTypeSupported(Class<?> clazz) {
    return JAVA_OBJECT_CONVERTERS.get(clazz) != null;
  }

	/**
	 * Boolean distance map.
	 */
	private static final Map<Class<?>, Integer> BOOLEAN_DISTANCE_MAP = new HashMap<Class<?>, Integer>();
	static {
		BOOLEAN_DISTANCE_MAP.put(Boolean.class, new Integer(1));
		BOOLEAN_DISTANCE_MAP.put(Boolean.TYPE, new Integer(1));
		BOOLEAN_DISTANCE_MAP.put(Object.class, new Integer(2));
	}

	/**
	 * Number distance map.
	 */
	private static final Map<Class<?>, Integer> NUMBER_DISTANCE_MAP = new HashMap<Class<?>, Integer>();
	static {
		NUMBER_DISTANCE_MAP.put(Byte.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Byte.TYPE, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Short.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Short.TYPE, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Integer.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Integer.TYPE, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Long.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Long.TYPE, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Float.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Float.TYPE, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Double.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Double.TYPE, new Integer(1));
		NUMBER_DISTANCE_MAP.put(BigInteger.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(BigDecimal.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Character.class, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Character.TYPE, new Integer(1));
		NUMBER_DISTANCE_MAP.put(Object.class, new Integer(2));
		NUMBER_DISTANCE_MAP.put(String.class, new Integer(3));
		if (!RAW_BYTE_ARRAY) {
			NUMBER_DISTANCE_MAP.put(byte[].class, new Integer(3));
		}
	}

	/**
	 * String distance map.
	 */
	private static final Map<Class<?>, Integer> STRING_DISTANCE_MAP = new HashMap<Class<?>, Integer>();
	static {
		STRING_DISTANCE_MAP.put(String.class, new Integer(1));
		if (!RAW_BYTE_ARRAY) {
			STRING_DISTANCE_MAP.put(byte[].class, new Integer(1));
		}
		STRING_DISTANCE_MAP.put(Object.class, new Integer(2));
		STRING_DISTANCE_MAP.put(Byte.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Byte.TYPE, new Integer(3));
		STRING_DISTANCE_MAP.put(Short.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Short.TYPE, new Integer(3));
		STRING_DISTANCE_MAP.put(Integer.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Integer.TYPE, new Integer(3));
		STRING_DISTANCE_MAP.put(Long.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Long.TYPE, new Integer(3));
		STRING_DISTANCE_MAP.put(Float.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Float.TYPE, new Integer(3));
		STRING_DISTANCE_MAP.put(Double.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Double.TYPE, new Integer(3));
		STRING_DISTANCE_MAP.put(BigInteger.class, new Integer(3));
		STRING_DISTANCE_MAP.put(BigDecimal.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Character.class, new Integer(3));
		STRING_DISTANCE_MAP.put(Character.TYPE, new Integer(3));
	}

	/**
	 * Function distance map.
	 */
	private static final Map<Class<?>, Integer> FUNCTION_DISTANCE_MAP = new HashMap<Class<?>, Integer>();
	static {
		FUNCTION_DISTANCE_MAP.put(JavaFunction.class, new Integer(1));
		FUNCTION_DISTANCE_MAP.put(Object.class, new Integer(2));
	}

	/**
	 * Lua value converters.
	 */
	private static final Map<Class<?>, LuaValueConverter<?>> LUA_VALUE_CONVERTERS = new HashMap<Class<?>, LuaValueConverter<?>>();
	static {
		LuaValueConverter<Boolean> booleanConverter = new LuaValueConverter<Boolean>() {
			@Override
			public Boolean convert(LuaState luaState, int index) {
				return Boolean.valueOf(luaState.toBoolean(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Boolean.class, booleanConverter);
		LUA_VALUE_CONVERTERS.put(Boolean.TYPE, booleanConverter);

		LuaValueConverter<Byte> byteConverter = new LuaValueConverter<Byte>() {
			@Override
			public Byte convert(LuaState luaState, int index) {
				return Byte.valueOf((byte) luaState.toInteger(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Byte.class, byteConverter);
		LUA_VALUE_CONVERTERS.put(Byte.TYPE, byteConverter);
		LuaValueConverter<Short> shortConverter = new LuaValueConverter<Short>() {
			@Override
			public Short convert(LuaState luaState, int index) {
				return Short.valueOf((short) luaState.toInteger(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Short.class, shortConverter);
		LUA_VALUE_CONVERTERS.put(Short.TYPE, shortConverter);
		LuaValueConverter<Integer> integerConverter = new LuaValueConverter<Integer>() {
			@Override
			public Integer convert(LuaState luaState, int index) {
				return Integer.valueOf(luaState.toInteger(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Integer.class, integerConverter);
		LUA_VALUE_CONVERTERS.put(Integer.TYPE, integerConverter);
		LuaValueConverter<Long> longConverter = new LuaValueConverter<Long>() {
			@Override
			public Long convert(LuaState luaState, int index) {
				return Long.valueOf((long) luaState.toNumber(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Long.class, longConverter);
		LUA_VALUE_CONVERTERS.put(Long.TYPE, longConverter);
		LuaValueConverter<Float> floatConverter = new LuaValueConverter<Float>() {
			@Override
			public Float convert(LuaState luaState, int index) {
				return Float.valueOf((float) luaState.toNumber(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Float.class, floatConverter);
		LUA_VALUE_CONVERTERS.put(Float.TYPE, floatConverter);
		LuaValueConverter<Double> doubleConverter = new LuaValueConverter<Double>() {
			@Override
			public Double convert(LuaState luaState, int index) {
				return Double.valueOf(luaState.toNumber(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Double.class, doubleConverter);
		LUA_VALUE_CONVERTERS.put(Double.TYPE, doubleConverter);
		LuaValueConverter<BigInteger> bigIntegerConverter = new LuaValueConverter<BigInteger>() {
			@Override
			public BigInteger convert(LuaState luaState, int index) {
				return BigDecimal.valueOf(luaState.toNumber(index))
						.setScale(0, BigDecimal.ROUND_HALF_EVEN).toBigInteger();
			}
		};
		LUA_VALUE_CONVERTERS.put(BigInteger.class, bigIntegerConverter);
		LuaValueConverter<BigDecimal> bigDecimalConverter = new LuaValueConverter<BigDecimal>() {
			@Override
			public BigDecimal convert(LuaState luaState, int index) {
				return BigDecimal.valueOf(luaState.toNumber(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(BigDecimal.class, bigDecimalConverter);
		LuaValueConverter<Character> characterConverter = new LuaValueConverter<Character>() {
			@Override
			public Character convert(LuaState luaState, int index) {
				return Character.valueOf((char) luaState.toInteger(index));
			}
		};
		LUA_VALUE_CONVERTERS.put(Character.class, characterConverter);
		LUA_VALUE_CONVERTERS.put(Character.TYPE, characterConverter);
		LuaValueConverter<String> stringConverter = new LuaValueConverter<String>() {
			@Override
			public String convert(LuaState luaState, int index) {
				return luaState.toString(index);
			}
		};
		LUA_VALUE_CONVERTERS.put(String.class, stringConverter);
		if (!RAW_BYTE_ARRAY) {
			LuaValueConverter<byte[]> byteArrayConverter = new LuaValueConverter<byte[]>() {
				@Override
				public byte[] convert(LuaState luaState, int index) {
					return luaState.toByteArray(index);
				}
			};
			LUA_VALUE_CONVERTERS.put(byte[].class, byteArrayConverter);
		}
	}

	/**
	 * Java object converters.
	 */
	private static final Map<Class<?>, JavaObjectConverter<?>> JAVA_OBJECT_CONVERTERS = new HashMap<Class<?>, JavaObjectConverter<?>>();
	static {
		JavaObjectConverter<Boolean> booleanConverter = new JavaObjectConverter<Boolean>() {
			@Override
			public void convert(LuaState luaState, Boolean booleanValue) {
				luaState.pushBoolean(booleanValue.booleanValue());
			}
		};
		JAVA_OBJECT_CONVERTERS.put(Boolean.class, booleanConverter);
		JAVA_OBJECT_CONVERTERS.put(Boolean.TYPE, booleanConverter);
		JavaObjectConverter<Number> numberConverter = new JavaObjectConverter<Number>() {
			@Override
			public void convert(LuaState luaState, Number number) {
				luaState.pushNumber(number.doubleValue());
			}
		};
		JAVA_OBJECT_CONVERTERS.put(Byte.class, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Byte.TYPE, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Short.class, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Short.TYPE, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Integer.class, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Integer.TYPE, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Long.class, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Long.TYPE, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Float.class, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Float.TYPE, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Double.class, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(Double.TYPE, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(BigInteger.class, numberConverter);
		JAVA_OBJECT_CONVERTERS.put(BigDecimal.class, numberConverter);
		JavaObjectConverter<Character> characterConverter = new JavaObjectConverter<Character>() {
			@Override
			public void convert(LuaState luaState, Character character) {
				luaState.pushInteger(character.charValue());
			}
		};
		JAVA_OBJECT_CONVERTERS.put(Character.class, characterConverter);
		JAVA_OBJECT_CONVERTERS.put(Character.TYPE, characterConverter);
		JavaObjectConverter<String> stringConverter = new JavaObjectConverter<String>() {
			@Override
			public void convert(LuaState luaState, String string) {
				luaState.pushString(string);
			}
		};
		JAVA_OBJECT_CONVERTERS.put(String.class, stringConverter);
		if (!RAW_BYTE_ARRAY) {
			JavaObjectConverter<byte[]> byteArrayConverter = new JavaObjectConverter<byte[]>() {
				@Override
				public void convert(LuaState luaState, byte[] byteArray) {
					luaState.pushByteArray(byteArray);
				}
			};
			JAVA_OBJECT_CONVERTERS.put(byte[].class, byteArrayConverter);
		}
	}

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

	// -- Construction
	/**
	 * Singleton.
	 */
	private DefaultConverter() {
	}

	// -- Java converter methods
	@Override
	public int getTypeDistance(LuaState luaState, int index, Class<?> formalType) {
		// Handle none
		LuaType luaType = luaState.type(index);
		if (luaType == null) {
			return Integer.MAX_VALUE;
		}

		// Handle void
		if (formalType == Void.TYPE) {
			return Integer.MAX_VALUE;
		}

		// Handle Lua value proxy
		if (formalType == LuaValueProxy.class) {
			return 0;
		}

		// Handle Lua types
		switch (luaType) {
		case NIL:
			return 1;
		case BOOLEAN:
			Integer distance = BOOLEAN_DISTANCE_MAP.get(formalType);
			if (distance != null) {
				return distance.intValue();
			}
			break;
		case NUMBER:
			distance = NUMBER_DISTANCE_MAP.get(formalType);
			if (distance != null) {
				return distance.intValue();
			}
			break;
		case STRING:
			distance = STRING_DISTANCE_MAP.get(formalType);
			if (distance != null) {
				return distance.intValue();
			}
			break;
		case TABLE:
			if (formalType == Map.class || formalType == List.class
					|| formalType.isArray()) {
				return 1;
			}
			if (formalType == Object.class) {
				return 2;
			}
			break;
		case FUNCTION:
			if (luaState.isJavaFunction(index)) {
				distance = FUNCTION_DISTANCE_MAP.get(formalType);
				if (distance != null) {
					return distance.intValue();
				}
			}
			break;
		case USERDATA:
			Object object = luaState.toJavaObjectRaw(index);
			if (object != null) {
				Class<?> type;
				if (object instanceof TypedJavaObject) {
					TypedJavaObject typedJavaObject = (TypedJavaObject) object;
					if (typedJavaObject.isStrong()) {
						if (formalType.isAssignableFrom(typedJavaObject
								.getClass())) {
							return 1;
						}
					}
					type = typedJavaObject.getType();
				} else {
					type = object.getClass();
				}
				if (formalType.isAssignableFrom(type)) {
					return 1;
				}
			}
			break;
		}

		// Handle object
		if (formalType == Object.class) {
			return Integer.MAX_VALUE - 1;
		}

		// Unsupported conversion
		return Integer.MAX_VALUE;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T convertLuaValue(LuaState luaState, int index,
			Class<T> formalType) {
		// Handle none
		LuaType luaType = luaState.type(index);
		if (luaType == null) {
			throw new IllegalArgumentException("undefined index: " + index);
		}

		// Handle void
		if (formalType == Void.TYPE) {
			throw new ClassCastException(String.format(
					"cannot convert %s to %s", luaState.typeName(index),
					formalType.getCanonicalName()));
		}

		// Handle Lua value proxy
		if (formalType == LuaValueProxy.class) {
			return (T) luaState.getProxy(index);
		}

		// Handle Lua types
		switch (luaType) {
		case NIL:
			return null;
		case BOOLEAN:
			LuaValueConverter<?> luaValueConverter;
			luaValueConverter = LUA_VALUE_CONVERTERS.get(formalType);
			if (luaValueConverter != null) {
				return (T) luaValueConverter.convert(luaState, index);
			}
			if (formalType == Object.class) {
				return (T) Boolean.valueOf(luaState.toBoolean(index));
			}
			break;
		case NUMBER:
			luaValueConverter = LUA_VALUE_CONVERTERS.get(formalType);
			if (luaValueConverter != null) {
				return (T) luaValueConverter.convert(luaState, index);
			}
			if (formalType == Object.class) {
				return (T) Double.valueOf(luaState.toNumber(index));
			}
			break;
		case STRING:
			luaValueConverter = LUA_VALUE_CONVERTERS.get(formalType);
			if (luaValueConverter != null) {
				return (T) luaValueConverter.convert(luaState, index);
			}
			if (formalType == Object.class) {
				return (T) luaState.toString(index);
			}
			break;
		case TABLE:
			if (formalType == Map.class || formalType == Object.class) {
				final LuaValueProxy luaValueProxy = luaState.getProxy(index);
				return (T) new AbstractTableMap<Object>() {
					@Override
					protected Object convertKey(int index) {
						return getLuaState().toJavaObject(index, Object.class);
					}

					@Override
					public LuaState getLuaState() {
						return luaValueProxy.getLuaState();
					}

					@Override
					public void pushValue() {
						luaValueProxy.pushValue();
					}
				};
			}
			if (formalType == List.class) {
				final LuaValueProxy luaValueProxy = luaState.getProxy(index);
				return (T) new AbstractTableList() {
					@Override
					public LuaState getLuaState() {
						return luaValueProxy.getLuaState();
					}

					@Override
					public void pushValue() {
						luaValueProxy.pushValue();
					}
				};
			}
			if (formalType.isArray()) {
				int length = luaState.rawLen(index);
				Class<?> componentType = formalType.getComponentType();
				Object array = Array.newInstance(formalType.getComponentType(),
						length);
				for (int i = 0; i < length; i++) {
					luaState.rawGet(index, i + 1);
					try {
						Array.set(array, i,
								convertLuaValue(luaState, -1, componentType));
					} finally {
						luaState.pop(1);
					}
				}
				return (T) array;
			}
			break;
		case FUNCTION:
			if (luaState.isJavaFunction(index)) {
				if (formalType == JavaFunction.class
						|| formalType == Object.class) {
					return (T) luaState.toJavaFunction(index);
				}
			}
			break;
		case USERDATA:
			Object object = luaState.toJavaObjectRaw(index);
			if (object != null) {
				if (object instanceof TypedJavaObject) {
					TypedJavaObject typedJavaObject = (TypedJavaObject) object;
					if (typedJavaObject.isStrong()) {
						if (formalType.isAssignableFrom(typedJavaObject
								.getClass())) {
							return (T) typedJavaObject;
						}
					}
					return (T) ((TypedJavaObject) object).getObject();
				} else {
					return (T) object;
				}
			}
			break;
		}

		// Handle object
		if (formalType == Object.class) {
			return (T) luaState.getProxy(index);
		}

		// Unsupported conversion
		throw new ClassCastException(String.format("cannot convert %s to %s",
				luaState.typeName(index), formalType.getCanonicalName()));
	}

	@SuppressWarnings("unchecked")
	@Override
	public void convertJavaObject(LuaState luaState, Object object) {
		// Handle null
		if (object == null) {
			luaState.pushNil();
			return;
		}

		// Handle known Java types
		JavaObjectConverter<Object> javaObjectConverter = (JavaObjectConverter<Object>) JAVA_OBJECT_CONVERTERS
				.get(object.getClass());
		if (javaObjectConverter != null) {
			javaObjectConverter.convert(luaState, object);
			return;
		}
		if (object instanceof JavaFunction) {
			luaState.pushJavaFunction((JavaFunction) object);
			return;
		}
		if (object instanceof LuaValueProxy) {
			LuaValueProxy luaValueProxy = (LuaValueProxy) object;
			if (!luaValueProxy.getLuaState().equals(luaState)) {
				throw new IllegalArgumentException(
						"Lua value proxy is from a different Lua state");
			}
			luaValueProxy.pushValue();
			return;
		}

		// Push as is
		luaState.pushJavaObjectRaw(object);
	}

	// -- Nested types
	/**
	 * Converts Lua values.
	 */
	private interface LuaValueConverter<T> {
		/**
		 * Converts a Lua value to a Java object.
		 */
		public T convert(LuaState luaState, int index);
	}

	/**
	 * Converts Java object.
	 */
	private interface JavaObjectConverter<T> {
		/**
		 * Converts a Java object to a Lua value.
		 */
		public void convert(LuaState luaState, T object);
	}
}
