﻿// 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.

using System;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using EnvDTE;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.Internal.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Roslyn.Hosting.Diagnostics.Waiters;
using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.IntegrationTest.Utilities.InProcess
{
    /// <summary>
    /// Base class for all components that run inside of the Visual Studio process.
    /// <list type="bullet">
    /// <item>Every in-proc component should provide a public, static, parameterless "Create" method.
    /// This will be called to construct the component in the VS process.</item>
    /// <item>Public methods on in-proc components should be instance methods to ensure that they are
    /// marshalled properly and execute in the VS process. Static methods will execute in the process
    /// in which they are called.</item>
    /// </list>
    /// </summary>
    internal abstract class InProcComponent : MarshalByRefObject
    {
        private static JoinableTaskFactory? _joinableTaskFactory;

        protected InProcComponent()
        {
            // Make sure SVsExtensionManager loads before trying to execute any test commands
            JoinableTaskFactory.Run(async () =>
            {
                // Workaround for deadlock loading ExtensionManagerPackage prior to
                // https://devdiv.visualstudio.com/DevDiv/_git/VSExtensibility/pullrequest/381506
                await AsyncServiceProvider.GlobalProvider.GetServiceAsync(typeof(SVsExtensionManager));
            });
        }

        private static Dispatcher CurrentApplicationDispatcher
            => Application.Current.Dispatcher;

        protected static JoinableTaskFactory JoinableTaskFactory
        {
            get
            {
                if (_joinableTaskFactory is null)
                {
#pragma warning disable RS0030 // Do not used banned APIs (this code only runs in integration tests)
                    Interlocked.CompareExchange(ref _joinableTaskFactory, ThreadHelper.JoinableTaskFactory.WithPriority(CurrentApplicationDispatcher, DispatcherPriority.Background), null);
#pragma warning restore RS0030 // Do not used banned APIs
                }

                return _joinableTaskFactory;
            }
        }

        protected static void InvokeOnUIThread(Action<CancellationToken> action)
        {
            using var cancellationTokenSource = new CancellationTokenSource(Helper.HangMitigatingTimeout);
            var operation = JoinableTaskFactory.RunAsync(async () =>
            {
                await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationTokenSource.Token);

                action(cancellationTokenSource.Token);
            });

            operation.Task.Wait(cancellationTokenSource.Token);
        }

        protected static T InvokeOnUIThread<T>(Func<CancellationToken, T> action)
        {
            using var cancellationTokenSource = new CancellationTokenSource(Helper.HangMitigatingTimeout);
            var operation = JoinableTaskFactory.RunAsync(async () =>
            {
                await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationTokenSource.Token);

                return action(cancellationTokenSource.Token);
            });

            operation.Task.Wait(cancellationTokenSource.Token);
            return operation.Task.Result;
        }

        protected static TInterface GetGlobalService<TService, TInterface>()
            where TService : class
            where TInterface : class
        => InvokeOnUIThread(cancellationToken => (TInterface)ServiceProvider.GlobalProvider.GetService(typeof(TService)));

        protected static TService GetComponentModelService<TService>()
            where TService : class
         => InvokeOnUIThread(cancellationToken => GetComponentModel().GetService<TService>());

        protected static TestWaitingService GetWaitingService()
            => new(GetComponentModel().DefaultExportProvider.GetExport<AsynchronousOperationListenerProvider>().Value);

        protected static DTE GetDTE()
            => GetGlobalService<SDTE, DTE>();

        protected static IComponentModel GetComponentModel()
            => GetGlobalService<SComponentModel, IComponentModel>();

        protected static bool IsCommandAvailable(string commandName)
            => GetDTE().Commands.Item(commandName).IsAvailable;

        protected static void ExecuteCommand(string commandName, string args = "")
        {
            var task = Task.Run(() => GetDTE().ExecuteCommand(commandName, args));
            task.Wait(Helper.HangMitigatingTimeout);
        }

        /// <summary>
        /// Waiting for the application to 'idle' means that it is done pumping messages (including WM_PAINT).
        /// </summary>
        protected static void WaitForApplicationIdle(TimeSpan timeout)
#pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs
            => CurrentApplicationDispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle).Wait(timeout);
#pragma warning restore VSTHRD001 // Avoid legacy thread switching APIs

        protected static void WaitForSystemIdle()
#pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs
            => CurrentApplicationDispatcher.Invoke(() => { }, DispatcherPriority.SystemIdle);
#pragma warning restore VSTHRD001 // Avoid legacy thread switching APIs

        // Ensure InProcComponents live forever
        public override object? InitializeLifetimeService() => null;
    }
}
