﻿// 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.ComponentModel.Design;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Serialization.Formatters;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.IntegrationTest.Utilities;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;

namespace Microsoft.VisualStudio.IntegrationTest.Setup
{
    internal sealed class IntegrationTestServiceCommands : IDisposable
    {
        #region VSCT Identifiers
        public const int menuidIntegrationTestService = 0x5001;

        public const int grpidTestWindowRunTopLevelMenu = 0x0110;
        public const int grpidIntegrationTestService = 0x5101;

        public const int cmdidStartIntegrationTestService = 0x5201;
        public const int cmdidStopIntegrationTestService = 0x5204;

        public static readonly Guid guidTestWindowCmdSet = new Guid("1E198C22-5980-4E7E-92F3-F73168D1FB63");
        #endregion

        private static readonly BinaryServerFormatterSinkProvider DefaultSinkProvider = new BinaryServerFormatterSinkProvider()
        {
            TypeFilterLevel = TypeFilterLevel.Full
        };

        private readonly Package _package;

        private readonly MenuCommand _startMenuCmd;
        private readonly MenuCommand _stopMenuCmd;

        private IntegrationService? _service;
        private IpcServerChannel? _serviceChannel;

#pragma warning disable IDE0052 // Remove unread private members - used to hold the marshalled integration test service
        private ObjRef? _marshalledService;
#pragma warning restore IDE0052 // Remove unread private members

        private readonly IVsRunningDocumentTable _runningDocumentTable;
        private IVsRunningDocTableEvents? s_runningDocTableEventListener;
        private uint s_runningDocTableEventListenerCookie;

        private IntegrationTestServiceCommands(Package package)
        {
            _package = package ?? throw new ArgumentNullException(nameof(package));

            var startMenuCmdId = new CommandID(guidTestWindowCmdSet, cmdidStartIntegrationTestService);
            _startMenuCmd = new MenuCommand(StartServiceCallback, startMenuCmdId)
            {
                Enabled = true,
                Visible = true
            };

            var stopMenuCmdId = new CommandID(guidTestWindowCmdSet, cmdidStopIntegrationTestService);
            _stopMenuCmd = new MenuCommand(StopServiceCallback, stopMenuCmdId)
            {
                Enabled = false,
                Visible = false
            };

            if (ServiceProvider.GetService(typeof(IMenuCommandService)) is OleMenuCommandService menuCommandService)
            {
                menuCommandService.AddCommand(_startMenuCmd);
                menuCommandService.AddCommand(_stopMenuCmd);
            }

            _runningDocumentTable = (IVsRunningDocumentTable)ServiceProvider.GetService(typeof(SVsRunningDocumentTable));
            Assumes.Present(_runningDocumentTable);
        }

        public static IntegrationTestServiceCommands? Instance { get; private set; }

        private IServiceProvider ServiceProvider => _package;

        public static void Initialize(Package package)
        {
            Instance = new IntegrationTestServiceCommands(package);
        }

        /// <summary>
        /// Supports deserialization of types passed to APIs injected into the Visual Studio process by
        /// <see cref="IntegrationService.Execute"/>.
        /// </summary>
        private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
            => AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => assembly.FullName == args.Name);

        public void Dispose()
            => StopServiceCallback(this, EventArgs.Empty);

        /// <summary>
        /// Starts the IPC server for the Integration Test service.
        /// </summary>
        private void StartServiceCallback(object sender, EventArgs e)
        {
            if (_startMenuCmd.Enabled)
            {
                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;

                TestTraceListener.Install();

                _service = new IntegrationService();

                _serviceChannel = new IpcServerChannel(
                    name: $"Microsoft.VisualStudio.IntegrationTest.ServiceChannel_{Process.GetCurrentProcess().Id}",
                    portName: _service.PortName,
                    sinkProvider: DefaultSinkProvider
                );

                var serviceType = typeof(IntegrationService);
                _marshalledService = RemotingServices.Marshal(_service, serviceType.FullName, serviceType);

                _serviceChannel.StartListening(null);

                // Async initialization is a workaround for deadlock loading ExtensionManagerPackage prior to
                // https://devdiv.visualstudio.com/DevDiv/_git/VSExtensibility/pullrequest/381506
                _ = Task.Run(async () =>
                {
                    var componentModel = (IComponentModel?)await AsyncServiceProvider.GlobalProvider.GetServiceAsync(typeof(SComponentModel)).ConfigureAwait(false);
                    Assumes.Present(componentModel);

                    var asyncCompletionTracker = componentModel.GetService<AsyncCompletionTracker>();
                    asyncCompletionTracker.StartListening();

#pragma warning disable RS0030 // Do not used banned APIs
                    await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
#pragma warning restore RS0030 // Do not used banned APIs

                    var listenerProvider = componentModel.GetService<IAsynchronousOperationListenerProvider>();
                    s_runningDocTableEventListener = new RunningDocumentTableEventListener(listenerProvider.GetListener(FeatureAttribute.Workspace));
                    ErrorHandler.ThrowOnFailure(_runningDocumentTable.AdviseRunningDocTableEvents(s_runningDocTableEventListener, out s_runningDocTableEventListenerCookie));
                });

                SwapAvailableCommands(_startMenuCmd, _stopMenuCmd);
            }
        }

