// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Test.Utilities;

public sealed class EditorTestHostDocument : TestHostDocument
{
    private static readonly ImmutableArray<string> s_defaultRoles =
    [
        PredefinedTextViewRoles.Analyzable,
        PredefinedTextViewRoles.Document,
        PredefinedTextViewRoles.Editable,
        PredefinedTextViewRoles.Interactive,
        PredefinedTextViewRoles.Zoomable,
    ];

    private readonly ImmutableArray<string> _roles;

    private IWpfTextView? _textView;

    /// <summary>
    /// The <see cref="ITextBuffer2"/> for this document. Null if not yet created.
    /// </summary>
    private ITextBuffer2? _textBuffer;

    internal EditorTestHostDocument(
        ExportProvider exportProvider,
        HostLanguageServices? languageServiceProvider,
        string code,
        string name,
        string filePath,
        int? cursorPosition,
        IDictionary<string, ImmutableArray<TextSpan>> spans,
        SourceCodeKind sourceCodeKind = SourceCodeKind.Regular,
        IReadOnlyList<string>? folders = null,
        bool isLinkFile = false,
        IDocumentServiceProvider? documentServiceProvider = null,
        ImmutableArray<string> roles = default,
        ITextBuffer2? textBuffer = null,
        ISourceGenerator? generator = null)
        : base(
            exportProvider,
            languageServiceProvider,
            code,
            name,
            filePath,
            cursorPosition,
            spans,
            sourceCodeKind,
            folders,
            isLinkFile,
            documentServiceProvider,
            generator)
    {
        _roles = roles.IsDefault ? s_defaultRoles : roles;

        if (textBuffer != null)
        {
            _textBuffer = textBuffer;
            InitialTextSnapshot = textBuffer.CurrentSnapshot;
        }
    }

    internal EditorTestHostDocument(
        string text = "",
        string displayName = "",
        SourceCodeKind sourceCodeKind = SourceCodeKind.Regular,
        DocumentId? id = null,
        string? filePath = null,
        IReadOnlyList<string>? folders = null,
        ExportProvider? exportProvider = null,
        IDocumentServiceProvider? documentServiceProvider = null)
        : base(
            text,
            displayName,
            sourceCodeKind,
            id,
            filePath,
            folders,
            exportProvider,
            documentServiceProvider)
    {
    }

    // TODO: delete this
    /// <summary>
    /// The <see cref="ITextSnapshot"/> when the buffer was first created, which can be used for tracking changes to the current buffer.
    /// </summary>
    public ITextSnapshot InitialTextSnapshot
    {
        get
        {
            Contract.ThrowIfNull(field);
            return field;
        }

        private set;
    }

    public IWpfTextView GetTextView()
    {
        if (_textView == null)
        {
            Contract.ThrowIfNull(ExportProvider, $"Can only create text view for {nameof(TestHostDocument)} created with {nameof(ExportProvider)}");
            WpfTestRunner.RequireWpfFact($"Creates an {nameof(IWpfTextView)} through {nameof(TestHostDocument)}.{nameof(GetTextView)}");

            var factory = ExportProvider.GetExportedValue<ITextEditorFactoryService>();

            // Every default role but outlining. Starting in 15.2, the editor
            // OutliningManager imports JoinableTaskContext in a way that's 
            // difficult to satisfy in our unit tests. Since we don't directly
            // depend on it, just disable it
            var roles = factory.CreateTextViewRoleSet(_roles);
            _textView = factory.CreateTextView(this.GetTextBuffer(), roles);
            if (this.CursorPosition.HasValue)
            {
                _textView.Caret.MoveTo(new SnapshotPoint(_textView.TextSnapshot, CursorPosition.Value));
            }
            else if (this.SelectedSpans.IsSingle())
            {
                var span = this.SelectedSpans.Single();
                _textView.Selection.Select(new SnapshotSpan(_textView.TextSnapshot, new Span(span.Start, span.Length)), false);
            }
        }

        return _textView;
    }

    public ITextBuffer2 GetTextBuffer()
    {
        var workspace = (EditorTestWorkspace?)LanguageServiceProvider?.WorkspaceServices.Workspace;

        if (_textBuffer == null)
        {
            Contract.ThrowIfNull(LanguageServiceProvider, $"To get a text buffer for a {nameof(TestHostDocument)}, it must have been parented in a project.");
            var contentType = LanguageServiceProvider.GetRequiredService<IContentTypeLanguageService>().GetDefaultContentType();

            _textBuffer = workspace!.GetOrCreateBufferForPath(FilePath, contentType, LanguageServiceProvider.Language, InitialText);
            InitialTextSnapshot = _textBuffer.CurrentSnapshot;
        }

        if (workspace != null)
        {
            // Open (or reopen) any files that were closed in this call. We do this for all linked copies at once.
            foreach (var linkedId in workspace.CurrentSolution.GetDocumentIdsWithFilePath(FilePath).Concat(this.Id))
            {
                if (workspace.IsDocumentOpen(linkedId))
                    continue;

                if (workspace.GetTestDocument(linkedId) is { } testDocument)
                {
                    if (testDocument.IsSourceGenerated)
                    {
                        var threadingContext = workspace.GetService<IThreadingContext>();
                        var document = threadingContext.JoinableTaskFactory.Run(() => workspace.CurrentSolution.GetSourceGeneratedDocumentAsync(testDocument.Id, CancellationToken.None).AsTask());
                        Contract.ThrowIfNull(document);

                        workspace.OnSourceGeneratedDocumentOpened(_textBuffer.AsTextContainer(), document);
                    }
                    else
                    {
                        // If there is a linked file, we'll start the non-linked one as being the primary context, which some tests depend on.
                        workspace.OnDocumentOpened(linkedId, _textBuffer.AsTextContainer(), isCurrentContext: !testDocument.IsLinkFile);
                    }
                }
                else if (workspace.GetTestAdditionalDocument(linkedId) is { } testAdditionalDocument)
                {
                    workspace.OnAdditionalDocumentOpened(linkedId, _textBuffer.AsTextContainer());
                }
                else if (workspace.GetTestAnalyzerConfigDocument(linkedId) is { } testAnalyzerConfigDocument)
                {
                    workspace.OnAnalyzerConfigDocumentOpened(linkedId, _textBuffer.AsTextContainer());
                }
            }
        }

        return _textBuffer;
    }

    public override void Open()
        => GetOpenTextContainer();

    public SourceTextContainer GetOpenTextContainer()
        => this.GetTextBuffer().AsTextContainer();

    internal void CloseTextView()
    {
        if (_textView != null && !_textView.IsClosed)
        {
            _textView.Close();
            _textView = null;
        }
    }

    internal void Update(SourceText newText)
    {
        var buffer = GetTextBuffer();
        using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null);
        var oldText = buffer.CurrentSnapshot.AsText();
        var changes = newText.GetTextChanges(oldText);

        foreach (var change in changes)
        {
            edit.Replace(change.Span.Start, change.Span.Length, change.NewText);
        }

        edit.Apply();
    }
}
