/*******************************************************************************
 * Copyright (c) 2009-2011 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.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.luaj.vm2.LuaString;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;

/**
 * Helper class to coerce values from lua to Java within the luajava library. 
 * <p>
 * This class is primarily used by the {@link LuajavaLib},
 * but can also be used directly when working with Java/lua bindings.
 * <p>
 * To coerce to specific Java values, generally the {@code toType()} methods
 * on {@link LuaValue} may be used:
 * <ul>
 * <li>{@link LuaValue#toboolean()}</li>
 * <li>{@link LuaValue#tobyte()}</li>
 * <li>{@link LuaValue#tochar()}</li>
 * <li>{@link LuaValue#toshort()}</li>
 * <li>{@link LuaValue#toint()}</li>
 * <li>{@link LuaValue#tofloat()}</li>
 * <li>{@link LuaValue#todouble()}</li>
 * <li>{@link LuaValue#tojstring()}</li>
 * <li>{@link LuaValue#touserdata()}</li>
 * <li>{@link LuaValue#touserdata(Class)}</li>
 * </ul>
 * <p>
 * For data in lua tables, the various methods on {@link LuaTable} can be used directly
 * to convert data to something more useful.
 *
 * @see LuajavaLib
 * @see CoerceJavaToLua
 */
public class CoerceLuaToJava {

	static int SCORE_NULL_VALUE     =    0x10;
	static int SCORE_WRONG_TYPE     =   0x100;
	static int SCORE_UNCOERCIBLE    = 0x10000;

	static interface Coercion {
		public int score( LuaValue value );
		public Object coerce( LuaValue value );
	};

	/**
	 * Coerce a LuaValue value to a specified java class
	 * @param value LuaValue to coerce
	 * @param clazz Class to coerce into
	 * @return Object of type clazz (or a subclass) with the corresponding value.
	 */
	public static Object coerce(LuaValue value, Class clazz) {
		return getCoercion(clazz).coerce(value);
	}

	static final Map COERCIONS = Collections.synchronizedMap(new HashMap());

	static final class BoolCoercion implements Coercion {
		public String toString() {
			return "BoolCoercion()";
		}
		public int score( LuaValue value ) {
			switch ( value.type() ) {
				case LuaValue.TBOOLEAN:
					return 0;
			}
			return 1;
		}

		public Object coerce(LuaValue value) {
			return value.toboolean()? Boolean.TRUE: Boolean.FALSE;
		}
	}

	static final class NumericCoercion implements Coercion {
		static final int TARGET_TYPE_BYTE = 0;
		static final int TARGET_TYPE_CHAR = 1;
		static final int TARGET_TYPE_SHORT = 2;
		static final int TARGET_TYPE_INT = 3;
		static final int TARGET_TYPE_LONG = 4;
		static final int TARGET_TYPE_FLOAT = 5;
		static final int TARGET_TYPE_DOUBLE = 6;
		static final String[] TYPE_NAMES = { "byte", "char", "short", "int", "long", "float", "double" };
		final int targetType;
		public String toString() {
			return "NumericCoercion("+TYPE_NAMES[targetType]+")";
		}
		NumericCoercion(int targetType) {
			this.targetType = targetType;
		}
		public int score( LuaValue value ) {
			int fromStringPenalty = 0;
			if ( value.type() == LuaValue.TSTRING ) {
				value = value.tonumber();
				if ( value.isnil() ) {
					return SCORE_UNCOERCIBLE;
				}
				fromStringPenalty = 4;
			}
			if ( value.isint() ) {
				switch ( targetType ) {
					case TARGET_TYPE_BYTE: {
						int i = value.toint();
						return fromStringPenalty + ((i==(byte)i)? 0: SCORE_WRONG_TYPE);
					}
					case TARGET_TYPE_CHAR: {
						int i = value.toint();
						return fromStringPenalty + ((i==(byte)i)? 1: (i==(char)i)? 0: SCORE_WRONG_TYPE);
					}
					case TARGET_TYPE_SHORT: {
						int i = value.toint();
						return fromStringPenalty +
								((i==(byte)i)? 1: (i==(short)i)? 0: SCORE_WRONG_TYPE);
					}
					case TARGET_TYPE_INT: {
						int i = value.toint();
						return fromStringPenalty +
								((i==(byte)i)? 2: ((i==(char)i) || (i==(short)i))? 1: 0);
					}
					case TARGET_TYPE_FLOAT: return fromStringPenalty + 1;
					case TARGET_TYPE_LONG: return fromStringPenalty + 1;
					case TARGET_TYPE_DOUBLE: return fromStringPenalty + 2;
					default: return SCORE_WRONG_TYPE;
				}
			} else if ( value.isnumber() ) {
				switch ( targetType ) {
					case TARGET_TYPE_BYTE: return SCORE_WRONG_TYPE;
					case TARGET_TYPE_CHAR: return SCORE_WRONG_TYPE;
					case TARGET_TYPE_SHORT: return SCORE_WRONG_TYPE;
					case TARGET_TYPE_INT: return SCORE_WRONG_TYPE;
					case TARGET_TYPE_LONG: {
						double d = value.todouble();
						return fromStringPenalty + ((d==(long)d)? 0: SCORE_WRONG_TYPE);
					}
					case TARGET_TYPE_FLOAT: {
						double d = value.todouble();
						return fromStringPenalty + ((d==(float)d)? 0: SCORE_WRONG_TYPE);
					}
					case TARGET_TYPE_DOUBLE: {
						double d = value.todouble();
						return fromStringPenalty + (((d==(long)d) || (d==(float)d))? 1: 0);
					}
					default: return SCORE_WRONG_TYPE;
				}
			} else {
				return SCORE_UNCOERCIBLE;
			}
		}

