﻿// 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.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Undo;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.VisualStudio.LanguageServices.Implementation.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;
using VsTextSpan = Microsoft.VisualStudio.TextManager.Interop.TextSpan;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.Venus
{
    internal static class ContainedLanguageCodeSupport
    {
        public static bool IsValidId(Document document, string identifier)
        {
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
            return syntaxFacts.IsValidIdentifier(identifier);
        }

        public static bool TryGetBaseClassName(Document document, string className, CancellationToken cancellationToken, out string baseClassName)
        {
            baseClassName = null;
            var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);
            if (type == null || type.BaseType == null)
            {
                return false;
            }

            baseClassName = type.BaseType.ToDisplayString();
            return true;
        }

        public static string CreateUniqueEventName(
            Document document, IGlobalOptionService globalOptions, string className, string objectName, string nameOfEvent, CancellationToken cancellationToken)
        {
            var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);
            var name = objectName + "_" + nameOfEvent;

            var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);

            var tree = document.GetSyntaxTreeSynchronously(cancellationToken);
            var typeNode = type.DeclaringSyntaxReferences.Where(r => r.SyntaxTree == tree).Select(r => r.GetSyntax(cancellationToken)).First();
            var codeModel = document.GetRequiredLanguageService<ICodeModelNavigationPointService>();
            var options = document.GetLineFormattingOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);
            var point = codeModel.GetStartPoint(typeNode, options, EnvDTE.vsCMPart.vsCMPartBody);
            var reservedNames = semanticModel.LookupSymbols(point.Value.Position, type).Select(m => m.Name);

            return NameGenerator.EnsureUniqueness(name, reservedNames, document.GetLanguageService<ISyntaxFactsService>().IsCaseSensitive);
        }

        /// <summary>
        /// Determine what methods of <paramref name=" className"/> could possibly be used as event
        /// handlers.
        /// </summary>
        /// <param name="document">The document containing <paramref name="className"/>.</param>
        /// <param name="className">The name of the type whose methods should be considered.</param>
        /// <param name="objectTypeName">The fully qualified name of the type containing a member
        /// that is an event. (E.g. "System.Web.Forms.Button")</param>
        /// <param name="nameOfEvent">The name of the member in <paramref name="objectTypeName"/>
        /// that is the event (E.g. "Clicked")</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>The display name of the method, and a unique to for the method.</returns>
        public static IEnumerable<Tuple<string, string>> GetCompatibleEventHandlers(
            Document document, string className, string objectTypeName, string nameOfEvent, CancellationToken cancellationToken)
        {
            var compilation = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
            var type = compilation.GetTypeByMetadataName(className);
            if (type == null)
            {
                throw new InvalidOperationException();
            }

            var eventMember = GetEventSymbol(document, objectTypeName, nameOfEvent, type, cancellationToken);
            if (eventMember == null)
            {
                throw new InvalidOperationException();
            }

            var eventType = ((IEventSymbol)eventMember).Type;
            if (eventType.Kind != SymbolKind.NamedType)
            {
                throw new InvalidOperationException(ServicesVSResources.Event_type_is_invalid);
            }

            var methods = type.GetMembers().OfType<IMethodSymbol>().Where(m => m.CompatibleSignatureToDelegate((INamedTypeSymbol)eventType));
            return methods.Select(m => Tuple.Create(m.Name, ConstructMemberId(m)));
        }

        public static string GetEventHandlerMemberId(Document document, string className, string objectTypeName, string nameOfEvent, string eventHandlerName, CancellationToken cancellationToken)
        {
            var nameAndId = GetCompatibleEventHandlers(document, className, objectTypeName, nameOfEvent, cancellationToken).SingleOrDefault(pair => pair.Item1 == eventHandlerName);
            return nameAndId?.Item2;
        }

        /// <summary>
        /// Ensure that an event handler exists for a given event.
        /// </summary>
        /// <param name="thisDocument">The document corresponding to this operation.</param>
        /// <param name="targetDocument">The document to generate the event handler in if it doesn't
        /// exist.</param>
        /// <param name="className">The name of the type to generate the event handler in.</param>
        /// <param name="objectName">The name of the event member (if <paramref
        /// name="useHandlesClause"/> is true)</param>
        /// <param name="objectTypeName">The name of the type containing the event.</param>
        /// <param name="nameOfEvent">The name of the event member in <paramref
        /// name="objectTypeName"/></param>
        /// <param name="eventHandlerName">The name of the method to be hooked up to the
        /// event.</param>
        /// <param name="itemidInsertionPoint">The VS itemid of the file to generate the event
        /// handler in.</param>
        /// <param name="useHandlesClause">If true, a vb "Handles" clause will be generated for the
        /// handler.</param>
        /// <param name="additionalFormattingRule">An additional formatting rule that can be used to
        /// format the newly inserted method</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>Either the unique id of the method if it already exists, or the unique id of
        /// the to be generated method, the text of the to be generated method, and the position in
        /// <paramref name="itemidInsertionPoint"/> where the text should be inserted.</returns>
