﻿// 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.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Text
{
    public static partial class Extensions
    {
        /// <summary>
        /// ITextBuffer implementation of SourceTextContainer
        /// </summary>
        internal class TextBufferContainer : SourceTextContainer
        {
            private readonly WeakReference<ITextBuffer> _weakEditorBuffer;
            private readonly object _gate = new object();
            private readonly ITextBufferCloneService? _textBufferCloneService;

            private event EventHandler<TextChangeEventArgs>? EtextChanged;
            private SourceText _currentText;

            private TextBufferContainer(ITextBuffer editorBuffer)
            {
                Contract.ThrowIfNull(editorBuffer);

                _weakEditorBuffer = new WeakReference<ITextBuffer>(editorBuffer);
                editorBuffer.Properties.TryGetProperty(typeof(ITextBufferCloneService), out _textBufferCloneService);
                _currentText = SnapshotSourceText.From(_textBufferCloneService, editorBuffer.CurrentSnapshot, this);
            }

            /// <summary>
            /// A weak map of all Editor ITextBuffers and their associated SourceTextContainer
            /// </summary>
            private static readonly ConditionalWeakTable<ITextBuffer, TextBufferContainer> s_textContainerMap = new();

            public static TextBufferContainer From(ITextBuffer buffer)
            {
                if (buffer == null)
                {
                    throw new ArgumentNullException(nameof(buffer));
                }

                return s_textContainerMap.GetValue(buffer, static buffer => new TextBufferContainer(buffer));
            }

            public ITextBuffer? TryFindEditorTextBuffer()
                => _weakEditorBuffer.GetTarget();

            public override SourceText CurrentText
            {
                get
                {
                    var editorBuffer = this.TryFindEditorTextBuffer();
                    return editorBuffer != null
                        ? editorBuffer.CurrentSnapshot.AsText()
                        : _currentText;
                }
            }

            public override event EventHandler<TextChangeEventArgs> TextChanged
            {
                add
                {
                    lock (_gate)
                    {
                        var textBuffer = this.TryFindEditorTextBuffer();
                        if (this.EtextChanged == null && textBuffer != null)
                        {
                            textBuffer.ChangedHighPriority += this.OnTextContentChanged;
                        }

                        this.EtextChanged += value;
                    }
                }

                remove
                {
                    lock (_gate)
                    {
                        this.EtextChanged -= value;

                        var textBuffer = this.TryFindEditorTextBuffer();
                        if (this.EtextChanged == null && textBuffer != null)
                        {
                            textBuffer.ChangedHighPriority -= this.OnTextContentChanged;
                        }
                    }
                }
            }

            private void OnTextContentChanged(object? sender, TextContentChangedEventArgs args)
            {
                var changed = this.EtextChanged;
                if (changed == null)
                {
                    return;
                }

                // we should process all changes even though there is no text changes
                // otherwise, Workspace.CurrentSolution won't move forward to latest ITextSnapshot

                // this should convert given editor snapshots to roslyn forked snapshots
                var oldText = (SnapshotSourceText)args.Before.AsText();
                var newText = SnapshotSourceText.From(_textBufferCloneService, args.After);
                _currentText = newText;

                var changes = ImmutableArray.CreateRange(args.Changes.Select(c => new TextChangeRange(new TextSpan(c.OldSpan.Start, c.OldSpan.Length), c.NewLength)));
                var eventArgs = new TextChangeEventArgs(oldText, newText, changes);

                this.LastEventArgs = eventArgs;
                changed(sender, eventArgs);
            }

            // These are the event args that were last sent from this text container when the text
            // content may have changed.
            public TextChangeEventArgs? LastEventArgs { get; private set; }
        }
    }
}
