﻿// 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.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor
{
    [Export(typeof(ITextViewConnectionListener))]
    [ContentType(ContentTypeNames.RoslynContentType)]
    [ContentType(ContentTypeNames.XamlContentType)]
    [TextViewRole(PredefinedTextViewRoles.Interactive)]
    [Export(typeof(ITextBufferAssociatedViewService))]
    internal class TextBufferAssociatedViewService : ITextViewConnectionListener, ITextBufferAssociatedViewService
    {
#if DEBUG
        private static readonly HashSet<ITextView> s_registeredViews = new();
#endif

        private static readonly object s_gate = new();
        private static readonly ConditionalWeakTable<ITextBuffer, HashSet<ITextView>> s_map =
            new();

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public TextBufferAssociatedViewService()
        {
        }

        public event EventHandler<SubjectBuffersConnectedEventArgs> SubjectBuffersConnected;
        public event EventHandler<SubjectBuffersConnectedEventArgs> SubjectBuffersDisconnected;

        void ITextViewConnectionListener.SubjectBuffersConnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers)
        {
            lock (s_gate)
            {
                // only add roslyn type to tracking map
                foreach (var buffer in subjectBuffers.Where(b => IsSupportedContentType(b.ContentType)))
                {
                    if (!s_map.TryGetValue(buffer, out var set))
                    {
                        set = new HashSet<ITextView>();
                        s_map.Add(buffer, set);
                    }

                    set.Add(textView);
                    DebugRegisterView_NoLock(textView);
                }
            }

            this.SubjectBuffersConnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection()));
        }

        void ITextViewConnectionListener.SubjectBuffersDisconnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers)
        {
            lock (s_gate)
            {
                // we need to check all buffers reported since we will be called after actual changes have happened. 
                // for example, if content type of a buffer changed, we will be called after it is changed, rather than before it.
                foreach (var buffer in subjectBuffers)
                {
                    if (s_map.TryGetValue(buffer, out var set))
                    {
                        set.Remove(textView);
                        if (set.Count == 0)
                        {
                            s_map.Remove(buffer);
                        }
                    }
                }
            }

            this.SubjectBuffersDisconnected?.Invoke(this, new SubjectBuffersConnectedEventArgs(textView, subjectBuffers.ToReadOnlyCollection()));
        }

        private static bool IsSupportedContentType(IContentType contentType)
        {
            // This list should match the list of exported content types above
            return contentType.IsOfType(ContentTypeNames.RoslynContentType) ||
                   contentType.IsOfType(ContentTypeNames.XamlContentType);
        }

        private static IList<ITextView> GetTextViews(ITextBuffer textBuffer)
        {
            lock (s_gate)
            {
                if (!s_map.TryGetValue(textBuffer, out var set))
                {
                    return SpecializedCollections.EmptyList<ITextView>();
                }

                return set.ToList();
            }
        }

        public IEnumerable<ITextView> GetAssociatedTextViews(ITextBuffer textBuffer)
            => GetTextViews(textBuffer);

        private static bool HasFocus(ITextView textView)
            => textView.HasAggregateFocus;

        public static bool AnyAssociatedViewHasFocus(ITextBuffer textBuffer)
        {
            if (textBuffer == null)
            {
                return false;
            }

            var views = GetTextViews(textBuffer);
            if (views.Count == 0)
            {
                // We haven't seen the view yet.  Assume it is visible.
                return true;
            }

            return views.Any(HasFocus);
        }

        [Conditional("DEBUG")]
        private static void DebugRegisterView_NoLock(ITextView textView)
        {
#if DEBUG
            if (s_registeredViews.Add(textView))
            {
                textView.Closed += OnTextViewClose;
            }
#endif
        }

#if DEBUG
        private static void OnTextViewClose(object sender, EventArgs e)
        {
            var view = sender as ITextView;

            lock (s_gate)
            {
                foreach (var buffer in view.BufferGraph.GetTextBuffers(b => IsSupportedContentType(b.ContentType)))
                {
                    if (s_map.TryGetValue(buffer, out var set))
                    {
                        Contract.ThrowIfTrue(set.Contains(view));
                    }
                }

                s_registeredViews.Remove(view);
            }
        }
#endif
    }
}
