// 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 System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp;

internal sealed partial class Controller
{
    internal sealed partial class Session
    {
        public void ComputeModel(
            ImmutableArray<ISignatureHelpProvider> providers,
            SignatureHelpTriggerInfo triggerInfo)
        {
            this.Computation.ThreadingContext.ThrowIfNotOnUIThread();

            var caretPosition = Controller.TextView.GetCaretPoint(Controller.SubjectBuffer).Value;
            var disconnectedBufferGraph = new DisconnectedBufferGraph(Controller.SubjectBuffer, Controller.TextView.TextBuffer);

            // If we've already computed a model, then just use that.  Otherwise, actually
            // compute a new model and send that along.
            Computation.ChainTaskAndNotifyControllerWhenFinished(
                (currentModel, cancellationToken) => ComputeModelInBackgroundAsync(
                    currentModel, providers, caretPosition, disconnectedBufferGraph, triggerInfo, cancellationToken));
        }

        private async Task<Model> ComputeModelInBackgroundAsync(
            Model currentModel,
            ImmutableArray<ISignatureHelpProvider> providers,
            SnapshotPoint caretPosition,
            DisconnectedBufferGraph disconnectedBufferGraph,
            SignatureHelpTriggerInfo triggerInfo,
            CancellationToken cancellationToken)
        {
            try
            {
                using (Logger.LogBlock(FunctionId.SignatureHelp_ModelComputation_ComputeModelInBackground, cancellationToken))
                {
                    this.Computation.ThreadingContext.ThrowIfNotOnBackgroundThread();
                    cancellationToken.ThrowIfCancellationRequested();

                    var document = Controller.DocumentProvider.GetDocument(caretPosition.Snapshot, cancellationToken);
                    if (document == null)
                    {
                        return currentModel;
                    }

                    // Let LSP handle signature help in the cloud scenario
                    if (Controller.SubjectBuffer.IsInLspEditorContext())
                    {
                        return null;
                    }

                    if (triggerInfo.TriggerReason == SignatureHelpTriggerReason.RetriggerCommand)
                    {
                        if (currentModel == null)
                        {
                            return null;
                        }

                        if (triggerInfo.TriggerCharacter.HasValue &&
                            !currentModel.Provider.RetriggerCharacters.Contains(triggerInfo.TriggerCharacter.Value))
                        {
                            return currentModel;
                        }
                    }

                    // first try to query the providers that can trigger on the specified character
                    var (provider, items) = await SignatureHelpService.GetSignatureHelpAsync(
                        providers,
                        document,
                        caretPosition,
                        triggerInfo,
                        cancellationToken).ConfigureAwait(false);

                    if (provider == null)
                    {
                        // No provider produced items. So we can't produce a model
                        return null;
                    }

                    if (currentModel != null &&
                        currentModel.Provider == provider &&
                        currentModel.GetCurrentSpanInSubjectBuffer(disconnectedBufferGraph.SubjectBufferSnapshot).Span.Start == items.ApplicableSpan.Start &&
                        currentModel.Items.IndexOf(currentModel.SelectedItem) == items.SelectedItemIndex &&
                        currentModel.SemanticParameterIndex == items.SemanticParameterIndex &&
                        currentModel.SyntacticArgumentCount == items.SyntacticArgumentCount &&
                        currentModel.ArgumentName == items.ArgumentName)
                    {
                        // The new model is the same as the current model.  Return the currentModel
                        // so we keep the active selection.
                        return currentModel;
                    }

                    var selectedItem = GetSelectedItem(currentModel, items, provider, out var userSelected);

                    var model = new Model(
                        disconnectedBufferGraph,
                        items.ApplicableSpan,
                        provider,
                        items.Items,
                        selectedItem,
                        items.SemanticParameterIndex,
                        items.SyntacticArgumentCount,
                        items.ArgumentName,
                        selectedParameter: 0,
                        userSelected);

                    var syntaxFactsService = document.GetLanguageService<ISyntaxFactsService>();
                    var isCaseSensitive = syntaxFactsService == null || syntaxFactsService.IsCaseSensitive;
                    var selection = DefaultSignatureHelpSelector.GetSelection(
                        model.Items,
                        model.SelectedItem,
                        model.UserSelected,
                        model.SemanticParameterIndex,
                        model.SyntacticArgumentCount,
                        model.ArgumentName,
                        isCaseSensitive);

                    return model.WithSelectedItem(selection.SelectedItem, selection.UserSelected)
                                .WithSelectedParameter(selection.SelectedParameter);
                }
            }
            catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
            {
                throw ExceptionUtilities.Unreachable();
            }
        }

        private static SignatureHelpItem GetSelectedItem(Model currentModel, SignatureHelpItems items, ISignatureHelpProvider provider, out bool userSelected)
        {
            // Try to find the most appropriate item in the list to select by default.

            // If it's the same provider as the previous model we have, and we had a user-selection,
            // then try to return the user-selection.
            if (currentModel != null && currentModel.Provider == provider && currentModel.UserSelected)
            {
                var userSelectedItem = items.Items.FirstOrDefault(i => DisplayPartsMatch(i, currentModel.SelectedItem));
                if (userSelectedItem != null)
                {
                    userSelected = true;
                    return userSelectedItem;
                }
            }

            userSelected = false;

            // If the provider specified a selected item, then pick that one.
            if (items.SelectedItemIndex.HasValue)
            {
                return items.Items[items.SelectedItemIndex.Value];
            }

            SignatureHelpItem lastSelectionOrDefault = null;
            if (currentModel != null && currentModel.Provider == provider)
            {
                // If the provider did not pick a default, and it's the same provider as the previous
                // model we have, then try to return the same item that we had before.
                lastSelectionOrDefault = items.Items.FirstOrDefault(i => DisplayPartsMatch(i, currentModel.SelectedItem));
            }

            // Otherwise, just pick the first item we have.
            lastSelectionOrDefault ??= items.Items.First();

            return lastSelectionOrDefault;
        }

        private static bool DisplayPartsMatch(SignatureHelpItem i1, SignatureHelpItem i2)
            => i1.GetAllParts().SequenceEqual(i2.GetAllParts(), CompareParts);

        private static bool CompareParts(TaggedText p1, TaggedText p2)
            => p1.ToString() == p2.ToString();
    }
}
