/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/
using System; using Microsoft;


#if !SILVERLIGHT

using System.Collections.Generic;
using System.Diagnostics;
#if CODEPLEX_40
using System.Linq.Expressions;
#else
using Microsoft.Linq.Expressions;
#endif
using System.Runtime.InteropServices;
#if CODEPLEX_40
using System.Dynamic;
using System.Dynamic.Utils;
#else
using Microsoft.Scripting;
using Microsoft.Scripting.Utils;
#endif
using ComTypes = System.Runtime.InteropServices.ComTypes;

#if CODEPLEX_40
namespace System.Dynamic {
#else
namespace Microsoft.Scripting {
#endif
    internal sealed class ComInvokeBinder {
        private readonly ComMethodDesc _methodDesc;
        private readonly Expression _method;        // ComMethodDesc to be called
        private readonly Expression _dispatch;      // IDispatch

        private readonly CallInfo _callInfo;
        private readonly DynamicMetaObject[] _args;
        private readonly bool[] _isByRef;
        private readonly Expression _instance;

        private BindingRestrictions _restrictions;

        private VarEnumSelector _varEnumSelector;
        private string[] _keywordArgNames;
        private int _totalExplicitArgs; // Includes the individial elements of ArgumentKind.Dictionary (if any)

        private ParameterExpression _dispatchObject;
        private ParameterExpression _dispatchPointer;
        private ParameterExpression _dispId;
        private ParameterExpression _dispParams;
        private ParameterExpression _paramVariants;
        private ParameterExpression _invokeResult;
        private ParameterExpression _returnValue;
        private ParameterExpression _dispIdsOfKeywordArgsPinned;
        private ParameterExpression _propertyPutDispId;

        internal ComInvokeBinder(
                CallInfo callInfo, 
                DynamicMetaObject[] args,
                bool[] isByRef,
                BindingRestrictions restrictions, 
                Expression method, 
                Expression dispatch, 
                ComMethodDesc methodDesc
                ) {

            Debug.Assert(callInfo != null, "arguments");
            Debug.Assert(args != null, "args");
            Debug.Assert(isByRef != null, "isByRef");
            Debug.Assert(method != null, "method");
            Debug.Assert(dispatch != null, "dispatch");

            Debug.Assert(TypeUtils.AreReferenceAssignable(typeof(ComMethodDesc), method.Type), "method");
            Debug.Assert(TypeUtils.AreReferenceAssignable(typeof(IDispatch), dispatch.Type), "dispatch");

            _method = method;
            _dispatch = dispatch;
            _methodDesc = methodDesc;

            _callInfo = callInfo;
            _args = args;
            _isByRef = isByRef;
            _restrictions = restrictions;

            // Set Instance to some value so that CallBinderHelper has the right number of parameters to work with
            _instance = dispatch;
        }

        private ParameterExpression DispatchObjectVariable {
            get { return EnsureVariable(ref _dispatchObject, typeof(IDispatch), "dispatchObject"); }
        }

        private ParameterExpression DispatchPointerVariable {
            get { return EnsureVariable(ref _dispatchPointer, typeof(IntPtr), "dispatchPointer"); }
        }

        private ParameterExpression DispIdVariable {
            get { return EnsureVariable(ref _dispId, typeof(int), "dispId"); }
        }

        private ParameterExpression DispParamsVariable {
            get { return EnsureVariable(ref _dispParams, typeof(ComTypes.DISPPARAMS), "dispParams"); }
        }

        private ParameterExpression InvokeResultVariable {
            get { return EnsureVariable(ref _invokeResult, typeof(Variant), "invokeResult"); }
        }

        private ParameterExpression ReturnValueVariable {
            get { return EnsureVariable(ref _returnValue, typeof(object), "returnValue"); }
        }

        private ParameterExpression DispIdsOfKeywordArgsPinnedVariable {
            get { return EnsureVariable(ref _dispIdsOfKeywordArgsPinned, typeof(GCHandle), "dispIdsOfKeywordArgsPinned"); }
        }

        private ParameterExpression PropertyPutDispIdVariable {
            get { return EnsureVariable(ref _propertyPutDispId, typeof(int), "propertyPutDispId"); }
        }

        private ParameterExpression ParamVariantsVariable {
            get {
                if (_paramVariants == null) {
                    _paramVariants = Expression.Variable(VariantArray.GetStructType(_args.Length), "paramVariants");
                }
                return _paramVariants;
            }
        }

