﻿// 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.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.TextManager.Interop;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.Snippets
{
    using Workspace = Microsoft.CodeAnalysis.Workspace;

    internal abstract class AbstractSnippetCommandHandler :
        ForegroundThreadAffinitizedObject,
        ICommandHandler<TabKeyCommandArgs>,
        ICommandHandler<BackTabKeyCommandArgs>,
        ICommandHandler<ReturnKeyCommandArgs>,
        ICommandHandler<EscapeKeyCommandArgs>,
        ICommandHandler<InsertSnippetCommandArgs>,
        IChainedCommandHandler<AutomaticLineEnderCommandArgs>
    {
        protected readonly SignatureHelpControllerProvider SignatureHelpControllerProvider;
        protected readonly IEditorCommandHandlerServiceFactory EditorCommandHandlerServiceFactory;
        protected readonly IVsEditorAdaptersFactoryService EditorAdaptersFactoryService;
        protected readonly SVsServiceProvider ServiceProvider;
        protected readonly IGlobalOptionService GlobalOptions;

        public string DisplayName => FeaturesResources.Snippets;

        public AbstractSnippetCommandHandler(
            IThreadingContext threadingContext,
            SignatureHelpControllerProvider signatureHelpControllerProvider,
            IEditorCommandHandlerServiceFactory editorCommandHandlerServiceFactory,
            IVsEditorAdaptersFactoryService editorAdaptersFactoryService,
            IGlobalOptionService globalOptions,
            SVsServiceProvider serviceProvider)
            : base(threadingContext)
        {
            SignatureHelpControllerProvider = signatureHelpControllerProvider;
            EditorCommandHandlerServiceFactory = editorCommandHandlerServiceFactory;
            EditorAdaptersFactoryService = editorAdaptersFactoryService;
            ServiceProvider = serviceProvider;
            GlobalOptions = globalOptions;
        }

        protected abstract AbstractSnippetExpansionClient GetSnippetExpansionClient(ITextView textView, ITextBuffer subjectBuffer);
        protected abstract bool IsSnippetExpansionContext(Document document, int startPosition, CancellationToken cancellationToken);
        protected abstract bool TryInvokeInsertionUI(ITextView textView, ITextBuffer subjectBuffer, bool surroundWith = false);

        protected virtual bool TryInvokeSnippetPickerOnQuestionMark(ITextView textView, ITextBuffer textBuffer)
            => false;

        public bool ExecuteCommand(TabKeyCommandArgs args, CommandExecutionContext context)
        {
            AssertIsForeground();
            if (!AreSnippetsEnabled(args))
            {
                return false;
            }

            if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
                snippetExpansionClient.TryHandleTab())
            {
                return true;
            }

            // Insert snippet/show picker only if we don't have a selection: the user probably wants to indent instead
            if (args.TextView.Selection.IsEmpty)
            {
                if (TryHandleTypedSnippet(args.TextView, args.SubjectBuffer, context.OperationContext.UserCancellationToken))
                {
                    return true;
                }

                if (TryInvokeSnippetPickerOnQuestionMark(args.TextView, args.SubjectBuffer))
                {
                    return true;
                }
            }

            return false;
        }

        public CommandState GetCommandState(TabKeyCommandArgs args)
        {
            AssertIsForeground();

            if (!AreSnippetsEnabled(args))
            {
                return CommandState.Unspecified;
            }

            if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
            {
                return CommandState.Unspecified;
            }

            return CommandState.Available;
        }

        public CommandState GetCommandState(AutomaticLineEnderCommandArgs args, Func<CommandState> nextCommandHandler)
        {
            return nextCommandHandler();
        }

        public void ExecuteCommand(AutomaticLineEnderCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
        {
            AssertIsForeground();
            if (AreSnippetsEnabled(args)
                && args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient)
                && snippetExpansionClient.IsFullMethodCallSnippet)
            {
                // Commit the snippet. Leave the caret in place, but clear the selection. Subsequent handlers in the
                // chain will handle the remaining Smart Break Line operations.
                snippetExpansionClient.CommitSnippet(leaveCaret: true);
                args.TextView.Selection.Clear();
            }

            nextCommandHandler();
        }

        public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext context)
        {
            AssertIsForeground();
            if (!AreSnippetsEnabled(args))
            {
                return false;
            }

            if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
                snippetExpansionClient.TryHandleReturn())
            {
                return true;
            }

            return false;
        }

        public CommandState GetCommandState(ReturnKeyCommandArgs args)
        {
            AssertIsForeground();

            if (!AreSnippetsEnabled(args))
            {
                return CommandState.Unspecified;
            }

            if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
            {
                return CommandState.Unspecified;
            }

            return CommandState.Available;
        }

        public bool ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext context)
        {
            AssertIsForeground();
            if (!AreSnippetsEnabled(args))
            {
                return false;
            }

            if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
                snippetExpansionClient.TryHandleEscape())
            {
                return true;
            }

            return false;
        }

        public CommandState GetCommandState(EscapeKeyCommandArgs args)
        {
            AssertIsForeground();

            if (!AreSnippetsEnabled(args))
            {
                return CommandState.Unspecified;
            }

            if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
            {
                return CommandState.Unspecified;
            }

            return CommandState.Available;
        }

        public bool ExecuteCommand(BackTabKeyCommandArgs args, CommandExecutionContext context)
        {
            AssertIsForeground();
            if (!AreSnippetsEnabled(args))
            {
                return false;
            }

            if (args.TextView.Properties.TryGetProperty(typeof(AbstractSnippetExpansionClient), out AbstractSnippetExpansionClient snippetExpansionClient) &&
                snippetExpansionClient.TryHandleBackTab())
            {
                return true;
            }

            return false;
        }

        public CommandState GetCommandState(BackTabKeyCommandArgs args)
        {
            AssertIsForeground();

            if (!AreSnippetsEnabled(args))
            {
                return CommandState.Unspecified;
            }

            if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out _))
            {
                return CommandState.Unspecified;
            }

            return CommandState.Available;
        }

        public bool ExecuteCommand(InsertSnippetCommandArgs args, CommandExecutionContext context)
        {
            AssertIsForeground();

            if (!AreSnippetsEnabled(args))
            {
                return false;
            }

            return TryInvokeInsertionUI(args.TextView, args.SubjectBuffer);
        }

        public CommandState GetCommandState(InsertSnippetCommandArgs args)
        {
            AssertIsForeground();

            if (!AreSnippetsEnabled(args))
            {
                return CommandState.Unspecified;
            }

            if (!Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out var workspace))
            {
                return CommandState.Unspecified;
            }

            if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument))
            {
                return CommandState.Unspecified;
            }

            return CommandState.Available;
        }

        protected bool TryHandleTypedSnippet(ITextView textView, ITextBuffer subjectBuffer, CancellationToken cancellationToken)
        {
            AssertIsForeground();

            var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document == null)
            {
                return false;
            }

            var currentText = subjectBuffer.AsTextContainer().CurrentText;
            var syntaxFactsService = document.GetLanguageService<ISyntaxFactsService>();

            var endPositionInSubjectBuffer = textView.GetCaretPoint(subjectBuffer);
            if (endPositionInSubjectBuffer == null)
            {
                return false;
            }

            var endPosition = endPositionInSubjectBuffer.Value.Position;
            var startPosition = endPosition;

            // Find the snippet shortcut
            while (startPosition > 0)
            {
                var c = currentText[startPosition - 1];
                if (!syntaxFactsService.IsIdentifierPartCharacter(c) && c != '#' && c != '~')
                {
                    break;
                }

                startPosition--;
            }

            if (startPosition == endPosition)
            {
                return false;
            }

            if (!IsSnippetExpansionContext(document, startPosition, cancellationToken))
            {
                return false;
            }

            return GetSnippetExpansionClient(textView, subjectBuffer).TryInsertExpansion(startPosition, endPosition, cancellationToken);
        }

        protected bool TryGetExpansionManager(out IVsExpansionManager expansionManager)
        {
            var textManager = (IVsTextManager2)ServiceProvider.GetService(typeof(SVsTextManager));
            if (textManager == null)
            {
                expansionManager = null;
                return false;
            }

            textManager.GetExpansionManager(out expansionManager);
            return expansionManager != null;
        }

        protected bool AreSnippetsEnabled(EditorCommandArgs args)
        {
            // Don't execute in cloud environment, should be handled by LSP
            if (args.SubjectBuffer.IsInLspEditorContext())
            {
                return false;
            }

            return GlobalOptions.GetOption(InternalFeatureOnOffOptions.Snippets) &&
                // TODO (https://github.com/dotnet/roslyn/issues/5107): enable in interactive
                !(Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out var workspace) && workspace.Kind == WorkspaceKind.Interactive);
        }
    }
}
