﻿// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Remote
{
    /// <summary>
    /// Delivers global notifications to remote services.
    /// </summary>
    internal sealed class GlobalNotificationRemoteDeliveryService : IDisposable
    {
        private enum GlobalNotificationState
        {
            NotStarted,
            Started
        }

        /// <summary>
        /// Lock for the <see cref="_globalNotificationsTask"/> task chain.  Each time we hear 
        /// about a global operation starting or stopping (i.e. a build) we will '.ContinueWith'
        /// this task chain with a new notification to the OOP side.  This way all the messages
        /// are properly serialized and appear in the right order (i.e. we don't hear about a 
        /// stop prior to hearing about the relevant start).
        /// </summary>
        private readonly object _globalNotificationsGate = new object();
        private Task<GlobalNotificationState> _globalNotificationsTask = Task.FromResult(GlobalNotificationState.NotStarted);

        private readonly SolutionServices _services;
        private readonly CancellationToken _cancellationToken;

        public GlobalNotificationRemoteDeliveryService(SolutionServices services, CancellationToken cancellationToken)
        {
            _services = services;
            _cancellationToken = cancellationToken;

            RegisterGlobalOperationNotifications();
        }

        public void Dispose()
        {
            UnregisterGlobalOperationNotifications();
        }

        private void RegisterGlobalOperationNotifications()
        {
            // We are in the VS layer, so getting the IGlobalOperationNotificationService must succeed.
            var globalOperationService = _services.ExportProvider.GetExports<IGlobalOperationNotificationService>().Single().Value;
            globalOperationService.Started += OnGlobalOperationStarted;
            globalOperationService.Stopped += OnGlobalOperationStopped;
        }

        private void UnregisterGlobalOperationNotifications()
        {
            var globalOperationService = _services.ExportProvider.GetExports<IGlobalOperationNotificationService>().Single().Value;
            globalOperationService.Started -= OnGlobalOperationStarted;
            globalOperationService.Stopped -= OnGlobalOperationStopped;
        }

        private void OnGlobalOperationStarted(object? sender, EventArgs e)
        {
            lock (_globalNotificationsGate)
            {
                // Pass TaskContinuationOptions.OnlyOnRanToCompletion to avoid delivering further notifications once the task gets canceled or fails.
                // The cancellation happens only when VS is shutting down. The task might fail if communication with OOP fails. 
                // Once that happens there is not point in sending more notifications to the remote service.

                _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
                    SendStartNotificationAsync, _cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
            }
        }

        private async Task<GlobalNotificationState> SendStartNotificationAsync(Task<GlobalNotificationState> previousTask)
        {
            // Can only transition from NotStarted->Started.  If we hear about
            // anything else, do nothing.
            if (previousTask.Result != GlobalNotificationState.NotStarted)
            {
                return previousTask.Result;
            }

            var client = await RemoteHostClient.TryGetClientAsync(_services, _cancellationToken).ConfigureAwait(false);
            if (client == null)
            {
                return previousTask.Result;
            }

            _ = await client.TryInvokeAsync<IRemoteGlobalNotificationDeliveryService>(
                (service, cancellationToken) => service.OnGlobalOperationStartedAsync(cancellationToken),
                _cancellationToken).ConfigureAwait(false);

            return GlobalNotificationState.Started;
        }

        private void OnGlobalOperationStopped(object? sender, EventArgs e)
        {
            lock (_globalNotificationsGate)
            {
                // Pass TaskContinuationOptions.OnlyOnRanToCompletion to avoid delivering further notifications once the task gets canceled or fails.
                // The cancellation happens only when VS is shutting down. The task might fail if communication with OOP fails. 
                // Once that happens there is not point in sending more notifications to the remote service.

                _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
                    previous => SendStoppedNotificationAsync(previous), _cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
            }
        }

        private async Task<GlobalNotificationState> SendStoppedNotificationAsync(Task<GlobalNotificationState> previousTask)
        {
            // Can only transition from Started->NotStarted.  If we hear about
            // anything else, do nothing.
            if (previousTask.Result != GlobalNotificationState.Started)
            {
                return previousTask.Result;
            }

            var client = await RemoteHostClient.TryGetClientAsync(_services, _cancellationToken).ConfigureAwait(false);
            if (client == null)
            {
                return previousTask.Result;
            }

            _ = await client.TryInvokeAsync<IRemoteGlobalNotificationDeliveryService>(
                (service, cancellationToken) => service.OnGlobalOperationStoppedAsync(cancellationToken),
                _cancellationToken).ConfigureAwait(false);

            // Mark that we're stopped now.
            return GlobalNotificationState.NotStarted;
        }
    }
}
