using System;
using System.Collections.Generic;
using KeraLua;
using NLua.Method;
using NLua.Extensions;

namespace NLua
{
    using LuaState = KeraLua.Lua;
    sealed class CheckType
    {
        readonly Dictionary<Type, ExtractValue> _extractValues = new Dictionary<Type, ExtractValue>();
        readonly ExtractValue _extractNetObject;
        readonly ObjectTranslator _translator;

        public CheckType(ObjectTranslator translator)
        {
            _translator = translator;
            _extractValues.Add(typeof(object), GetAsObject);
            _extractValues.Add(typeof(sbyte), GetAsSbyte);
            _extractValues.Add(typeof(byte), GetAsByte);
            _extractValues.Add(typeof(short), GetAsShort);
            _extractValues.Add(typeof(ushort), GetAsUshort);
            _extractValues.Add(typeof(int), GetAsInt);
            _extractValues.Add(typeof(uint), GetAsUint);
            _extractValues.Add(typeof(long), GetAsLong);
            _extractValues.Add(typeof(ulong), GetAsUlong);
            _extractValues.Add(typeof(double), GetAsDouble);
            _extractValues.Add(typeof(char), GetAsChar);
            _extractValues.Add(typeof(float), GetAsFloat);
            _extractValues.Add(typeof(decimal), GetAsDecimal);
            _extractValues.Add(typeof(bool), GetAsBoolean);
            _extractValues.Add(typeof(string), GetAsString);
            _extractValues.Add(typeof(char[]), GetAsCharArray);
            _extractValues.Add(typeof(byte[]), GetAsByteArray);
            _extractValues.Add(typeof(LuaFunction), GetAsFunction);
            _extractValues.Add(typeof(LuaTable), GetAsTable);
            _extractValues.Add(typeof(LuaUserData), GetAsUserdata);
            _extractNetObject = GetAsNetObject;
        }

        /*
         * Checks if the value at Lua stack index stackPos matches paramType, 
         * returning a conversion function if it does and null otherwise.
         */
        internal ExtractValue GetExtractor(ProxyType paramType)
        {
            return GetExtractor(paramType.UnderlyingSystemType);
        }

        internal ExtractValue GetExtractor(Type paramType)
        {
            if (paramType.IsByRef)
                paramType = paramType.GetElementType();

            return _extractValues.ContainsKey(paramType) ? _extractValues[paramType] : _extractNetObject;
        }

        internal ExtractValue CheckLuaType(LuaState luaState, int stackPos, Type paramType)
        {
            LuaType luatype = luaState.Type(stackPos);

            if (paramType.IsByRef)
                paramType = paramType.GetElementType();

            var underlyingType = Nullable.GetUnderlyingType(paramType);

            if (underlyingType != null)
            {
                paramType = underlyingType;  // Silently convert nullable types to their non null requics
            }


            bool netParamIsNumeric = paramType == typeof(int) ||
                                     paramType == typeof(uint) ||
                                     paramType == typeof(long) ||
                                     paramType == typeof(ulong) ||
                                     paramType == typeof(short) ||
                                     paramType == typeof(ushort) ||
                                     paramType == typeof(float) ||
                                     paramType == typeof(double) ||
                                     paramType == typeof(decimal) ||
                                     paramType == typeof(byte);

            // If it is a nullable
            if (underlyingType != null)
            {
                // null can always be assigned to nullable
                if (luatype == LuaType.Nil)
                {
                    // Return the correct extractor anyways
                    if (netParamIsNumeric || paramType == typeof(bool))
                        return _extractValues[paramType];
                    return _extractNetObject;
                }
            }

            if (paramType == typeof(object))
                return _extractValues[paramType];

            //CP: Added support for generic parameters
            if (paramType.IsGenericParameter)
            {
                if (luatype == LuaType.Boolean)
                    return _extractValues[typeof(bool)];
                if (luatype == LuaType.String)
                    return _extractValues[typeof(string)];
                if (luatype == LuaType.Table)
                    return _extractValues[typeof(LuaTable)];
                if (luatype == LuaType.UserData)
                    return _extractValues[typeof(object)];
                if (luatype == LuaType.Function)
                    return _extractValues[typeof(LuaFunction)];
                if (luatype == LuaType.Number)
                    return _extractValues[typeof(double)];
            }
            bool netParamIsString = paramType == typeof(string) || paramType == typeof(char[]) || paramType == typeof(byte[]);

            if (netParamIsNumeric)
            {
                if (luaState.IsNumber(stackPos) && !netParamIsString)
                    return _extractValues[paramType];
            }
            else if (paramType == typeof(bool))
            {
                if (luaState.IsBoolean(stackPos))
                    return _extractValues[paramType];
            }
            else if (netParamIsString)
            {
                if (luaState.IsString(stackPos))
                    return _extractValues[paramType];
                if (luatype == LuaType.Nil)
                    return _extractNetObject; // kevinh - silently convert nil to a null string pointer
            }
            else if (paramType == typeof(LuaTable))
            {
                if (luatype == LuaType.Table || luatype == LuaType.Nil)
                    return _extractValues[paramType];
            }
            else if (paramType == typeof(LuaUserData))
            {
                if (luatype == LuaType.UserData || luatype == LuaType.Nil)
                    return _extractValues[paramType];
            }
            else if (paramType == typeof(LuaFunction))
            {
                if (luatype == LuaType.Function || luatype == LuaType.Nil)
                    return _extractValues[paramType];
            }
            else if (typeof(Delegate).IsAssignableFrom(paramType) && luatype == LuaType.Function)
                return new DelegateGenerator(_translator, paramType).ExtractGenerated;
            else if (paramType.IsInterface && luatype == LuaType.Table)
                return new ClassGenerator(_translator, paramType).ExtractGenerated;
            else if ((paramType.IsInterface || paramType.IsClass) && luatype == LuaType.Nil)
            {
                // kevinh - allow nil to be silently converted to null - extractNetObject will return null when the item ain't found
                return _extractNetObject;
            }
            else if (luaState.Type(stackPos) == LuaType.Table)
            {
                if (luaState.GetMetaField(stackPos, "__index") != LuaType.Nil)
                {
                    object obj = _translator.GetNetObject(luaState, -1);
                    luaState.SetTop(-2);
                    if (obj != null && paramType.IsInstanceOfType(obj))
                        return _extractNetObject;
                }
                else
                    return null;
            }
            else
            {
                object obj = _translator.GetNetObject(luaState, stackPos);
                if (obj != null && paramType.IsInstanceOfType(obj))
                    return _extractNetObject;
            }

            return null;
        }