		public Object coerce(LuaValue value) {
			switch ( targetType ) {
				case TARGET_TYPE_BYTE: return Byte.valueOf( (byte) value.toint() );
				case TARGET_TYPE_CHAR: return Character.valueOf( (char) value.toint() );
				case TARGET_TYPE_SHORT: return Short.valueOf( (short) value.toint() );
				case TARGET_TYPE_INT: return Integer.valueOf( (int) value.toint() );
				case TARGET_TYPE_LONG: return Long.valueOf( (long) value.todouble() );
				case TARGET_TYPE_FLOAT: return Float.valueOf( (float) value.todouble() );
				case TARGET_TYPE_DOUBLE: return Double.valueOf( (double) value.todouble() );
				default: return null;
			}
		}
	}

	static final class StringCoercion implements Coercion {
		public static final int TARGET_TYPE_STRING = 0;
		public static final int TARGET_TYPE_BYTES = 1;
		final int targetType;
		public StringCoercion(int targetType) {
			this.targetType = targetType;
		}
		public String toString() {
			return "StringCoercion("+(targetType==TARGET_TYPE_STRING? "String": "byte[]")+")";
		}
		public int score(LuaValue value) {
			switch ( value.type() ) {
				case LuaValue.TSTRING:
					return value.checkstring().isValidUtf8()?
							(targetType==TARGET_TYPE_STRING? 0: 1):
							(targetType==TARGET_TYPE_BYTES? 0: SCORE_WRONG_TYPE);
				case LuaValue.TNIL:
					return SCORE_NULL_VALUE;
				default:
					return targetType == TARGET_TYPE_STRING? SCORE_WRONG_TYPE: SCORE_UNCOERCIBLE;
			}
		}
		public Object coerce(LuaValue value) {
			if ( value.isnil() )
				return null;
			if ( targetType == TARGET_TYPE_STRING )
				return value.tojstring();
			LuaString s = value.checkstring();
			byte[] b = new byte[s.m_length];
			s.copyInto(0, b, 0, b.length);
			return b;
		}
	}

	static final class ArrayCoercion implements Coercion {
		final Class componentType;
		final Coercion componentCoercion;
		public ArrayCoercion(Class componentType) {
			this.componentType = componentType;
			this.componentCoercion = getCoercion(componentType);
		}
		public String toString() {
			return "ArrayCoercion("+componentType.getName()+")";
		}
		public int score(LuaValue value) {
			switch ( value.type() ) {
				case LuaValue.TTABLE:
					return value.length()==0? 0: componentCoercion.score( value.get(1) );
				case LuaValue.TUSERDATA:
					return inheritanceLevels( componentType, value.touserdata().getClass().getComponentType() );
				case LuaValue.TNIL:
					return SCORE_NULL_VALUE;
				default:
					return SCORE_UNCOERCIBLE;
			}
		}
		public Object coerce(LuaValue value) {
			switch ( value.type() ) {
				case LuaValue.TTABLE: {
					int n = value.length();
					Object a = Array.newInstance(componentType, n);
					for ( int i=0; i<n; i++ )
						Array.set(a, i, componentCoercion.coerce(value.get(i+1)));
					return a;
				}
				case LuaValue.TUSERDATA:
					return value.touserdata();
				case LuaValue.TNIL:
					return null;
				default:
					return null;
			}

		}
	}

