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

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Http;

internal static class ResultsOfTHelper
{
    public const DynamicallyAccessedMemberTypes RequireMethods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods;
    private static readonly MethodInfo PopulateMetadataMethod = typeof(ResultsOfTHelper).GetMethod(nameof(PopulateMetadata), BindingFlags.Static | BindingFlags.NonPublic)!;

    // TODO: Improve calling static interface method with reflection
    // https://github.com/dotnet/aspnetcore/issues/46267
    public static void PopulateMetadataIfTargetIsIEndpointMetadataProvider<[DynamicallyAccessedMembers(RequireMethods)] TTarget>(MethodInfo method, EndpointBuilder builder)
    {
        if (typeof(IEndpointMetadataProvider).IsAssignableFrom(typeof(TTarget)))
        {
            var parameters = new object[] { method, builder };

            if (RuntimeFeature.IsDynamicCodeSupported)
            {
                InvokeGenericPopulateMetadata(parameters);
            }
            else
            {
                // Prioritize explicit implementation.
                var populateMetadataMethod = typeof(TTarget).GetMethod("Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata", BindingFlags.Static | BindingFlags.NonPublic);
                if (populateMetadataMethod is null)
                {
                    populateMetadataMethod = typeof(TTarget).GetMethod("PopulateMetadata", BindingFlags.Static | BindingFlags.Public);
                }
                // Method won't be found if it is from a default interface implementation.
                // Improve with https://github.com/dotnet/aspnetcore/issues/46267
                if (populateMetadataMethod is null)
                {
                    throw new InvalidOperationException($"Couldn't populate metadata for {typeof(TTarget).Name}. PopulateMetadata must by defined on the result type. A default interface implementation isn't supported with AOT.");
                }
                Debug.Assert(populateMetadataMethod != null, $"Couldn't find PopulateMetadata method on {typeof(TTarget)}.");

                populateMetadataMethod.Invoke(null, BindingFlags.DoNotWrapExceptions, binder: null, parameters, culture: null);
            }
        }

        // TODO: Remove IL3050 suppress when https://github.com/dotnet/linker/issues/2715 is complete.
        [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Validated with IsDynamicCodeSupported check.")]
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:MakeGenericMethod",
            Justification = "The call to MakeGenericMethod is safe due to the fact that  PopulateMetadata<TTarget> does not have a DynamicallyAccessMembers attribute and TTarget is annotated to preserve all methods to preserve the PopulateMetadata method.")]
        static void InvokeGenericPopulateMetadata(object[] parameters)
        {
            PopulateMetadataMethod.MakeGenericMethod(typeof(TTarget)).Invoke(null, parameters);
        }
    }

    private static void PopulateMetadata<TTarget>(MethodInfo method, EndpointBuilder builder) where TTarget : IEndpointMetadataProvider
    {
        TTarget.PopulateMetadata(method, builder);
    }
}
