﻿// 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.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Text
{
    public static partial class Extensions
    {
        /// <summary>
        /// ITextSnapshot implementation of SourceText
        /// </summary>
        private class SnapshotSourceText : SourceText
        {
            private static readonly Func<int, int, string> s_textLog = (v1, v2) => string.Format("FullRange : from {0} to {1}", v1, v2);

            /// <summary>
            /// The <see cref="ITextImage"/> backing the SourceText instance
            /// </summary>
            public readonly ITextImage TextImage;

            private readonly ITextBufferCloneService? _textBufferCloneService;

            private readonly Encoding? _encoding;
            private readonly TextBufferContainer? _container;

            private SnapshotSourceText(ITextBufferCloneService? textBufferCloneService, ITextSnapshot editorSnapshot, SourceHashAlgorithm checksumAlgorithm, TextBufferContainer container)
                : base(checksumAlgorithm: checksumAlgorithm)
            {
                Contract.ThrowIfNull(editorSnapshot);

                _textBufferCloneService = textBufferCloneService;
                this.TextImage = RecordReverseMapAndGetImage(editorSnapshot);
                _encoding = editorSnapshot.TextBuffer.GetEncodingOrUTF8();
                _container = container;
            }

            public SnapshotSourceText(ITextBufferCloneService? textBufferCloneService, ITextImage textImage, Encoding? encoding, SourceHashAlgorithm checksumAlgorithm, TextBufferContainer? container)
                : base(checksumAlgorithm: checksumAlgorithm)
            {
                Contract.ThrowIfNull(textImage);

                _textBufferCloneService = textBufferCloneService;
                this.TextImage = textImage;
                _encoding = encoding;
                _container = container;
            }

            /// <summary>
            /// A weak map of all Editor ITextSnapshots and their associated SourceText
            /// </summary>
            private static readonly ConditionalWeakTable<ITextSnapshot, SnapshotSourceText> s_textSnapshotMap = new ConditionalWeakTable<ITextSnapshot, SnapshotSourceText>();

            /// <summary>
            /// Reverse map of roslyn text to editor snapshot. unlike forward map, this doesn't strongly hold onto editor snapshot so that 
            /// we don't leak editor snapshot which should go away once editor is closed. roslyn source's lifetime is not usually tied to view.
            /// </summary>
            private static readonly ConditionalWeakTable<ITextImage, WeakReference<ITextSnapshot>> s_textImageToEditorSnapshotMap = new ConditionalWeakTable<ITextImage, WeakReference<ITextSnapshot>>();

            public static SourceText From(ITextBufferCloneService? textBufferCloneService, ITextSnapshot editorSnapshot)
            {
                if (editorSnapshot == null)
                {
                    throw new ArgumentNullException(nameof(editorSnapshot));
                }

                if (!s_textSnapshotMap.TryGetValue(editorSnapshot, out var snapshot))
                {
                    // Explicitly obtain the TextBufferContainer before calling GetValue to avoid reentrancy in
                    // ConditionalWeakTable. https://github.com/dotnet/roslyn/issues/28256
                    var container = TextBufferContainer.From(editorSnapshot.TextBuffer);

                    // Avoid capturing `textBufferCloneServiceOpt` on the fast path
                    var tempTextBufferCloneService = textBufferCloneService;
                    snapshot = s_textSnapshotMap.GetValue(editorSnapshot, s => new SnapshotSourceText(tempTextBufferCloneService, s, SourceHashAlgorithms.OpenDocumentChecksumAlgorithm, container));
                }

                return snapshot;
            }

            /// <summary>
            /// This only exist to break circular dependency on creating buffer. nobody except extension itself should use it
            /// </summary>
            internal static SourceText From(ITextBufferCloneService? textBufferCloneService, ITextSnapshot editorSnapshot, TextBufferContainer container)
            {
                if (editorSnapshot == null)
                {
                    throw new ArgumentNullException(nameof(editorSnapshot));
                }

                Contract.ThrowIfFalse(editorSnapshot.TextBuffer == container.GetTextBuffer());
                return s_textSnapshotMap.GetValue(editorSnapshot, s => new SnapshotSourceText(textBufferCloneService, s, SourceHashAlgorithms.OpenDocumentChecksumAlgorithm, container));
            }

            public override Encoding? Encoding
            {
                get { return _encoding; }
            }

            public ITextSnapshot? TryFindEditorSnapshot()
                => TryFindEditorSnapshot(this.TextImage);

            public override SourceTextContainer Container
            {
                get
                {
                    return _container ?? base.Container;
                }
            }

            public override int Length
            {
                get
                {
                    var res = this.TextImage.Length;
                    return res;
                }
            }

            public override char this[int position]
            {
                get { return this.TextImage[position]; }
            }

            #region Lines
            protected override TextLineCollection GetLinesCore()
                => new LineInfo(this);

            private class LineInfo : TextLineCollection
            {
                private readonly SnapshotSourceText _text;

                public LineInfo(SnapshotSourceText text)
                    => _text = text;

                public override int Count
                {
                    get { return _text.TextImage.LineCount; }
                }

                public override TextLine this[int index]
                {
                    get
                    {
                        var line = _text.TextImage.GetLineFromLineNumber(index);
                        return TextLine.FromSpan(_text, TextSpan.FromBounds(line.Start, line.End));
                    }
                }

                public override int IndexOf(int position)
                    => _text.TextImage.GetLineNumberFromPosition(position);

                public override TextLine GetLineFromPosition(int position)
                    => this[this.IndexOf(position)];

                public override LinePosition GetLinePosition(int position)
                {
                    var textLine = _text.TextImage.GetLineFromPosition(position);
                    return new LinePosition(textLine.LineNumber, position - textLine.Start);
                }
            }
            #endregion

            public override string ToString()
                => this.TextImage.GetText();

            public override string ToString(TextSpan textSpan)
            {
                var editorSpan = new Span(textSpan.Start, textSpan.Length);
                var res = this.TextImage.GetText(editorSpan);
                return res;
            }

            public override SourceText WithChanges(IEnumerable<TextChange> changes)
            {
                if (changes == null)
                {
                    throw new ArgumentNullException(nameof(changes));
                }

                if (!changes.Any())
                {
                    return this;
                }

                // check whether we can use text buffer factory
                var factory = _textBufferCloneService;
                if (factory == null)
                {
                    // if we can't get the factory, use the default implementation
                    return base.WithChanges(changes);
                }

                // otherwise, create a new cloned snapshot
                var buffer = factory.CloneWithUnknownContentType(TextImage);
                var baseSnapshot = buffer.CurrentSnapshot;

                // apply the change to the buffer
                using (var edit = buffer.CreateEdit())
                {
                    foreach (var change in changes)
                    {
                        edit.Replace(change.Span.ToSpan(), change.NewText);
                    }

                    edit.Apply();
                }

                return new ChangedSourceText(
                    textBufferCloneService: _textBufferCloneService,
                    baseText: this,
                    baseSnapshot: ((ITextSnapshot2)baseSnapshot).TextImage,
                    currentSnapshot: ((ITextSnapshot2)buffer.CurrentSnapshot).TextImage);
            }

            private static ITextImage RecordReverseMapAndGetImage(ITextSnapshot editorSnapshot)
            {
                Contract.ThrowIfNull(editorSnapshot);

                var textImage = ((ITextSnapshot2)editorSnapshot).TextImage;
                Contract.ThrowIfNull(textImage);

                // If we're already in the map, there's nothing to update.  Do a quick check
                // to avoid two allocations per call to RecordTextSnapshotAndGetImage.
                if (!s_textImageToEditorSnapshotMap.TryGetValue(textImage, out var weakReference))
                {
                    // put reverse entry that won't hold onto anything
                    weakReference = s_textImageToEditorSnapshotMap.GetValue(
                        textImage, _ => new WeakReference<ITextSnapshot>(editorSnapshot));
                }

#if DEBUG
                // forward and reversed map is 1:1 map. snapshot can't be different
                var snapshot = weakReference.GetTarget();
                Contract.ThrowIfFalse(snapshot == editorSnapshot);
#endif
                return textImage;
            }

            private static ITextSnapshot? TryFindEditorSnapshot(ITextImage textImage)
            {
                if (!s_textImageToEditorSnapshotMap.TryGetValue(textImage, out var weakReference) ||
                    !weakReference.TryGetTarget(out var editorSnapshot))
                {
                    return null;
                }

                return editorSnapshot;
            }

            /// <summary>
            /// Use a separate class for closed files to simplify memory leak investigations
            /// </summary>
            internal sealed class ClosedSnapshotSourceText : SnapshotSourceText
            {
                public ClosedSnapshotSourceText(ITextBufferCloneService? textBufferCloneService, ITextImage textImage, Encoding? encoding, SourceHashAlgorithm checksumAlgorithm)
                    : base(textBufferCloneService, textImage, encoding, checksumAlgorithm, container: null)
                {
                }
            }

            /// <summary>
            /// Perf: Optimize calls to GetChangeRanges after WithChanges by using editor snapshots
            /// </summary>
            private class ChangedSourceText : SnapshotSourceText
            {
                private readonly SnapshotSourceText _baseText;
                private readonly ITextImage _baseSnapshot;

                public ChangedSourceText(ITextBufferCloneService? textBufferCloneService, SnapshotSourceText baseText, ITextImage baseSnapshot, ITextImage currentSnapshot)
                    : base(textBufferCloneService, currentSnapshot, baseText.Encoding, baseText.ChecksumAlgorithm, container: null)
                {
                    _baseText = baseText;
                    _baseSnapshot = baseSnapshot;
                }

                public override IReadOnlyList<TextChangeRange> GetChangeRanges(SourceText oldText)
                {
                    if (oldText == null)
                    {
                        throw new ArgumentNullException(nameof(oldText));
                    }

                    // if they are the same text there is no change.
                    if (oldText == this)
                    {
                        return TextChangeRange.NoChanges;
                    }

                    if (oldText != _baseText)
                    {
                        return new[] { new TextChangeRange(new TextSpan(0, oldText.Length), this.Length) };
                    }

                    return GetChangeRanges(_baseSnapshot, _baseSnapshot.Length, this.TextImage);
                }
            }

            public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
                => this.TextImage.CopyTo(sourceIndex, destination, destinationIndex, count);

            public override void Write(TextWriter textWriter, TextSpan span, CancellationToken cancellationToken)
                => this.TextImage.Write(textWriter, span.ToSpan());

            #region GetChangeRangesImplementation 

            public override IReadOnlyList<TextChangeRange> GetChangeRanges(SourceText oldText)
            {
                if (oldText == null)
                {
                    throw new ArgumentNullException(nameof(oldText));
                }

                // if they are the same text there is no change.
                if (oldText == this)
                {
                    return TextChangeRange.NoChanges;
                }

                // first, check whether the text buffer is still alive.
                if (this.Container is TextBufferContainer container)
                {
                    var lastEventArgs = container.LastEventArgs;
                    if (lastEventArgs != null && lastEventArgs.OldText == oldText && lastEventArgs.NewText == this)
                    {
                        return lastEventArgs.Changes;
                    }
                }

                var oldSnapshot = oldText.TryFindCorrespondingEditorTextImage();
                var newSnapshot = this.TryFindCorrespondingEditorTextImage();
                return GetChangeRanges(oldSnapshot, oldText.Length, newSnapshot);
            }

            private IReadOnlyList<TextChangeRange> GetChangeRanges(ITextImage? oldImage, int oldTextLength, ITextImage? newImage)
            {
                if (oldImage == null ||
                    newImage == null ||
                    oldImage.Version.Identifier != newImage.Version.Identifier)
                {
                    // Claim its all changed
                    Logger.Log(FunctionId.Workspace_SourceText_GetChangeRanges, "Invalid Snapshots");
                    return ImmutableArray.Create(new TextChangeRange(new TextSpan(0, oldTextLength), this.Length));
                }
                else if (AreSameReiteratedVersion(oldImage, newImage))
                {
                    // content of two snapshot must be same even if versions are different
                    return TextChangeRange.NoChanges;
                }
                else
                {
                    return GetChangeRanges(oldImage, newImage, forward: oldImage.Version.VersionNumber <= newImage.Version.VersionNumber);
                }
            }

            private static bool AreSameReiteratedVersion(ITextImage oldImage, ITextImage newImage)
            {
                var oldSnapshot = TryFindEditorSnapshot(oldImage);
                var newSnapshot = TryFindEditorSnapshot(newImage);

                return oldSnapshot != null && newSnapshot != null && oldSnapshot.Version.ReiteratedVersionNumber == newSnapshot.Version.ReiteratedVersionNumber;
            }

            private static readonly Func<ITextChange, TextChangeRange> s_forwardTextChangeRange = c => CreateTextChangeRange(c, forward: true);
            private static readonly Func<ITextChange, TextChangeRange> s_backwardTextChangeRange = c => CreateTextChangeRange(c, forward: false);

            private static IReadOnlyList<TextChangeRange> GetChangeRanges(ITextImage snapshot1, ITextImage snapshot2, bool forward)
            {
                var oldSnapshot = forward ? snapshot1 : snapshot2;
                var newSnapshot = forward ? snapshot2 : snapshot1;

                INormalizedTextChangeCollection? changes = null;
                for (var oldVersion = oldSnapshot.Version;
                    oldVersion != newSnapshot.Version;
                    oldVersion = oldVersion.Next)
                {
                    if (oldVersion.Changes.Count != 0)
                    {
                        if (changes != null)
                        {
                            // Oops - more than one "textual" change between these snapshots, bail and try to find smallest changes span
                            Logger.Log(FunctionId.Workspace_SourceText_GetChangeRanges, s_textLog, snapshot1.Version.VersionNumber, snapshot2.Version.VersionNumber);

                            return new[] { GetChangeRanges(oldSnapshot.Version, newSnapshot.Version, forward) };
                        }
                        else
                        {
                            changes = oldVersion.Changes;
                        }
                    }
                }

                if (changes == null)
                {
                    return ImmutableArray.Create<TextChangeRange>();
                }
                else
                {
                    return ImmutableArray.CreateRange(changes.Select(forward ? s_forwardTextChangeRange : s_backwardTextChangeRange));
                }
            }

            private static TextChangeRange GetChangeRanges(ITextImageVersion oldVersion, ITextImageVersion newVersion, bool forward)
            {
                TextChangeRange? range = null;
                var iterator = GetMultipleVersionTextChanges(oldVersion, newVersion, forward);
                foreach (var changes in forward ? iterator : iterator.Reverse())
                {
                    range = range.Accumulate(changes);
                }

                RoslynDebug.Assert(range.HasValue);
                return range.Value;
            }

            private static IEnumerable<IEnumerable<TextChangeRange>> GetMultipleVersionTextChanges(
                ITextImageVersion oldVersion, ITextImageVersion newVersion, bool forward)
            {
                for (var version = oldVersion; version != newVersion; version = version.Next)
                {
                    yield return version.Changes.Select(forward ? s_forwardTextChangeRange : s_backwardTextChangeRange);
                }
            }

            private static TextChangeRange CreateTextChangeRange(ITextChange change, bool forward)
            {
                if (forward)
                {
                    return new TextChangeRange(new TextSpan(change.OldSpan.Start, change.OldSpan.Length), change.NewLength);
                }
                else
                {
                    return new TextChangeRange(new TextSpan(change.NewSpan.Start, change.NewSpan.Length), change.OldLength);
                }
            }
            #endregion
        }
    }
}
