﻿// 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.ErrorReporting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Completion.Providers
{
    internal abstract partial class AbstractPartialTypeCompletionProvider<TSyntaxContext> : LSPCompletionProvider
        where TSyntaxContext : SyntaxContext
    {
        protected AbstractPartialTypeCompletionProvider()
        {
        }

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

                var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                var node = GetPartialTypeSyntaxNode(tree, position, cancellationToken);

                if (node != null)
                {
                    var semanticModel = await document.ReuseExistingSpeculativeModelAsync(node, cancellationToken).ConfigureAwait(false);
                    if (semanticModel.GetDeclaredSymbol(node, cancellationToken) is INamedTypeSymbol declaredSymbol)
                    {
                        var syntaxContextService = document.GetRequiredLanguageService<ISyntaxContextService>();
                        var syntaxContext = (TSyntaxContext)syntaxContextService.CreateContext(document, semanticModel, position, cancellationToken);
                        var symbols = LookupCandidateSymbols(syntaxContext, declaredSymbol, cancellationToken);
                        var items = symbols?.Select(s => CreateCompletionItem(s, syntaxContext));

                        if (items != null)
                        {
                            completionContext.AddItems(items);
                        }
                    }
                }
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General))
            {
                // nop
            }
        }

        private CompletionItem CreateCompletionItem(INamedTypeSymbol symbol, TSyntaxContext context)
        {
            var (displayText, suffix, insertionText) = GetDisplayAndSuffixAndInsertionText(symbol, context);

            return SymbolCompletionItem.CreateWithSymbolId(
                displayText: displayText,
                displayTextSuffix: suffix,
                insertionText: insertionText,
                symbols: ImmutableArray.Create(symbol),
                contextPosition: context.Position,
                properties: GetProperties(symbol, context),
                rules: CompletionItemRules.Default);
        }

        protected abstract ImmutableDictionary<string, string> GetProperties(INamedTypeSymbol symbol, TSyntaxContext context);

        protected abstract SyntaxNode? GetPartialTypeSyntaxNode(SyntaxTree tree, int position, CancellationToken cancellationToken);

        protected abstract (string displayText, string suffix, string insertionText) GetDisplayAndSuffixAndInsertionText(INamedTypeSymbol symbol, TSyntaxContext context);

        protected virtual IEnumerable<INamedTypeSymbol>? LookupCandidateSymbols(TSyntaxContext context, INamedTypeSymbol declaredSymbol, CancellationToken cancellationToken)
        {
            if (declaredSymbol == null)
            {
                throw new ArgumentNullException(nameof(declaredSymbol));
            }

            var semanticModel = context.SemanticModel;

            if (declaredSymbol.ContainingSymbol is not INamespaceOrTypeSymbol containingSymbol)
            {
                return SpecializedCollections.EmptyEnumerable<INamedTypeSymbol>();
            }

            return semanticModel.LookupNamespacesAndTypes(context.Position, containingSymbol)
                                .OfType<INamedTypeSymbol>()
                                .Where(symbol => declaredSymbol.TypeKind == symbol.TypeKind &&
                                                 NotNewDeclaredMember(symbol, context) &&
                                                 InSameProject(symbol, semanticModel.Compilation));
        }

        private static bool InSameProject(INamedTypeSymbol symbol, Compilation compilation)
            => symbol.DeclaringSyntaxReferences.Any(static (r, compilation) => compilation.SyntaxTrees.Contains(r.SyntaxTree), compilation);

        private static bool NotNewDeclaredMember(INamedTypeSymbol symbol, TSyntaxContext context)
        {
            return symbol.DeclaringSyntaxReferences
                         .Select(reference => reference.GetSyntax())
                         .Any(node => !(node.SyntaxTree == context.SyntaxTree && node.Span.IntersectsWith(context.Position)));
        }

        internal override Task<CompletionDescription> GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
            => SymbolCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken);

        public override Task<TextChange?> GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken)
        {
            var insertionText = SymbolCompletionItem.GetInsertionText(selectedItem);
            return Task.FromResult<TextChange?>(new TextChange(selectedItem.Span, insertionText));
        }
    }
}
