﻿// 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.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp
{
    internal partial class Controller :
        AbstractController<Controller.Session, Model, ISignatureHelpPresenterSession, ISignatureHelpSession>,
        IChainedCommandHandler<TypeCharCommandArgs>,
        IChainedCommandHandler<InvokeSignatureHelpCommandArgs>
    {
        private readonly IAsyncCompletionBroker _completionBroker;

        private readonly IList<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> _allProviders;
        private ImmutableArray<ISignatureHelpProvider> _providers;
        private IContentType _lastSeenContentType;

        public string DisplayName => EditorFeaturesResources.Signature_Help;

        public Controller(
            IGlobalOptionService globalOptions,
            IThreadingContext threadingContext,
            ITextView textView,
            ITextBuffer subjectBuffer,
            IIntelliSensePresenter<ISignatureHelpPresenterSession, ISignatureHelpSession> presenter,
            IAsynchronousOperationListener asyncListener,
            IDocumentProvider documentProvider,
            IList<Lazy<ISignatureHelpProvider, OrderableLanguageMetadata>> allProviders,
            IAsyncCompletionBroker completionBroker)
            : base(globalOptions, threadingContext, textView, subjectBuffer, presenter, asyncListener, documentProvider, "SignatureHelp")
        {
            _completionBroker = completionBroker;
            _allProviders = allProviders;
        }

        // For testing purposes.
        internal Controller(
            IGlobalOptionService globalOptions,
            IThreadingContext threadingContext,
            ITextView textView,
            ITextBuffer subjectBuffer,
            IIntelliSensePresenter<ISignatureHelpPresenterSession, ISignatureHelpSession> presenter,
            IAsynchronousOperationListener asyncListener,
            IDocumentProvider documentProvider,
            IList<ISignatureHelpProvider> providers,
            IAsyncCompletionBroker completionBroker)
            : base(globalOptions, threadingContext, textView, subjectBuffer, presenter, asyncListener, documentProvider, "SignatureHelp")
        {
            _providers = providers.ToImmutableArray();
            _completionBroker = completionBroker;
        }

        public event EventHandler<ModelUpdatedEventsArgs> ModelUpdated;

        private SnapshotPoint GetCaretPointInViewBuffer()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            return this.TextView.Caret.Position.BufferPosition;
        }

        internal override void OnModelUpdated(Model modelOpt, bool updateController)
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();

            if (updateController)
            {
                if (modelOpt == null)
                {
                    this.StopModelComputation();
                }
                else
                {
                    var triggerSpan = modelOpt.GetCurrentSpanInView(this.TextView.TextSnapshot);

                    // We want the span to actually only go up to the caret.  So get the expected span
                    // and then update its end point accordingly.
                    var updatedSpan = new SnapshotSpan(triggerSpan.Snapshot, Span.FromBounds(
                        triggerSpan.Start,
                        Math.Max(Math.Min(triggerSpan.End, GetCaretPointInViewBuffer().Position), triggerSpan.Start)));

                    var trackingSpan = updatedSpan.CreateTrackingSpan(SpanTrackingMode.EdgeInclusive);

                    this.sessionOpt.PresenterSession.PresentItems(
                         trackingSpan, modelOpt.Items, modelOpt.SelectedItem, modelOpt.SelectedParameter);
                }
            }

            ModelUpdated?.Invoke(this, new ModelUpdatedEventsArgs(modelOpt));
        }

        private void StartSession(
            ImmutableArray<ISignatureHelpProvider> providers, SignatureHelpTriggerInfo triggerInfo)
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            VerifySessionIsInactive();

            this.sessionOpt = new Session(this, Presenter.CreateSession(TextView, SubjectBuffer, null));
            this.sessionOpt.ComputeModel(providers, triggerInfo);
        }

        private ImmutableArray<ISignatureHelpProvider> GetProviders()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();

            var snapshot = this.SubjectBuffer.CurrentSnapshot;
            var currentContentType = snapshot.ContentType;

            // if a file's content-type changes (e.g., File.cs is renamed to File.vb) after the list of providers has been populated, then we need to re-filter
            if (_providers == null || currentContentType != _lastSeenContentType)
            {
                var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
                if (document != null)
                {
                    _providers = document.Project.Solution.Services.SelectMatchingExtensionValues(
                        _allProviders, this.SubjectBuffer.ContentType).ToImmutableArray();
                    _lastSeenContentType = currentContentType;
                }
            }

            return _providers;
        }

        private void Retrigger()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            if (!IsSessionActive)
            {
                return;
            }

            if (!this.TextView.GetCaretPoint(this.SubjectBuffer).HasValue)
            {
                StopModelComputation();
                return;
            }

            sessionOpt.ComputeModel(GetProviders(), new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.RetriggerCommand));
        }
    }
}
