﻿// 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.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.InlineRename;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename
{
    [Export(typeof(IInlineRenameService))]
    [Export(typeof(InlineRenameService))]
    [method: ImportingConstructor]
    [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    internal sealed class InlineRenameService(
        IThreadingContext threadingContext,
        IUIThreadOperationExecutor uiThreadOperationExecutor,
        ITextBufferAssociatedViewService textBufferAssociatedViewService,
        ITextBufferFactoryService textBufferFactoryService,
        ITextBufferCloneService textBufferCloneService,
        IFeatureServiceFactory featureServiceFactory,
        IGlobalOptionService globalOptions,
        [ImportMany] IEnumerable<IRefactorNotifyService> refactorNotifyServices,
        IAsynchronousOperationListenerProvider listenerProvider) : IInlineRenameService
    {
        private readonly IThreadingContext _threadingContext = threadingContext;
        private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor = uiThreadOperationExecutor;
        private readonly ITextBufferAssociatedViewService _textBufferAssociatedViewService = textBufferAssociatedViewService;
        private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.Rename);
        private readonly IEnumerable<IRefactorNotifyService> _refactorNotifyServices = refactorNotifyServices;
        private readonly ITextBufferFactoryService _textBufferFactoryService = textBufferFactoryService;
        private readonly ITextBufferCloneService _textBufferCloneService = textBufferCloneService;
        private readonly IFeatureServiceFactory _featureServiceFactory = featureServiceFactory;

        internal readonly IGlobalOptionService GlobalOptions = globalOptions;

        private InlineRenameSession? _activeRenameSession;

        public InlineRenameSessionInfo StartInlineSession(
            Document document,
            TextSpan textSpan,
            CancellationToken cancellationToken)
        {
            return _threadingContext.JoinableTaskFactory.Run(() => StartInlineSessionAsync(document, textSpan, cancellationToken));
        }

        public async Task<InlineRenameSessionInfo> StartInlineSessionAsync(
            Document document,
            TextSpan textSpan,
            CancellationToken cancellationToken)
        {
            if (_activeRenameSession != null)
            {
                throw new InvalidOperationException(EditorFeaturesResources.An_active_inline_rename_session_is_still_active_Complete_it_before_starting_a_new_one);
            }

            var editorRenameService = document.GetRequiredLanguageService<IEditorInlineRenameService>();
            var renameInfo = await editorRenameService.GetRenameInfoAsync(document, textSpan.Start, cancellationToken).ConfigureAwait(false);

            var readOnlyOrCannotNavigateToSpanSessionInfo = await IsReadOnlyOrCannotNavigateToSpanAsync(
                _threadingContext, renameInfo, document, cancellationToken).ConfigureAwait(false);
            if (readOnlyOrCannotNavigateToSpanSessionInfo != null)
            {
                return readOnlyOrCannotNavigateToSpanSessionInfo;
            }

            if (!renameInfo.CanRename)
            {
                return new InlineRenameSessionInfo(renameInfo.LocalizedErrorMessage);
            }

            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            var snapshot = text.FindCorrespondingEditorTextSnapshot();
            Contract.ThrowIfNull(snapshot, "The document used for starting the inline rename session should still be open and associated with a snapshot.");

            var fileRenameInfo = renameInfo.GetFileRenameInfo();
            var canRenameFile = fileRenameInfo == InlineRenameFileRenameInfo.Allowed;

            var options = new SymbolRenameOptions(
                RenameOverloads: renameInfo.MustRenameOverloads || GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameOverloads),
                RenameInStrings: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInStrings),
                RenameInComments: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInComments),
                RenameFile: canRenameFile && GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameFile));

            var previewChanges = GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.PreviewChanges);

            // The session currently has UI thread affinity.
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            ActiveSession = new InlineRenameSession(
                _threadingContext,
                this,
                document.Project.Solution.Workspace,
                renameInfo.TriggerSpan.ToSnapshotSpan(snapshot),
                renameInfo,
                options,
                previewChanges,
                _uiThreadOperationExecutor,
                _textBufferAssociatedViewService,
                _textBufferFactoryService,
                _textBufferCloneService,
                _featureServiceFactory,
                _refactorNotifyServices,
                _asyncListener);

            return new InlineRenameSessionInfo(ActiveSession);

            static async Task<InlineRenameSessionInfo?> IsReadOnlyOrCannotNavigateToSpanAsync(
                IThreadingContext threadingContext, IInlineRenameInfo renameInfo, Document document, CancellationToken cancellationToken)
            {
                if (renameInfo is IInlineRenameInfo inlineRenameInfo && inlineRenameInfo.DefinitionLocations != default)
                {
                    var workspace = document.Project.Solution.Workspace;
                    var navigationService = workspace.Services.GetRequiredService<IDocumentNavigationService>();
                    using var _ = PooledObjects.ArrayBuilder<(ITextBuffer, SnapshotSpan)>.GetInstance(out var buffersAndSpans);
                    foreach (var documentSpan in inlineRenameInfo.DefinitionLocations)
                    {
                        var sourceText = await documentSpan.Document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                        var textSnapshot = sourceText.FindCorrespondingEditorTextSnapshot();

                        if (textSnapshot != null)
                        {
                            var buffer = textSnapshot.TextBuffer;
                            var originalSpan = documentSpan.SourceSpan.ToSnapshotSpan(textSnapshot).TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);
                            buffersAndSpans.Add((buffer, originalSpan));
                        }

                        var canNavigate = await navigationService.CanNavigateToSpanAsync(
                            workspace, document.Id, documentSpan.SourceSpan, cancellationToken).ConfigureAwait(false);
                        if (!canNavigate)
                        {
                            return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_in_a_location_that_cannot_be_navigated_to);
                        }
                    }

                    await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                    foreach (var (buffer, originalSpan) in buffersAndSpans)
                    {
                        if (buffer.IsReadOnly(originalSpan))
                        {
                            return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_contained_in_a_read_only_file);
                        }
                    }
                }

                return null;
            }
        }

        IInlineRenameSession? IInlineRenameService.ActiveSession => _activeRenameSession;

        internal InlineRenameSession? ActiveSession
        {
            get
            {
                _threadingContext.ThrowIfNotOnUIThread();

                return _activeRenameSession;
            }

            set
            {
                _threadingContext.ThrowIfNotOnUIThread();

                // This is also checked in InlineRenameSession (which should be the only thing that ever sets this).
                // However, this just adds an extra level of safety to make sure nothing bad is about to happen.
                Contract.ThrowIfTrue(_activeRenameSession != null && value != null, "Cannot assign an active rename session when one is already in progress.");

                var previousSession = _activeRenameSession;
                _activeRenameSession = value;
                ActiveSessionChanged?.Invoke(this, new ActiveSessionChangedEventArgs(previousSession!));
            }
        }

        /// <summary>
        /// Raised when the ActiveSession property has changed.
        /// </summary>
        internal event EventHandler<ActiveSessionChangedEventArgs>? ActiveSessionChanged;

        internal class ActiveSessionChangedEventArgs(InlineRenameSession previousSession) : EventArgs
        {
            public InlineRenameSession PreviousSession { get; } = previousSession;
        }
    }
}
