﻿// 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.

using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.StringCopyPaste;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste
{
    internal partial class StringCopyPasteCommandHandler :
        IChainedCommandHandler<CutCommandArgs>,
        IChainedCommandHandler<CopyCommandArgs>
    {
        public const string KeyAndVersion = nameof(StringCopyPasteCommandHandler) + "V1";

        public CommandState GetCommandState(CutCommandArgs args, Func<CommandState> nextCommandHandler)
            => nextCommandHandler();

        public CommandState GetCommandState(CopyCommandArgs args, Func<CommandState> nextCommandHandler)
            => nextCommandHandler();

        public void ExecuteCommand(CutCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
            => ExecuteCutOrCopyCommand(args.TextView, args.SubjectBuffer, nextCommandHandler, executionContext);

        public void ExecuteCommand(CopyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
            => ExecuteCutOrCopyCommand(args.TextView, args.SubjectBuffer, nextCommandHandler, executionContext);

        private void ExecuteCutOrCopyCommand(ITextView textView, ITextBuffer subjectBuffer, Action nextCommandHandler, CommandExecutionContext executionContext)
        {
            Contract.ThrowIfFalse(_threadingContext.HasMainThread);
            var (dataToStore, copyPasteService) = CaptureCutCopyInformation(textView, subjectBuffer, executionContext.OperationContext.UserCancellationToken);

            // Ensure that the copy always goes through all other handlers.
            nextCommandHandler();

            // Always try to store our data to the clipboard (if we have access to the clipboard service).  Even if we
            // didn't capture any useful data, we want to store that to blow away any prior stored data we have.
            copyPasteService?.TrySetClipboardData(KeyAndVersion, dataToStore ?? "");
        }

        private static (string? dataToStore, IStringCopyPasteService service) CaptureCutCopyInformation(
            ITextView textView, ITextBuffer subjectBuffer, CancellationToken cancellationToken)
        {
            var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document == null)
                return default;

            var copyPasteService = document.Project.Solution.Services.GetService<IStringCopyPasteService>();
            if (copyPasteService == null)
                return default;

            var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken);

            var spans = textView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer);

            // We only support smart copy/paste when a single selection is copied (and a single selection is pasted
            // over).  This vastly simplifies the logic we need, and it means we don't have to try to reimplement the 
            // editor logic for what it means when you are copying X selections and pasting over Y selections.
            if (spans.Count != 1)
                return default;

            var span = spans[0];
            var snapshot = span.Snapshot;
            if (span.IsEmpty)
            {
                // cut/copy on an empty span means "cut/copy the entire line".
                var line = snapshot.GetLineFromPosition(span.Start);
                span = line.ExtentIncludingLineBreak;
            }

            var stringExpression = TryGetCompatibleContainingStringExpression(
                parsedDocument, new NormalizedSnapshotSpanCollection(span));
            if (stringExpression is null)
                return default;

            var virtualCharService = document.GetRequiredLanguageService<IVirtualCharLanguageService>();
            var stringData = StringCopyPasteData.TryCreate(virtualCharService, stringExpression, span.Span.ToTextSpan());

            return (stringData?.ToJson(), copyPasteService);
        }
    }
}
