using System.Collections.Concurrent;
using System.Runtime.InteropServices;

namespace MBN_STOCK_WEBVIEW.Playout.Runtime;

internal interface IStaDispatcher : IAsyncDisposable
{
    bool IsQuarantined { get; }

    int? ManagedThreadId { get; }

    Task<T> InvokeAsync<T>(
        Func<T> callback,
        TimeSpan timeout,
        CancellationToken cancellationToken,
        Action<T>? abandonedResultCleanup = null,
        Action? abandonedFailureCleanup = null,
        Action? preventFutureDispatch = null);
}

internal interface IStaDispatcherFactory
{
    IStaDispatcher Create(int capacity);
}

internal sealed class StaDispatcherFactory : IStaDispatcherFactory
{
    public IStaDispatcher Create(int capacity) => new StaDispatcher(capacity);
}

internal sealed class StaDispatcher : IStaDispatcher
{
    private const uint QsAllInput = 0x04ff;
    private const uint MwmoInputAvailable = 0x0004;
    private const uint PmRemove = 0x0001;
    private const uint WaitObject0 = 0;
    private const uint WaitFailed = 0xffffffff;

    private readonly ConcurrentQueue<StaWorkItem> _queue = new();
    private readonly SemaphoreSlim _slots;
    private readonly AutoResetEvent _workAvailable = new(false);
    private readonly Thread _thread;
    private int _state;
    private int _managedThreadId;

    public StaDispatcher(int capacity)
    {
        if (capacity < 1)
        {
            throw new ArgumentOutOfRangeException(nameof(capacity));
        }

        _slots = new SemaphoreSlim(capacity, capacity);
        _thread = new Thread(ThreadMain)
        {
            IsBackground = true,
            Name = "MBN K3D STA Dispatcher"
        };
        _thread.SetApartmentState(ApartmentState.STA);
        _thread.Start();
    }

    public bool IsQuarantined => Volatile.Read(ref _state) == 1;

    public int? ManagedThreadId
    {
        get
        {
            var value = Volatile.Read(ref _managedThreadId);
            return value == 0 ? null : value;
        }
    }

    public async Task<T> InvokeAsync<T>(
        Func<T> callback,
        TimeSpan timeout,
        CancellationToken cancellationToken,
        Action<T>? abandonedResultCleanup = null,
        Action? abandonedFailureCleanup = null,
        Action? preventFutureDispatch = null)
    {
        ArgumentNullException.ThrowIfNull(callback);
        if (timeout <= TimeSpan.Zero)
        {
            throw new ArgumentOutOfRangeException(nameof(timeout));
        }

        ThrowIfUnavailable();
        await _slots.WaitAsync(cancellationToken).ConfigureAwait(false);

        var item = new StaWorkItem<T>(
            callback,
            abandonedResultCleanup,
            abandonedFailureCleanup);
        var queued = false;
        try
        {
            ThrowIfUnavailable();
            using var registration = cancellationToken.Register(
                static state => ((StaWorkItem)state!).CancelBeforeExecution(),
                item);

            _queue.Enqueue(item);
            queued = true;
            _workAvailable.Set();

            var completion = item.Completion;
            var timeoutTask = Task.Delay(timeout);
            var completed = await Task.WhenAny(completion, timeoutTask).ConfigureAwait(false);
            if (completed == completion || completion.IsCompleted)
            {
                return await completion.ConfigureAwait(false);
            }

            if (item.TryTimeout(preventFutureDispatch))
            {
                Quarantine();
            }

            return await completion.ConfigureAwait(false);
        }
        catch
        {
            if (!queued)
            {
                _slots.Release();
            }

            throw;
        }
    }

    public ValueTask DisposeAsync()
    {
        if (Interlocked.Exchange(ref _state, 2) == 2)
        {
            return ValueTask.CompletedTask;
        }

        FailQueuedItems(new ObjectDisposedException(nameof(StaDispatcher)));
        _workAvailable.Set();
        if (Thread.CurrentThread != _thread)
        {
            _thread.Join(TimeSpan.FromSeconds(2));
        }

        _slots.Dispose();
        _workAvailable.Dispose();
        return ValueTask.CompletedTask;
    }

    private void ThreadMain()
    {
        Volatile.Write(ref _managedThreadId, Environment.CurrentManagedThreadId);
        var waitHandles = new[] { _workAvailable.SafeWaitHandle.DangerousGetHandle() };

        while (Volatile.Read(ref _state) != 2)
        {
            PumpMessages();
            if (_queue.TryDequeue(out var item))
            {
                _slots.Release();
                if (Volatile.Read(ref _state) == 0)
                {
                    item.Execute();
                }
                else
                {
                    item.Fail(new StaDispatcherQuarantinedException());
                }

                continue;
            }

            var waitResult = MsgWaitForMultipleObjectsEx(
                1,
                waitHandles,
                250,
                QsAllInput,
                MwmoInputAvailable);
            if (waitResult == WaitFailed)
            {
                Quarantine();
            }
            else if (waitResult == WaitObject0 + 1)
            {
                PumpMessages();
            }
        }

        PumpMessages();
    }

