﻿// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ErrorReporting
{
    internal sealed class VisualStudioInfoBar
    {
        private readonly IThreadingContext _threadingContext;
        private readonly SVsServiceProvider _serviceProvider;
        private readonly IAsynchronousOperationListener _listener;

        /// <summary>
        /// Keep track of the messages that are currently being shown to the user.  If we would 
        /// show the same message again, block that from happening so we don't spam the user with
        /// the same message.  When the info bar item is dismissed though, we then may show the
        /// same message in the future.  This is important for user clarity as it's possible for 
        /// a feature to fail for some reason, then work fine for a while, then fail again.  We want
        /// the second failure message to be reported to ensure the user is not confused.
        /// 
        /// Accessed on UI thread.
        /// </summary>
        private readonly HashSet<string> _currentlyShowingMessages = new();

        public VisualStudioInfoBar(
            IThreadingContext threadingContext,
            SVsServiceProvider serviceProvider,
            IAsynchronousOperationListenerProvider listenerProvider)
        {
            _threadingContext = threadingContext;
            _serviceProvider = serviceProvider;
            _listener = listenerProvider.GetListener(FeatureAttribute.InfoBar);
        }

        public void ShowInfoBar(string message, params InfoBarUI[] items)
        {
            // We can be called from any thread since errors can occur anywhere, however we can only construct and InfoBar from the UI thread.
            _threadingContext.JoinableTaskFactory.RunAsync(async () =>
            {
                using var _ = _listener.BeginAsyncOperation(nameof(ShowInfoBar));

                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(_threadingContext.DisposalToken);

                // If we're already shown this same message to the user, then do not bother showing it
                // to them again.  It will just be noisy.
                if (!_currentlyShowingMessages.Contains(message) &&
                    _serviceProvider.GetService(typeof(SVsShell)) is IVsShell shell &&
                    ErrorHandler.Succeeded(shell.GetProperty((int)__VSSPROPID7.VSSPROPID_MainWindowInfoBarHost, out var globalInfoBar)) &&
                    globalInfoBar is IVsInfoBarHost infoBarHost &&
                    _serviceProvider.GetService(typeof(SVsInfoBarUIFactory)) is IVsInfoBarUIFactory factory)
                {
                    // create action item list
                    var actionItems = new List<IVsInfoBarActionItem>();

                    foreach (var item in items)
                    {
                        switch (item.Kind)
                        {
                            case InfoBarUI.UIKind.Button:
                                actionItems.Add(new InfoBarButton(item.Title));
                                break;
                            case InfoBarUI.UIKind.HyperLink:
                                actionItems.Add(new InfoBarHyperlink(item.Title));
                                break;
                            case InfoBarUI.UIKind.Close:
                                break;
                            default:
                                throw ExceptionUtilities.UnexpectedValue(item.Kind);
                        }
                    }

                    var infoBarModel = new InfoBarModel(
                        new[] { new InfoBarTextSpan(message) },
                        actionItems,
                        KnownMonikers.StatusInformation,
                        isCloseButtonVisible: true);

                    var infoBarUI = factory.CreateInfoBar(infoBarModel);
                    if (infoBarUI == null)
                        return;

                    uint? infoBarCookie = null;
                    var eventSink = new InfoBarEvents(items, onClose: () =>
                    {
                        Contract.ThrowIfFalse(_threadingContext.JoinableTaskContext.IsOnMainThread);

                        // Remove the message from the list that we're keeping track of.  Future identical
                        // messages can now be shown.
                        _currentlyShowingMessages.Remove(message);

                        // Run given onClose action if there is one.
                        items.FirstOrDefault(i => i.Kind == InfoBarUI.UIKind.Close).Action?.Invoke();

                        if (infoBarCookie.HasValue)
                        {
                            infoBarUI.Unadvise(infoBarCookie.Value);
                        }
                    });

                    infoBarUI.Advise(eventSink, out var cookie);
                    infoBarCookie = cookie;

                    infoBarHost.AddInfoBar(infoBarUI);

                    _currentlyShowingMessages.Add(message);
                }
            });
        }

        private sealed class InfoBarEvents : IVsInfoBarUIEvents
        {
            private readonly InfoBarUI[] _items;
            private readonly Action _onClose;

            public InfoBarEvents(InfoBarUI[] items, Action onClose)
            {
                Contract.ThrowIfNull(onClose);

                _items = items;
                _onClose = onClose;
            }

            public void OnActionItemClicked(IVsInfoBarUIElement infoBarUIElement, IVsInfoBarActionItem actionItem)
            {
                var item = _items.FirstOrDefault(i => i.Title == actionItem.Text);
                if (item.IsDefault)
                {
                    return;
                }

                item.Action?.Invoke();

                if (!item.CloseAfterAction)
                {
                    return;
                }

                infoBarUIElement.Close();
            }

            public void OnClosed(IVsInfoBarUIElement infoBarUIElement)
                => _onClose();
        }
    }
}