#pragma warning disable IDE0060 // Remove unused parameter - API used by partner (Venus), avoid build break.
        public static Tuple<string, string, VsTextSpan> EnsureEventHandler(
            Document thisDocument,
            Document targetDocument,
            string className,
            string objectName,
            string objectTypeName,
            string nameOfEvent,
            string eventHandlerName,
            uint itemidInsertionPoint,
            bool useHandlesClause,
            AbstractFormattingRule additionalFormattingRule,
            IGlobalOptionService globalOptions,
            CancellationToken cancellationToken)
#pragma warning restore IDE0060 // Remove unused parameter
        {
            var thisCompilation = thisDocument.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
            var type = thisCompilation.GetTypeByMetadataName(className);

            var existingEventHandlers = GetCompatibleEventHandlers(targetDocument, className, objectTypeName, nameOfEvent, cancellationToken);
            var existingHandler = existingEventHandlers.SingleOrDefault(e => e.Item1 == eventHandlerName);
            if (existingHandler != null)
            {
                return Tuple.Create(existingHandler.Item2, (string)null, default(VsTextSpan));
            }

            // Okay, it doesn't exist yet.  Let's create it.
            var codeGenerationService = targetDocument.GetLanguageService<ICodeGenerationService>();
            var syntaxFactory = targetDocument.GetLanguageService<SyntaxGenerator>();
            var eventMember = GetEventSymbol(thisDocument, objectTypeName, nameOfEvent, type, cancellationToken);
            if (eventMember == null)
            {
                throw new InvalidOperationException();
            }

            var eventType = ((IEventSymbol)eventMember).Type;
            if (eventType.Kind != SymbolKind.NamedType || ((INamedTypeSymbol)eventType).DelegateInvokeMethod == null)
            {
                throw new InvalidOperationException(ServicesVSResources.Event_type_is_invalid);
            }

            var handlesExpressions = useHandlesClause
                ? ImmutableArray.Create(syntaxFactory.MemberAccessExpression(
                        objectName != null ? syntaxFactory.IdentifierName(objectName) : syntaxFactory.ThisExpression(),
                        syntaxFactory.IdentifierName(nameOfEvent)))
                : default;

            var invokeMethod = ((INamedTypeSymbol)eventType).DelegateInvokeMethod;
            var newMethod = CodeGenerationSymbolFactory.CreateMethodSymbol(
                attributes: default,
                accessibility: Accessibility.Protected,
                modifiers: new DeclarationModifiers(),
                returnType: targetDocument.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetSpecialType(SpecialType.System_Void),
                refKind: RefKind.None,
                explicitInterfaceImplementations: default,
                name: eventHandlerName,
                typeParameters: default,
                parameters: invokeMethod.Parameters,
                statements: default,
                handlesExpressions: handlesExpressions);

            var annotation = new SyntaxAnnotation();
            newMethod = annotation.AddAnnotationToSymbol(newMethod);
            var codeModel = targetDocument.Project.Services.GetRequiredService<ICodeModelNavigationPointService>();
            var syntaxFacts = targetDocument.Project.Services.GetRequiredService<ISyntaxFactsService>();

            var targetSyntaxTree = targetDocument.GetSyntaxTreeSynchronously(cancellationToken);

            var position = type.Locations.First(loc => loc.SourceTree == targetSyntaxTree).SourceSpan.Start;
            var destinationType = syntaxFacts.GetContainingTypeDeclaration(targetSyntaxTree.GetRoot(cancellationToken), position);
            var documentOptions = targetDocument.GetLineFormattingOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);
            var insertionPoint = codeModel.GetEndPoint(destinationType, documentOptions, EnvDTE.vsCMPart.vsCMPartBody);

            if (insertionPoint == null)
            {
                throw new InvalidOperationException(ServicesVSResources.Can_t_find_where_to_insert_member);
            }

            var fallbackOptions = globalOptions.GetCleanCodeGenerationOptions(targetDocument.Project.Services);
            var options = targetDocument.GetCleanCodeGenerationOptionsAsync(fallbackOptions, cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);

            var info = codeGenerationService.GetInfo(new CodeGenerationContext(autoInsertionLocation: false), options.GenerationOptions, destinationType.SyntaxTree.Options);
            var newType = codeGenerationService.AddMethod(destinationType, newMethod, info, cancellationToken);
            var newRoot = targetSyntaxTree.GetRoot(cancellationToken).ReplaceNode(destinationType, newType);

            newRoot = Simplifier.ReduceAsync(
                targetDocument.WithSyntaxRoot(newRoot), Simplifier.Annotation, options.CleanupOptions.SimplifierOptions, cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetSyntaxRootSynchronously(cancellationToken);

            var formattingRules = additionalFormattingRule.Concat(Formatter.GetDefaultFormattingRules(targetDocument));

            newRoot = Formatter.Format(
                newRoot,
                Formatter.Annotation,
                targetDocument.Project.Solution.Services,
                options.CleanupOptions.FormattingOptions,
                formattingRules,
                cancellationToken);

            var newMember = newRoot.GetAnnotatedNodesAndTokens(annotation).Single();
            var newMemberText = newMember.ToFullString();

            // In VB, the final newline is likely a statement terminator in the parent - just add
            // one on so that things don't get messed.
            if (!newMemberText.EndsWith(Environment.NewLine, StringComparison.Ordinal))
            {
                newMemberText += Environment.NewLine;
            }

            return Tuple.Create(ConstructMemberId(newMethod), newMemberText, insertionPoint.Value.ToVsTextSpan());
        }

        public static bool TryGetMemberNavigationPoint(
            Document thisDocument,
            IGlobalOptionService globalOptions,
            string className,
            string uniqueMemberID,
            out VsTextSpan textSpan,
            out Document targetDocument,
            CancellationToken cancellationToken)
        {
            targetDocument = null;
            textSpan = default;

            var type = thisDocument.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);
            var member = LookupMemberId(type, uniqueMemberID);

            if (member == null)
            {
                return false;
            }

            var codeModel = thisDocument.Project.Services.GetService<ICodeModelNavigationPointService>();
            var memberNode = member.DeclaringSyntaxReferences.Select(r => r.GetSyntax(cancellationToken)).FirstOrDefault();
            if (memberNode != null)
            {
                var memberNodeDocument = thisDocument.Project.Solution.GetDocument(memberNode.SyntaxTree);
                var options = memberNodeDocument.GetLineFormattingOptionsAsync(globalOptions, cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);
                var navigationPoint = codeModel.GetStartPoint(memberNode, options, EnvDTE.vsCMPart.vsCMPartNavigate);
                if (navigationPoint != null)
                {
                    targetDocument = memberNodeDocument;
                    textSpan = navigationPoint.Value.ToVsTextSpan();
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Get the display names and unique ids of all the members of the given type in <paramref
        /// name="className"/>.
        /// </summary>
        public static IEnumerable<Tuple<string, string>> GetMembers(
            Document document, string className, CODEMEMBERTYPE codeMemberType, CancellationToken cancellationToken)
        {
            var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);

            var compilation = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
            var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);

            var allMembers = codeMemberType == CODEMEMBERTYPE.CODEMEMBERTYPE_EVENTS
                ? semanticModel.LookupSymbols(position: type.Locations[0].SourceSpan.Start, container: type, name: null)
                : type.GetMembers();

            var members = allMembers.Where(m => IncludeMember(m, codeMemberType, compilation));
            return members.Select(m => Tuple.Create(m.Name, ConstructMemberId(m)));
        }

        /// <summary>
        /// Try to do a symbolic rename the specified symbol.
        /// </summary>
        /// <returns>False ONLY if it can't resolve the name.  Other errors result in the normal
        /// exception being propagated.</returns>
        public static bool TryRenameElement(
            Document document,
            ContainedLanguageRenameType clrt,
            string oldFullyQualifiedName,
            string newFullyQualifiedName,
            IEnumerable<IRefactorNotifyService> refactorNotifyServices,
            CancellationToken cancellationToken)
        {
            var symbol = FindSymbol(document, clrt, oldFullyQualifiedName, cancellationToken);
            if (symbol == null)
            {
                return false;
            }

            if (CodeAnalysis.Workspace.TryGetWorkspace(document.GetTextSynchronously(cancellationToken).Container, out var workspace))
            {
                var newName = newFullyQualifiedName[(newFullyQualifiedName.LastIndexOf('.') + 1)..];
                var options = new SymbolRenameOptions();
                var newSolution = Renamer.RenameSymbolAsync(document.Project.Solution, symbol, options, newName, cancellationToken).WaitAndGetResult_Venus(cancellationToken);
                var changedDocuments = newSolution.GetChangedDocuments(document.Project.Solution);

                var undoTitle = string.Format(EditorFeaturesResources.Rename_0_to_1, symbol.Name, newName);
                using (var workspaceUndoTransaction = workspace.OpenGlobalUndoTransaction(undoTitle))
                {
                    // Notify third parties about the coming rename operation on the workspace, and let
                    // any exceptions propagate through
                    refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: true);

                    if (!workspace.TryApplyChanges(newSolution))
                    {
                        Exceptions.ThrowEFail();
                    }

                    // Notify third parties about the completed rename operation on the workspace, and
                    // let any exceptions propagate through
                    refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: true);

                    workspaceUndoTransaction.Commit();
                }

                RenameTrackingDismisser.DismissRenameTracking(workspace, changedDocuments);
                return true;
            }
            else
            {
                return false;
            }
        }

        private static bool IncludeMember(ISymbol member, CODEMEMBERTYPE memberType, Compilation compilation)
        {
            if (!member.CanBeReferencedByName)
            {
                return false;
            }

            switch (memberType)
            {
                case CODEMEMBERTYPE.CODEMEMBERTYPE_EVENT_HANDLERS:
                    // NOTE: the Dev10 C# codebase just returned 
                    if (member.Kind != SymbolKind.Method)
                    {
                        return false;
                    }

                    var method = (IMethodSymbol)member;
                    if (!method.ReturnsVoid)
                    {
                        return false;
                    }

                    if (method.Parameters.Length != 2)
                    {
                        return false;
                    }

                    if (!method.Parameters[0].Type.Equals(compilation.ObjectType))
                    {
                        return false;
                    }

                    if (!method.Parameters[1].Type.InheritsFromOrEquals(compilation.EventArgsType()))
                    {
                        return false;
                    }

                    return true;

                case CODEMEMBERTYPE.CODEMEMBERTYPE_EVENTS:
                    return member.Kind == SymbolKind.Event;

                case CODEMEMBERTYPE.CODEMEMBERTYPE_USER_FUNCTIONS:
                    return member.Kind == SymbolKind.Method;

                default:
                    throw new ArgumentException("InvalidValue", nameof(memberType));
            }
        }

        private static ISymbol FindSymbol(
            Document document, ContainedLanguageRenameType renameType, string fullyQualifiedName, CancellationToken cancellationToken)
        {
            switch (renameType)
            {
                case ContainedLanguageRenameType.CLRT_CLASS:
                    return document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(fullyQualifiedName);

                case ContainedLanguageRenameType.CLRT_CLASSMEMBER:
                    var lastDot = fullyQualifiedName.LastIndexOf('.');
                    var typeName = fullyQualifiedName[..lastDot];
                    var memberName = fullyQualifiedName.Substring(lastDot + 1, fullyQualifiedName.Length - lastDot - 1);
                    var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(typeName);
                    var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
                    var membersOfName = type.GetMembers(memberName);
                    return membersOfName.SingleOrDefault();

                case ContainedLanguageRenameType.CLRT_NAMESPACE:
                    var ns = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GlobalNamespace;
                    var parts = fullyQualifiedName.Split('.');
                    for (var i = 0; i < parts.Length && ns != null; i++)
                    {
                        ns = ns.GetNamespaceMembers().SingleOrDefault(n => n.Name == parts[i]);
                    }

                    return ns;

                case ContainedLanguageRenameType.CLRT_OTHER:
                    throw new NotSupportedException(ServicesVSResources.Can_t_rename_other_elements);

                default:
                    throw new InvalidOperationException(ServicesVSResources.Unknown_rename_type);
            }
        }

        internal static string ConstructMemberId(ISymbol member)
        {
            if (member.Kind == SymbolKind.Method)
            {
                return string.Format("{0}({1})", member.Name, string.Join(",", ((IMethodSymbol)member).Parameters.Select(p => p.Type.ToDisplayString())));
            }
            else if (member.Kind == SymbolKind.Event)
            {
                return member.Name + "(EVENT)";
            }
            else
            {
                throw new NotSupportedException(ServicesVSResources.IDs_are_not_supported_for_this_symbol_type);
            }
        }

        internal static ISymbol LookupMemberId(INamedTypeSymbol type, string uniqueMemberID)
        {
            var memberName = uniqueMemberID[..uniqueMemberID.IndexOf('(')];
            var members = type.GetMembers(memberName).Where(m => m.Kind == SymbolKind.Method);

            foreach (var m in members)
            {
                if (ConstructMemberId(m) == uniqueMemberID)
                {
                    return m;
                }
            }

            return null;
        }

        private static ISymbol GetEventSymbol(
            Document document, string objectTypeName, string nameOfEvent, INamedTypeSymbol type, CancellationToken cancellationToken)
        {
            var compilation = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
            var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);

            var objectType = compilation.GetTypeByMetadataName(objectTypeName);
            if (objectType == null)
            {
                throw new InvalidOperationException();
            }

            var containingTree = document.GetSyntaxTreeSynchronously(cancellationToken);
            var typeLocation = type.Locations.FirstOrDefault(d => d.SourceTree == containingTree);
            if (typeLocation == null)
            {
                throw new InvalidOperationException();
            }

            return semanticModel.LookupSymbols(typeLocation.SourceSpan.Start, objectType, nameOfEvent).SingleOrDefault(m => m.Kind == SymbolKind.Event);
        }
    }
}
