/* ****************************************************************************
 *
 * 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 System.Collections.Generic;
using System.Dynamic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.Scripting.Utils;

namespace Microsoft.Scripting.Actions {
    /// <summary>
    /// A binder which can combine multiple binders into a single dynamic site.  The creator
    /// of this needs to perform the mapping of parameters, constants, and sub-site expressions
    /// and provide a List of BinderMappingInfo representing this data.  From there the ComboBinder
    /// just processes the list to create the resulting code.
    /// </summary>
    public class ComboBinder : DynamicMetaObjectBinder {
        private readonly BinderMappingInfo[] _metaBinders;

        public ComboBinder(params BinderMappingInfo[] binders)
            : this((ICollection<BinderMappingInfo>)binders) {
        }

        public ComboBinder(ICollection<BinderMappingInfo> binders) {
            Assert.NotNullItems(binders);

            _metaBinders = ArrayUtils.ToArray(binders);
        }

        public override DynamicMetaObject Bind(DynamicMetaObject target, params DynamicMetaObject[] args) {
            args = ArrayUtils.Insert(target, args);

            List<DynamicMetaObject> results = new List<DynamicMetaObject>(_metaBinders.Length);
            List<Expression> steps = new List<Expression>();
            List<ParameterExpression> temps = new List<ParameterExpression>();
            BindingRestrictions restrictions = BindingRestrictions.Empty;

            for (int i = 0; i < _metaBinders.Length; i++) {
                BinderMappingInfo curBinder = _metaBinders[i];

                DynamicMetaObject[] tmpargs = GetArguments(args, results, i);
                DynamicMetaObject next = curBinder.Binder.Bind(tmpargs[0], ArrayUtils.RemoveFirst(tmpargs));
                if (i != 0) {
                    // If the rule contains an embedded "update", replace it with a defer
                    var visitor = new ReplaceUpdateVisitor { Binder = curBinder.Binder, Arguments = tmpargs };
                    next = new DynamicMetaObject(visitor.Visit(next.Expression), next.Restrictions);
                }

                restrictions = restrictions.Merge(next.Restrictions);
                if (next.Expression.NodeType == ExpressionType.Throw) {
                    // end of the line... the expression is throwing, none of the other 
                    // binders will have an opportunity to run.
                    steps.Add(next.Expression);
                    break;
                }

                ParameterExpression tmp = Expression.Variable(next.Expression.Type, "comboTemp" + i.ToString());
                temps.Add(tmp);

                steps.Add(Expression.Assign(tmp, next.Expression));
                results.Add(new DynamicMetaObject(tmp, next.Restrictions));
            }

            return new DynamicMetaObject(
                Expression.Block(
                    temps.ToArray(),
                    steps.ToArray()
                ),
                restrictions
            );
        }

        public override Type ReturnType {
            get {
                return _metaBinders[_metaBinders.Length - 1].Binder.ReturnType;
            }
        }

        private sealed class ReplaceUpdateVisitor : ExpressionVisitor {
            internal DynamicMetaObjectBinder Binder;
            internal DynamicMetaObject[] Arguments;

            protected override Expression VisitGoto(GotoExpression node) {
                if (node.Target == CallSiteBinder.UpdateLabel) {
                    return Binder.Defer(Arguments).Expression;
                }
                return base.Visit(node);
            }
        }


        private DynamicMetaObject[] GetArguments(DynamicMetaObject[] args, IList<DynamicMetaObject> results, int metaBinderIndex) {
            BinderMappingInfo indices = _metaBinders[metaBinderIndex];

            DynamicMetaObject[] res = new DynamicMetaObject[indices.MappingInfo.Count];
            for (int i = 0; i < res.Length; i++) {
                ParameterMappingInfo mappingInfo = indices.MappingInfo[i];

                if (mappingInfo.IsAction) {
                    // input is the result of a previous bind
                    res[i] = results[mappingInfo.ActionIndex];
                } else if (mappingInfo.IsParameter) {
                    // input is one of the original arguments
                    res[i] = args[mappingInfo.ParameterIndex];
                } else {
                    // input is a constant
                    res[i] = new DynamicMetaObject(
                        mappingInfo.Constant,
                        BindingRestrictions.Empty,
                        mappingInfo.Constant.Value
                    );
                }
            }

            return res;
        }

        public override int GetHashCode() {
            int res = 6551;
            foreach (BinderMappingInfo metaBinder in _metaBinders) {
                res ^= metaBinder.Binder.GetHashCode();

                foreach (ParameterMappingInfo mapInfo in metaBinder.MappingInfo) {
                    res ^= mapInfo.GetHashCode();
                }
            }

            return res;
        }

        public override bool Equals(object obj) {
            ComboBinder other = obj as ComboBinder;
            if (other != null) {
                if (_metaBinders.Length != other._metaBinders.Length) {
                    return false;
                }

                for (int i = 0; i < _metaBinders.Length; i++) {
                    BinderMappingInfo self = _metaBinders[i];
                    BinderMappingInfo otherBinders = other._metaBinders[i];

                    if (!self.Binder.Equals(otherBinders.Binder) ||
                        self.MappingInfo.Count != otherBinders.MappingInfo.Count) {
                        return false;
                    }

                    for (int j = 0; j < self.MappingInfo.Count; j++) {
                        if (!self.MappingInfo[j].Equals(otherBinders.MappingInfo[j])) {
                            return false;
                        }
                    }
                }
                return true;
            }

            return false;
        }
    }

    /// <summary>
    /// Provides a mapping for inputs of combo action expressions.  The input can map
    /// to either an input of the new dynamic site, an input of a previous DynamicExpression,
    /// or a ConstantExpression which has been pulled out of the dynamic site arguments.
    /// </summary>
    public class ParameterMappingInfo {
        private readonly int _parameterIndex;
        private readonly int _actionIndex;
        private ConstantExpression _fixedInput;

        private ParameterMappingInfo(int param, int action, ConstantExpression fixedInput) {
            _parameterIndex = param;
            _actionIndex = action;
            _fixedInput = fixedInput;
        }

        public static ParameterMappingInfo Parameter(int index) {
            return new ParameterMappingInfo(index, -1, null);
        }

        public static ParameterMappingInfo Action(int index) {
            return new ParameterMappingInfo(-1, index, null);
        }

        public static ParameterMappingInfo Fixed(ConstantExpression e) {
            return new ParameterMappingInfo(-1, -1, e);
        }

        public int ParameterIndex {
            get {
                return _parameterIndex;
            }
        }

        public int ActionIndex {
            get {
                return _actionIndex;
            }
        }

        public ConstantExpression Constant {
            get {
                return _fixedInput;
            }
        }

        public bool IsParameter {
            get {
                return _parameterIndex != -1;
            }
        }

        public bool IsAction {
            get {
                return _actionIndex != -1;
            }
        }

        public bool IsConstant {
            get {
                return _fixedInput != null;
            }
        }

        public override bool Equals(object obj) {
            ParameterMappingInfo pmi = obj as ParameterMappingInfo;
            if (pmi == null) {
                return false;
            }

            if (pmi.ParameterIndex == ParameterIndex && pmi.ActionIndex == ActionIndex) {
                if (Constant != null) {
                    if (pmi.Constant == null) {
                        return false;
                    }

                    return Constant.Value == pmi.Constant.Value;
                } else {
                    return pmi.Constant == null;
                }
            }

            return false;
        }

        public override int GetHashCode() {
            int res = ParameterIndex.GetHashCode() ^ ActionIndex.GetHashCode();
            if (Constant != null) {
                if (Constant.Value != null) {
                    res ^= Constant.Value.GetHashCode();
                }
            }
            return res;
        }

        public override string ToString() {
            if (IsAction) {
                return "Action" + ActionIndex.ToString();
            } else if (IsParameter) {
                return "Parameter" + ParameterIndex.ToString();
            } else {
                object value = Constant.Value;
                if (value == null) {
                    return "(null)";
                }

                return value.ToString();
            }
        }
    }

    /// <summary>
    /// Contains the mapping information for a single Combo Binder.  This includes the original
    /// meta-binder and the mapping of parameters, sub-sites, and constants into the binding.
    /// </summary>
    public class BinderMappingInfo {
        public DynamicMetaObjectBinder Binder;
        public IList<ParameterMappingInfo> MappingInfo;

        public BinderMappingInfo(DynamicMetaObjectBinder binder, IList<ParameterMappingInfo> mappingInfo) {
            Binder = binder;
            MappingInfo = mappingInfo;
        }

        public BinderMappingInfo(DynamicMetaObjectBinder binder, params ParameterMappingInfo[] mappingInfos)
            : this(binder, (IList<ParameterMappingInfo>)mappingInfos) {
        }

        public override string ToString() {
            StringBuilder res = new StringBuilder();
            res.Append(Binder.ToString());
            res.Append(" ");
            string comma = "";
            foreach (ParameterMappingInfo info in MappingInfo) {
                res.Append(comma);
                res.Append(info.ToString());
                comma = ", ";
            }
            return res.ToString();
        }
    }
}
