﻿// 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.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.DebuggerIntelliSense
{
    internal class DebuggerIntelliSenseFilter : AbstractVsTextViewFilter, IDisposable, IFeatureController
    {
        private readonly IFeatureServiceFactory _featureServiceFactory;
        private AbstractDebuggerIntelliSenseContext _context;
        private IOleCommandTarget _originalNextCommandFilter;
        private IFeatureDisableToken _completionDisabledToken;

        public DebuggerIntelliSenseFilter(
            IWpfTextView wpfTextView,
            IComponentModel componentModel,
            IFeatureServiceFactory featureServiceFactory)
            : base(wpfTextView, componentModel)
        {
            _featureServiceFactory = featureServiceFactory;
        }

        internal void EnableCompletion()
        {
            if (_completionDisabledToken == null)
            {
                return;
            }

            _completionDisabledToken.Dispose();
            _completionDisabledToken = null;
        }

        internal void DisableCompletion()
        {
            var featureService = _featureServiceFactory.GetOrCreate(WpfTextView);
            _completionDisabledToken ??= featureService.Disable(PredefinedEditorFeatureNames.Completion, this);
        }

        internal void SetNextFilter(IOleCommandTarget nextFilter)
        {
            _originalNextCommandFilter = nextFilter;
            SetNextFilterWorker();
        }

        private void SetNextFilterWorker()
        {
            // We have a new _originalNextCommandFilter or new _context, reset NextCommandTarget chain based on their values.
            // The chain is formed like this: 
            //     IVsCommandHandlerServiceAdapter (our command handlers migrated to the modern editor commanding)
            //         -> original next command filter
            var nextCommandFilter = _originalNextCommandFilter;
            // The next filter is set in response to debugger calling IVsImmediateStatementCompletion2.InstallStatementCompletion(),
            // followed IVsImmediateStatementCompletion2.SetCompletionContext() that sets the context.
            // Check context in case debugger hasn't called IVsImmediateStatementCompletion2.SetCompletionContext() yet - before that
            // we cannot set up command handling on correct view and buffer.
            if (_context != null)
            {
                // Chain in editor command handler service. It will execute all our command handlers migrated to the modern editor commanding
                // on the same text view and buffer as this.CurrentHandlers.
                var vsCommandHandlerServiceAdapterFactory = ComponentModel.GetService<IVsCommandHandlerServiceAdapterFactory>();
                var vsCommandHandlerServiceAdapter = vsCommandHandlerServiceAdapterFactory.Create(ConvertTextView(),
                    GetSubjectBufferContainingCaret(), // our override doesn't actually check the caret and always returns _context.Buffer
                    nextCommandFilter);
                nextCommandFilter = vsCommandHandlerServiceAdapter;
            }

            this.NextCommandTarget = nextCommandFilter;
        }

        // If they haven't given us a context, or we aren't enabled, we should pass along to the next thing in the chain,
        // instead of trying to have our command handlers to work.
        public override int Exec(ref Guid pguidCmdGroup, uint commandId, uint executeInformation, IntPtr pvaIn, IntPtr pvaOut)
        {
            if (_context == null)
            {
                return NextCommandTarget.Exec(pguidCmdGroup, commandId, executeInformation, pvaIn, pvaOut);
            }

            // NOTE: at this point base.Exec will still call NextCommandTarget.Exec like above, but we
            // are skipping some special handling in a few cases, and also enabling GC Low Latency mode.
            return base.Exec(ref pguidCmdGroup, commandId, executeInformation, pvaIn, pvaOut);
        }

        // Our immediate window filter has to override this behavior in the default
        // AbstractOLECommandTarget because they'll send us SCROLLUP when the key typed was CANCEL
        // (see below)
        protected override int ExecuteVisualStudio2000(ref Guid pguidCmdGroup, uint commandId, uint executeInformation, IntPtr pvaIn, IntPtr pvaOut)
        {
            // We have to ask the buffer to make itself writable, if it isn't already
            _context.DebuggerTextLines.GetStateFlags(out var bufferFlags);
            _context.DebuggerTextLines.SetStateFlags((uint)((BUFFERSTATEFLAGS)bufferFlags & ~BUFFERSTATEFLAGS.BSF_USER_READONLY));

            // If the caret is outside our projection, defer to the next command target.
            var caretPosition = _context.DebuggerTextView.GetCaretPoint(_context.Buffer);
            if (caretPosition == null)
            {
                return NextCommandTarget.Exec(ref pguidCmdGroup, commandId, executeInformation, pvaIn, pvaOut);
            }

            int result;
            switch ((VSConstants.VSStd2KCmdID)commandId)
            {
                // If we see a RETURN, and we're in the immediate window, we'll want to rebuild
                // spans after all the other command handlers have run.
                case VSConstants.VSStd2KCmdID.RETURN:
                    result = NextCommandTarget.Exec(ref pguidCmdGroup, commandId, executeInformation, pvaIn, pvaOut);
                    _context.RebuildSpans();
                    break;

                // After handling typechar of '?', start completion.
                case VSConstants.VSStd2KCmdID.TYPECHAR:
                    result = NextCommandTarget.Exec(ref pguidCmdGroup, commandId, executeInformation, pvaIn, pvaOut);

                    if ((char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn) == '?')
                    {
                        if (_context.CompletionStartsOnQuestionMark)
                        {
                            // The subject buffer passed in through the command
                            // target isn't the one we want, because we've
                            // definitely remapped buffers. Ask our context for
                            // the real subject buffer.
                            NextCommandTarget.Exec(VSConstants.VSStd2K, (uint)VSConstants.VSStd2KCmdID.SHOWMEMBERLIST,
                                executeInformation, pvaIn, pvaOut);
                        }
                    }

                    break;

                default:
                    return base.ExecuteVisualStudio2000(ref pguidCmdGroup, commandId, executeInformation, pvaIn, pvaOut);
            }

            _context.DebuggerTextLines.SetStateFlags(bufferFlags);

            return result;
        }

        protected override ITextBuffer GetSubjectBufferContainingCaret()
        {
            Contract.ThrowIfNull(_context);
            return _context.Buffer;
        }

        protected override ITextView ConvertTextView()
            => _context.DebuggerTextView;

        internal void SetContext(AbstractDebuggerIntelliSenseContext context)
        {
            // If there was an old context, it must be cleaned before calling SetContext.
            Debug.Assert(_context == null);
            _context = context;
            this.SetNextFilterWorker();
        }

        internal void RemoveContext()
        {
            if (_context != null)
            {
                _context.Dispose();
                _context = null;
            }
        }

        internal void SetContentType(bool install)
            => _context?.SetContentType(install);

        public void Dispose()
        {
            _completionDisabledToken?.Dispose();

            RemoveContext();
        }
    }
}