	/**
	 * Determine levels of inheritance between a base class and a subclass
	 * @param baseclass base class to look for
	 * @param subclass class from which to start looking
	 * @return number of inheritance levels between subclass and baseclass,
	 * or SCORE_UNCOERCIBLE if not a subclass
	 */
	static final int inheritanceLevels( Class baseclass, Class subclass ) {
		if ( subclass == null )
			return SCORE_UNCOERCIBLE;
		if ( baseclass == subclass )
			return 0;
		int min = Math.min( SCORE_UNCOERCIBLE, inheritanceLevels( baseclass, subclass.getSuperclass() ) + 1 );
		Class[] ifaces = subclass.getInterfaces();
		for ( int i=0; i<ifaces.length; i++ )
			min = Math.min(min, inheritanceLevels(baseclass, ifaces[i]) + 1 );
		return min;
	}

	static final class ObjectCoercion implements Coercion {
		final Class targetType;
		ObjectCoercion(Class targetType) {
			this.targetType = targetType;
		}
		public String toString() {
			return "ObjectCoercion("+targetType.getName()+")";
		}
		public int score(LuaValue value) {
			switch ( value.type() ) {
				case LuaValue.TNUMBER:
					if (value.isinttype()) {
						return inheritanceLevels( targetType, value.isint()? Integer.class: Long.class );
					}
					return inheritanceLevels( targetType, Double.class );
				case LuaValue.TBOOLEAN:
					return inheritanceLevels( targetType, Boolean.class );
				case LuaValue.TSTRING:
					return inheritanceLevels( targetType, String.class );
				case LuaValue.TUSERDATA:
					return inheritanceLevels( targetType, value.touserdata().getClass() );
				case LuaValue.TNIL:
					return SCORE_NULL_VALUE;
				default:
					return inheritanceLevels( targetType, value.getClass() );
			}
		}
		public Object coerce(LuaValue value) {
			switch ( value.type() ) {
				case LuaValue.TNUMBER:
					if (value.isinttype()) {
						return value.isint()? (Object) Integer.valueOf(value.toint()): (Object) Long.valueOf(value.tolong());
					}
					return Double.valueOf(value.todouble());
				case LuaValue.TBOOLEAN:
					return value.toboolean()? Boolean.TRUE: Boolean.FALSE;
				case LuaValue.TSTRING:
					return value.tojstring();
				case LuaValue.TUSERDATA:
					return value.optuserdata(targetType, null);
				case LuaValue.TNIL:
					return null;
				default:
					return value;
			}
		}
	}

	static {
		Coercion boolCoercion   = new BoolCoercion();
		Coercion byteCoercion   = new NumericCoercion(NumericCoercion.TARGET_TYPE_BYTE);
		Coercion charCoercion   = new NumericCoercion(NumericCoercion.TARGET_TYPE_CHAR);
		Coercion shortCoercion  = new NumericCoercion(NumericCoercion.TARGET_TYPE_SHORT);
		Coercion intCoercion    = new NumericCoercion(NumericCoercion.TARGET_TYPE_INT);
		Coercion longCoercion   = new NumericCoercion(NumericCoercion.TARGET_TYPE_LONG);
		Coercion floatCoercion  = new NumericCoercion(NumericCoercion.TARGET_TYPE_FLOAT);
		Coercion doubleCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_DOUBLE);
		Coercion stringCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_STRING);
		Coercion bytesCoercion  = new StringCoercion(StringCoercion.TARGET_TYPE_BYTES);

		COERCIONS.put( Boolean.TYPE, boolCoercion );
		COERCIONS.put( Boolean.class, boolCoercion );
		COERCIONS.put( Byte.TYPE, byteCoercion );
		COERCIONS.put( Byte.class, byteCoercion );
		COERCIONS.put( Character.TYPE, charCoercion );
		COERCIONS.put( Character.class, charCoercion );
		COERCIONS.put( Short.TYPE, shortCoercion );
		COERCIONS.put( Short.class, shortCoercion );
		COERCIONS.put( Integer.TYPE, intCoercion );
		COERCIONS.put( Integer.class, intCoercion );
		COERCIONS.put( Long.TYPE, longCoercion );
		COERCIONS.put( Long.class, longCoercion );
		COERCIONS.put( Float.TYPE, floatCoercion );
		COERCIONS.put( Float.class, floatCoercion );
		COERCIONS.put( Double.TYPE, doubleCoercion );
		COERCIONS.put( Double.class, doubleCoercion );
		COERCIONS.put( String.class, stringCoercion );
		COERCIONS.put( byte[].class, bytesCoercion );
	}

	static Coercion getCoercion(Class c) {
		Coercion co = (Coercion) COERCIONS.get( c );
		if ( co != null ) {
			return co;
		}
		if ( c.isArray() ) {
			Class typ = c.getComponentType();
			co = new ArrayCoercion(c.getComponentType());
		} else {
			co = new ObjectCoercion(c);
		}
		COERCIONS.put( c, co );
		return co;
	}
}
