﻿// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename
{
    internal abstract partial class AbstractEditorInlineRenameService
    {
        /// <summary>
        /// Represents information about the ability to rename a particular location.
        /// </summary>
        private partial class SymbolInlineRenameInfo : IInlineRenameInfo
        {
            private const string AttributeSuffix = "Attribute";

            private readonly SymbolicRenameInfo _info;

            private Document Document => _info.Document!;
            private readonly CodeCleanupOptionsProvider _fallbackOptions;
            private readonly IEnumerable<IRefactorNotifyService> _refactorNotifyServices;

            /// <summary>
            /// Whether or not we shortened the trigger span (say because we were renaming an attribute,
            /// and we didn't select the 'Attribute' portion of the name).
            /// </summary>
            private bool IsRenamingAttributePrefix => _info.IsRenamingAttributePrefix;

            public bool CanRename { get; }
            public string? LocalizedErrorMessage => null;
            public TextSpan TriggerSpan { get; }
            public bool HasOverloads { get; }
            public bool MustRenameOverloads => _info.ForceRenameOverloads;

            /// <summary>
            /// The locations of the potential rename candidates for the symbol.
            /// </summary>
            public ImmutableArray<DocumentSpan> DefinitionLocations => _info.DocumentSpans;

            public ISymbol RenameSymbol => _info.Symbol!;

            public SymbolInlineRenameInfo(
                IEnumerable<IRefactorNotifyService> refactorNotifyServices,
                SymbolicRenameInfo info,
                CodeCleanupOptionsProvider fallbackOptions,
                CancellationToken cancellationToken)
            {
                Contract.ThrowIfTrue(info.IsError);
                this.CanRename = true;

                _info = info;
                _refactorNotifyServices = refactorNotifyServices;
                _fallbackOptions = fallbackOptions;

                this.HasOverloads = RenameUtilities.GetOverloadedSymbols(this.RenameSymbol).Any();

                this.TriggerSpan = GetReferenceEditSpan(new InlineRenameLocation(this.Document, info.TriggerToken.Span), info.TriggerText, cancellationToken);
            }

            /// <summary>
            /// Given a span of text, we need to return the subspan that is editable and
            /// contains the current replacementText.
            ///
            /// These cases are currently handled:
            ///     - Escaped identifiers                          [goo] => goo
            ///     - Type suffixes in VB                          goo$ => goo
            ///     - Qualified names from complexification        A.goo => goo
            ///     - Optional Attribute suffixes                  XAttribute => X
            ///         Careful here:                              XAttribute => XAttribute if renamesymbol is XAttributeAttribute
            ///     - Compiler-generated EventHandler suffix       XEventHandler => X
            ///     - Compiler-generated get_ and set_ prefixes    get_X => X
            /// </summary>
            public TextSpan GetReferenceEditSpan(InlineRenameLocation location, string triggerText, CancellationToken cancellationToken)
            {
                var searchName = this.RenameSymbol.Name;
                if (this.IsRenamingAttributePrefix)
                {
                    // We're only renaming the attribute prefix part.  We want to adjust the span of
                    // the reference we've found to only update the prefix portion.
                    searchName = _info.GetWithoutAttributeSuffix(this.RenameSymbol.Name);
                }

                var index = triggerText.LastIndexOf(searchName, StringComparison.Ordinal);
                if (index < 0)
                {
                    // Couldn't even find the search text at this reference location.  This might happen
                    // if the user used things like unicode escapes.  IN that case, we'll have to rename
                    // the entire identifier.
                    return location.TextSpan;
                }

                return new TextSpan(location.TextSpan.Start + index, searchName.Length);
            }

            public TextSpan? GetConflictEditSpan(InlineRenameLocation location, string triggerText, string replacementText, CancellationToken cancellationToken)
            {
                var position = triggerText.LastIndexOf(replacementText, StringComparison.Ordinal);

                if (this.IsRenamingAttributePrefix)
                {
                    // We're only renaming the attribute prefix part.  We want to adjust the span of
                    // the reference we've found to only update the prefix portion.
                    var index = triggerText.LastIndexOf(replacementText + AttributeSuffix, StringComparison.Ordinal);
                    position = index >= 0 ? index : position;
                }

                if (position < 0)
                {
                    return null;
                }

                return new TextSpan(location.TextSpan.Start + position, replacementText.Length);
            }

            public string DisplayName => RenameSymbol.Name;
            public string FullDisplayName => RenameSymbol.ToDisplayString();
            public Glyph Glyph => RenameSymbol.GetGlyph();

            public string GetFinalSymbolName(string replacementText)
                => _info.GetFinalSymbolName(replacementText);

            public async Task<IInlineRenameLocationSet> FindRenameLocationsAsync(SymbolRenameOptions options, CancellationToken cancellationToken)
            {
                var solution = this.Document.Project.Solution;
                var locations = await Renamer.FindRenameLocationsAsync(
                    solution, this.RenameSymbol, options, _fallbackOptions, cancellationToken).ConfigureAwait(false);

                return new InlineRenameLocationSet(this, locations);
            }

            public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable<DocumentId> changedDocumentIDs, string replacementText)
            {
                return _refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol,
                    this.GetFinalSymbolName(replacementText), throwOnFailure: false);
            }

            public bool TryOnAfterGlobalSymbolRenamed(Workspace workspace, IEnumerable<DocumentId> changedDocumentIDs, string replacementText)
            {
                return _refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocumentIDs, RenameSymbol,
                    this.GetFinalSymbolName(replacementText), throwOnFailure: false);
            }

            public InlineRenameFileRenameInfo GetFileRenameInfo()
            {
                if (RenameSymbol.Kind == SymbolKind.NamedType &&
                    this.Document.Project.Solution.CanApplyChange(ApplyChangesKind.ChangeDocumentInfo))
                {
                    if (RenameSymbol.Locations.Length > 1)
                    {
                        return InlineRenameFileRenameInfo.TypeWithMultipleLocations;
                    }

                    // Get the document that the symbol is defined in to compare
                    // the name with the symbol name. If they match allow
                    // rename file rename as part of the symbol rename
                    var symbolSourceDocument = this.Document.Project.Solution.GetDocument(RenameSymbol.Locations.Single().SourceTree);
                    if (symbolSourceDocument != null && WorkspacePathUtilities.TypeNameMatchesDocumentName(symbolSourceDocument, RenameSymbol.Name))
                    {
                        return InlineRenameFileRenameInfo.Allowed;
                    }

                    return InlineRenameFileRenameInfo.TypeDoesNotMatchFileName;
                }

                return InlineRenameFileRenameInfo.NotAllowed;
            }
        }
    }
}
