using System.Collections.Concurrent;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;

namespace MBN_STOCK_WEBVIEW.Playout.Interop;

internal enum K3dLifecycleCallbackKind
{
    ScenePlayed,
    CutOut,
    StopAll
}

internal readonly record struct K3dLifecycleCallback(
    K3dLifecycleCallbackKind Kind,
    bool IsSuccess,
    int? OutputChannelIndex,
    int? LayerIndex);

public interface IK3dEventSink
{
    void OnCallback(string methodName, object?[] arguments);
}

internal interface IK3dEventHandlerFactory
{
    bool SupportsCallbacks { get; }

    object Create(IK3dEventSink sink);
}

internal sealed class ActivatorK3dEventHandlerFactory(ILateBoundComActivator activator)
    : IK3dEventHandlerFactory
{
    private readonly ILateBoundComActivator _activator =
        activator ?? throw new ArgumentNullException(nameof(activator));

    public bool SupportsCallbacks => false;

    public object Create(IK3dEventSink sink)
    {
        ArgumentNullException.ThrowIfNull(sink);
        return _activator.Create(K3dComConstants.KaEventHandlerClassGuid);
    }
}

internal sealed class InstalledK3dEventHandlerFactory : IK3dEventHandlerFactory
{
    public bool SupportsCallbacks => true;

    public object Create(IK3dEventSink sink)
    {
        ArgumentNullException.ThrowIfNull(sink);
        var interfaceType = InstalledK3dInteropMetadata.Resolve().EventHandlerInterfaceType;
        return DynamicK3dEventHandlerBuilder.Create(interfaceType, sink);
    }
}

internal sealed class K3dEventQueue : IK3dEventSink
{
    private readonly ConcurrentQueue<K3dLifecycleCallback> _callbacks = new();
    private readonly int _capacity;
    private int _count;
    private int _overflowed;
    private int _helloObserved;

    public K3dEventQueue(int capacity = 256)
    {
        if (capacity < 1)
        {
            throw new ArgumentOutOfRangeException(nameof(capacity));
        }

        _capacity = capacity;
    }

    public bool HelloObserved => Volatile.Read(ref _helloObserved) != 0;

    public bool HasOverflowed => Volatile.Read(ref _overflowed) != 0;

    public int PendingCount => Volatile.Read(ref _count);

    public void OnCallback(string methodName, object?[] arguments)
    {
        // Nothing may escape across the native callback boundary.
        try
        {
            if (string.Equals(methodName, "OnHello", StringComparison.Ordinal))
            {
                Volatile.Write(ref _helloObserved, 1);
                return;
            }

            K3dLifecycleCallback callback;
            if (string.Equals(methodName, "OnScenePlayed", StringComparison.Ordinal) &&
                TryReadInt(arguments, 0, out var playSuccess) &&
                TryReadInt(arguments, 1, out var playChannel) &&
                TryReadInt(arguments, 2, out var playLayer))
            {
                callback = new K3dLifecycleCallback(
                    K3dLifecycleCallbackKind.ScenePlayed,
                    playSuccess != 0,
                    playChannel,
                    playLayer);
            }
            else if (string.Equals(methodName, "OnCutOut", StringComparison.Ordinal) &&
                     TryReadInt(arguments, 0, out var cutSuccess) &&
                     TryReadInt(arguments, 1, out var cutChannel) &&
                     TryReadInt(arguments, 2, out var cutLayer))
            {
                callback = new K3dLifecycleCallback(
                    K3dLifecycleCallbackKind.CutOut,
                    cutSuccess != 0,
                    cutChannel,
                    cutLayer);
            }
            else if (string.Equals(methodName, "OnStopAll", StringComparison.Ordinal) &&
                     TryReadInt(arguments, 0, out var stopSuccess))
            {
                callback = new K3dLifecycleCallback(
                    K3dLifecycleCallbackKind.StopAll,
                    stopSuccess != 0,
                    null,
                    null);
            }
            else
            {
                return;
            }

            var count = Interlocked.Increment(ref _count);
            if (count > _capacity)
            {
                Interlocked.Decrement(ref _count);
                Volatile.Write(ref _overflowed, 1);
                return;
            }

            _callbacks.Enqueue(callback);
        }
        catch
        {
            Volatile.Write(ref _overflowed, 1);
        }
    }

