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

#nullable disable

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Routing
{
    internal static class ParameterPolicyActivator
    {
        public static T ResolveParameterPolicy<T>(
            IDictionary<string, Type> inlineParameterPolicyMap,
            IServiceProvider serviceProvider,
            string inlineParameterPolicy,
            out string parameterPolicyKey)
            where T : IParameterPolicy
        {
            // IServiceProvider could be null
            // DefaultInlineConstraintResolver can be created without an IServiceProvider and then call this method

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

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

            string argumentString;
            var indexOfFirstOpenParens = inlineParameterPolicy.IndexOf('(');
            if (indexOfFirstOpenParens >= 0 && inlineParameterPolicy.EndsWith(")", StringComparison.Ordinal))
            {
                parameterPolicyKey = inlineParameterPolicy.Substring(0, indexOfFirstOpenParens);
                argumentString = inlineParameterPolicy.Substring(
                    indexOfFirstOpenParens + 1,
                    inlineParameterPolicy.Length - indexOfFirstOpenParens - 2);
            }
            else
            {
                parameterPolicyKey = inlineParameterPolicy;
                argumentString = null;
            }

            if (!inlineParameterPolicyMap.TryGetValue(parameterPolicyKey, out var parameterPolicyType))
            {
                return default;
            }

            if (!typeof(T).IsAssignableFrom(parameterPolicyType))
            {
                if (!typeof(IParameterPolicy).IsAssignableFrom(parameterPolicyType))
                {
                    // Error if type is not a parameter policy
                    throw new RouteCreationException(
                                Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint(
                                                            parameterPolicyType, parameterPolicyKey, typeof(T).Name));
                }

                // Return null if type is parameter policy but is not the exact type
                // This is used by IInlineConstraintResolver for backwards compatibility
                // e.g. looking for an IRouteConstraint but get a different IParameterPolicy type
                return default;
            }

            try
            {
                return (T)CreateParameterPolicy(serviceProvider, parameterPolicyType, argumentString);
            }
            catch (RouteCreationException)
            {
                throw;
            }
            catch (Exception exception)
            {
                throw new RouteCreationException(
                    $"An error occurred while trying to create an instance of '{parameterPolicyType.FullName}'.",
                    exception);
            }
        }

        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2006:UnrecognizedReflectionPattern", Justification = "This type comes from the ConstraintMap.")]
        private static IParameterPolicy CreateParameterPolicy(IServiceProvider serviceProvider, Type parameterPolicyType, string argumentString)
        {
            ConstructorInfo activationConstructor = null;
            object[] parameters = null;
            var constructors = parameterPolicyType.GetConstructors();

            // If there is only one constructor and it has a single parameter, pass the argument string directly
            // This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas.
            if (constructors.Length == 1 && GetNonConvertableParameterTypeCount(serviceProvider, constructors[0].GetParameters()) == 1)
            {
                activationConstructor = constructors[0];
                parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), new string[] { argumentString });
            }
            else
            {
                var arguments = argumentString?.Split(',', StringSplitOptions.TrimEntries) ?? Array.Empty<string>();

                // We want to find the constructors that match the number of passed in arguments
                // We either want a single match, or a single best match. The best match is the one with the most
                // arguments that can be resolved from DI
                //
                // For example, ctor(string, IService) will beat ctor(string)
                var matchingConstructors = constructors
                    .Where(ci => GetNonConvertableParameterTypeCount(serviceProvider, ci.GetParameters()) == arguments.Length)
                    .OrderByDescending(ci => ci.GetParameters().Length)
                    .ToArray();

                if (matchingConstructors.Length == 0)
                {
                    throw new RouteCreationException(
                                Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor(
                                                       parameterPolicyType.Name, arguments.Length));
                }
                else
                {
                    // When there are multiple matching constructors, choose the one with the most service arguments
                    if (matchingConstructors.Length == 1
                        || matchingConstructors[0].GetParameters().Length > matchingConstructors[1].GetParameters().Length)
                    {
                        activationConstructor = matchingConstructors[0];
                    }
                    else
                    {
                        throw new RouteCreationException(
                                    Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors(
                                                           parameterPolicyType.Name, matchingConstructors[0].GetParameters().Length));
                    }

                    parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), arguments);
                }
            }

            return (IParameterPolicy)activationConstructor.Invoke(parameters);
        }

        private static int GetNonConvertableParameterTypeCount(IServiceProvider serviceProvider, ParameterInfo[] parameters)
        {
            if (serviceProvider == null)
            {
                return parameters.Length;
            }

            var count = 0;
            for (var i = 0; i < parameters.Length; i++)
            {
                if (typeof(IConvertible).IsAssignableFrom(parameters[i].ParameterType))
                {
                    count++;
                }
            }

            return count;
        }

        private static object[] ConvertArguments(IServiceProvider serviceProvider, ParameterInfo[] parameterInfos, string[] arguments)
        {
            var parameters = new object[parameterInfos.Length];
            var argumentPosition = 0;
            for (var i = 0; i < parameterInfos.Length; i++)
            {
                var parameter = parameterInfos[i];
                var parameterType = parameter.ParameterType;

                if (serviceProvider != null && !typeof(IConvertible).IsAssignableFrom(parameterType))
                {
                    parameters[i] = serviceProvider.GetRequiredService(parameterType);
                }
                else
                {
                    parameters[i] = Convert.ChangeType(arguments[argumentPosition], parameterType, CultureInfo.InvariantCulture);
                    argumentPosition++;
                }
            }

            return parameters;
        }
    }
}
