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

#nullable enable

using System;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Builder
{
    /// <summary>
    /// Provides extension methods for <see cref="IRouteBuilder"/> to add routes.
    /// </summary>
    public static class MapRouteRouteBuilderExtensions
    {
        /// <summary>
        /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name and template.
        /// </summary>
        /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
        /// <param name="name">The name of the route.</param>
        /// <param name="template">The URL pattern of the route.</param>
        /// <returns>A reference to this instance after the operation has completed.</returns>
        public static IRouteBuilder MapRoute(
            this IRouteBuilder routeBuilder,
            string? name,
            string? template)
        {
            MapRoute(routeBuilder, name, template, defaults: null);
            return routeBuilder;
        }

        /// <summary>
        /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name, template, and default values.
        /// </summary>
        /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
        /// <param name="name">The name of the route.</param>
        /// <param name="template">The URL pattern of the route.</param>
        /// <param name="defaults">
        /// An object that contains default values for route parameters. The object's properties represent the names
        /// and values of the default values.
        /// </param>
        /// <returns>A reference to this instance after the operation has completed.</returns>
        public static IRouteBuilder MapRoute(
            this IRouteBuilder routeBuilder,
            string? name,
            string? template,
            object? defaults)
        {
            return MapRoute(routeBuilder, name, template, defaults, constraints: null);
        }

        /// <summary>
        /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name, template, default values, and
        /// constraints.
        /// </summary>
        /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
        /// <param name="name">The name of the route.</param>
        /// <param name="template">The URL pattern of the route.</param>
        /// <param name="defaults">
        /// An object that contains default values for route parameters. The object's properties represent the names
        /// and values of the default values.
        /// </param>
        /// <param name="constraints">
        /// An object that contains constraints for the route. The object's properties represent the names and values
        /// of the constraints.
        /// </param>
        /// <returns>A reference to this instance after the operation has completed.</returns>
        public static IRouteBuilder MapRoute(
            this IRouteBuilder routeBuilder,
            string? name,
            string? template,
            object? defaults,
            object? constraints)
        {
            return MapRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null);
        }

        /// <summary>
        /// Adds a route to the <see cref="IRouteBuilder"/> with the specified name, template, default values, and
        /// data tokens.
        /// </summary>
        /// <param name="routeBuilder">The <see cref="IRouteBuilder"/> to add the route to.</param>
        /// <param name="name">The name of the route.</param>
        /// <param name="template">The URL pattern of the route.</param>
        /// <param name="defaults">
        /// An object that contains default values for route parameters. The object's properties represent the names
        /// and values of the default values.
        /// </param>
        /// <param name="constraints">
        /// An object that contains constraints for the route. The object's properties represent the names and values
        /// of the constraints.
        /// </param>
        /// <param name="dataTokens">
        /// An object that contains data tokens for the route. The object's properties represent the names and values
        /// of the data tokens.
        /// </param>
        /// <returns>A reference to this instance after the operation has completed.</returns>
        public static IRouteBuilder MapRoute(
            this IRouteBuilder routeBuilder,
            string? name,
            string? template,
            object? defaults,
            object? constraints,
            object? dataTokens)
        {
            if (routeBuilder.DefaultHandler == null)
            {
                throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder)));
            }

            routeBuilder.Routes.Add(new Route(
                routeBuilder.DefaultHandler,
                name,
                template,
                new RouteValueDictionary(defaults),
                new RouteValueDictionary(constraints)!,
                new RouteValueDictionary(dataTokens),
                CreateInlineConstraintResolver(routeBuilder.ServiceProvider)));

            return routeBuilder;
        }

        private static IInlineConstraintResolver CreateInlineConstraintResolver(IServiceProvider serviceProvider)
        {
            var inlineConstraintResolver = serviceProvider
                .GetRequiredService<IInlineConstraintResolver>();

            var parameterPolicyFactory = serviceProvider
                .GetRequiredService<ParameterPolicyFactory>();

            // This inline constraint resolver will return a null constraint for non-IRouteConstraint
            // parameter policies so Route does not error
            return new BackCompatInlineConstraintResolver(inlineConstraintResolver, parameterPolicyFactory);
        }

        private class BackCompatInlineConstraintResolver : IInlineConstraintResolver
        {
            private readonly IInlineConstraintResolver _inner;
            private readonly ParameterPolicyFactory _parameterPolicyFactory;

            public BackCompatInlineConstraintResolver(IInlineConstraintResolver inner, ParameterPolicyFactory parameterPolicyFactory)
            {
                _inner = inner;
                _parameterPolicyFactory = parameterPolicyFactory;
            }

            public IRouteConstraint? ResolveConstraint(string inlineConstraint)
            {
                var routeConstraint = _inner.ResolveConstraint(inlineConstraint);
                if (routeConstraint != null)
                {
                    return routeConstraint;
                }

                var parameterPolicy = _parameterPolicyFactory.Create(null!, inlineConstraint);
                if (parameterPolicy != null)
                {
                    // Logic inside Route will skip adding NullRouteConstraint
                    return NullRouteConstraint.Instance;
                }

                return null;
            }
        }
    }
}