    private void Quarantine()
    {
        if (Interlocked.CompareExchange(ref _state, 1, 0) != 0)
        {
            return;
        }

        FailQueuedItems(new StaDispatcherQuarantinedException());
        _workAvailable.Set();
    }

    private void FailQueuedItems(Exception exception)
    {
        while (_queue.TryDequeue(out var queued))
        {
            _slots.Release();
            queued.Fail(exception);
        }
    }

    private void ThrowIfUnavailable()
    {
        var state = Volatile.Read(ref _state);
        if (state == 1)
        {
            throw new StaDispatcherQuarantinedException();
        }

        ObjectDisposedException.ThrowIf(state == 2, this);
    }

    private static void PumpMessages()
    {
        while (PeekMessage(out var message, IntPtr.Zero, 0, 0, PmRemove))
        {
            TranslateMessage(in message);
            DispatchMessage(in message);
        }
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern uint MsgWaitForMultipleObjectsEx(
        uint count,
        [In] IntPtr[] handles,
        uint milliseconds,
        uint wakeMask,
        uint flags);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool PeekMessage(
        out NativeMessage message,
        IntPtr window,
        uint filterMinimum,
        uint filterMaximum,
        uint removeMessage);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool TranslateMessage(in NativeMessage message);

    [DllImport("user32.dll")]
    private static extern IntPtr DispatchMessage(in NativeMessage message);

    [StructLayout(LayoutKind.Sequential)]
    private struct NativeMessage
    {
        public IntPtr Window;
        public uint Message;
        public UIntPtr WParam;
        public IntPtr LParam;
        public uint Time;
        public NativePoint Point;
        public uint Private;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct NativePoint
    {
        public int X;
        public int Y;
    }

    private abstract class StaWorkItem
    {
        // 0 queued, 1 running, 2 completed/cancelled/timed out
        private int _executionState;

        public void Execute()
        {
            if (Interlocked.CompareExchange(ref _executionState, 1, 0) != 0)
            {
                return;
            }

            ExecuteCore();
        }

        public void CancelBeforeExecution()
        {
            if (Interlocked.CompareExchange(ref _executionState, 2, 0) == 0)
            {
                CancelCore();
            }
        }

        public bool TryTimeout(Action? preventFutureDispatch)
        {
            while (true)
            {
                var state = Volatile.Read(ref _executionState);
                if (state == 2)
                {
                    return false;
                }

                if (Interlocked.CompareExchange(ref _executionState, 2, state) == state)
                {
                    try
                    {
                        preventFutureDispatch?.Invoke();
                    }
                    catch
                    {
                        // A timeout must still complete and quarantine the dispatcher.
                    }

                    TimeoutCore();
                    return true;
                }
            }
        }

        public void Fail(Exception exception)
        {
            if (Interlocked.Exchange(ref _executionState, 2) != 2)
            {
                FailCore(exception);
            }
        }

        protected bool TryCompleteExecution() =>
            Interlocked.CompareExchange(ref _executionState, 2, 1) == 1;

        protected abstract void ExecuteCore();

        protected abstract void CancelCore();

        protected abstract void TimeoutCore();

        protected abstract void FailCore(Exception exception);
    }

    private sealed class StaWorkItem<T> : StaWorkItem
    {
        private readonly Func<T> _callback;
        private readonly Action<T>? _abandonedResultCleanup;
        private readonly Action? _abandonedFailureCleanup;
        private readonly TaskCompletionSource<T> _completion =
            new(TaskCreationOptions.RunContinuationsAsynchronously);

        public StaWorkItem(
            Func<T> callback,
            Action<T>? abandonedResultCleanup,
            Action? abandonedFailureCleanup)
        {
            _callback = callback;
            _abandonedResultCleanup = abandonedResultCleanup;
            _abandonedFailureCleanup = abandonedFailureCleanup;
        }

        public Task<T> Completion => _completion.Task;

        protected override void ExecuteCore()
        {
            try
            {
                var result = _callback();
                if (TryCompleteExecution())
                {
                    _completion.TrySetResult(result);
                }
                else
                {
                    try
                    {
                        _abandonedResultCleanup?.Invoke(result);
                    }
                    catch
                    {
                        // Late cleanup is best effort and must not terminate the STA pump.
                    }
                }
            }
            catch (Exception exception)
            {
                if (TryCompleteExecution())
                {
                    _completion.TrySetException(exception);
                }
                else
                {
                    try
                    {
                        _abandonedFailureCleanup?.Invoke();
                    }
                    catch
                    {
                        // Late cleanup is best effort and must not terminate the STA pump.
                    }
                }
            }
        }

        protected override void CancelCore() =>
            _completion.TrySetCanceled();

        protected override void TimeoutCore() =>
            _completion.TrySetException(new StaOperationTimedOutException());

        protected override void FailCore(Exception exception) =>
            _completion.TrySetException(exception);
    }
}

internal sealed class StaOperationTimedOutException : TimeoutException
{
    public StaOperationTimedOutException()
        : base("The STA operation timed out and its outcome is unknown.")
    {
    }
}

internal sealed class StaDispatcherQuarantinedException : InvalidOperationException
{
    public StaDispatcherQuarantinedException()
        : base("The STA dispatcher is quarantined.")
    {
    }
}