        /*
         * The following functions return the value in the Lua stack
         * index stackPos as the desired type if it can, or null
         * otherwise.
         */
        private object GetAsSbyte(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (sbyte)luaState.ToInteger(stackPos);

            return (sbyte)luaState.ToNumber(stackPos);
        }

        private object GetAsByte(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (byte)luaState.ToInteger(stackPos);

            return (byte)luaState.ToNumber(stackPos);
        }

        private object GetAsShort(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (short)luaState.ToInteger(stackPos);

            return (short)luaState.ToNumber(stackPos);
        }

        private object GetAsUshort(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (ushort)luaState.ToInteger(stackPos);

            return (ushort)luaState.ToNumber(stackPos);
        }

        private object GetAsInt(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (int)luaState.ToInteger(stackPos);

            return (int)luaState.ToNumber(stackPos);
        }

        private object GetAsUint(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (uint)luaState.ToInteger(stackPos);

            return (uint)luaState.ToNumber(stackPos);
        }

        private object GetAsLong(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return luaState.ToInteger(stackPos);

            return (long)luaState.ToNumber(stackPos);
        }

        private object GetAsUlong(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (ulong)luaState.ToInteger(stackPos);

            return (ulong)luaState.ToNumber(stackPos);
        }

        private object GetAsDouble(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (double)luaState.ToInteger(stackPos);

            return luaState.ToNumber(stackPos);
        }

        private object GetAsChar(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (char)luaState.ToInteger(stackPos);

            return (char)luaState.ToNumber(stackPos);
        }

        private object GetAsFloat(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (float)luaState.ToInteger(stackPos);

            return (float)luaState.ToNumber(stackPos);
        }

        private object GetAsDecimal(LuaState luaState, int stackPos)
        {
            if (!luaState.IsNumericType(stackPos))
                return null;

            if (luaState.IsInteger(stackPos))
                return (decimal)luaState.ToInteger(stackPos);

            return (decimal)luaState.ToNumber(stackPos);
        }

        private object GetAsBoolean(LuaState luaState, int stackPos)
        {
            return luaState.ToBoolean(stackPos);
        }

        private object GetAsCharArray(LuaState luaState, int stackPos)
        {
            if (!luaState.IsString(stackPos))
                return null;
            string retVal = luaState.ToString(stackPos, false);
            return retVal.ToCharArray();
        }

        private object GetAsByteArray(LuaState luaState, int stackPos)
        {
            if (!luaState.IsString(stackPos))
                return null;

            byte [] retVal = luaState.ToBuffer(stackPos, false);
            return retVal;
        }

        private object GetAsString(LuaState luaState, int stackPos)
        {
            if (!luaState.IsString(stackPos))
                return null;
            return luaState.ToString(stackPos, false);
        }

        private object GetAsTable(LuaState luaState, int stackPos)
        {
            return _translator.GetTable(luaState, stackPos);
        }

        private object GetAsFunction(LuaState luaState, int stackPos)
        {
            return _translator.GetFunction(luaState, stackPos);
        }

        private object GetAsUserdata(LuaState luaState, int stackPos)
        {
            return _translator.GetUserData(luaState, stackPos);
        }

        public object GetAsObject(LuaState luaState, int stackPos)
        {
            if (luaState.Type(stackPos) == LuaType.Table)
            {
                if (luaState.GetMetaField(stackPos, "__index") != LuaType.Nil)
                {
                    if (luaState.CheckMetaTable(-1, _translator.Tag))
                    {
                        luaState.Insert(stackPos);
                        luaState.Remove(stackPos + 1);
                    }
                    else
                        luaState.SetTop(-2);
                }
            }

            object obj = _translator.GetObject(luaState, stackPos);
            return obj;
        }

        public object GetAsNetObject(LuaState luaState, int stackPos)
        {
            object obj = _translator.GetNetObject(luaState, stackPos);

            if (obj != null || luaState.Type(stackPos) != LuaType.Table)
                return obj;

            if (luaState.GetMetaField(stackPos, "__index") == LuaType.Nil)
                return null;

            if (luaState.CheckMetaTable(-1, _translator.Tag))
            {
                luaState.Insert(stackPos);
                luaState.Remove(stackPos + 1);
                obj = _translator.GetNetObject(luaState, stackPos);
            }
            else
                luaState.SetTop(-2);

            return obj;
        }
    }
}