    public bool TryDequeue(out K3dLifecycleCallback callback)
    {
        if (!_callbacks.TryDequeue(out callback))
        {
            return false;
        }

        Interlocked.Decrement(ref _count);
        return true;
    }

    private static bool TryReadInt(object?[] arguments, int index, out int value)
    {
        value = 0;
        if ((uint)index >= (uint)arguments.Length || arguments[index] is null)
        {
            return false;
        }

        try
        {
            value = Convert.ToInt32(
                arguments[index],
                System.Globalization.CultureInfo.InvariantCulture);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

internal static class DynamicK3dEventHandlerBuilder
{
    private static readonly ConcurrentDictionary<Type, ConstructorInfo> Constructors = new();
    private static readonly MethodInfo SinkCallbackMethod = typeof(IK3dEventSink).GetMethod(
        nameof(IK3dEventSink.OnCallback),
        BindingFlags.Public | BindingFlags.Instance)
        ?? throw new MissingMethodException(nameof(IK3dEventSink.OnCallback));
    private static readonly IReadOnlySet<string> ForwardedCallbacks = new HashSet<string>(
    [
        "OnHello",
        "OnScenePlayed",
        "OnCutOut",
        "OnStopAll"
    ], StringComparer.Ordinal);

    public static object Create(Type eventHandlerInterface, IK3dEventSink sink)
    {
        ArgumentNullException.ThrowIfNull(eventHandlerInterface);
        ArgumentNullException.ThrowIfNull(sink);
        if (!eventHandlerInterface.IsInterface || !eventHandlerInterface.IsImport ||
            eventHandlerInterface.GUID != Guid.Parse("C21817DB-C0D3-4647-8D85-027338DCF832"))
        {
            throw new InvalidOperationException("The installed K3D callback interface is invalid.");
        }

        var constructor = Constructors.GetOrAdd(eventHandlerInterface, BuildConstructor);
        return constructor.Invoke([sink]);
    }

    private static ConstructorInfo BuildConstructor(Type eventHandlerInterface)
    {
        var assembly = AssemblyBuilder.DefineDynamicAssembly(
            new AssemblyName($"MBN.K3D.Callback.{Guid.NewGuid():N}"),
            AssemblyBuilderAccess.RunAndCollect);
        var module = assembly.DefineDynamicModule("Callbacks");
        var type = module.DefineType(
            $"K3dEventHandler_{Guid.NewGuid():N}",
            TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class);
        type.AddInterfaceImplementation(eventHandlerInterface);

        var comVisible = typeof(ComVisibleAttribute).GetConstructor([typeof(bool)])!;
        type.SetCustomAttribute(new CustomAttributeBuilder(comVisible, [true]));
        var classInterface = typeof(ClassInterfaceAttribute).GetConstructor([typeof(ClassInterfaceType)])!;
        type.SetCustomAttribute(new CustomAttributeBuilder(
            classInterface,
            [ClassInterfaceType.None]));

        var sinkField = type.DefineField(
            "_sink",
            typeof(IK3dEventSink),
            FieldAttributes.Private | FieldAttributes.InitOnly);
        var constructor = type.DefineConstructor(
            MethodAttributes.Public,
            CallingConventions.Standard,
            [typeof(IK3dEventSink)]);
        var constructorIl = constructor.GetILGenerator();
        constructorIl.Emit(OpCodes.Ldarg_0);
        constructorIl.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)!);
        constructorIl.Emit(OpCodes.Ldarg_0);
        constructorIl.Emit(OpCodes.Ldarg_1);
        constructorIl.Emit(OpCodes.Stfld, sinkField);
        constructorIl.Emit(OpCodes.Ret);

        var interfaces = eventHandlerInterface.GetInterfaces().Append(eventHandlerInterface);
        var methods = interfaces
            .SelectMany(item => item.GetMethods(
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            .Distinct()
            .ToArray();
        if (methods.Length == 0 || methods.Any(method => method.ReturnType != typeof(void)))
        {
            throw new InvalidOperationException("The installed K3D callback surface is unexpected.");
        }

        foreach (var method in methods)
        {
            ImplementMethod(type, sinkField, method);
        }

        var generated = type.CreateType()
            ?? throw new InvalidOperationException("The K3D callback adapter could not be generated.");
        return generated.GetConstructor([typeof(IK3dEventSink)])
            ?? throw new MissingMethodException("The K3D callback adapter constructor is missing.");
    }

    private static void ImplementMethod(
        TypeBuilder type,
        FieldBuilder sinkField,
        MethodInfo interfaceMethod)
    {
        var parameters = interfaceMethod.GetParameters();
        var method = type.DefineMethod(
            interfaceMethod.Name,
            MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final |
            MethodAttributes.HideBySig | MethodAttributes.NewSlot,
            CallingConventions.Standard,
            typeof(void),
            parameters.Select(parameter => parameter.ParameterType).ToArray());
        for (var index = 0; index < parameters.Length; index++)
        {
            method.DefineParameter(index + 1, parameters[index].Attributes, parameters[index].Name);
        }

        var il = method.GetILGenerator();
        if (ForwardedCallbacks.Contains(interfaceMethod.Name))
        {
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, sinkField);
            il.Emit(OpCodes.Ldstr, interfaceMethod.Name);
            EmitInt(il, parameters.Length);
            il.Emit(OpCodes.Newarr, typeof(object));
            for (var index = 0; index < parameters.Length; index++)
            {
                il.Emit(OpCodes.Dup);
                EmitInt(il, index);
                EmitLoadArgument(il, index + 1);
                var parameterType = parameters[index].ParameterType;
                if (parameterType.IsByRef)
                {
                    parameterType = parameterType.GetElementType()
                        ?? throw new InvalidOperationException("The K3D callback parameter is invalid.");
                    il.Emit(OpCodes.Ldobj, parameterType);
                }

                if (parameterType.IsValueType)
                {
                    il.Emit(OpCodes.Box, parameterType);
                }

                il.Emit(OpCodes.Stelem_Ref);
            }

            il.Emit(OpCodes.Callvirt, SinkCallbackMethod);
        }

        il.Emit(OpCodes.Ret);
        type.DefineMethodOverride(method, interfaceMethod);
    }

    private static void EmitLoadArgument(ILGenerator il, int index)
    {
        switch (index)
        {
            case 0: il.Emit(OpCodes.Ldarg_0); break;
            case 1: il.Emit(OpCodes.Ldarg_1); break;
            case 2: il.Emit(OpCodes.Ldarg_2); break;
            case 3: il.Emit(OpCodes.Ldarg_3); break;
            default: il.Emit(OpCodes.Ldarg, index); break;
        }
    }

    private static void EmitInt(ILGenerator il, int value)
    {
        switch (value)
        {
            case 0: il.Emit(OpCodes.Ldc_I4_0); break;
            case 1: il.Emit(OpCodes.Ldc_I4_1); break;
            case 2: il.Emit(OpCodes.Ldc_I4_2); break;
            case 3: il.Emit(OpCodes.Ldc_I4_3); break;
            case 4: il.Emit(OpCodes.Ldc_I4_4); break;
            case 5: il.Emit(OpCodes.Ldc_I4_5); break;
            case 6: il.Emit(OpCodes.Ldc_I4_6); break;
            case 7: il.Emit(OpCodes.Ldc_I4_7); break;
            case 8: il.Emit(OpCodes.Ldc_I4_8); break;
            default: il.Emit(OpCodes.Ldc_I4, value); break;
        }
    }
}
