// 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.Reflection.Runtime.General;

namespace System.Reflection.Runtime.BindingFlagSupport
{
    internal static class Shared
    {
        //
        // This is similar to FilterApplyMethodBase from CoreClr with some important differences:
        //
        //   - Does *not* filter on Public|NonPublic|Instance|Static|FlatternHierarchy. Caller is expected to have done that.
        //
        //   - ArgumentTypes cannot be null.
        //
        // Used by Type.GetMethodImpl(), Type.GetConstructorImpl(), Type.InvokeMember() and Activator.CreateInstance(). Does some
        // preliminary weeding out of candidate methods based on the supplied calling convention and parameter list lengths.
        //
        // Candidates must pass this screen before we involve the binder.
        //
        public static bool QualifiesBasedOnParameterCount(this MethodBase methodBase, BindingFlags bindingFlags, CallingConventions callConv, Type?[] argumentTypes)
        {
            Debug.Assert(methodBase is not null);
            Debug.Assert(argumentTypes is not null);
#if DEBUG
            bindingFlags &= ~(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase);
#endif

            #region Check CallingConvention
            if ((callConv & CallingConventions.Any) == 0)
            {
                if ((callConv & CallingConventions.VarArgs) != 0 && (methodBase.CallingConvention & CallingConventions.VarArgs) == 0)
                    return false;

                if ((callConv & CallingConventions.Standard) != 0 && (methodBase.CallingConvention & CallingConventions.Standard) == 0)
                    return false;
            }
            #endregion

            #region ArgumentTypes
            ParameterInfo[] parameterInfos = methodBase.GetParametersNoCopy();

            if (argumentTypes.Length != parameterInfos.Length)
            {
                #region Invoke Member, Get\Set & Create Instance specific case
                // If the number of supplied arguments differs than the number in the signature AND
                // we are not filtering for a dynamic call -- InvokeMethod or CreateInstance -- filter out the method.
                if ((bindingFlags & (BindingFlags.InvokeMethod | BindingFlags.CreateInstance | BindingFlags.GetProperty | BindingFlags.SetProperty)) == 0)
                    return false;

                bool testForParamArray = false;
                bool excessSuppliedArguments = argumentTypes.Length > parameterInfos.Length;

                if (excessSuppliedArguments)
                { // more supplied arguments than parameters, additional arguments could be vararg
                    #region Varargs
                    // If method is not vararg, additional arguments can not be passed as vararg
                    if ((methodBase.CallingConvention & CallingConventions.VarArgs) == 0)
                    {
                        testForParamArray = true;
                    }
                    else
                    {
                        // If Binding flags did not include varargs we would have filtered this vararg method.
                        // This Invariant established during callConv check.
                        Debug.Assert((callConv & CallingConventions.VarArgs) != 0);
                    }
                    #endregion
                }
                else
                {// fewer supplied arguments than parameters, missing arguments could be optional
                    #region OptionalParamBinding
                    if ((bindingFlags & BindingFlags.OptionalParamBinding) == 0)
                    {
                        testForParamArray = true;
                    }
                    else
                    {
                        // From our existing code, our policy here is that if a parameterInfo
                        // is optional then all subsequent parameterInfos shall be optional.

                        // Thus, iff the first parameterInfo is not optional then this MethodInfo is no longer a canidate.
                        if (!parameterInfos[argumentTypes.Length].IsOptional)
                            testForParamArray = true;
                    }
                    #endregion
                }

                #region ParamArray
                if (testForParamArray)
                {
                    if (parameterInfos.Length == 0)
                        return false;

                    // The last argument of the signature could be a param array.
                    bool shortByMoreThanOneSuppliedArgument = argumentTypes.Length < parameterInfos.Length - 1;

                    if (shortByMoreThanOneSuppliedArgument)
                        return false;

                    ParameterInfo lastParameter = parameterInfos[parameterInfos.Length - 1];

                    if (!lastParameter.ParameterType.IsArray)
                        return false;

                    if (!lastParameter.IsDefined(typeof(ParamArrayAttribute), false))
                        return false;
                }
                #endregion

                #endregion
            }
            else
            {
                #region Exact Binding
                if ((bindingFlags & BindingFlags.ExactBinding) != 0)
                {
                    // Legacy behavior is to ignore ExactBinding when InvokeMember is specified.
                    // Why filter by InvokeMember? If the answer is we leave this to the binder then why not leave
                    // all the rest of this  to the binder too? Further, what other semanitc would the binder
                    // use for BindingFlags.ExactBinding besides this one? Further, why not include CreateInstance
                    // in this if statement? That's just InvokeMethod with a constructor, right?
                    if ((bindingFlags & (BindingFlags.InvokeMethod)) == 0)
                    {
                        for (int i = 0; i < parameterInfos.Length; i++)
                        {
                            // a null argument type implies a null arg which is always a perfect match
                            if (argumentTypes[i] is not null && !argumentTypes[i].MatchesParameterTypeExactly(parameterInfos[i]))
                                return false;
                        }
                    }
                }
                #endregion
            }
            #endregion

            return true;
        }

        //
        // If member is a virtual member that implicitly overrides a member in a base class, return the overridden member.
        // Otherwise, return null.
        //
        // - MethodImpls ignored. (I didn't say it made sense, this is just how the desktop api we're porting behaves.)
        // - Implemented interfaces ignores. (I didn't say it made sense, this is just how the desktop api we're porting behaves.)
        //
        public static M GetImplicitlyOverriddenBaseClassMember<M>(this M member, MemberPolicies<M> policies) where M : MemberInfo
        {
            bool isVirtual;
            bool isNewSlot;
            policies.GetMemberAttributes(member, out _, out _, out isVirtual, out isNewSlot);
            if (isNewSlot || !isVirtual)
            {
                return null;
            }
            string name = member.Name;
            Type type = member.DeclaringType!;
            for (;;)
            {
                Type? baseType = type.BaseType;
                if (baseType == null)
                {
                    return null;
                }
                type = baseType;
                foreach (M candidate in policies.GetDeclaredMembers(type))
                {
                    if (candidate.Name != name)
                    {
                        continue;
                    }
                    bool isCandidateVirtual;
                    policies.GetMemberAttributes(member, out _, out _, out isCandidateVirtual, out _);
                    if (!isCandidateVirtual)
                    {
                        continue;
                    }
                    if (!policies.ImplicitlyOverrides(candidate, member))
                    {
                        continue;
                    }
                    return candidate;
                }
            }
        }
    }
}
