// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.


using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.Routing
{
    internal class ControllerRequestDelegateFactory : IRequestDelegateFactory
    {
        private readonly ControllerActionInvokerCache _controllerActionInvokerCache;
        private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories;
        private readonly int _maxModelValidationErrors;
        private readonly int? _maxValidationDepth;
        private readonly int _maxModelBindingRecursionDepth;
        private readonly ILogger _logger;
        private readonly DiagnosticListener _diagnosticListener;
        private readonly IActionResultTypeMapper _mapper;
        private readonly IActionContextAccessor _actionContextAccessor;
        private readonly bool _enableActionInvokers;

        public ControllerRequestDelegateFactory(
            ControllerActionInvokerCache controllerActionInvokerCache,
            IOptions<MvcOptions> optionsAccessor,
            ILoggerFactory loggerFactory,
            DiagnosticListener diagnosticListener,
            IActionResultTypeMapper mapper)
            : this(controllerActionInvokerCache, optionsAccessor, loggerFactory, diagnosticListener, mapper, null)
        {
        }

        public ControllerRequestDelegateFactory(
            ControllerActionInvokerCache controllerActionInvokerCache,
            IOptions<MvcOptions> optionsAccessor,
            ILoggerFactory loggerFactory,
            DiagnosticListener diagnosticListener,
            IActionResultTypeMapper mapper,
            IActionContextAccessor? actionContextAccessor)
        {
            _controllerActionInvokerCache = controllerActionInvokerCache;
            _valueProviderFactories = optionsAccessor.Value.ValueProviderFactories.ToArray();
            _maxModelValidationErrors = optionsAccessor.Value.MaxModelValidationErrors;
            _maxValidationDepth = optionsAccessor.Value.MaxValidationDepth;
            _maxModelBindingRecursionDepth = optionsAccessor.Value.MaxModelBindingRecursionDepth;
            _enableActionInvokers = optionsAccessor.Value.EnableActionInvokers;
            _logger = loggerFactory.CreateLogger<ControllerActionInvoker>();
            _diagnosticListener = diagnosticListener;
            _mapper = mapper;
            _actionContextAccessor = actionContextAccessor ?? ActionContextAccessor.Null;
        }

        public RequestDelegate? CreateRequestDelegate(ActionDescriptor actionDescriptor, RouteValueDictionary? dataTokens)
        {
            // Fallback to action invoker extensibility so that invokers can override any default behaviors
            if (_enableActionInvokers || actionDescriptor is not ControllerActionDescriptor)
            {
                return null;
            }

            return context =>
            {
                RouteData routeData;

                if (dataTokens is null or { Count: 0 })
                {
                    routeData = new RouteData(context.Request.RouteValues);
                }
                else
                {
                    routeData = new RouteData();
                    routeData.PushState(router: null, context.Request.RouteValues, dataTokens);
                }

                var actionContext = new ActionContext(context, routeData, actionDescriptor);

                var controllerContext = new ControllerContext(actionContext)
                {
                    // PERF: These are rarely going to be changed, so let's go copy-on-write.
                    ValueProviderFactories = new CopyOnWriteList<IValueProviderFactory>(_valueProviderFactories)
                };

                controllerContext.ModelState.MaxAllowedErrors = _maxModelValidationErrors;
                controllerContext.ModelState.MaxValidationDepth = _maxValidationDepth;
                controllerContext.ModelState.MaxStateDepth = _maxModelBindingRecursionDepth;

                var (cacheEntry, filters) = _controllerActionInvokerCache.GetCachedResult(controllerContext);

                var invoker = new ControllerActionInvoker(
                    _logger,
                    _diagnosticListener,
                    _actionContextAccessor,
                    _mapper,
                    controllerContext,
                    cacheEntry,
                    filters);

                return invoker.InvokeAsync();
            };
        }
    }
}
