﻿// 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.Threading;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders
{
    internal abstract class AbstractSpecialTypePreselectingKeywordRecommender(
        SyntaxKind keywordKind,
        bool isValidInPreprocessorContext = false,
        bool shouldFormatOnCommit = false) : AbstractSyntacticSingleKeywordRecommender(keywordKind, isValidInPreprocessorContext, shouldFormatOnCommit)
    {
        protected abstract SpecialType SpecialType { get; }
        protected abstract bool IsValidContextWorker(int position, CSharpSyntaxContext context, CancellationToken cancellationToken);

        // When the keyword is the inferred type in this context, we should treat it like its corresponding type symbol
        // in terms of MatchPripority, so the selection can be determined by how well it matches the filter text instead,
        // e.g. selecting "string" over "String" when user typed "str".
        protected override int PreselectMatchPriority => SymbolMatchPriority.PreferType;

        protected override bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken)
            => context.InferredTypes.Any(static (t, self) => t.SpecialType == self.SpecialType, this);

        protected sealed override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken)
        {
            // Filter out all special-types from locations where we think we only want something task-like.
            if (context.IsTaskLikeTypeContext)
                return false;

            return IsValidContextWorker(position, context, cancellationToken) ||
                IsAfterRefOrReadonlyInTopLevelOrMemberDeclaration(context, position, cancellationToken);
        }

        private static bool IsAfterRefOrReadonlyInTopLevelOrMemberDeclaration(CSharpSyntaxContext context, int position, CancellationToken cancellationToken)
        {
            var syntaxTree = context.SyntaxTree;
            if (!syntaxTree.IsAfterKeyword(position, SyntaxKind.RefKeyword, cancellationToken) &&
                !syntaxTree.IsAfterKeyword(position, SyntaxKind.ReadOnlyKeyword, cancellationToken))
            {
                return false;
            }

            var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
            token = token.GetPreviousTokenIfTouchingWord(position);

            // if we have `readonly` move backwards to see if we have `ref readonly`.
            if (token.Kind() is SyntaxKind.ReadOnlyKeyword)
                token = syntaxTree.FindTokenOnLeftOfPosition(token.SpanStart, cancellationToken);

            // if we're not after `ref` or `ref readonly` then don't offer a type-keyword here.
            if (token.Kind() != SyntaxKind.RefKeyword)
                return false;

            // If we're inside a type, this is always to have a ref/readonly type name.
            var containingType = token.GetAncestor<TypeDeclarationSyntax>();
            if (containingType != null)
                return true;

            // If not in a type, but in a namespace, this is not ok to have a ref/readonly type name.
            var containingNamespace = token.GetAncestor<BaseNamespaceDeclarationSyntax>();
            if (containingNamespace != null)
                return false;

            // otherwise, we're at top level.  Can have a ref/readonly top-level local/function.
            return true;
        }
    }
}