        /// <summary>Stops the IPC server for the Integration Test service.</summary>
        private void StopServiceCallback(object sender, EventArgs e)
        {
            if (_stopMenuCmd.Enabled)
            {
                AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomainAssemblyResolve;

                if (_serviceChannel != null)
                {
                    _serviceChannel.StopListening(null);
                    _serviceChannel = null;
                }

                _marshalledService = null;
                _service = null;

                var componentModel = (IComponentModel)ServiceProvider.GetService(typeof(SComponentModel));
                var asyncCompletionTracker = componentModel.GetService<AsyncCompletionTracker>();
                asyncCompletionTracker.StopListening();

                ErrorHandler.ThrowOnFailure(_runningDocumentTable.UnadviseRunningDocTableEvents(s_runningDocTableEventListenerCookie));
                s_runningDocTableEventListener = null;
                s_runningDocTableEventListenerCookie = 0;

                SwapAvailableCommands(_stopMenuCmd, _startMenuCmd);
            }
        }

        private static void SwapAvailableCommands(MenuCommand commandToDisable, MenuCommand commandToEnable)
        {
            commandToDisable.Enabled = false;
            commandToDisable.Visible = false;

            commandToEnable.Enabled = true;
            commandToEnable.Visible = true;
        }

        /// <summary>
        /// This event listener is an adapter to expose asynchronous file save operations to Roslyn via its standard
        /// workspace event waiters.
        /// </summary>
        private sealed class RunningDocumentTableEventListener : IVsRunningDocTableEvents, IVsRunningDocTableEvents7
        {
            private readonly IAsynchronousOperationListener _asynchronousOperationListener;

            public RunningDocumentTableEventListener(IAsynchronousOperationListener asynchronousOperationListener)
            {
                _asynchronousOperationListener = asynchronousOperationListener;
            }

            int IVsRunningDocTableEvents.OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
                => VSConstants.S_OK;

            int IVsRunningDocTableEvents.OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
                => VSConstants.S_OK;

            int IVsRunningDocTableEvents.OnAfterSave(uint docCookie)
                => VSConstants.S_OK;

            int IVsRunningDocTableEvents.OnAfterAttributeChange(uint docCookie, uint grfAttribs)
                => VSConstants.S_OK;

            int IVsRunningDocTableEvents.OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame)
                => VSConstants.S_OK;

            int IVsRunningDocTableEvents.OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame)
                => VSConstants.S_OK;

#pragma warning disable CS8768 // Nullability of reference types in return type doesn't match implemented member (possibly because of nullability attributes). (Signature was corrected in https://devdiv.visualstudio.com/DevDiv/_git/VS/pullrequest/390178)
            IVsTask? IVsRunningDocTableEvents7.OnBeforeSaveAsync(uint cookie, uint flags, IVsTask? saveTask)
#pragma warning restore CS8768 // Nullability of reference types in return type doesn't match implemented member (possibly because of nullability attributes).
            {
                if (saveTask is not null)
                {
#pragma warning disable RS0030 // Do not used banned APIs
                    _ = ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
#pragma warning restore RS0030 // Do not used banned APIs
                    {
                        // Track asynchronous save operations via Roslyn's Workspace events
                        using var _ = _asynchronousOperationListener.BeginAsyncOperation("OnBeforeSaveAsync");
                        await saveTask;
                    });
                }

                // No additional work for the caller to handle
                return null;
            }

#pragma warning disable CS8768 // Nullability of reference types in return type doesn't match implemented member (possibly because of nullability attributes). (Signature was corrected in https://devdiv.visualstudio.com/DevDiv/_git/VS/pullrequest/390178)
            IVsTask? IVsRunningDocTableEvents7.OnAfterSaveAsync(uint cookie, uint flags)
#pragma warning restore CS8768 // Nullability of reference types in return type doesn't match implemented member (possibly because of nullability attributes).
            {
                // No additional work for the caller to handle
                return null;
            }
        }
    }
}
