﻿// 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.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Projection;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense
{
    /// <summary>
    /// Helper class to use type-safety to enforce we're using TextSpans from the
    /// TextView's buffer.  Intellisense primarily uses spans from the SubjectBuffer
    /// which need to be mapped to ViewTextSpans before comparing to view positions
    /// such as the current caret location.
    /// </summary>
    internal readonly struct ViewTextSpan(TextSpan textSpan)
    {
        public readonly TextSpan TextSpan = textSpan;
    }

    internal readonly struct DisconnectedBufferGraph(ITextBuffer subjectBuffer, ITextBuffer viewBuffer)
    {
        // The subject buffer's snapshot at the point of the initial model's creation
        public readonly ITextSnapshot SubjectBufferSnapshot = subjectBuffer.CurrentSnapshot;

        // The TextView's snapshot at the point of the initial model's creation
        public readonly ITextSnapshot ViewSnapshot = viewBuffer.CurrentSnapshot;

        // The relation of the subject buffer to the TextView's top buffer.  This information
        // is used with the subjectBufferSnapshot and viewSnapshot to map spans even when 
        // the buffers might be temporarily disconnected during Razor/Venus remappings.
        public readonly BufferMapDirection SubjectBufferToTextViewDirection = IBufferGraphExtensions.ClassifyBufferMapDirection(
                subjectBuffer,
                viewBuffer);

        // Normally, we could just use a BufferGraph to do the mapping, but our subjectBuffer may be 
        // disconnected from the view when we are asked to do this mapping.
        public ViewTextSpan GetSubjectBufferTextSpanInViewBuffer(TextSpan textSpan)
        {
            switch (SubjectBufferToTextViewDirection)
            {
                // The view and subject buffer are the same
                case BufferMapDirection.Identity:
                    return new ViewTextSpan(textSpan);

                // The subject buffer contains the view buffer.  This happens in debugger intellisense.
                case BufferMapDirection.Down:
                    {
                        var projection = SubjectBufferSnapshot as IProjectionSnapshot;
                        var span = MapDownToSnapshot(textSpan.ToSpan(), projection, ViewSnapshot);
                        return new ViewTextSpan(span.ToTextSpan());
                    }

                // The view buffer contains the subject buffer.  This is the typical Razor setup.
                case BufferMapDirection.Up:
                    {
                        var projection = ViewSnapshot as IProjectionSnapshot;
                        var span = MapUpToSnapshot(textSpan.ToSpan(), SubjectBufferSnapshot, projection);
                        return new ViewTextSpan(span.ToTextSpan());
                    }

                default:
                    throw ExceptionUtilities.Unreachable();
            }
        }

        private static Span MapUpToSnapshot(Span span, ITextSnapshot start, IProjectionSnapshot target)
        {
            var spans = MapUpToSnapshotRecursive(new SnapshotSpan(start, span), target);
            return spans.First();
        }

        // Do a depth first search through the projection graph to find the first mapping
        private static IEnumerable<Span> MapUpToSnapshotRecursive(SnapshotSpan start, IProjectionSnapshot target)
        {
            foreach (var source in target.SourceSnapshots)
            {
                if (source == start.Snapshot)
                {
                    foreach (var result in target.MapFromSourceSnapshot(start))
                    {
                        yield return result;
                    }
                }
                else if (source is IProjectionSnapshot sourceProjection)
                {
                    foreach (var span in MapUpToSnapshotRecursive(start, sourceProjection))
                    {
                        foreach (var result in target.MapFromSourceSnapshot(new SnapshotSpan(source, span)))
                        {
                            yield return result;
                        }
                    }
                }
            }

            yield break;
        }

        private static Span MapDownToSnapshot(Span span, IProjectionSnapshot start, ITextSnapshot target)
        {
            var sourceSpans = new Queue<SnapshotSpan>(start.MapToSourceSnapshots(span));
            while (true)
            {
                var sourceSpan = sourceSpans.Dequeue();
                if (sourceSpan.Snapshot == target)
                {
                    return sourceSpan.Span;
                }
                else if (sourceSpan.Snapshot is IProjectionSnapshot)
                {
                    foreach (var s in (sourceSpan.Snapshot as IProjectionSnapshot).MapToSourceSnapshots(sourceSpan.Span))
                    {
                        sourceSpans.Enqueue(s);
                    }
                }
            }
        }
    }
}
