﻿// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.EventHookup;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
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.EventHookup
{
    internal partial class EventHookupCommandHandler : IChainedCommandHandler<TabKeyCommandArgs>
    {
        public void ExecuteCommand(TabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            if (!_globalOptions.GetOption(EventHookupOptionsStorage.EventHookup))
            {
                nextHandler();
                return;
            }

            if (EventHookupSessionManager.CurrentSession == null)
            {
                nextHandler();
                return;
            }

            // Handling tab is currently uncancellable.
            HandleTabWorker(args.TextView, args.SubjectBuffer, nextHandler, CancellationToken.None);
        }

        public CommandState GetCommandState(TabKeyCommandArgs args, Func<CommandState> nextHandler)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            if (EventHookupSessionManager.CurrentSession != null)
            {
                return CommandState.Available;
            }
            else
            {
                return nextHandler();
            }
        }

        private void HandleTabWorker(ITextView textView, ITextBuffer subjectBuffer, Action nextHandler, CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnUIThread();

            // For test purposes only!
            if (EventHookupSessionManager.CurrentSession.TESTSessionHookupMutex != null)
            {
                try
                {
                    EventHookupSessionManager.CurrentSession.TESTSessionHookupMutex.ReleaseMutex();
                }
                catch (ApplicationException)
                {
                }
            }

            // Blocking wait (if necessary) to determine whether to consume the tab and
            // generate the event handler.
            EventHookupSessionManager.CurrentSession.GetEventNameTask.Wait(cancellationToken);

            string eventHandlerMethodName = null;
            if (EventHookupSessionManager.CurrentSession.GetEventNameTask.Status == TaskStatus.RanToCompletion)
            {
                eventHandlerMethodName = EventHookupSessionManager.CurrentSession.GetEventNameTask.WaitAndGetResult(cancellationToken);
            }

            if (eventHandlerMethodName == null ||
                EventHookupSessionManager.CurrentSession.TextView != textView)
            {
                nextHandler();
                EventHookupSessionManager.CancelAndDismissExistingSessions();
                return;
            }

            // This tab means we should generate the event handler method. Begin the code
            // generation process.
            GenerateAndAddEventHandler(textView, subjectBuffer, eventHandlerMethodName, nextHandler, cancellationToken);
        }

        private void GenerateAndAddEventHandler(ITextView textView, ITextBuffer subjectBuffer, string eventHandlerMethodName, Action nextHandler, CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnUIThread();

            using (Logger.LogBlock(FunctionId.EventHookup_Generate_Handler, cancellationToken))
            {
                EventHookupSessionManager.CancelAndDismissExistingSessions();

                var workspace = textView.TextSnapshot.TextBuffer.GetWorkspace();
                if (workspace == null)
                {
                    nextHandler();
                    EventHookupSessionManager.CancelAndDismissExistingSessions();
                    return;
                }

                var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges();
                Contract.ThrowIfNull(document, "Event Hookup could not find the document for the IBufferView.");

                var position = textView.GetCaretPoint(subjectBuffer).Value.Position;
                var solutionWithEventHandler = CreateSolutionWithEventHandler(
                    document,
                    eventHandlerMethodName,
                    position,
                    out var plusEqualTokenEndPosition,
                    _globalOptions,
                    cancellationToken);

                Contract.ThrowIfNull(solutionWithEventHandler, "Event Hookup could not create solution with event handler.");

                // The new solution is created, so start user observable changes

                Contract.ThrowIfFalse(workspace.TryApplyChanges(solutionWithEventHandler), "Event Hookup could not update the solution.");

                // The += token will not move during this process, so it is safe to use that
                // position as a location from which to find the identifier we're renaming.
                BeginInlineRename(textView, plusEqualTokenEndPosition, cancellationToken);
            }
        }

        private Solution CreateSolutionWithEventHandler(
            Document document,
            string eventHandlerMethodName,
            int position,
            out int plusEqualTokenEndPosition,
            IGlobalOptionService globalOptions,
            CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnUIThread();

            // Mark the += token with an annotation so we can find it after formatting
            var plusEqualsTokenAnnotation = new SyntaxAnnotation();

            var documentWithNameAndAnnotationsAdded = AddMethodNameAndAnnotationsToSolution(document, eventHandlerMethodName, position, plusEqualsTokenAnnotation, cancellationToken);
            var semanticDocument = SemanticDocument.CreateAsync(documentWithNameAndAnnotationsAdded, cancellationToken).WaitAndGetResult(cancellationToken);
            var options = (CSharpCodeGenerationOptions)document.GetCodeGenerationOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult(cancellationToken);
            var updatedRoot = AddGeneratedHandlerMethodToSolution(semanticDocument, options, eventHandlerMethodName, plusEqualsTokenAnnotation, cancellationToken);

            if (updatedRoot == null)
            {
                plusEqualTokenEndPosition = 0;
                return null;
            }

            var cleanupOptions = documentWithNameAndAnnotationsAdded.GetCodeCleanupOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult(cancellationToken);
            var simplifiedDocument = Simplifier.ReduceAsync(documentWithNameAndAnnotationsAdded.WithSyntaxRoot(updatedRoot), Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).WaitAndGetResult(cancellationToken);
            var formattedDocument = Formatter.FormatAsync(simplifiedDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).WaitAndGetResult(cancellationToken);

            var newRoot = formattedDocument.GetSyntaxRootSynchronously(cancellationToken);
            plusEqualTokenEndPosition = newRoot.GetAnnotatedNodesAndTokens(plusEqualsTokenAnnotation)
                                               .Single().Span.End;

            return document.Project.Solution.WithDocumentText(
                formattedDocument.Id, formattedDocument.GetTextSynchronously(cancellationToken));
        }

        private static Document AddMethodNameAndAnnotationsToSolution(
            Document document,
            string eventHandlerMethodName,
            int position,
            SyntaxAnnotation plusEqualsTokenAnnotation,
            CancellationToken cancellationToken)
        {
            // First find the event hookup to determine if we are in a static context.
            var root = document.GetSyntaxRootSynchronously(cancellationToken);
            var plusEqualsToken = root.FindTokenOnLeftOfPosition(position);
            var eventHookupExpression = plusEqualsToken.GetAncestor<AssignmentExpressionSyntax>();
            var typeDecl = eventHookupExpression.GetAncestor<TypeDeclarationSyntax>();

            var textToInsert = eventHandlerMethodName + ";";
            if (!eventHookupExpression.IsInStaticContext() && typeDecl is not null)
            {
                // This will be simplified later if it's not needed.
                textToInsert = "this." + textToInsert;
            }

            // Next, perform a textual insertion of the event handler method name.
            var textChange = new TextChange(new TextSpan(position, 0), textToInsert);
            var newText = document.GetTextSynchronously(cancellationToken).WithChanges(textChange);
            var documentWithNameAdded = document.WithText(newText);

            // Now find the event hookup again to add the appropriate annotations.
            root = documentWithNameAdded.GetSyntaxRootSynchronously(cancellationToken);
            plusEqualsToken = root.FindTokenOnLeftOfPosition(position);
            eventHookupExpression = plusEqualsToken.GetAncestor<AssignmentExpressionSyntax>();

            var updatedEventHookupExpression = eventHookupExpression
                .ReplaceToken(plusEqualsToken, plusEqualsToken.WithAdditionalAnnotations(plusEqualsTokenAnnotation))
                .WithRight(eventHookupExpression.Right.WithAdditionalAnnotations(Simplifier.Annotation))
                .WithAdditionalAnnotations(Formatter.Annotation);

            var rootWithUpdatedEventHookupExpression = root.ReplaceNode(eventHookupExpression, updatedEventHookupExpression);
            return documentWithNameAdded.WithSyntaxRoot(rootWithUpdatedEventHookupExpression);
        }

        private static SyntaxNode AddGeneratedHandlerMethodToSolution(
            SemanticDocument document,
            CSharpCodeGenerationOptions options,
            string eventHandlerMethodName,
            SyntaxAnnotation plusEqualsTokenAnnotation,
            CancellationToken cancellationToken)
        {
            var root = document.Root;
            var eventHookupExpression = root.GetAnnotatedNodesAndTokens(plusEqualsTokenAnnotation).Single().AsToken().GetAncestor<AssignmentExpressionSyntax>();

            var typeDecl = eventHookupExpression.GetAncestor<TypeDeclarationSyntax>();

            var generatedMethodSymbol = GetMethodSymbol(document, eventHandlerMethodName, eventHookupExpression, cancellationToken);

            if (generatedMethodSymbol == null)
            {
                return null;
            }

            var container = (SyntaxNode)typeDecl ?? eventHookupExpression.GetAncestor<CompilationUnitSyntax>();

            var codeGenerator = document.Document.GetRequiredLanguageService<ICodeGenerationService>();
            var codeGenOptions = codeGenerator.GetInfo(new CodeGenerationContext(afterThisLocation: eventHookupExpression.GetLocation()), options, root.SyntaxTree.Options);
            var newContainer = codeGenerator.AddMethod(container, generatedMethodSymbol, codeGenOptions, cancellationToken);

            return root.ReplaceNode(container, newContainer);
        }

        private static IMethodSymbol GetMethodSymbol(
            SemanticDocument semanticDocument,
            string eventHandlerMethodName,
            AssignmentExpressionSyntax eventHookupExpression,
            CancellationToken cancellationToken)
        {
            var semanticModel = semanticDocument.SemanticModel;
            var symbolInfo = semanticModel.GetSymbolInfo(eventHookupExpression.Left, cancellationToken);

            var symbol = symbolInfo.Symbol;
            if (symbol == null || symbol.Kind != SymbolKind.Event)
            {
                return null;
            }

            var typeInference = semanticDocument.Document.GetLanguageService<ITypeInferenceService>();
            var delegateType = typeInference.InferDelegateType(semanticModel, eventHookupExpression.Right, cancellationToken);
            if (delegateType == null || delegateType.DelegateInvokeMethod == null)
            {
                return null;
            }

            var syntaxFactory = semanticDocument.Document.GetLanguageService<SyntaxGenerator>();
            var delegateInvokeMethod = delegateType.DelegateInvokeMethod.RemoveInaccessibleAttributesAndAttributesOfTypes(semanticDocument.SemanticModel.Compilation.Assembly);

            return CodeGenerationSymbolFactory.CreateMethodSymbol(
                attributes: default,
                accessibility: Accessibility.Private,
                modifiers: new DeclarationModifiers(isStatic: eventHookupExpression.IsInStaticContext()),
                returnType: delegateInvokeMethod.ReturnType,
                refKind: delegateInvokeMethod.RefKind,
                explicitInterfaceImplementations: default,
                name: eventHandlerMethodName,
                typeParameters: default,
                parameters: delegateInvokeMethod.Parameters,
                statements: ImmutableArray.Create(
                    CodeGenerationHelpers.GenerateThrowStatement(syntaxFactory, semanticDocument, "System.NotImplementedException")));
        }

        private void BeginInlineRename(ITextView textView, int plusEqualTokenEndPosition, CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnUIThread();

            if (_inlineRenameService.ActiveSession == null)
            {
                var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges();
                if (document != null)
                {
                    // In the middle of a user action, cannot cancel.
                    var root = document.GetSyntaxRootSynchronously(cancellationToken);
                    var token = root.FindTokenOnRightOfPosition(plusEqualTokenEndPosition);
                    var editSpan = token.Span;
                    var memberAccessExpression = token.GetAncestor<MemberAccessExpressionSyntax>();
                    if (memberAccessExpression != null)
                    {
                        // the event hookup might look like `MyEvent += this.GeneratedHandlerName;`
                        editSpan = memberAccessExpression.Name.Span;
                    }

                    _inlineRenameService.StartInlineSession(document, editSpan, cancellationToken);
                    textView.SetSelection(editSpan.ToSnapshotSpan(textView.TextSnapshot));
                }
            }
        }
    }
}