        private static ParameterExpression EnsureVariable(ref ParameterExpression var, Type type, string name) {
            if (var != null) {
                return var;
            }
            return var = Expression.Variable(type, name);
        }

        private static Type MarshalType(DynamicMetaObject mo, bool isByRef) {
            Type marshalType = (mo.Value == null && mo.HasValue && !mo.LimitType.IsValueType) ? null : mo.LimitType;

            // we are not checking that mo.Expression is writeable or whether evaluating it has no sideeffects
            // the assumption is that whoever matched it with ByRef arginfo took care of this.
            if (isByRef) {
                // Null just means that null was supplied.
                if (marshalType == null) {
                    marshalType = mo.Expression.Type;
                }
                marshalType = marshalType.MakeByRefType();
            }
            return marshalType;
        }

        internal DynamicMetaObject Invoke() {
            _keywordArgNames = _callInfo.ArgumentNames.ToArray();
            _totalExplicitArgs = _args.Length;
            
            Type[] marshalArgTypes = new Type[_args.Length];

            // We already tested the instance, so no need to test it again
            for (int i = 0; i < _args.Length; i++) {
                DynamicMetaObject curMo = _args[i];
                _restrictions = _restrictions.Merge(ComBinderHelpers.GetTypeRestrictionForDynamicMetaObject(curMo));
                marshalArgTypes[i] = MarshalType(curMo, _isByRef[i]);
            }

            _varEnumSelector = new VarEnumSelector(marshalArgTypes);

            return new DynamicMetaObject(
                CreateScope(MakeIDispatchInvokeTarget()),
                BindingRestrictions.Combine(_args).Merge(_restrictions)
            );
        }

        private static void AddNotNull(List<ParameterExpression> list, ParameterExpression var) {
            if (var != null) list.Add(var);
        }

        private Expression CreateScope(Expression expression) {
            List<ParameterExpression> vars = new List<ParameterExpression>();
            AddNotNull(vars, _dispatchObject);
            AddNotNull(vars, _dispatchPointer);
            AddNotNull(vars, _dispId);
            AddNotNull(vars, _dispParams);
            AddNotNull(vars, _paramVariants);
            AddNotNull(vars, _invokeResult);
            AddNotNull(vars, _returnValue);
            AddNotNull(vars, _dispIdsOfKeywordArgsPinned);
            AddNotNull(vars, _propertyPutDispId);
            return vars.Count > 0 ? Expression.Block(vars, expression) : expression;
        }

