/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Apache.Ignite.Core.Impl.Common
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Reflection.Emit;
    using Apache.Ignite.Core.Binary;
    using Apache.Ignite.Core.Common;

    /// <summary>
    /// Converts generic and non-generic delegates.
    /// </summary>
    [CLSCompliant(false)]
    public static class DelegateConverter
    {
        /** */
        private const string DefaultMethodName = "Invoke";

        /** */
        private static readonly MethodInfo ReadObjectMethod = typeof (IBinaryRawReader).GetMethod("ReadObject");

        /** */
        public static readonly MethodInfo ConvertArrayMethod = typeof(DelegateConverter).GetMethod(
            "ConvertArray",
            BindingFlags.Static | BindingFlags.NonPublic);

        /** */
        public static readonly MethodInfo ConvertToSbyteArrayMethod = typeof(DelegateConverter).GetMethod(
            "ConvertToSbyteArray",
            BindingFlags.Static | BindingFlags.Public);

        /** */
        public static readonly MethodInfo ConvertToUshortArrayMethod = typeof(DelegateConverter).GetMethod(
            "ConvertToUshortArray",
            BindingFlags.Static | BindingFlags.Public);

        /** */
        public static readonly MethodInfo ConvertToUintArrayMethod = typeof(DelegateConverter).GetMethod(
            "ConvertToUintArray",
            BindingFlags.Static | BindingFlags.Public);

        /** */
        public static readonly MethodInfo ConvertToUlongArrayMethod = typeof(DelegateConverter).GetMethod(
            "ConvertToUlongArray",
            BindingFlags.Static | BindingFlags.Public);

        /// <summary>
        /// Compiles a function without arguments.
        /// </summary>
        /// <param name="targetType">Type of the target.</param>
        /// <returns>Compiled function that calls specified method on specified target.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static Func<object, object> CompileFunc(Type targetType)
        {
            var method = targetType.GetMethod(DefaultMethodName);
            Debug.Assert(method != null, "method != null");

            var targetParam = Expression.Parameter(typeof(object));
            var targetParamConverted = Expression.Convert(targetParam, targetType);

            var callExpr = Expression.Call(targetParamConverted, method);
            var convertResultExpr = Expression.Convert(callExpr, typeof(object));

            return Expression.Lambda<Func<object, object>>(convertResultExpr, targetParam).Compile();
        }

        /// <summary>
        /// Compiles a function with arbitrary number of arguments.
        /// </summary>
        /// <typeparam name="T">Resulting delegate type.</typeparam>
        /// <param name="targetType">Type of the target.</param>
        /// <param name="argTypes">Argument types.</param>
        /// <param name="convertToObject">
        /// Flags that indicate whether func params and/or return value should be converted from/to object.
        /// </param>
        /// <param name="methodName">Name of the method.</param>
        /// <returns>
        /// Compiled function that calls specified method on specified target.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static T CompileFunc<T>(Type targetType, Type[] argTypes, bool[] convertToObject = null,
            string methodName = null)
            where T : class
        {
            var method = targetType.GetMethod(methodName ?? DefaultMethodName, argTypes);

            return CompileFunc<T>(targetType, method, argTypes, convertToObject);
        }

        /// <summary>
        /// Compiles a function with arbitrary number of arguments.
        /// </summary>
        /// <typeparam name="T">Resulting delegate type.</typeparam>
        /// <param name="method">Method.</param>
        /// <param name="targetType">Type of the target.</param>
        /// <param name="argTypes">Argument types.</param>
        /// <param name="convertToObject">
        /// Flags that indicate whether func params and/or return value should be converted from/to object.
        /// </param>
        /// <returns>
        /// Compiled function that calls specified method on specified target.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static T CompileFunc<T>(Type targetType, MethodInfo method, Type[] argTypes,
            bool[] convertToObject = null)
            where T : class
        {
            if (argTypes == null)
            {
                var args = method.GetParameters();
                argTypes = new Type[args.Length];

                for (int i = 0; i < args.Length; i++)
                    argTypes[i] = args[i].ParameterType;
            }

            Debug.Assert(convertToObject == null || (convertToObject.Length == argTypes.Length + 1));
            Debug.Assert(method != null);

            targetType = method.IsStatic ? null : (targetType ?? method.DeclaringType);

            var targetParam = Expression.Parameter(typeof(object));

            Expression targetParamConverted = null;
            ParameterExpression[] argParams;
            int argParamsOffset = 0;

            if (targetType != null)
            {
                targetParamConverted = Expression.Convert(targetParam, targetType);
                argParams = new ParameterExpression[argTypes.Length + 1];
                argParams[0] = targetParam;
                argParamsOffset = 1;
            }
            else
                argParams = new ParameterExpression[argTypes.Length];  // static method

            var argParamsConverted = new Expression[argTypes.Length];

            for (var i = 0; i < argTypes.Length; i++)
            {
                if (convertToObject == null || convertToObject[i])
                {
                    var argParam = Expression.Parameter(typeof (object));
                    argParams[i + argParamsOffset] = argParam;
                    argParamsConverted[i] = Expression.Convert(argParam, argTypes[i]);
                }
                else
                {
                    var argParam = Expression.Parameter(argTypes[i]);
                    argParams[i + argParamsOffset] = argParam;
                    argParamsConverted[i] = argParam;
                }
            }

            Expression callExpr = Expression.Call(targetParamConverted, method, argParamsConverted);

            if (convertToObject == null || convertToObject[argTypes.Length])
                callExpr = Expression.Convert(callExpr, typeof(object));

            return Expression.Lambda<T>(callExpr, argParams).Compile();
        }
        /// <summary>
        /// Compiles a function with a single object[] argument which maps array items to actual arguments.
        /// </summary>
        /// <param name="method">Method.</param>
        /// <returns>
        /// Compiled function that calls specified method.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static Func<object, object[], object> CompileFuncFromArray(MethodInfo method)
        {
            Debug.Assert(method != null);
            Debug.Assert(method.DeclaringType != null);

            var targetParam = Expression.Parameter(typeof(object));
            var targetParamConverted = Expression.Convert(targetParam, method.DeclaringType);

            var arrParam = Expression.Parameter(typeof(object[]));

            var methodParams = method.GetParameters();
            var argParams = new Expression[methodParams.Length];

            for (var i = 0; i < methodParams.Length; i++)
            {
                var arrElem = Expression.ArrayIndex(arrParam, Expression.Constant(i));
                argParams[i] = Convert(arrElem, methodParams[i].ParameterType);
            }

            Expression callExpr = Expression.Call(targetParamConverted, method, argParams);

            if (callExpr.Type == typeof(void))
            {
                // Convert action to function
                var action = Expression.Lambda<Action<object, object[]>>(callExpr, targetParam, arrParam).Compile();
                return (obj, args) =>
                {
                    action(obj, args);
                    return null;
                };
            }

            callExpr = Expression.Convert(callExpr, typeof(object));

            return Expression.Lambda<Func<object, object[], object>>(callExpr, targetParam, arrParam).Compile();
        }

        /// <summary>
        /// Compiles a generic ctor with arbitrary number of arguments.
        /// </summary>
        /// <typeparam name="T">Result func type.</typeparam>
        /// <param name="ctor">Constructor info.</param>
        /// <param name="argTypes">Argument types.</param>
        /// <param name="convertResultToObject">
        /// Flag that indicates whether ctor return value should be converted to object.</param>
        /// <param name="convertParamsFromObject">
        /// Flag that indicates whether ctor args are object and should be converted to concrete type.</param>
        /// <returns>
        /// Compiled generic constructor.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static T CompileCtor<T>(ConstructorInfo ctor, Type[] argTypes, bool convertResultToObject = true,
            bool convertParamsFromObject = true)
        {
            Debug.Assert(ctor != null);

            var args = new ParameterExpression[argTypes.Length];
            var argsConverted = new Expression[argTypes.Length];

            for (var i = 0; i < argTypes.Length; i++)
            {
                if (convertParamsFromObject)
                {
                    var arg = Expression.Parameter(typeof(object));
                    args[i] = arg;
                    argsConverted[i] = Expression.Convert(arg, argTypes[i]);
                }
                else
                {
                    argsConverted[i] = args[i] = Expression.Parameter(argTypes[i]);
                }
            }

            Expression ctorExpr = Expression.New(ctor, argsConverted);  // ctor takes args of specific types

            if (convertResultToObject)
                ctorExpr = Expression.Convert(ctorExpr, typeof(object)); // convert ctor result to object

            return Expression.Lambda<T>(ctorExpr, args).Compile();  // lambda takes args as objects
        }

        /// <summary>
        /// Compiles a generic ctor with arbitrary number of arguments
        /// that takes an uninitialized object as a first arguments.
        /// </summary>
        /// <typeparam name="T">Result func type.</typeparam>
        /// <param name="ctor">Constructor info.</param>
        /// <param name="argTypes">Argument types.</param>
        /// <returns>
        /// Compiled generic constructor.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static T CompileUninitializedObjectCtor<T>(ConstructorInfo ctor, Type[] argTypes)
        {
            Debug.Assert(ctor != null);
            Debug.Assert(ctor.DeclaringType != null);
            Debug.Assert(argTypes != null);

            argTypes = new[] {typeof(object)}.Concat(argTypes).ToArray();

            var helperMethod = new DynamicMethod(string.Empty, typeof(void), argTypes, ctor.Module, true);
            var il = helperMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            if (ctor.DeclaringType.IsValueType)
                il.Emit(OpCodes.Unbox, ctor.DeclaringType);   // modify boxed copy

            if (argTypes.Length > 1)
                il.Emit(OpCodes.Ldarg_1);

            if (argTypes.Length > 2)
                il.Emit(OpCodes.Ldarg_2);

            if (argTypes.Length > 3)
                throw new NotSupportedException("Not supported: too many ctor args.");

            il.Emit(OpCodes.Call, ctor);
            il.Emit(OpCodes.Ret);

            var constructorInvoker = helperMethod.CreateDelegate(typeof(T));

            return (T) (object) constructorInvoker;
        }

        /// <summary>
        /// Compiles a generic ctor with arbitrary number of arguments.
        /// </summary>
        /// <typeparam name="T">Result func type.</typeparam>
        /// <param name="type">Type to be created by ctor.</param>
        /// <param name="argTypes">Argument types.</param>
        /// <param name="convertResultToObject">
        /// Flag that indicates whether ctor return value should be converted to object.
        /// </param>
        /// <returns>
        /// Compiled generic constructor.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static T CompileCtor<T>(Type type, Type[] argTypes, bool convertResultToObject = true)
        {
            var ctor = type.GetConstructor(argTypes);

            return CompileCtor<T>(ctor, argTypes, convertResultToObject);
        }

        /// <summary>
        /// Compiles a constructor that reads all arguments from a binary reader.
        /// </summary>
        /// <typeparam name="T">Result type</typeparam>
        /// <param name="ctor">The ctor.</param>
        /// <param name="innerCtorFunc">Function to retrieve reading constructor for an argument.
        /// Can be null or return null, in this case the argument will be read directly via ReadObject.</param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static Func<IBinaryRawReader, T> CompileCtor<T>(ConstructorInfo ctor,
            Func<Type, ConstructorInfo> innerCtorFunc)
        {
            Debug.Assert(ctor != null);

            var readerParam = Expression.Parameter(typeof (IBinaryRawReader));

            var ctorExpr = GetConstructorExpression(ctor, innerCtorFunc, readerParam, typeof(T));

            return Expression.Lambda<Func<IBinaryRawReader, T>>(ctorExpr, readerParam).Compile();
        }

        /// <summary>
        /// Gets the constructor expression.
        /// </summary>
        /// <param name="ctor">The ctor.</param>
        /// <param name="innerCtorFunc">The inner ctor function.</param>
        /// <param name="readerParam">The reader parameter.</param>
        /// <param name="resultType">Type of the result.</param>
        /// <returns>
        /// Ctor call expression.
        /// </returns>
        private static Expression GetConstructorExpression(ConstructorInfo ctor,
            Func<Type, ConstructorInfo> innerCtorFunc, Expression readerParam, Type resultType)
        {
            var ctorParams = ctor.GetParameters();

            var paramsExpr = new List<Expression>(ctorParams.Length);

            foreach (var param in ctorParams)
            {
                var paramType = param.ParameterType;

                var innerCtor = innerCtorFunc != null ? innerCtorFunc(paramType) : null;

                if (innerCtor != null)
                {
                    var readExpr = GetConstructorExpression(innerCtor, innerCtorFunc, readerParam, paramType);

                    paramsExpr.Add(readExpr);
                }
                else
                {
                    var readMethod = ReadObjectMethod.MakeGenericMethod(paramType);

                    var readExpr = Expression.Call(readerParam, readMethod);

                    paramsExpr.Add(readExpr);
                }
            }

            Expression ctorExpr = Expression.New(ctor, paramsExpr);

            ctorExpr = Expression.Convert(ctorExpr, resultType);

            return ctorExpr;
        }

        /// <summary>
        /// Compiles the field setter.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <returns>Compiled field setter.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static Action<object, object> CompileFieldSetter(FieldInfo field)
        {
            Debug.Assert(field != null);
            Debug.Assert(field.DeclaringType != null);

            var targetParam = Expression.Parameter(typeof(object));
            var valParam = Expression.Parameter(typeof(object));
            var valParamConverted = Expression.Convert(valParam, field.FieldType);

            var assignExpr = Expression.Call(GetWriteFieldMethod(field), targetParam, valParamConverted);

            return Expression.Lambda<Action<object, object>>(assignExpr, targetParam, valParam).Compile();
        }

        /// <summary>
        /// Compiles the property setter.
        /// </summary>
        /// <param name="prop">The property.</param>
        /// <returns>Compiled property setter.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static Action<object, object> CompilePropertySetter(PropertyInfo prop)
        {
            Debug.Assert(prop != null);
            Debug.Assert(prop.DeclaringType != null);

            var targetParam = Expression.Parameter(typeof(object));
            var targetParamConverted = Expression.Convert(targetParam, prop.DeclaringType);

            var valParam = Expression.Parameter(typeof(object));
            var valParamConverted = Expression.Convert(valParam, prop.PropertyType);

            var fld = Expression.Property(targetParamConverted, prop);

            var assignExpr = Expression.Assign(fld, valParamConverted);

            return Expression.Lambda<Action<object, object>>(assignExpr, targetParam, valParam).Compile();
        }

        /// <summary>
        /// Compiles the property setter.
        /// </summary>
        /// <param name="prop">The property.</param>
        /// <returns>Compiled property setter.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static Func<object, object> CompilePropertyGetter(PropertyInfo prop)
        {
            Debug.Assert(prop != null);
            Debug.Assert(prop.DeclaringType != null);

            var targetParam = Expression.Parameter(typeof(object));
            var targetParamConverted = prop.GetGetMethod().IsStatic
                ? null
                // ReSharper disable once AssignNullToNotNullAttribute (incorrect warning)
                : Expression.Convert(targetParam, prop.DeclaringType);

            var fld = Expression.Property(targetParamConverted, prop);

            var fldConverted = Expression.Convert(fld, typeof (object));

            return Expression.Lambda<Func<object, object>>(fldConverted, targetParam).Compile();
        }

        /// <summary>
        /// Compiles the property setter.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <returns>Compiled property setter.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static Func<object, object> CompileFieldGetter(FieldInfo field)
        {
            Debug.Assert(field != null);
            Debug.Assert(field.DeclaringType != null);

            var targetParam = Expression.Parameter(typeof(object));
            var targetParamConverted = field.IsStatic
                ? null
                : Expression.Convert(targetParam, field.DeclaringType);

            var fld = Expression.Field(targetParamConverted, field);

            var fldConverted = Expression.Convert(fld, typeof (object));

            return Expression.Lambda<Func<object, object>>(fldConverted, targetParam).Compile();
        }

        /// <summary>
        /// Gets a method to write a field (including private and readonly).
        /// NOTE: Expression Trees can't write readonly fields.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <returns>Resulting MethodInfo.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static DynamicMethod GetWriteFieldMethod(FieldInfo field)
        {
            Debug.Assert(field != null);

            var declaringType = field.DeclaringType;

            Debug.Assert(declaringType != null);

            var method = new DynamicMethod(string.Empty, null, new[] { typeof(object), field.FieldType },
                declaringType, true);

            var il = method.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            if (declaringType.IsValueType)
                il.Emit(OpCodes.Unbox, declaringType);   // modify boxed copy

            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Stfld, field);
            il.Emit(OpCodes.Ret);

            return method;
        }

        /// <summary>
        /// Gets the constructor with exactly matching signature.
        /// <para />
        /// Type.GetConstructor matches compatible ones (i.e. taking object instead of concrete type).
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="types">The argument types.</param>
        /// <returns>Constructor info.</returns>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public static ConstructorInfo GetConstructorExact(Type type, Type[] types)
        {
            Debug.Assert(type != null);
            Debug.Assert(types != null);

            foreach (var constructorInfo in type.GetConstructors(
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
            {
                var ctorTypes = constructorInfo.GetParameters().Select(x => x.ParameterType);

                if (ctorTypes.SequenceEqual(types))
                    return constructorInfo;
            }

            return null;
        }

        /// <summary>
        /// Converts expression to a given type.
        /// </summary>
        private static Expression Convert(Expression value, Type targetType)
        {
            if (targetType.IsArray)
            {
                var elType = targetType.GetElementType();
                Debug.Assert(elType != null);

                if (elType == typeof(sbyte))
                {
                    return Expression.Call(null, ConvertToSbyteArrayMethod, value);
                }

                if (elType == typeof(ushort))
                {
                    return Expression.Call(null, ConvertToUshortArrayMethod, value);
                }

                if (elType == typeof(uint))
                {
                    return Expression.Call(null, ConvertToUintArrayMethod, value);
                }

                if (elType == typeof(ulong))
                {
                    return Expression.Call(null, ConvertToUlongArrayMethod, value);
                }

                if (elType != typeof(object))
                {
                    var convertMethod = ConvertArrayMethod.MakeGenericMethod(targetType.GetElementType());

                    return Expression.Call(null, convertMethod, value);
                }

                return Expression.Convert(value, targetType);
            }

            // For byte/sbyte and the like, simple Convert fails
            // E.g. the following does not work:   (sbyte)(object)((byte)1)
            // But this does:                      (sbyte)(byte)(object)((byte)1)
            // So for every "unsupported" type like sbyte, ushort, uint, ulong
            // we have to do an additional conversion
            if (targetType == typeof(sbyte))
            {
                value = Expression.Convert(value, typeof(byte));
            }
            else if (targetType == typeof(ushort))
            {
                value = Expression.Convert(value, typeof(short));
            }
            else if (targetType == typeof(uint))
            {
                value = Expression.Convert(value, typeof(int));
            }
            else if (targetType == typeof(ulong))
            {
                value = Expression.Convert(value, typeof(long));
            }

            return Expression.Convert(value, targetType);
        }

        /// <summary>
        /// Converts object array to typed array.
        /// </summary>
        // ReSharper disable once UnusedMember.Local (used by reflection).
        private static T[] ConvertArray<T>(object arrObj)
        {
            var arr = arrObj as Array;
            if (arr == null)
            {
                return null;
            }

            var res = new T[arr.Length];

            Array.Copy(arr, res, arr.Length);

            return res;
        }

        /// <summary>
        /// Converts to sbyte array.
        /// </summary>
        // ReSharper disable once UnusedMember.Global
        public static sbyte[] ConvertToSbyteArray(object arrObj)
        {
            return ConvertValueTypeArray<byte, sbyte>(arrObj, 1);
        }

        /// <summary>
        /// Converts to ushort array.
        /// </summary>
        // ReSharper disable once UnusedMember.Global
        public static ushort[] ConvertToUshortArray(object arrObj)
        {
            return ConvertValueTypeArray<short, ushort>(arrObj, 2);
        }

        /// <summary>
        /// Converts to uint array.
        /// </summary>
        // ReSharper disable once UnusedMember.Global
        public static uint[] ConvertToUintArray(object arrObj)
        {
            return ConvertValueTypeArray<int, uint>(arrObj, 4);
        }

        /// <summary>
        /// Converts to ulong array.
        /// </summary>
        // ReSharper disable once UnusedMember.Global
        public static ulong[] ConvertToUlongArray(object arrObj)
        {
            return ConvertValueTypeArray<long, ulong>(arrObj, 8);
        }

        /// <summary>
        /// Converts value type array to another type using direct copy.
        /// </summary>
        private static T[] ConvertValueTypeArray<TFrom, T>(object arrObj, int elementSize)
        {
            if (arrObj == null)
            {
                return null;
            }

            var arr = arrObj as TFrom[];
            if (arr == null)
            {
                throw new IgniteException(string.Format("Can't convert '{0}' to '{1}'", arrObj.GetType(), typeof(T[])));
            }

            var res = new T[arr.Length];

            Buffer.BlockCopy(arr, 0, res, 0, arr.Length * elementSize);

            return res;
        }
    }
}
