// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.Internal
{
    internal class ObjectMethodExecutor
    {
        // ReSharper disable once InconsistentNaming
        private static readonly ConstructorInfo _objectMethodExecutorAwaitableConstructor =
            typeof(ObjectMethodExecutorAwaitable).GetConstructor(new[]
            {
                typeof(object), // customAwaitable
                typeof(Func<object, object>), // getAwaiterMethod
                typeof(Func<object, bool>), // isCompletedMethod
                typeof(Func<object, object>), // getResultMethod
                typeof(Action<object, Action>), // onCompletedMethod
                typeof(Action<object, Action>) // unsafeOnCompletedMethod
            });

        private readonly MethodExecutor _executor;
        private readonly MethodExecutorAsync _executorAsync;
        private readonly object[] _parameterDefaultValues;

        private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues)
        {
            if (methodInfo == null)
            {
                throw new ArgumentNullException(nameof(methodInfo));
            }

            MethodInfo = methodInfo;
            MethodParameters = methodInfo.GetParameters();
            TargetTypeInfo = targetTypeInfo;
            MethodReturnType = methodInfo.ReturnType;

            var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo);

            IsMethodAsync = isAwaitable;
            AsyncResultType = isAwaitable ? coercedAwaitableInfo.AwaitableInfo.ResultType : null;

            // Upstream code may prefer to use the sync-executor even for async methods, because if it knows
            // that the result is a specific Task<T> where T is known, then it can directly cast to that type
            // and await it without the extra heap allocations involved in the _executorAsync code path.
            _executor = GetExecutor(methodInfo, targetTypeInfo);

            if (IsMethodAsync)
            {
                _executorAsync = GetExecutorAsync(methodInfo, targetTypeInfo, coercedAwaitableInfo);
            }

            _parameterDefaultValues = parameterDefaultValues;
        }

        public MethodInfo MethodInfo { get; }

        public ParameterInfo[] MethodParameters { get; }

        public TypeInfo TargetTypeInfo { get; }

        public Type AsyncResultType { get; }

        // This field is made internal set because it is set in unit tests.
        public Type MethodReturnType { get; internal set; }

        public bool IsMethodAsync { get; }

        public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo)
        {
            return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null);
        }

        public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo,
            object[] parameterDefaultValues)
        {
            if (parameterDefaultValues == null)
            {
                throw new ArgumentNullException(nameof(parameterDefaultValues));
            }

            return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues);
        }

        /// <summary>
        /// Executes the configured method on <paramref name="target" />. This can be used whether or not
        /// the configured method is asynchronous.
        /// </summary>
        /// <remarks>
        /// Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than
        /// ExecuteAsync if you know at compile time what the return type is, because then you can directly
        /// "await" that value (via a cast), and then the generated code will be able to reference the
        /// resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated
        /// code will have to treat the resulting awaitable as a boxed object, because it doesn't know at
        /// compile time what type it would be.
        /// </remarks>
        /// <param name="target">The object whose method is to be executed.</param>
        /// <param name="parameters">Parameters to pass to the method.</param>
        /// <returns>The method return value.</returns>
        public object Execute(object target, params object[] parameters)
        {
            return _executor(target, parameters);
        }

        /// <summary>
        /// Executes the configured method on <paramref name="target" />. This can only be used if the configured
        /// method is asynchronous.
        /// </summary>
        /// <remarks>
        /// If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync,
        /// which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations
        /// as compared with using Execute and then using "await" on the result value typecasted to the known
        /// awaitable type. The possible extra heap allocations are for:
        /// 1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally
        /// it's a reference type, and you normally create a new instance per call).
        /// 2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance
        /// of it, and if it is, it will have to be boxed so the calling code can reference it as an object).
        /// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling
        /// code doesn't know what type it's going to be).
        /// </remarks>
        /// <param name="target">The object whose method is to be executed.</param>
        /// <param name="parameters">Parameters to pass to the method.</param>
        /// <returns>An object that you can "await" to get the method return value.</returns>
        public ObjectMethodExecutorAwaitable ExecuteAsync(object target, params object[] parameters)
        {
            return _executorAsync(target, parameters);
        }

        public object GetDefaultValueForParameter(int index)
        {
            if (_parameterDefaultValues == null)
            {
                throw new InvalidOperationException(
                    $"Cannot call {nameof(GetDefaultValueForParameter)}, because no parameter default values were supplied.");
            }

            if (index < 0 || index > MethodParameters.Length - 1)
            {
                throw new ArgumentOutOfRangeException(nameof(index));
            }

            return _parameterDefaultValues[index];
        }

        private static MethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo)
        {
            // Parameters to executor
            var targetParameter = Expression.Parameter(typeof(object), "target");
            var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

            // Build parameter list
            var parameters = new List<Expression>();
            var paramInfos = methodInfo.GetParameters();
            for (var i = 0; i < paramInfos.Length; i++)
            {
                var paramInfo = paramInfos[i];
                var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
                var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);

                // valueCast is "(Ti) parameters[i]"
                parameters.Add(valueCast);
            }

            // Call method
            var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
            var methodCall = Expression.Call(instanceCast, methodInfo, parameters);

            // methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)"
            // Create function
            if (methodCall.Type == typeof(void))
            {
                var lambda = Expression.Lambda<VoidMethodExecutor>(methodCall, targetParameter, parametersParameter);
                var voidExecutor = lambda.Compile();
                return WrapVoidMethod(voidExecutor);
            }
            else
            {
                // must coerce methodCall to match ActionExecutor signature
                var castMethodCall = Expression.Convert(methodCall, typeof(object));
                var lambda = Expression.Lambda<MethodExecutor>(castMethodCall, targetParameter, parametersParameter);
                return lambda.Compile();
            }
        }

        private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor)
        {
            return delegate(object target, object[] parameters)
            {
                executor(target, parameters);
                return null;
            };
        }

        private static MethodExecutorAsync GetExecutorAsync(
            MethodInfo methodInfo,
            TypeInfo targetTypeInfo,
            CoercedAwaitableInfo coercedAwaitableInfo)
        {
            // Parameters to executor
            var targetParameter = Expression.Parameter(typeof(object), "target");
            var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

            // Build parameter list
            var parameters = new List<Expression>();
            var paramInfos = methodInfo.GetParameters();
            for (var i = 0; i < paramInfos.Length; i++)
            {
                var paramInfo = paramInfos[i];
                var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
                var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);

                // valueCast is "(Ti) parameters[i]"
                parameters.Add(valueCast);
            }

            // Call method
            var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
            var methodCall = Expression.Call(instanceCast, methodInfo, parameters);

            // Using the method return value, construct an ObjectMethodExecutorAwaitable based on
            // the info we have about its implementation of the awaitable pattern. Note that all
            // the funcs/actions we construct here are precompiled, so that only one instance of
            // each is preserved throughout the lifetime of the ObjectMethodExecutor.

            // var getAwaiterFunc = (object awaitable) =>
            //     (object)((CustomAwaitableType)awaitable).GetAwaiter();
            var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable");
            var awaitableInfo = coercedAwaitableInfo.AwaitableInfo;
            var postCoercionMethodReturnType = coercedAwaitableInfo.CoercerResultType ?? methodInfo.ReturnType;
            var getAwaiterFunc = Expression.Lambda<Func<object, object>>(
                Expression.Convert(
                    Expression.Call(
                        Expression.Convert(customAwaitableParam, postCoercionMethodReturnType),
                        awaitableInfo.GetAwaiterMethod),
                    typeof(object)),
                customAwaitableParam).Compile();

            // var isCompletedFunc = (object awaiter) =>
            //     ((CustomAwaiterType)awaiter).IsCompleted;
            var isCompletedParam = Expression.Parameter(typeof(object), "awaiter");
            var isCompletedFunc = Expression.Lambda<Func<object, bool>>(
                Expression.MakeMemberAccess(
                    Expression.Convert(isCompletedParam, awaitableInfo.AwaiterType),
                    awaitableInfo.AwaiterIsCompletedProperty),
                isCompletedParam).Compile();

            var getResultParam = Expression.Parameter(typeof(object), "awaiter");
            Func<object, object> getResultFunc;
            if (awaitableInfo.ResultType == typeof(void))
            {
                getResultFunc = Expression.Lambda<Func<object, object>>(
                    Expression.Block(
                        Expression.Call(
                            Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
                            awaitableInfo.AwaiterGetResultMethod),
                        Expression.Constant(null)
                    ),
                    getResultParam).Compile();
            }
            else
            {
                getResultFunc = Expression.Lambda<Func<object, object>>(
                    Expression.Convert(
                        Expression.Call(
                            Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
                            awaitableInfo.AwaiterGetResultMethod),
                        typeof(object)),
                    getResultParam).Compile();
            }

            // var onCompletedFunc = (object awaiter, Action continuation) => {
            //     ((CustomAwaiterType)awaiter).OnCompleted(continuation);
            // };
            var onCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
            var onCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
            var onCompletedFunc = Expression.Lambda<Action<object, Action>>(
                Expression.Call(
                    Expression.Convert(onCompletedParam1, awaitableInfo.AwaiterType),
                    awaitableInfo.AwaiterOnCompletedMethod,
                    onCompletedParam2),
                onCompletedParam1,
                onCompletedParam2).Compile();

            Action<object, Action> unsafeOnCompletedFunc = null;
            if (awaitableInfo.AwaiterUnsafeOnCompletedMethod != null)
            {
                // var unsafeOnCompletedFunc = (object awaiter, Action continuation) => {
                //     ((CustomAwaiterType)awaiter).UnsafeOnCompleted(continuation);
                // };
                var unsafeOnCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
                var unsafeOnCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
                unsafeOnCompletedFunc = Expression.Lambda<Action<object, Action>>(
                    Expression.Call(
                        Expression.Convert(unsafeOnCompletedParam1, awaitableInfo.AwaiterType),
                        awaitableInfo.AwaiterUnsafeOnCompletedMethod,
                        unsafeOnCompletedParam2),
                    unsafeOnCompletedParam1,
                    unsafeOnCompletedParam2).Compile();
            }

            // If we need to pass the method call result through a coercer function to get an
            // awaitable, then do so.
            var coercedMethodCall = coercedAwaitableInfo.RequiresCoercion
                ? Expression.Invoke(coercedAwaitableInfo.CoercerExpression, methodCall)
                : (Expression) methodCall;

            // return new ObjectMethodExecutorAwaitable(
            //     (object)coercedMethodCall,
            //     getAwaiterFunc,
            //     isCompletedFunc,
            //     getResultFunc,
            //     onCompletedFunc,
            //     unsafeOnCompletedFunc);
            var returnValueExpression = Expression.New(
                _objectMethodExecutorAwaitableConstructor,
                Expression.Convert(coercedMethodCall, typeof(object)),
                Expression.Constant(getAwaiterFunc),
                Expression.Constant(isCompletedFunc),
                Expression.Constant(getResultFunc),
                Expression.Constant(onCompletedFunc),
                Expression.Constant(unsafeOnCompletedFunc, typeof(Action<object, Action>)));

            var lambda =
                Expression.Lambda<MethodExecutorAsync>(returnValueExpression, targetParameter, parametersParameter);
            return lambda.Compile();
        }

        private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, params object[] parameters);

        private delegate object MethodExecutor(object target, params object[] parameters);

        private delegate void VoidMethodExecutor(object target, object[] parameters);
    }
}