﻿// 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.

#nullable disable

using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp
{
    internal partial class Controller
    {
        CommandState IChainedCommandHandler<TypeCharCommandArgs>.GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextHandler)
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();

            // We just defer to the editor here.  We do not interfere with typing normal characters.
            return nextHandler();
        }

        void IChainedCommandHandler<TypeCharCommandArgs>.ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context)
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();

            var allProviders = GetProviders();
            if (allProviders == null)
            {
                nextHandler();
                return;
            }

            // Note: while we're doing this, we don't want to hear about buffer changes (since we
            // know they're going to happen).  So we disconnect and reconnect to the event
            // afterwards.  That way we can hear about changes to the buffer that don't happen
            // through us.
            this.TextView.TextBuffer.PostChanged -= OnTextViewBufferPostChanged;
            try
            {
                nextHandler();
            }
            finally
            {
                this.TextView.TextBuffer.PostChanged += OnTextViewBufferPostChanged;
            }

            // We only want to process typechar if it is a normal typechar and no one else is
            // involved.  i.e. if there was a typechar, but someone processed it and moved the caret
            // somewhere else then we don't want signature help.  Also, if a character was typed but
            // something intercepted and placed different text into the editor, then we don't want
            // to proceed. 
            //
            // Note: we do not want to pass along a text version here.  It is expected that multiple
            // version changes may happen when we call 'nextHandler' and we will still want to
            // proceed.  For example, if the user types "WriteL(", then that will involve two text
            // changes as completion commits that out to "WriteLine(".  But we still want to provide
            // sig help in this case.
            if (this.TextView.TypeCharWasHandledStrangely(this.SubjectBuffer, args.TypedChar))
            {
                // If we were computing anything, we stop.  We only want to process a typechar
                // if it was a normal character.
                DismissSessionIfActive();
                return;
            }

            // Separate the sig help providers into two buckets; one bucket for those that were triggered
            // by the typed character, and those that weren't.  To keep our queries to a minimum, we first
            // check with the textually triggered providers.  If none of those produced any sig help items
            // then we query the other providers to see if they can produce anything viable.  This takes
            // care of cases where the filtered set of providers didn't provide anything but one of the
            // other providers could still be valid, but doesn't explicitly treat the typed character as
            // a trigger character.
            var (textuallyTriggeredProviders, untriggeredProviders) = FilterProviders(allProviders, args.TypedChar);
            var triggerInfo = new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.TypeCharCommand, args.TypedChar);

            if (!IsSessionActive)
            {
                // No computation at all.  If this is not a trigger character, we just ignore it and
                // stay in this state.  Otherwise, if it's a trigger character, start up a new
                // computation and start computing the model in the background.
                if (textuallyTriggeredProviders.Any())
                {
                    // First create the session that represents that we now have a potential 
                    // signature help list. Then tell it to start computing.
                    StartSession(textuallyTriggeredProviders, triggerInfo);
                    return;
                }
                else
                {
                    // No need to do anything.  Just stay in the state where we have no session.
                    return;
                }
            }
            else
            {
                var computed = false;
                if (allProviders.Any(static (p, args) => p.IsRetriggerCharacter(args.TypedChar), args))
                {
                    // The user typed a character that might close the scope of the current model.
                    // In this case, we should requery all providers.
                    //
                    // e.g.     Math.Max(Math.Min(1,2)$$
                    sessionOpt.ComputeModel(allProviders, new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.RetriggerCommand, triggerInfo.TriggerCharacter));
                    computed = true;
                }

                if (textuallyTriggeredProviders.Any())
                {
                    // The character typed was something like "(".  It can both filter a list if
                    // it was in a string like: Goo(bar, "(
                    //
                    // Or it can trigger a new list. Ask the computation to compute again.
                    sessionOpt.ComputeModel(
                        textuallyTriggeredProviders.Concat(untriggeredProviders), triggerInfo);
                    computed = true;
                }

                if (!computed)
                {
                    // A character was typed and we haven't updated our model; do so now.
                    sessionOpt.ComputeModel(allProviders, new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.RetriggerCommand));
                }
            }
        }

        private (ImmutableArray<ISignatureHelpProvider> matched, ImmutableArray<ISignatureHelpProvider> unmatched) FilterProviders(
            ImmutableArray<ISignatureHelpProvider> providers, char ch)
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();

            using var matchedProvidersDisposer = ArrayBuilder<ISignatureHelpProvider>.GetInstance(out var matchedProviders);
            using var unmatchedProvidersDisposer = ArrayBuilder<ISignatureHelpProvider>.GetInstance(out var unmatchedProviders);
            foreach (var provider in providers)
            {
                if (provider.IsTriggerCharacter(ch))
                {
                    matchedProviders.Add(provider);
                }
                else
                {
                    unmatchedProviders.Add(provider);
                }
            }

            return (matchedProviders.ToImmutable(), unmatchedProviders.ToImmutable());
        }
    }
}