        private Expression GenerateTryBlock() {
            //
            // Declare variables
            //
            ParameterExpression excepInfo = Expression.Variable(typeof(ExcepInfo), "excepInfo");
            ParameterExpression argErr = Expression.Variable(typeof(uint), "argErr");
            ParameterExpression hresult = Expression.Variable(typeof(int), "hresult");

            List<Expression> tryStatements = new List<Expression>();
            Expression expr;

            if (_keywordArgNames.Length > 0) {
                string[] names = _keywordArgNames.AddFirst(_methodDesc.Name);

                tryStatements.Add(
                    Expression.Assign(
                        Expression.Field(
                            DispParamsVariable,
                            typeof(ComTypes.DISPPARAMS).GetField("rgdispidNamedArgs")
                        ),
                        Expression.Call(typeof(UnsafeMethods).GetMethod("GetIdsOfNamedParameters"),
                            DispatchObjectVariable,
                            Expression.Constant(names),
                            DispIdVariable,
                            DispIdsOfKeywordArgsPinnedVariable
                        )
                    )
                );
            }

            //
            // Marshal the arguments to Variants
            //
            // For a call like this:
            //   comObj.Foo(100, 101, 102, x=123, z=125)
            // DISPPARAMS needs to be setup like this:
            //   cArgs:             5
            //   cNamedArgs:        2
            //   rgArgs:            123, 125, 102, 101, 100
            //   rgdispidNamedArgs: dispid x, dispid z (the dispids of x and z respectively)

            Expression[] parameters = MakeArgumentExpressions();

            int reverseIndex = _varEnumSelector.VariantBuilders.Length - 1;
            int positionalArgs = _varEnumSelector.VariantBuilders.Length - _keywordArgNames.Length; // args passed by position order and not by name
            for (int i = 0; i < _varEnumSelector.VariantBuilders.Length; i++, reverseIndex--) {
                int variantIndex;
                if (i >= positionalArgs) {
                    // Named arguments are in order at the start of rgArgs
                    variantIndex = i - positionalArgs;
                } else {
                    // Positial arguments are in reverse order at the tail of rgArgs
                    variantIndex = reverseIndex;
                }
                VariantBuilder variantBuilder = _varEnumSelector.VariantBuilders[i];

                Expression marshal = variantBuilder.InitializeArgumentVariant(
                    VariantArray.GetStructField(ParamVariantsVariable, variantIndex),
                    parameters[i + 1]
                );

                if (marshal != null) {
                    tryStatements.Add(marshal);
                }
            }


            //
            // Call Invoke
            //

            ComTypes.INVOKEKIND invokeKind;
            if (_methodDesc.IsPropertyPut) {
                if (_methodDesc.IsPropertyPutRef) {
                    invokeKind = ComTypes.INVOKEKIND.INVOKE_PROPERTYPUTREF;
                } else {
                    invokeKind = ComTypes.INVOKEKIND.INVOKE_PROPERTYPUT;
                }
            } else {
                // INVOKE_PROPERTYGET should only be needed for COM objects without typeinfo, where we might have to treat properties as methods
                invokeKind = ComTypes.INVOKEKIND.INVOKE_FUNC | ComTypes.INVOKEKIND.INVOKE_PROPERTYGET;
            }

            MethodCallExpression invoke = Expression.Call(
                typeof(UnsafeMethods).GetMethod("IDispatchInvoke"),
                DispatchPointerVariable,
                DispIdVariable,
                Expression.Constant(invokeKind),
                DispParamsVariable,
                InvokeResultVariable,
                excepInfo,
                argErr
            );

            expr = Expression.Assign(hresult, invoke);
            tryStatements.Add(expr);

            //
            // ComRuntimeHelpers.CheckThrowException(hresult, excepInfo, argErr, ThisParameter);
            //
            expr = Expression.Call(
                typeof(ComRuntimeHelpers).GetMethod("CheckThrowException"),
                hresult,
                excepInfo,
                argErr,
                Expression.Constant(_methodDesc.Name, typeof(string))
            );
            tryStatements.Add(expr);

            //
            // _returnValue = (ReturnType)_invokeResult.ToObject();
            //
            Expression invokeResultObject =
                Expression.Call(
                    InvokeResultVariable,
                    typeof(Variant).GetMethod("ToObject"));

            VariantBuilder[] variants = _varEnumSelector.VariantBuilders;

            Expression[] parametersForUpdates = MakeArgumentExpressions();
            tryStatements.Add(Expression.Assign(ReturnValueVariable, invokeResultObject));

            for (int i = 0, n = variants.Length; i < n; i++) {
                Expression updateFromReturn = variants[i].UpdateFromReturn(parametersForUpdates[i + 1]);
                if (updateFromReturn != null) {
                    tryStatements.Add(updateFromReturn);
                }
            }

            tryStatements.Add(Expression.Empty());

            return Expression.Block(new[] { excepInfo, argErr, hresult }, tryStatements);
        }

        private Expression GenerateFinallyBlock() {
            List<Expression> finallyStatements = new List<Expression>();

            //
            // UnsafeMethods.IUnknownRelease(dispatchPointer);
            //
            finallyStatements.Add(
                Expression.Call(
                    typeof(UnsafeMethods).GetMethod("IUnknownRelease"),
                    DispatchPointerVariable
                )
            );

            //
            // Clear memory allocated for marshalling
            //
            for (int i = 0, n = _varEnumSelector.VariantBuilders.Length; i < n; i++) {
                Expression clear = _varEnumSelector.VariantBuilders[i].Clear();
                if (clear != null) {
                    finallyStatements.Add(clear);
                }
            }

            //
            // _invokeResult.Clear()
            //

            finallyStatements.Add(
                Expression.Call(
                    InvokeResultVariable,
                    typeof(Variant).GetMethod("Clear")
                )
            );

            //
            // _dispIdsOfKeywordArgsPinned.Free()
            //
            if (_dispIdsOfKeywordArgsPinned != null) {
                finallyStatements.Add(
                    Expression.Call(
                        DispIdsOfKeywordArgsPinnedVariable,
                        typeof(GCHandle).GetMethod("Free")
                    )
                );
            }

            finallyStatements.Add(Expression.Empty());
            return Expression.Block(finallyStatements);
        }

