﻿// 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.ComponentModel.Composition;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.InteractiveWindow;
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.Text.Editor.OptionsExtensionMethods;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;

namespace Microsoft.CodeAnalysis.Interactive
{
    // This command handler must be invoked after the handlers specified in `Order` attribute
    // (those handlers also implement `ICommandHandler<PasteCommandArgs>`),
    // because it will intercept the paste command and skip the rest of handlers in chain.  
    [Export(typeof(ICommandHandler))]
    [ContentType(ContentTypeNames.RoslynContentType)]
    [Name(PredefinedCommandHandlerNames.InteractivePaste)]
    [Order(After = PredefinedCommandHandlerNames.Rename)]
    [Order(After = PredefinedCommandHandlerNames.FormatDocument)]
    [Order(After = PredefinedCommandHandlerNames.Commit)]
    [Order(After = PredefinedCompletionNames.CompletionCommandHandler)]
    internal sealed class InteractivePasteCommandHandler : ICommandHandler<PasteCommandArgs>
    {
        // The following two field definitions have to stay in sync with VS editor implementation

        /// <summary>
        /// A data format used to tag the contents of the clipboard so that it's clear
        /// the data has been put in the clipboard by our editor
        /// </summary>
        internal const string ClipboardLineBasedCutCopyTag = "VisualStudioEditorOperationsLineCutCopyClipboardTag";

        /// <summary>
        /// A data format used to tag the contents of the clipboard as a box selection.
        /// This is the same string that was used in VS9 and previous versions.
        /// </summary>
        internal const string BoxSelectionCutCopyTag = "MSDEVColumnSelect";

        private readonly IEditorOperationsFactoryService _editorOperationsFactoryService;
        private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry;

        // This is for unit test purpose only, do not explicitly set this field otherwise.
        internal IRoslynClipboard RoslynClipboard;

        public string DisplayName => EditorFeaturesResources.Paste_in_Interactive;

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public InteractivePasteCommandHandler(IEditorOperationsFactoryService editorOperationsFactoryService, ITextUndoHistoryRegistry textUndoHistoryRegistry)
        {
            _editorOperationsFactoryService = editorOperationsFactoryService;
            _textUndoHistoryRegistry = textUndoHistoryRegistry;
            RoslynClipboard = new SystemClipboardWrapper();
        }

        public bool ExecuteCommand(PasteCommandArgs args, CommandExecutionContext context)
        {
            // InteractiveWindow handles pasting by itself, which including checks for buffer types, etc.
            if (!args.TextView.TextBuffer.ContentType.IsOfType(PredefinedInteractiveContentTypes.InteractiveContentTypeName) &&
                RoslynClipboard.ContainsData(InteractiveClipboardFormat.Tag))
            {
                PasteInteractiveFormat(args.TextView);
                return true;
            }
            else
            {
                return false;
            }
        }

        public CommandState GetCommandState(PasteCommandArgs args)
            => CommandState.Unspecified;

        [MethodImpl(MethodImplOptions.NoInlining)]  // Avoid loading InteractiveWindow unless necessary
        private void PasteInteractiveFormat(ITextView textView)
        {
            var editorOperations = _editorOperationsFactoryService.GetEditorOperations(textView);

            var data = RoslynClipboard.GetDataObject();
            Debug.Assert(data != null);

            var dataHasLineCutCopyTag = data.GetDataPresent(ClipboardLineBasedCutCopyTag);
            var dataHasBoxCutCopyTag = data.GetDataPresent(BoxSelectionCutCopyTag);
            Debug.Assert(!(dataHasLineCutCopyTag && dataHasBoxCutCopyTag));

            string text;
            try
            {
                text = InteractiveClipboardFormat.Deserialize(RoslynClipboard.GetData(InteractiveClipboardFormat.Tag));
            }
            catch (InvalidDataException)
            {
                text = "<bad clipboard data>";
            }

            using var transaction = _textUndoHistoryRegistry.GetHistory(textView.TextBuffer).CreateTransaction(EditorFeaturesResources.Paste);
            editorOperations.AddBeforeTextBufferChangePrimitive();
            if (dataHasLineCutCopyTag && textView.Selection.IsEmpty)
            {
                editorOperations.MoveToStartOfLine(extendSelection: false);
                editorOperations.InsertText(text);
            }
            else if (dataHasBoxCutCopyTag)
            {
                // If the caret is on a blank line, treat this like a normal stream insertion
                if (textView.Selection.IsEmpty && !HasNonWhiteSpaceCharacter(textView.Caret.Position.BufferPosition.GetContainingLine()))
                {
                    // trim the last newline before paste
                    var trimmed = text.Remove(text.LastIndexOf(textView.Options.GetNewLineCharacter()));
                    editorOperations.InsertText(trimmed);
                }
                else
                {
                    editorOperations.InsertTextAsBox(text, out _, out _);
                }
            }
            else
            {
                editorOperations.InsertText(text);
            }

            editorOperations.AddAfterTextBufferChangePrimitive();
            transaction.Complete();
        }

        private static bool HasNonWhiteSpaceCharacter(ITextSnapshotLine line)
        {
            var snapshot = line.Snapshot;
            var start = line.Start.Position;
            var count = line.Length;
            for (var i = 0; i < count; i++)
            {
                if (!char.IsWhiteSpace(snapshot[start + i]))
                {
                    return true;
                }
            }

            return false;
        }

        // The mock clipboard used in tests will implement this interface 
        internal interface IRoslynClipboard
        {
            bool ContainsData(string format);
            object GetData(string format);
            IDataObject GetDataObject();
        }

        // In product code, we use this simple wrapper around system clipboard.
        // Maybe at some point we can elevate this class and interface so they could be shared among Roslyn code base.
        private class SystemClipboardWrapper : IRoslynClipboard
        {
            public bool ContainsData(string format)
                => Clipboard.ContainsData(format);

            public object GetData(string format)
                => Clipboard.GetData(format);

            public IDataObject GetDataObject()
                => Clipboard.GetDataObject();
        }
    }
}
