﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Reflection;
using System.Threading;
using System.Windows.Threading;
using Microsoft.CodeAnalysis.Test.Utilities;

namespace Roslyn.Test.Utilities
{
    public sealed class StaTaskScheduler : IDisposable
    {
        /// <summary>Gets a <see cref="StaTaskScheduler"/> for the current <see cref="AppDomain"/>.</summary>
        /// <remarks>We use a count of 1, because the editor ends up re-using <see cref="DispatcherObject"/>
        /// instances between tests, so we need to always use the same thread for our Sta tests.</remarks>
        public static StaTaskScheduler DefaultSta { get; } = new StaTaskScheduler();

        /// <summary>The STA threads used by the scheduler.</summary>
        public Thread StaThread { get; }

        public bool IsRunningInScheduler => StaThread.ManagedThreadId == Environment.CurrentManagedThreadId;

        static StaTaskScheduler()
        {
            // Overwrite xunit's app domain handling to not call AppDomain.Unload
            var getDefaultDomain = typeof(AppDomain).GetMethod("GetDefaultDomain", BindingFlags.NonPublic | BindingFlags.Static);
            var defaultDomain = (AppDomain)getDefaultDomain.Invoke(null, null);
            var hook = (XunitDisposeHook)defaultDomain.CreateInstanceFromAndUnwrap(typeof(XunitDisposeHook).Assembly.CodeBase, typeof(XunitDisposeHook).FullName, ignoreCase: false, BindingFlags.CreateInstance, binder: null, args: null, culture: null, activationAttributes: null);
            hook.Execute();

            // We've created an STA thread, which has some extra requirements for COM Runtime
            // Callable Wrappers (RCWs). If any COM object is created on the STA thread, calls to that
            // object must be made from that thread; when the RCW is no longer being used by any
            // managed code, the RCW is put into the finalizer queue, but to actually finalize it
            // it has to marshal to the STA thread to do the work. This means that in order to safely
            // clean up any RCWs, we need to ensure that the thread is pumping past the point of
            // all RCWs being finalized
            //
            // This constraint is particularly problematic if our tests are running in an AppDomain:
            // when the AppDomain is unloaded, any threads (including our STA thread) are going to be
            // aborted. Once the thread and AppDomain is being torn down, the CLR is going to try cleaning up
            // any RCWs associated them, because if the thread is gone for good there's no way
            // it could ever clean anything further up. The code there waits for the finalizer queue
            // -- but the finalizer queue might be already trying to clean up an RCW, which is marshaling
            // to the STA thread. This could then deadlock.
            //
            // The suggested workaround from the CLR team is to do an explicit GC.Collect and
            // WaitForPendingFinalizers before we let the AppDomain shut down. The belief is subscribing
            // to DomainUnload is a reasonable place to do it.
            AppDomain.CurrentDomain.DomainUnload += (sender, e) =>
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            };
        }

        /// <summary>Initializes a new instance of the <see cref="StaTaskScheduler"/> class.</summary>
        public StaTaskScheduler()
        {
            using (var threadStartedEvent = new ManualResetEventSlim(initialState: false))
            {
                DispatcherSynchronizationContext synchronizationContext = null;
                StaThread = new Thread(() =>
                {
                    var oldContext = SynchronizationContext.Current;
                    try
                    {
                        // All WPF Tests need a DispatcherSynchronizationContext and we dont want to block pending keyboard
                        // or mouse input from the user. So use background priority which is a single level below user input.
                        synchronizationContext = new DispatcherSynchronizationContext();

                        // xUnit creates its own synchronization context and wraps any existing context so that messages are
                        // still pumped as necessary. So we are safe setting it here, where we are not safe setting it in test.
                        SynchronizationContext.SetSynchronizationContext(synchronizationContext);

                        threadStartedEvent.Set();

                        Dispatcher.Run();
                    }
                    finally
                    {
                        SynchronizationContext.SetSynchronizationContext(oldContext);
                    }
                });
                StaThread.Name = $"{nameof(StaTaskScheduler)} thread";
                StaThread.IsBackground = true;
                StaThread.SetApartmentState(ApartmentState.STA);
                StaThread.Start();

                threadStartedEvent.Wait();
                DispatcherSynchronizationContext = synchronizationContext;
            }

            // Work around the WeakEventTable Shutdown race conditions
            AppContext.SetSwitch("Switch.MS.Internal.DoNotInvokeInWeakEventTableShutdownListener", isEnabled: true);
        }

        public DispatcherSynchronizationContext DispatcherSynchronizationContext
        {
            get;
        }

        /// <summary>
        /// Cleans up the scheduler by indicating that no more tasks will be queued.
        /// This method blocks until all threads successfully shutdown.
        /// </summary>
        public void Dispose()
        {
            if (StaThread.IsAlive)
            {
                DispatcherSynchronizationContext.Post(_ => Dispatcher.ExitAllFrames(), null);
                StaThread.Join();
            }
        }
    }
}
