// 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.Concurrent;
using System.Collections.Generic;
using System.Linq.Expressions;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
    /// <summary>
    /// This class holds the cache for the expression text that is computed by ExpressionHelper.
    /// </summary>
    public class ExpressionTextCache
    {
        /// <inheritdoc />
        public ConcurrentDictionary<LambdaExpression, string> Entries { get; } =
            new ConcurrentDictionary<LambdaExpression, string>(LambdaExpressionComparer.Instance);

        // This comparer is tightly coupled with the logic of ExpressionHelper.GetExpressionText.
        // It is not designed to accurately compare any two arbitrary LambdaExpressions.
        private class LambdaExpressionComparer : IEqualityComparer<LambdaExpression>
        {
            public static readonly LambdaExpressionComparer Instance = new LambdaExpressionComparer();

            public bool Equals(LambdaExpression lambdaExpression1, LambdaExpression lambdaExpression2)
            {
                if (ReferenceEquals(lambdaExpression1,lambdaExpression2))
                {
                    return true;
                }
                // We will cache only pure member access expressions. Hence we compare two expressions
                // to be equal only if they are identical member access expressions.
                var expression1 = lambdaExpression1.Body;
                var expression2 = lambdaExpression2.Body;

                while (true)
                {
                    if (expression1 == null && expression2 == null)
                    {
                        return true;
                    }

                    if (expression1 == null || expression2 == null)
                    {
                        return false;
                    }

                    if (expression1.NodeType != expression2.NodeType)
                    {
                        return false;
                    }

                    switch (expression1.NodeType)
                    {
                        case ExpressionType.MemberAccess:
                            var memberExpression1 = (MemberExpression)expression1;
                            var memberName1 = memberExpression1.Member.Name;
                            expression1 = memberExpression1.Expression;

                            var memberExpression2 = (MemberExpression)expression2;
                            var memberName2 = memberExpression2.Member.Name;
                            expression2 = memberExpression2.Expression;

                            // If identifier contains "__", it is "reserved for use by the implementation" and likely
                            // compiler- or Razor-generated e.g. the name of a field in a delegate's generated class.
                            if (memberName1.Contains("__") && memberName2.Contains("__"))
                            {
                                return true;
                            }

                            if (!string.Equals(memberName1, memberName2, StringComparison.OrdinalIgnoreCase))
                            {
                                return false;
                            }
                            break;

                        case ExpressionType.ArrayIndex:
                            // Shouldn't be cached. Just in case, ensure indexers are all different.
                            return false;

                        case ExpressionType.Call:
                            // Shouldn't be cached. Just in case, ensure indexers and other calls are all different.
                            return false;

                        default:
                            // Everything else terminates name generation. Haven't found a difference so far...
                            return true;
                    }
                }
            }

            public int GetHashCode(LambdaExpression lambdaExpression)
            {
                var expression = lambdaExpression.Body;
                var hashCodeCombiner = HashCodeCombiner.Start();

                while (true)
                {
                    if (expression != null && expression.NodeType == ExpressionType.MemberAccess)
                    {
                        var memberExpression = (MemberExpression)expression;
                        var memberName = memberExpression.Member.Name;

                        if (memberName.Contains("__"))
                        {
                            break;
                        }

                        hashCodeCombiner.Add(memberName, StringComparer.OrdinalIgnoreCase);
                        expression = memberExpression.Expression;
                    }
                    else
                    {
                        break;
                    }
                }

                return hashCodeCombiner.CombinedHash;
            }
        }
    }
}
