//Copyright 2010 Microsoft Corporation
//
//Licensed 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 System.Data.Services.Client
{
    #region Namespaces.

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

    #endregion Namespaces.

    internal class MemberAssignmentAnalysis : ExpressionVisitor
    {
        #region Fields.

        internal static readonly Expression[] EmptyExpressionArray = new Expression[0];

        private readonly Expression entity;

        private Exception incompatibleAssignmentsException;

        private bool multiplePathsFound;

        private List<Expression> pathFromEntity;

        #endregion Fields.

        #region Constructor.

        private MemberAssignmentAnalysis(Expression entity)
        {
            Debug.Assert(entity != null, "entity != null");

            this.entity = entity;
            this.pathFromEntity = new List<Expression>();
        }

        #endregion Constructor.

        #region Properties.

        internal Exception IncompatibleAssignmentsException
        {
            get { return this.incompatibleAssignmentsException; }
        }

        internal bool MultiplePathsFound
        {
            get { return this.multiplePathsFound; }
        }

        #endregion Properites.

        #region Methods.

        internal static MemberAssignmentAnalysis Analyze(Expression entityInScope, Expression assignmentExpression)
        {
            Debug.Assert(entityInScope != null, "entityInScope != null");
            Debug.Assert(assignmentExpression != null, "assignmentExpression != null");

            MemberAssignmentAnalysis result = new MemberAssignmentAnalysis(entityInScope);
            result.Visit(assignmentExpression);
            return result;
        }

        internal Exception CheckCompatibleAssignments(Type targetType, ref MemberAssignmentAnalysis previous)
        {
            if (previous == null)
            {
                previous = this;
                return null;
            }

            Expression[] previousExpressions = previous.GetExpressionsToTargetEntity();
            Expression[] candidateExpressions = this.GetExpressionsToTargetEntity();
            return CheckCompatibleAssignments(targetType, previousExpressions, candidateExpressions);
        }

        internal override Expression Visit(Expression expression)
        {
            if (this.multiplePathsFound || this.incompatibleAssignmentsException != null)
            {
                return expression;
            }

            return base.Visit(expression);
        }

        internal override Expression VisitConditional(ConditionalExpression c)
        {
            Expression result;

            var nullCheck = ResourceBinder.PatternRules.MatchNullCheck(this.entity, c);
            if (nullCheck.Match)
            {
                this.Visit(nullCheck.AssignExpression);
                result = c;
            }
            else
            {
                result = base.VisitConditional(c);
            }

            return result;
        }

        internal override Expression VisitParameter(ParameterExpression p)
        {
            if (p == this.entity)
            {
                if (this.pathFromEntity.Count != 0)
                {
                    this.multiplePathsFound = true;
                }
                else
                {
                    this.pathFromEntity.Add(p);
                }
            }

            return p;
        }

        internal override Expression VisitMemberInit(MemberInitExpression init)
        {
            Expression result = init;
            MemberAssignmentAnalysis previousNested = null;
            foreach (var binding in init.Bindings)
            {
                MemberAssignment assignment = binding as MemberAssignment;
                if (assignment == null)
                {
                    continue;
                }

                MemberAssignmentAnalysis nested = MemberAssignmentAnalysis.Analyze(this.entity, assignment.Expression);
                if (nested.MultiplePathsFound)
                {
                    this.multiplePathsFound = true;
                    break;
                }

                Exception incompatibleException = nested.CheckCompatibleAssignments(init.Type, ref previousNested);
                if (incompatibleException != null)
                {
                    this.incompatibleAssignmentsException = incompatibleException;
                    break;
                }

                if (this.pathFromEntity.Count == 0)
                {
                    this.pathFromEntity.AddRange(nested.GetExpressionsToTargetEntity());
                }
            }

            return result;
        }

        internal override Expression VisitMemberAccess(MemberExpression m)
        {
            Expression result = base.VisitMemberAccess(m);
            if (this.pathFromEntity.Contains(m.Expression))
            {
                this.pathFromEntity.Add(m);
            }

            return result;
        }

        internal override Expression VisitMethodCall(MethodCallExpression call)
        {
            if (ReflectionUtil.IsSequenceMethod(call.Method, SequenceMethod.Select))
            {
                this.Visit(call.Arguments[0]);
                return call;
            }

            return base.VisitMethodCall(call);
        }

        internal Expression[] GetExpressionsBeyondTargetEntity()
        {
            Debug.Assert(!this.multiplePathsFound, "this.multiplePathsFound -- otherwise GetExpressionsToTargetEntity won't return reliable (consistent) results");

            if (this.pathFromEntity.Count <= 1)
            {
                return EmptyExpressionArray;
            }

            Expression[] result = new Expression[1];
            result[0] = this.pathFromEntity[this.pathFromEntity.Count - 1];
            return result;
        }

        internal Expression[] GetExpressionsToTargetEntity()
        {
            Debug.Assert(!this.multiplePathsFound, "this.multiplePathsFound -- otherwise GetExpressionsToTargetEntity won't return reliable (consistent) results");

            if (this.pathFromEntity.Count <= 1)
            {
                return EmptyExpressionArray;
            }

            Expression[] result = new Expression[this.pathFromEntity.Count - 1];
            for (int i = 0; i < result.Length; i++)
            {
                result[i] = this.pathFromEntity[i];
            }

            return result;
        }

        private static Exception CheckCompatibleAssignments(Type targetType, Expression[] previous, Expression[] candidate)
        {
            Debug.Assert(targetType != null, "targetType != null");
            Debug.Assert(previous != null, "previous != null");
            Debug.Assert(candidate != null, "candidate != null");

            if (previous.Length != candidate.Length)
            {
                throw CheckCompatibleAssignmentsFail(targetType, previous, candidate);
            }

            for (int i = 0; i < previous.Length; i++)
            {
                Expression p = previous[i];
                Expression c = candidate[i];
                if (p.NodeType != c.NodeType)
                {
                    throw CheckCompatibleAssignmentsFail(targetType, previous, candidate);
                }

                if (p == c)
                {
                    continue;
                }

                if (p.NodeType != ExpressionType.MemberAccess)
                {
                    return CheckCompatibleAssignmentsFail(targetType, previous, candidate);
                }

                if (((MemberExpression)p).Member.Name != ((MemberExpression)c).Member.Name)
                {
                    return CheckCompatibleAssignmentsFail(targetType, previous, candidate);
                }
            }

            return null;
        }

        private static Exception CheckCompatibleAssignmentsFail(Type targetType, Expression[] previous, Expression[] candidate)
        {
            Debug.Assert(targetType != null, "targetType != null");
            Debug.Assert(previous != null, "previous != null");
            Debug.Assert(candidate != null, "candidate != null");

            string message = Strings.ALinq_ProjectionMemberAssignmentMismatch(targetType.FullName, previous.LastOrDefault(), candidate.LastOrDefault());
            return new NotSupportedException(message);
        }

        #endregion Methods.
    }
}
