// 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.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
    internal static class ExpressionHelper
    {
        public static string GetUncachedExpressionText(LambdaExpression expression)
            => GetExpressionText(expression, expressionTextCache: null);

        public static string GetExpressionText(LambdaExpression expression, ConcurrentDictionary<LambdaExpression, string> expressionTextCache)
        {
            if (expression == null)
            {
                throw new ArgumentNullException(nameof(expression));
            }

            if (expressionTextCache != null &&
                expressionTextCache.TryGetValue(expression, out var expressionText))
            {
                return expressionText;
            }

            // Determine size of string needed (length) and number of segments it contains (segmentCount). Put another
            // way, segmentCount tracks the number of times the loop below should iterate. This avoids adding ".model"
            // and / or an extra leading "." and then removing them after the loop. Other information collected in this
            // first loop helps with length and segmentCount adjustments. doNotCache is somewhat separate: If
            // true, expression strings are not cached for the expression.
            //
            // After the corrections below the first loop, length is usually exactly the size of the returned string.
            // However when containsIndexers is true, the calculation is approximate because either evaluating indexer
            // expressions multiple times or saving indexer strings can get expensive. Optimizing for the common case
            // of a collection (not a dictionary) with less than 100 elements. If that assumption proves to be
            // incorrect, the StringBuilder will be enlarged but hopefully just once.
            var doNotCache = false;
            var lastIsModel = false;
            var length = 0;
            var segmentCount = 0;
            var trailingMemberExpressions = 0;

            var part = expression.Body;
            while (part != null)
            {
                switch (part.NodeType)
                {
                    case ExpressionType.Call:
                        // Will exit loop if at Method().Property or [i,j].Property. In that case (like [i].Property),
                        // don't cache and don't remove ".Model" (if that's .Property).
                        doNotCache = true;
                        lastIsModel = false;

                        var methodExpression = (MethodCallExpression)part;
                        if (IsSingleArgumentIndexer(methodExpression))
                        {
                            length += "[99]".Length;
                            part = methodExpression.Object;
                            segmentCount++;
                            trailingMemberExpressions = 0;
                        }
                        else
                        {
                            // Unsupported.
                            part = null;
                        }
                        break;

                    case ExpressionType.ArrayIndex:
                        var binaryExpression = (BinaryExpression)part;

                        doNotCache = true;
                        lastIsModel = false;
                        length += "[99]".Length;
                        part = binaryExpression.Left;
                        segmentCount++;
                        trailingMemberExpressions = 0;
                        break;

                    case ExpressionType.MemberAccess:
                        var memberExpressionPart = (MemberExpression)part;
                        var name = memberExpressionPart.Member.Name;

                        // 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 (name.Contains("__"))
                        {
                            // Exit loop.
                            part = null;
                        }
                        else
                        {
                            lastIsModel = string.Equals("model", name, StringComparison.OrdinalIgnoreCase);
                            length += name.Length + 1;
                            part = memberExpressionPart.Expression;
                            segmentCount++;
                            trailingMemberExpressions++;
                        }
                        break;

                    case ExpressionType.Parameter:
                        // Unsupported but indicates previous member access was not the view's Model.
                        lastIsModel = false;
                        part = null;
                        break;

                    default:
                        // Unsupported.
                        part = null;
                        break;
                }
            }

            // If name would start with ".model", then strip that part away.
            if (lastIsModel)
            {
                length -= ".model".Length;
                segmentCount--;
                trailingMemberExpressions--;
            }

            // Trim the leading "." if present. The loop below special-cases the last property to avoid this addition.
            if (trailingMemberExpressions > 0)
            {
                length--;
            }

            Debug.Assert(segmentCount >= 0);
            if (segmentCount == 0)
            {
                Debug.Assert(!doNotCache);
                if (expressionTextCache != null)
                {
                    expressionTextCache.TryAdd(expression, string.Empty);
                }

                return string.Empty;
            }

            var builder = new StringBuilder(length);
            part = expression.Body;
            while (part != null && segmentCount > 0)
            {
                segmentCount--;
                switch (part.NodeType)
                {
                    case ExpressionType.Call:
                        Debug.Assert(doNotCache);
                        var methodExpression = (MethodCallExpression)part;

                        InsertIndexerInvocationText(builder, methodExpression.Arguments.Single(), expression);

                        part = methodExpression.Object;
                        break;

                    case ExpressionType.ArrayIndex:
                        Debug.Assert(doNotCache);
                        var binaryExpression = (BinaryExpression)part;

                        InsertIndexerInvocationText(builder, binaryExpression.Right, expression);

                        part = binaryExpression.Left;
                        break;

                    case ExpressionType.MemberAccess:
                        var memberExpression = (MemberExpression)part;
                        var name = memberExpression.Member.Name;
                        Debug.Assert(!name.Contains("__"));

                        builder.Insert(0, name);
                        if (segmentCount > 0)
                        {
                            // One or more parts to the left of this part are coming.
                            builder.Insert(0, '.');
                        }

                        part = memberExpression.Expression;
                        break;

                    default:
                        // Should be unreachable due to handling in above loop.
                        Debug.Assert(false);
                        break;
                }
            }

            Debug.Assert(segmentCount == 0);
            expressionText = builder.ToString();
            if (expressionTextCache != null && !doNotCache)
            {
                expressionTextCache.TryAdd(expression, expressionText);
            }

            return expressionText;
        }

        private static void InsertIndexerInvocationText(
            StringBuilder builder,
            Expression indexExpression,
            LambdaExpression parentExpression)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (indexExpression == null)
            {
                throw new ArgumentNullException(nameof(indexExpression));
            }

            if (parentExpression == null)
            {
                throw new ArgumentNullException(nameof(parentExpression));
            }

            if (parentExpression.Parameters == null)
            {
                throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(parentExpression.Parameters),
                    nameof(parentExpression)));
            }

            var converted = Expression.Convert(indexExpression, typeof(object));
            var fakeParameter = Expression.Parameter(typeof(object), null);
            var lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter);
            Func<object, object> func;

            try
            {
                func = CachedExpressionCompiler.Process(lambda) ?? lambda.Compile();
            }
            catch (InvalidOperationException ex)
            {
                var parameters = parentExpression.Parameters.ToArray();
                throw new InvalidOperationException(
                    Resources.FormatExpressionHelper_InvalidIndexerExpression(indexExpression, parameters[0].Name),
                    ex);
            }

            builder.Insert(0, ']');
            builder.Insert(0, Convert.ToString(func(null), CultureInfo.InvariantCulture));
            builder.Insert(0, '[');
        }

        public static bool IsSingleArgumentIndexer(Expression expression)
        {
            if (!(expression is MethodCallExpression methodExpression) || methodExpression.Arguments.Count != 1)
            {
                return false;
            }

            // Check whether GetDefaultMembers() (if present in CoreCLR) would return a member of this type. Compiler
            // names the indexer property, if any, in a generated [DefaultMember] attribute for the containing type.
            var declaringType = methodExpression.Method.DeclaringType;
            var defaultMember = declaringType.GetTypeInfo().GetCustomAttribute<DefaultMemberAttribute>(inherit: true);
            if (defaultMember == null)
            {
                return false;
            }

            // Find default property (the indexer) and confirm its getter is the method in this expression.
            var runtimeProperties = declaringType.GetRuntimeProperties();
            foreach (var property in runtimeProperties)
            {
                if ((string.Equals(defaultMember.MemberName, property.Name, StringComparison.Ordinal) &&
                    property.GetMethod == methodExpression.Method))
                {
                    return true;
                }
            }

            return false;
        }
    }
}
