﻿// 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 Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense
{
    internal abstract class AbstractController<TSession, TModel, TPresenterSession, TEditorSession> : IController<TModel>
        where TSession : class, ISession<TModel>
        where TPresenterSession : IIntelliSensePresenterSession
    {
        protected readonly IThreadingContext ThreadingContext;
        protected readonly IGlobalOptionService GlobalOptions;
        protected readonly ITextView TextView;
        protected readonly ITextBuffer SubjectBuffer;
        protected readonly IIntelliSensePresenter<TPresenterSession, TEditorSession> Presenter;
        protected readonly IDocumentProvider DocumentProvider;

        private readonly IAsynchronousOperationListener _asyncListener;
        private readonly string _asyncOperationId;

        // Null when we absolutely know we don't have any sort of item computation going on. Non
        // null the moment we think we start computing state. Null again once we decide we can
        // stop.
        protected TSession sessionOpt;

        protected bool IsSessionActive => sessionOpt != null;

        protected AbstractController(
            IGlobalOptionService globalOptions,
            IThreadingContext threadingContext,
            ITextView textView,
            ITextBuffer subjectBuffer,
            IIntelliSensePresenter<TPresenterSession, TEditorSession> presenter,
            IAsynchronousOperationListener asyncListener,
            IDocumentProvider documentProvider,
            string asyncOperationId)
        {
            this.GlobalOptions = globalOptions;
            ThreadingContext = threadingContext;
            this.TextView = textView;
            this.SubjectBuffer = subjectBuffer;
            this.Presenter = presenter;
            _asyncListener = asyncListener;
            this.DocumentProvider = documentProvider;
            _asyncOperationId = asyncOperationId;

            this.TextView.Closed += OnTextViewClosed;

            // Caret position changed only fires if the caret is explicitly moved.  It doesn't fire
            // when the caret is moved because of text change events.
            this.TextView.Caret.PositionChanged += this.OnCaretPositionChanged;
            this.TextView.TextBuffer.PostChanged += this.OnTextViewBufferPostChanged;
        }

        internal abstract void OnModelUpdated(TModel result, bool updateController);
        internal abstract void OnTextViewBufferPostChanged(object sender, EventArgs e);
        internal abstract void OnCaretPositionChanged(object sender, EventArgs e);

        private void OnTextViewClosed(object sender, EventArgs e)
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            DismissSessionIfActive();

            this.TextView.Closed -= OnTextViewClosed;
            this.TextView.Caret.PositionChanged -= this.OnCaretPositionChanged;
            this.TextView.TextBuffer.PostChanged -= this.OnTextViewBufferPostChanged;
        }

        public TModel WaitForController()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            VerifySessionIsActive();
            return sessionOpt.WaitForController();
        }

        void IController<TModel>.OnModelUpdated(TModel result, bool updateController)
        {
            // This is only called from the model computation if it was not cancelled.  And if it was 
            // not cancelled then we must have a pointer to it (as well as the presenter session).
            this.ThreadingContext.ThrowIfNotOnUIThread();
            VerifySessionIsActive();

            this.OnModelUpdated(result, updateController);
        }

        IAsyncToken IController<TModel>.BeginAsyncOperation(string name, object tag, string filePath, int lineNumber)
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            VerifySessionIsActive();
            name = String.IsNullOrEmpty(name)
                ? _asyncOperationId
                : $"{_asyncOperationId} - {name}";
            return _asyncListener.BeginAsyncOperation(name, tag, filePath: filePath, lineNumber: lineNumber);
        }

        protected void VerifySessionIsActive()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            Contract.ThrowIfFalse(IsSessionActive);
        }

        protected void VerifySessionIsInactive()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            Contract.ThrowIfTrue(IsSessionActive);
        }

        protected void DismissSessionIfActive()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            if (IsSessionActive)
            {
                this.StopModelComputation();
            }
        }

        public void StopModelComputation()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();
            VerifySessionIsActive();

            // Make a local copy so that we won't do anything that causes us to recurse and try to
            // dismiss this again.
            var localSession = sessionOpt;
            sessionOpt = null;
            localSession.Stop();
        }

        public bool TryHandleEscapeKey()
        {
            this.ThreadingContext.ThrowIfNotOnUIThread();

            // Escape simply dismissed a session if it's up. Otherwise let the next thing in the
            // chain handle us.
            if (!IsSessionActive)
            {
                return false;
            }

            // If we haven't even computed a model yet, then also send this command to anyone
            // listening.  It's unlikely that the command was intended for us (as we wouldn't
            // have even shown ui yet.
            var handledCommand = sessionOpt.InitialUnfilteredModel != null;

            // In the presence of an escape, we always stop what we're doing.
            this.StopModelComputation();

            return handledCommand;
        }
    }
}
