﻿// 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.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Recommendations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers
{
    /// <summary>
    /// Provides completion for uncommon unnamed symbols, like conversions, indexer and operators.  These completion 
    /// items will be brought up with <c>dot</c> like normal, but will end up inserting more than just a name into
    /// the editor.  For example, committing a conversion will insert the conversion prior to the expression being
    /// dotted off of.
    /// </summary>
    [ExportCompletionProvider(nameof(UnnamedSymbolCompletionProvider), LanguageNames.CSharp), Shared]
    [ExtensionOrder(After = nameof(SymbolCompletionProvider))]
    internal partial class UnnamedSymbolCompletionProvider : LSPCompletionProvider
    {
        /// <summary>
        /// CompletionItems for indexers/operators should be sorted below other suggestions like methods or properties
        /// of the type.  We accomplish this by placing a character known to be greater than all other normal identifier
        /// characters as the start of our item's name. This doesn't affect what we insert though as all derived
        /// providers have specialized logic for what they need to do.
        /// </summary> 
        private const string SortingPrefix = "\uFFFD";

        /// <summary>
        /// Used to store what sort of unnamed symbol a completion item represents.
        /// </summary>
        internal const string KindName = "Kind";
        internal const string IndexerKindName = "Indexer";
        internal const string OperatorKindName = "Operator";
        internal const string ConversionKindName = "Conversion";

        /// <summary>
        /// Used to store the doc comment for some operators/conversions.  This is because some of them will be
        /// synthesized, so there will be no symbol we can recover after the fact in <see cref="GetDescriptionAsync"/>.
        /// </summary>
        private const string DocumentationCommentXmlName = "DocumentationCommentXml";

        [ImportingConstructor]
        [System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public UnnamedSymbolCompletionProvider()
        {
        }

        internal override string Language => LanguageNames.CSharp;

        public override ImmutableHashSet<char> TriggerCharacters => ImmutableHashSet.Create('.');

        public override bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options)
            => text[insertedCharacterPosition] == '.';

        /// <summary>
        /// We keep operators sorted in a specific order.  We don't want to sort them alphabetically, but instead want
        /// to keep things like <c>==</c> and <c>!=</c> together.
        /// </summary>
        private static string SortText(int sortingGroupIndex, string sortTextSymbolPart)
            => $"{SortingPrefix}{sortingGroupIndex:000}_{sortTextSymbolPart}";

        /// <summary>
        /// Gets the dot-like token we're after, and also the start of the expression we'd want to place any text before.
        /// </summary>
        private static (SyntaxToken dotLikeToken, int expressionStart) GetDotAndExpressionStart(SyntaxNode root, int position, CancellationToken cancellationToken)
        {
            if (CompletionUtilities.GetDotTokenLeftOfPosition(root.SyntaxTree, position, cancellationToken) is not SyntaxToken dotToken)
                return default;

            // if we have `.Name`, we want to get the parent member-access of that to find the starting position.
            // Otherwise, if we have .. then we want the left side of that to find the starting position.
            var expression = dotToken.Kind() == SyntaxKind.DotToken
                ? dotToken.Parent as ExpressionSyntax
                : (dotToken.Parent as RangeExpressionSyntax)?.LeftOperand;

            if (expression == null)
                return default;

            // If we're after a ?. find the root of that conditional to find the start position of the expression.
            expression = expression.GetRootConditionalAccessExpression() ?? expression;
            return (dotToken, expression.SpanStart);
        }

        public override async Task ProvideCompletionsAsync(CompletionContext context)
        {
            var cancellationToken = context.CancellationToken;
            var document = context.Document;
            var position = context.Position;

            // Escape hatch feature flag to let us disable this feature remotely if we run into any issues with it, 
            if (context.CompletionOptions.UnnamedSymbolCompletionDisabled)
                return;

            var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var dotAndExprStart = GetDotAndExpressionStart(root, position, cancellationToken);
            if (dotAndExprStart == default)
                return;

            var recommender = document.GetRequiredLanguageService<IRecommendationService>();
            var syntaxContext = await context.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false);
            var semanticModel = syntaxContext.SemanticModel;

            var options = context.CompletionOptions.ToRecommendationServiceOptions();
            var recommendedSymbols = recommender.GetRecommendedSymbolsInContext(syntaxContext, options, cancellationToken);

            AddUnnamedSymbols(context, position, semanticModel, recommendedSymbols.UnnamedSymbols, cancellationToken);
        }

        private void AddUnnamedSymbols(
            CompletionContext context, int position, SemanticModel semanticModel, ImmutableArray<ISymbol> unnamedSymbols, CancellationToken cancellationToken)
        {
            // Add one 'this[]' entry for all the indexers this type may have.
            AddIndexers(context, unnamedSymbols.WhereAsArray(s => s.IsIndexer()));

            // Group all the related operators and add a single completion entry per group.
            var operatorGroups = unnamedSymbols.WhereAsArray(s => s.IsUserDefinedOperator()).GroupBy(op => op.Name);
            foreach (var opGroup in operatorGroups)
                AddOperatorGroup(context, opGroup.Key, opGroup);

            foreach (var symbol in unnamedSymbols)
            {
                cancellationToken.ThrowIfCancellationRequested();

                if (symbol.IsConversion())
                    AddConversion(context, semanticModel, position, (IMethodSymbol)symbol);
            }
        }

        public override Task<CompletionChange> GetChangeAsync(
            Document document,
            CompletionItem item,
            char? commitKey,
            CancellationToken cancellationToken)
        {
            var kind = item.Properties[KindName];
            return kind switch
            {
                IndexerKindName => GetIndexerChangeAsync(document, item, cancellationToken),
                OperatorKindName => GetOperatorChangeAsync(document, item, cancellationToken),
                ConversionKindName => GetConversionChangeAsync(document, item, cancellationToken),
                _ => throw ExceptionUtilities.UnexpectedValue(kind),
            };
        }

        internal override async Task<CompletionDescription?> GetDescriptionAsync(
            Document document,
            CompletionItem item,
            CompletionOptions options,
            SymbolDescriptionOptions displayOptions,
            CancellationToken cancellationToken)
        {
            var kind = item.Properties[KindName];
            return kind switch
            {
                IndexerKindName => await GetIndexerDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false),
                OperatorKindName => await GetOperatorDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false),
                ConversionKindName => await GetConversionDescriptionAsync(document, item, displayOptions, cancellationToken).ConfigureAwait(false),
                _ => throw ExceptionUtilities.UnexpectedValue(kind),
            };
        }

        private static Task<CompletionChange> ReplaceTextAfterOperatorAsync(Document document, CompletionItem item, string text, CancellationToken cancellationToken)
            => ReplaceTextAfterOperatorAsync(document, item, text, keepQuestion: false, positionOffset: 0, cancellationToken);

        private static async Task<CompletionChange> ReplaceTextAfterOperatorAsync(
            Document document,
            CompletionItem item,
            string text,
            bool keepQuestion,
            int positionOffset,
            CancellationToken cancellationToken)
        {
            var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var position = SymbolCompletionItem.GetContextPosition(item);

            var (dotToken, _) = GetDotAndExpressionStart(root, position, cancellationToken);
            var questionToken = dotToken.GetPreviousToken().Kind() == SyntaxKind.QuestionToken
                ? dotToken.GetPreviousToken()
                : (SyntaxToken?)null;

            var replacementStart = !keepQuestion && questionToken != null
                ? questionToken.Value.SpanStart
                : dotToken.SpanStart;
            var newPosition = replacementStart + text.Length + positionOffset;

            var tokenOnLeft = root.FindTokenOnLeftOfPosition(position, includeSkipped: true);
            return CompletionChange.Create(
                new TextChange(TextSpan.FromBounds(replacementStart, tokenOnLeft.Span.End), text),
                newPosition);
        }
    }
}
