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

using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.Serialization;

namespace System.Private.Windows.Core.BinaryFormat;

internal sealed class SerializationEvents
{
    private static readonly ConcurrentDictionary<Type, SerializationEvents> s_cache = new();

    private static readonly SerializationEvents s_noEvents = new();

    private readonly List<MethodInfo>? _onDeserializingMethods;
    private readonly List<MethodInfo>? _onDeserializedMethods;

    private SerializationEvents() { }

    private SerializationEvents(
        List<MethodInfo>? onDeserializingMethods,
        List<MethodInfo>? onDeserializedMethods)
    {
        _onDeserializingMethods = onDeserializingMethods;
        _onDeserializedMethods = onDeserializedMethods;
    }

    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:UnrecognizedReflectionPattern",
        Justification = "The Type is annotated correctly, it just can't pass through the lambda method.")]
    private static SerializationEvents GetSerializationEventsForType(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t) =>
        s_cache.GetOrAdd(t, CreateSerializationEvents);

    private static SerializationEvents CreateSerializationEvents([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type)
    {
        List<MethodInfo>? onDeserializingMethods = GetMethodsWithAttribute(typeof(OnDeserializingAttribute), type);
        List<MethodInfo>? onDeserializedMethods = GetMethodsWithAttribute(typeof(OnDeserializedAttribute), type);

        return onDeserializingMethods is null && onDeserializedMethods is null
            ? s_noEvents
            : new SerializationEvents(onDeserializingMethods, onDeserializedMethods);
    }

    private static List<MethodInfo>? GetMethodsWithAttribute(
        Type attribute,
        // Currently the only way to preserve base, non-public methods is to use All
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type? type)
    {
        List<MethodInfo>? attributedMethods = null;

        // Traverse the hierarchy to find all methods with the specified attribute.
        Type? baseType = type;
        while (baseType is not null && baseType != typeof(object))
        {
            MethodInfo[] methods = baseType.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            foreach (MethodInfo method in methods)
            {
                if (method.IsDefined(attribute, inherit: false))
                {
                    attributedMethods ??= [];
                    attributedMethods.Add(method);
                }
            }

            baseType = baseType.BaseType;
        }

        // We should invoke the methods starting from base.
        attributedMethods?.Reverse();

        return attributedMethods;
    }

    internal static Action<StreamingContext>? GetOnDeserializingForType(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type,
        object obj) =>
        GetSerializationEventsForType(type).GetOnDeserializing(obj);

    internal static Action<StreamingContext>? GetOnDeserializedForType(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type,
        object obj) =>
        GetSerializationEventsForType(type).GetOnDeserialized(obj);

    private Action<StreamingContext>? GetOnDeserialized(object obj) =>
        AddOnDelegate(obj, _onDeserializedMethods);

    private Action<StreamingContext>? GetOnDeserializing(object obj) =>
        AddOnDelegate(obj, _onDeserializingMethods);

    /// <summary>Add all methods to a delegate.</summary>
    private static Action<StreamingContext>? AddOnDelegate(object obj, List<MethodInfo>? methods)
    {
        Action<StreamingContext>? handler = null;

        if (methods is not null)
        {
            foreach (MethodInfo method in methods)
            {
                Action<StreamingContext> onDeserialized =
#if NETCOREAPP
                    method.CreateDelegate<Action<StreamingContext>>(obj);
#else
                    (Action<StreamingContext>)method.CreateDelegate(typeof(Action<StreamingContext>), obj);
#endif
                handler += onDeserialized;
            }
        }

        return handler;
    }
}
