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

using System.Buffers;
using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods for setting up essential MVC services in an <see cref="IServiceCollection" />.
/// </summary>
public static class MvcCoreServiceCollectionExtensions
{
    /// <summary>
    /// Adds the minimum essential MVC services to the specified <see cref="IServiceCollection" />. Additional services
    /// including MVC's support for authorization, formatters, and validation must be added separately using the
    /// <see cref="IMvcCoreBuilder"/> returned from this method.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
    /// <returns>An <see cref="IMvcCoreBuilder"/> that can be used to further configure the MVC services.</returns>
    /// <remarks>
    /// The <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/> approach for configuring
    /// MVC is provided for experienced MVC developers who wish to have full control over the set of default services
    /// registered. <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/> will register
    /// the minimum set of services necessary to route requests and invoke controllers. It is not expected that any
    /// application will satisfy its requirements with just a call to
    /// <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/>. Additional configuration using the
    /// <see cref="IMvcCoreBuilder"/> will be required.
    /// </remarks>
    public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services)
    {
        ArgumentNullException.ThrowIfNull(services);

        var environment = GetServiceFromCollection<IWebHostEnvironment>(services);
        var partManager = GetApplicationPartManager(services, environment);
        services.TryAddSingleton(partManager);

        ConfigureDefaultFeatureProviders(partManager);
        ConfigureDefaultServices(services);
        AddMvcCoreServices(services);

        var builder = new MvcCoreBuilder(services, partManager);

        return builder;
    }

    private static void ConfigureDefaultFeatureProviders(ApplicationPartManager manager)
    {
        if (!manager.FeatureProviders.OfType<ControllerFeatureProvider>().Any())
        {
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
        }
    }

    private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services, IWebHostEnvironment? environment)
    {
        var manager = GetServiceFromCollection<ApplicationPartManager>(services);
        if (manager == null)
        {
            manager = new ApplicationPartManager();

            var entryAssemblyName = environment?.ApplicationName;
            if (string.IsNullOrEmpty(entryAssemblyName))
            {
                return manager;
            }

            manager.PopulateDefaultParts(entryAssemblyName);
        }

        return manager;
    }

    private static T? GetServiceFromCollection<T>(IServiceCollection services)
    {
        return (T?)services
            .LastOrDefault(d => d.ServiceType == typeof(T))
            ?.ImplementationInstance;
    }

    /// <summary>
    /// Adds the minimum essential MVC services to the specified <see cref="IServiceCollection" />. Additional services
    /// including MVC's support for authorization, formatters, and validation must be added separately using the
    /// <see cref="IMvcCoreBuilder"/> returned from this method.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
    /// <param name="setupAction">An <see cref="Action{MvcOptions}"/> to configure the provided <see cref="MvcOptions"/>.</param>
    /// <returns>An <see cref="IMvcCoreBuilder"/> that can be used to further configure the MVC services.</returns>
    /// <remarks>
    /// The <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/> approach for configuring
    /// MVC is provided for experienced MVC developers who wish to have full control over the set of default services
    /// registered. <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/> will register
    /// the minimum set of services necessary to route requests and invoke controllers. It is not expected that any
    /// application will satisfy its requirements with just a call to
    /// <see cref="MvcCoreServiceCollectionExtensions.AddMvcCore(IServiceCollection)"/>. Additional configuration using the
    /// <see cref="IMvcCoreBuilder"/> will be required.
    /// </remarks>
    public static IMvcCoreBuilder AddMvcCore(
        this IServiceCollection services,
        Action<MvcOptions> setupAction)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(setupAction);

        var builder = services.AddMvcCore();
        services.Configure(setupAction);

        return builder;
    }

    // To enable unit testing
    internal static void AddMvcCoreServices(IServiceCollection services)
    {
        //
        // Options
        //
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());

        //
        // Action Discovery
        //
        // These are consumed only when creating action descriptors, then they can be deallocated
        services.TryAddSingleton<ApplicationModelFactory>();
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IApplicationModelProvider, DefaultApplicationModelProvider>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IApplicationModelProvider, ApiBehaviorApplicationModelProvider>());
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IActionDescriptorProvider, ControllerActionDescriptorProvider>());

        services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();

        //
        // Action Selection
        //
        services.TryAddSingleton<IActionSelector, ActionSelector>();
        services.TryAddSingleton<ActionConstraintCache>();

        // Will be cached by the DefaultActionSelector
        services.TryAddEnumerable(ServiceDescriptor.Transient<IActionConstraintProvider, DefaultActionConstraintProvider>());

        // Policies for Endpoints
        services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ActionConstraintMatcherPolicy>());

        //
        // Controller Factory
        //
        // This has a cache, so it needs to be a singleton
        services.TryAddSingleton<IControllerFactory, DefaultControllerFactory>();

        // Will be cached by the DefaultControllerFactory
        services.TryAddTransient<IControllerActivator, DefaultControllerActivator>();

        services.TryAddSingleton<IControllerFactoryProvider, ControllerFactoryProvider>();
        services.TryAddSingleton<IControllerActivatorProvider, ControllerActivatorProvider>();
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IControllerPropertyActivator, DefaultControllerPropertyActivator>());

        //
        // Action Invoker
        //
        // The IActionInvokerFactory is cacheable
        services.TryAddSingleton<IActionInvokerFactory, ActionInvokerFactory>();
        services.TryAddEnumerable(
            ServiceDescriptor.Transient<IActionInvokerProvider, ControllerActionInvokerProvider>());

        // These are stateless
        services.TryAddSingleton<ControllerActionInvokerCache>();
        services.TryAddEnumerable(
            ServiceDescriptor.Singleton<IFilterProvider, DefaultFilterProvider>());
        services.TryAddSingleton<IActionResultTypeMapper, ActionResultTypeMapper>();

        //
        // Request body limit filters
        //
        services.TryAddTransient<RequestSizeLimitFilter>();
        services.TryAddTransient<DisableRequestSizeLimitFilter>();
        services.TryAddTransient<RequestFormLimitsFilter>();

        //
        // ModelBinding, Validation
        //
        // The DefaultModelMetadataProvider does significant caching and should be a singleton.
        services.TryAddSingleton<IModelMetadataProvider, DefaultModelMetadataProvider>();
        services.TryAdd(ServiceDescriptor.Transient<ICompositeMetadataDetailsProvider>(s =>
        {
            var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
            return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders);
        }));
        services.TryAddSingleton<IModelBinderFactory, ModelBinderFactory>();
        services.TryAddSingleton<IObjectModelValidator>(s =>
        {
            var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
            var metadataProvider = s.GetRequiredService<IModelMetadataProvider>();
            return new DefaultObjectValidator(metadataProvider, options.ModelValidatorProviders, options);
        });
        services.TryAddSingleton<ClientValidatorCache>();
        services.TryAddSingleton<ParameterBinder>();

        //
        // Random Infrastructure
        //
        services.TryAddSingleton<MvcMarkerService, MvcMarkerService>();
        services.TryAddSingleton<ITypeActivatorCache, TypeActivatorCache>();
        services.TryAddSingleton<IUrlHelperFactory, UrlHelperFactory>();
        services.TryAddSingleton<IHttpRequestStreamReaderFactory, MemoryPoolHttpRequestStreamReaderFactory>();
        services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
        services.TryAddSingleton(ArrayPool<byte>.Shared);
        services.TryAddSingleton(ArrayPool<char>.Shared);
        services.TryAddSingleton<OutputFormatterSelector, DefaultOutputFormatterSelector>();
        services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<PhysicalFileResult>, PhysicalFileResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<VirtualFileResult>, VirtualFileResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<FileStreamResult>, FileStreamResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<FileContentResult>, FileContentResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<RedirectResult>, RedirectResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<LocalRedirectResult>, LocalRedirectResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<RedirectToActionResult>, RedirectToActionResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
        services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
        services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();

        //
        // Route Handlers
        //
        services.TryAddSingleton<MvcRouteHandler>(); // Only one per app
        services.TryAddTransient<MvcAttributeRouteHandler>(); // Many per app

        //
        // Endpoint Routing / Endpoints
        //
        services.TryAddSingleton<ControllerActionEndpointDataSourceFactory>();
        services.TryAddSingleton<OrderedEndpointsSequenceProviderCache>();
        services.TryAddSingleton<ControllerActionEndpointDataSourceIdProvider>();
        services.TryAddSingleton<ActionEndpointFactory>();
        services.TryAddSingleton<DynamicControllerEndpointSelectorCache>();
        services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, DynamicControllerEndpointMatcherPolicy>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IRequestDelegateFactory, ControllerRequestDelegateFactory>());

        //
        // Middleware pipeline filter related
        //
        services.TryAddSingleton<MiddlewareFilterConfigurationProvider>();
        // This maintains a cache of middleware pipelines, so it needs to be a singleton
        services.TryAddSingleton<MiddlewareFilterBuilder>();
        // Sets ApplicationBuilder on MiddlewareFilterBuilder
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, MiddlewareFilterBuilderStartupFilter>());

        // ProblemDetails
        services.TryAddSingleton<ProblemDetailsFactory, DefaultProblemDetailsFactory>();
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IProblemDetailsWriter, DefaultApiProblemDetailsWriter>());
    }

    private static void ConfigureDefaultServices(IServiceCollection services)
    {
        services.AddRouting();
    }
}
