#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System.Reflection;
using Grpc.Core;

namespace Grpc.Shared.Server;

internal static class BindMethodFinder
{
    private const BindingFlags BindMethodBindingFlags = BindingFlags.Public | BindingFlags.Static;

    internal static MethodInfo? GetBindMethod(Type serviceType)
    {
        // Prefer finding the bind method using attribute on the generated service
        var bindMethodInfo = GetBindMethodUsingAttribute(serviceType);

        if (bindMethodInfo == null)
        {
            // Fallback to searching for bind method using known type hierarchy that Grpc.Tools generates
            bindMethodInfo = GetBindMethodFallback(serviceType);
        }

        return bindMethodInfo;
    }

    internal static MethodInfo? GetBindMethodUsingAttribute(Type serviceType)
    {
        Type? currentServiceType = serviceType;
        BindServiceMethodAttribute? bindServiceMethod;
        do
        {
            // Search through base types for bind service attribute
            // We need to know the base service type because it is used with GetMethod below
            bindServiceMethod = currentServiceType.GetCustomAttribute<BindServiceMethodAttribute>();
            if (bindServiceMethod != null)
            {
                // Bind method will be public and static
                // Two parameters: ServiceBinderBase and the service type
                return bindServiceMethod.BindType.GetMethod(
                    bindServiceMethod.BindMethodName,
                    BindMethodBindingFlags,
                    binder: null,
                    new[] { typeof(ServiceBinderBase), currentServiceType },
                    Array.Empty<ParameterModifier>());
            }
        } while ((currentServiceType = currentServiceType.BaseType) != null);

        return null;
    }

    internal static MethodInfo? GetBindMethodFallback(Type serviceType)
    {
        // Search for the generated service base class
        var baseType = GetServiceBaseType(serviceType);
        if (baseType == null)
        {
            return null;
        }

        // We need to call Foo.BindService from the declaring type.
        var declaringType = baseType.DeclaringType;

        // The method we want to call is public static void BindService(ServiceBinderBase, BaseType)
        return declaringType?.GetMethod(
            "BindService",
            BindMethodBindingFlags,
            binder: null,
            new[] { typeof(ServiceBinderBase), baseType },
            Array.Empty<ParameterModifier>());
    }

    private static Type? GetServiceBaseType(Type serviceImplementation)
    {
        // TService is an implementation of the gRPC service. It ultimately derives from Foo.TServiceBase base class.
        // We need to access the static BindService method on Foo which implicitly derives from Object.
        var baseType = serviceImplementation.BaseType;

        // Handle services that have multiple levels of inheritence
        while (baseType?.BaseType?.BaseType != null)
        {
            baseType = baseType.BaseType;
        }

        return baseType;
    }
}