        /// <summary>
        /// Create a stub for the target of the optimized lopop.
        /// </summary>
        /// <returns></returns>
        private Expression MakeIDispatchInvokeTarget() {
            Debug.Assert(_varEnumSelector.VariantBuilders.Length == _totalExplicitArgs);

            List<Expression> exprs = new List<Expression>();

            //
            // _dispId = ((DispCallable)this).ComMethodDesc.DispId;
            //
            exprs.Add(
                Expression.Assign(
                    DispIdVariable,
                    Expression.Property(_method, typeof(ComMethodDesc).GetProperty("DispId"))
                )
            );

            //
            // _dispParams.rgvararg = RuntimeHelpers.UnsafeMethods.ConvertVariantByrefToPtr(ref _paramVariants._element0)
            //
            if (_totalExplicitArgs != 0) {
                exprs.Add(
                    Expression.Assign(
                        Expression.Field(
                            DispParamsVariable,
                            typeof(ComTypes.DISPPARAMS).GetField("rgvarg")
                        ),
                        Expression.Call(
                            typeof(UnsafeMethods).GetMethod("ConvertVariantByrefToPtr"),
                            VariantArray.GetStructField(ParamVariantsVariable, 0)
                        )
                    )
                );
            }

            //
            // _dispParams.cArgs = <number_of_params>;
            //
            exprs.Add(
                Expression.Assign(
                    Expression.Field(
                        DispParamsVariable,
                        typeof(ComTypes.DISPPARAMS).GetField("cArgs")
                    ),
                    Expression.Constant(_totalExplicitArgs)
                )
            );

            if (_methodDesc.IsPropertyPut) {
                //
                // dispParams.cNamedArgs = 1;
                // dispParams.rgdispidNamedArgs = RuntimeHelpers.UnsafeMethods.GetNamedArgsForPropertyPut()
                //
                exprs.Add(
                    Expression.Assign(
                        Expression.Field(
                            DispParamsVariable,
                            typeof(ComTypes.DISPPARAMS).GetField("cNamedArgs")
                        ),
                        Expression.Constant(1)
                    )
                );

                exprs.Add(
                    Expression.Assign(
                        PropertyPutDispIdVariable,
                        Expression.Constant(ComDispIds.DISPID_PROPERTYPUT)
                    )
                );

                exprs.Add(
                    Expression.Assign(
                        Expression.Field(
                            DispParamsVariable,
                            typeof(ComTypes.DISPPARAMS).GetField("rgdispidNamedArgs")
                        ),
                        Expression.Call(
                            typeof(UnsafeMethods).GetMethod("ConvertInt32ByrefToPtr"),
                            PropertyPutDispIdVariable
                        )
                    )
                );
            } else {
                //
                // _dispParams.cNamedArgs = N;
                //
                exprs.Add(
                    Expression.Assign(
                        Expression.Field(
                            DispParamsVariable,
                            typeof(ComTypes.DISPPARAMS).GetField("cNamedArgs")
                        ),
                        Expression.Constant(_keywordArgNames.Length)
                    )
                );
            }

            //
            // _dispatchObject = _dispatch
            // _dispatchPointer = Marshal.GetIDispatchForObject(_dispatchObject);
            //

            exprs.Add(Expression.Assign(DispatchObjectVariable, _dispatch));

            exprs.Add(
                Expression.Assign(
                    DispatchPointerVariable,
                    Expression.Call(
                        typeof(Marshal).GetMethod("GetIDispatchForObject"),
                        DispatchObjectVariable
                    )
                )
            );

            Expression tryStatements = GenerateTryBlock();
            Expression finallyStatements = GenerateFinallyBlock();

            exprs.Add(Expression.TryFinally(tryStatements, finallyStatements));

            exprs.Add(ReturnValueVariable);
            var vars = new List<ParameterExpression>();
            foreach (var variant in _varEnumSelector.VariantBuilders) {
                if (variant.TempVariable != null) {
                    vars.Add(variant.TempVariable);
                }
            }
            return Expression.Block(vars, exprs);
        }

        /// <summary>
        /// Gets expressions to access all the arguments. This includes the instance argument.
        /// </summary>
        private Expression[] MakeArgumentExpressions() {
            Expression[] res;
            int copy = 0;
            if (_instance != null) {
                res = new Expression[_args.Length + 1];
                res[copy++] = _instance;
            } else {
                res = new Expression[_args.Length];
            }

            for (int i = 0; i < _args.Length; i++) {
                res[copy++] = _args[i].Expression;
            }
            return res;
        }
    }
}

#endif
