﻿// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ReplacePropertyWithMethods
{
    internal abstract class AbstractReplacePropertyWithMethodsService<TIdentifierNameSyntax, TExpressionSyntax, TCrefSyntax, TStatementSyntax, TPropertySyntax>
        : IReplacePropertyWithMethodsService
        where TIdentifierNameSyntax : TExpressionSyntax
        where TExpressionSyntax : SyntaxNode
        where TCrefSyntax : SyntaxNode
        where TStatementSyntax : SyntaxNode
        where TPropertySyntax : SyntaxNode
    {
        public abstract SyntaxNode GetPropertyNodeToReplace(SyntaxNode propertyDeclaration);
        public abstract Task<ImmutableArray<SyntaxNode>> GetReplacementMembersAsync(
            Document document, IPropertySymbol property, SyntaxNode propertyDeclaration, IFieldSymbol propertyBackingField, string desiredGetMethodName, string desiredSetMethodName, CodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken);

        protected abstract TCrefSyntax? TryGetCrefSyntax(TIdentifierNameSyntax identifierName);
        protected abstract TCrefSyntax CreateCrefSyntax(TCrefSyntax originalCref, SyntaxToken identifierToken, SyntaxNode? parameterType);

        protected abstract TExpressionSyntax UnwrapCompoundAssignment(SyntaxNode compoundAssignment, TExpressionSyntax readExpression);
        public async Task<SyntaxNode?> GetPropertyDeclarationAsync(CodeRefactoringContext context)
            => await context.TryGetRelevantNodeAsync<TPropertySyntax>().ConfigureAwait(false);

        protected static SyntaxNode GetFieldReference(SyntaxGenerator generator, IFieldSymbol propertyBackingField)
        {
            var memberName = generator.IdentifierName(propertyBackingField.Name);
            if (propertyBackingField.IsStatic)
            {
                return propertyBackingField.ContainingType == null
                    ? memberName
                    : generator.MemberAccessExpression(
                        generator.TypeExpression(propertyBackingField.ContainingType),
                        memberName);
            }

            return generator.MemberAccessExpression(generator.ThisExpression(), memberName);
        }

        public async Task ReplaceReferenceAsync(
            Document document,
            SyntaxEditor editor, SyntaxNode identifierName,
            IPropertySymbol property, IFieldSymbol propertyBackingField,
            string desiredGetMethodName, string desiredSetMethodName,
            CancellationToken cancellationToken)
        {
            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var semanticFacts = document.GetRequiredLanguageService<ISemanticFactsService>();
            var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();

            var referenceReplacer = new ReferenceReplacer(
                this, semanticModel, syntaxFacts, semanticFacts, editor,
                (TIdentifierNameSyntax)identifierName, property, propertyBackingField,
                desiredGetMethodName, desiredSetMethodName, cancellationToken);
            referenceReplacer.Do();
        }

        private delegate TExpressionSyntax GetWriteValue(ReferenceReplacer replacer, SyntaxNode parent);

        private readonly struct ReferenceReplacer
        {
            private readonly AbstractReplacePropertyWithMethodsService<TIdentifierNameSyntax, TExpressionSyntax, TCrefSyntax, TStatementSyntax, TPropertySyntax> _service;
            private readonly SemanticModel _semanticModel;
            private readonly ISyntaxFactsService _syntaxFacts;
            private readonly ISemanticFactsService _semanticFacts;
            private readonly SyntaxEditor _editor;
            private readonly IPropertySymbol _property;
            private readonly IFieldSymbol _propertyBackingField;
            private readonly string _desiredGetMethodName;
            private readonly string _desiredSetMethodName;

            private readonly TIdentifierNameSyntax _identifierName;
            private readonly TExpressionSyntax _expression;
            private readonly TCrefSyntax? _cref;
            private readonly CancellationToken _cancellationToken;

            public ReferenceReplacer(
                AbstractReplacePropertyWithMethodsService<TIdentifierNameSyntax, TExpressionSyntax, TCrefSyntax, TStatementSyntax, TPropertySyntax> service,
                SemanticModel semanticModel,
                ISyntaxFactsService syntaxFacts,
                ISemanticFactsService semanticFacts,
                SyntaxEditor editor,
                TIdentifierNameSyntax identifierName,
                IPropertySymbol property,
                IFieldSymbol propertyBackingField,
                string desiredGetMethodName,
                string desiredSetMethodName,
                CancellationToken cancellationToken)
            {
                _service = service;
                _semanticModel = semanticModel;
                _syntaxFacts = syntaxFacts;
                _semanticFacts = semanticFacts;
                _editor = editor;
                _identifierName = identifierName;
                _property = property;
                _propertyBackingField = propertyBackingField;
                _desiredGetMethodName = desiredGetMethodName;
                _desiredSetMethodName = desiredSetMethodName;
                _cancellationToken = cancellationToken;

                _expression = _identifierName;
                _cref = _service.TryGetCrefSyntax(_identifierName);
                if (_syntaxFacts.IsNameOfSimpleMemberAccessExpression(_expression) ||
                    _syntaxFacts.IsNameOfMemberBindingExpression(_expression))
                {
                    _expression = (TExpressionSyntax)_expression.Parent!;
                }

                Contract.ThrowIfNull(_expression.Parent, $"Parent of {_expression} is null.");
            }

            // To avoid allocating lambdas each time we hit a reference, we instead
            // statically cache the lambdas here and invoke them on demand with the 
            // data they need when we hit a reference.

            // Note: the reason for these lambdas is so that as we rewrite the tree
            // we see the results of the rewrite as we go higher up.  For example,
            // if we have:
            //
            //      this.Prop = this.Prop + 1;
            //
            // then when we hit "this.Prop" on the left of the equals, we'll want
            // to see the results of the tree *after* we've replaced "this.Prop + 1"
            // with "this.GetProp() + 1".  If we don't do this, and instead examine
            // the "this.Prop" in the original tree, then we won't see that other
            // rewrite.
            //
            // The SyntaxEditor API works by passing in these callbacks when we 
            // replace a node N.  It will call us back with what N looks like after
            // all the rewrites that occurred underneath it.
            // 
            // In order to avoid allocating each time we hit a reference, we just
            // create these statically and pass them in.

            private static readonly GetWriteValue s_getWriteValueForLeftSideOfAssignment =
                (replacer, parent) =>
                {
                    return (TExpressionSyntax)replacer._syntaxFacts.GetRightHandSideOfAssignment(parent)!;
                };

            private static readonly GetWriteValue s_getWriteValueForIncrementOrDecrement =
                (replacer, parent) =>
                {
                    // We're being read from and written to (i.e. Prop++), we need to replace with a
                    // Get and a Set call.
                    var readExpression = replacer.GetReadExpression(
                        keepTrivia: false, conflictMessage: null);
                    var literalOne = replacer.Generator.LiteralExpression(1);

                    var writeValue = replacer._syntaxFacts.IsOperandOfIncrementExpression(replacer._expression)
                        ? replacer.Generator.AddExpression(readExpression, literalOne)
                        : replacer.Generator.SubtractExpression(readExpression, literalOne);

                    return (TExpressionSyntax)writeValue;
                };

            private static readonly GetWriteValue getWriteValueForCompoundAssignment =
                (replacer, parent) =>
                {
                    // We're being read from and written to from a compound assignment 
                    // (i.e. Prop *= X), we need to replace with a Get and a Set call.

                    var readExpression = replacer.GetReadExpression(
                        keepTrivia: false, conflictMessage: null);

                    // Convert "Prop *= X" into "Prop * X".
                    return replacer._service.UnwrapCompoundAssignment(parent, readExpression);
                };

            private static readonly Func<SyntaxNode, SyntaxGenerator, ReplaceParentArgs, SyntaxNode> replaceParentCallback =
                (parent, generator, args) =>
                {
                    var replacer = args.Replacer;

                    var writeValue = args.GetWriteValue(replacer, parent);
                    var writeExpression = replacer.GetWriteExpression(
                        writeValue, args.KeepTrivia, args.ConflictMessage);
                    if (replacer._expression.Parent is TStatementSyntax)
                    {
                        writeExpression = replacer.Generator.ExpressionStatement(writeExpression);
                    }

                    return writeExpression;
                };

            private SyntaxGenerator Generator => _editor.Generator;

            public void Do()
            {
                if (_cref != null)
                {
                    // We're in a documentation comment. Replace with a reference to the getter if one exists,
                    // otherwise to the setter.
                    _editor.ReplaceNode(_cref, GetCrefReference(_cref));
                }
                else if (_semanticFacts.IsInOutContext(_semanticModel, _expression, _cancellationToken) ||
                    _semanticFacts.IsInRefContext(_semanticModel, _expression, _cancellationToken))
                {
                    // Code wasn't legal (you can't reference a property in an out/ref position in C#).
                    // Just replace this with a simple GetCall, but mark it so it's clear there's an error.
                    ReplaceRead(
                        keepTrivia: true,
                        conflictMessage: FeaturesResources.Property_cannot_safely_be_replaced_with_a_method_call);
                }
                else if (_syntaxFacts.IsAttributeNamedArgumentIdentifier(_expression))
                {
                    // Can't replace a property used in an attribute argument.
                    var newIdentifierName = AddConflictAnnotation(_identifierName,
                        FeaturesResources.Property_cannot_safely_be_replaced_with_a_method_call);

                    _editor.ReplaceNode(_identifierName, newIdentifierName);
                }
                else if (_syntaxFacts.IsLeftSideOfAssignment(_expression))
                {
                    // We're only being written to here.  This is safe to replace with a call to the 
                    // setter.
                    ReplaceWrite(
                        s_getWriteValueForLeftSideOfAssignment,
                        keepTrivia: true,
                        conflictMessage: null);
                }
                else if (_syntaxFacts.IsLeftSideOfAnyAssignment(_expression))
                {
                    ReplaceWrite(
                        getWriteValueForCompoundAssignment,
                        keepTrivia: true,
                        conflictMessage: null);
                }
                else if (_syntaxFacts.IsOperandOfIncrementOrDecrementExpression(_expression))
                {
                    ReplaceWrite(
                        s_getWriteValueForIncrementOrDecrement,
                        keepTrivia: true,
                        conflictMessage: null);
                }
                else if (_syntaxFacts.IsInferredAnonymousObjectMemberDeclarator(_expression.Parent)) //.IsParentKind(SyntaxKind.AnonymousObjectMemberDeclarator))
                {
                    // If we have:   new { this.Prop }.  We need ot convert it to:
                    //               new { Prop = this.GetProp() }
                    var declarator = _expression.Parent;
                    var readExpression = GetReadExpression(keepTrivia: true, conflictMessage: null);

                    var newDeclarator = Generator.NamedAnonymousObjectMemberDeclarator(
                        _identifierName.WithoutTrivia(),
                        readExpression);

                    // We know declarator isn't null due to the earlier call to IsInferredAnonymousObjectMemberDeclarator
                    _editor.ReplaceNode(declarator!, newDeclarator);
                }
                else if (_syntaxFacts.IsRightOfQualifiedName(_identifierName))
                {
                    // Found a reference in a qualified name.  This happens for VB explicit interface
                    // names.  We don't want to update this.  (The "Implement IGoo.Bar" clause will be
                    // updated when we generate the actual Get/Set methods.
                    return;
                }
                else
                {
                    // No writes.  Replace this with an appropriate read.
                    ReplaceRead(keepTrivia: true, conflictMessage: null);
                }
            }

            private void ReplaceRead(bool keepTrivia, string? conflictMessage)
            {
                _editor.ReplaceNode(
                    _expression,
                    GetReadExpression(keepTrivia, conflictMessage));
            }

            private void ReplaceWrite(
                GetWriteValue getWriteValue,
                bool keepTrivia,
                string? conflictMessage)
            {
                Contract.ThrowIfNull(_expression.Parent, $"Parent of {_expression} is null.");

                // Call this overload so we can see this node after already replacing any 
                // references in the writing side of it.
                _editor.ReplaceNode(
                    _expression.Parent,
                    replaceParentCallback,
                    new ReplaceParentArgs(this, getWriteValue, keepTrivia, conflictMessage));
            }

            private TCrefSyntax GetCrefReference(TCrefSyntax originalCref)
            {
                SyntaxToken newIdentifierToken;
                SyntaxNode? parameterType;
                if (_property.GetMethod != null)
                {
                    newIdentifierToken = Generator.Identifier(_desiredGetMethodName);
                    parameterType = null;
                }
                else
                {
                    newIdentifierToken = Generator.Identifier(_desiredSetMethodName);
                    parameterType = Generator.TypeExpression(_property.Type);
                }

                return _service.CreateCrefSyntax(originalCref, newIdentifierToken, parameterType);
            }

            private SyntaxNode QualifyIfAppropriate(SyntaxNode newIdentifierName)
            {
                // See if already qualified appropriate.
                if (_expression is TIdentifierNameSyntax)
                {
                    var container = _propertyBackingField.IsStatic
                        ? Generator.TypeExpression(_property.ContainingType)
                        : Generator.ThisExpression();

                    return Generator.MemberAccessExpression(container, newIdentifierName)
                                    .WithAdditionalAnnotations(Simplifier.Annotation);
                }

                return newIdentifierName;
            }

            private TExpressionSyntax GetReadExpression(
                bool keepTrivia, string? conflictMessage)
            {
                if (ShouldReadFromBackingField())
                {
                    var newIdentifierToken = AddConflictAnnotation(Generator.Identifier(_propertyBackingField.Name), conflictMessage);
                    var newIdentifierName = QualifyIfAppropriate(Generator.IdentifierName(newIdentifierToken));

                    if (keepTrivia)
                    {
                        newIdentifierName = newIdentifierName.WithTriviaFrom(_identifierName);
                    }

                    return _expression.ReplaceNode(_identifierName, newIdentifierName);
                }
                else
                {
                    return GetGetInvocationExpression(keepTrivia, conflictMessage);
                }
            }

            private SyntaxNode GetWriteExpression(
                TExpressionSyntax writeValue,
                bool keepTrivia,
                string? conflictMessage)
            {
                if (ShouldWriteToBackingField())
                {
                    var newIdentifierToken = AddConflictAnnotation(Generator.Identifier(_propertyBackingField.Name), conflictMessage);
                    var newIdentifierName = QualifyIfAppropriate(Generator.IdentifierName(newIdentifierToken));

                    if (keepTrivia)
                    {
                        newIdentifierName = newIdentifierName.WithTriviaFrom(_identifierName);
                    }

                    return Generator.AssignmentStatement(
                        _expression.ReplaceNode(_identifierName, newIdentifierName),
                        writeValue);
                }
                else
                {
                    return GetSetInvocationExpression(writeValue, keepTrivia, conflictMessage);
                }
            }

            private TExpressionSyntax GetGetInvocationExpression(
                bool keepTrivia, string? conflictMessage)
            {
                return GetInvocationExpression(_desiredGetMethodName, argument: null, keepTrivia, conflictMessage);
            }

            private TExpressionSyntax GetInvocationExpression(
                string desiredName, SyntaxNode? argument, bool keepTrivia, string? conflictMessage)
            {
                var newIdentifier = AddConflictAnnotation(
                    Generator.Identifier(desiredName), conflictMessage);

                var newIdentifierName = Generator.IdentifierName(newIdentifier);
                if (keepTrivia)
                {
                    newIdentifierName = newIdentifierName.WithLeadingTrivia(_identifierName.GetLeadingTrivia());
                }

                var updatedExpression = _expression.ReplaceNode(_identifierName, newIdentifierName);

                var arguments = argument == null
                    ? SpecializedCollections.EmptyEnumerable<SyntaxNode>()
                    : SpecializedCollections.SingletonEnumerable(argument);

                var invocation = Generator.InvocationExpression(updatedExpression, arguments);
                if (keepTrivia)
                {
                    invocation = invocation.WithTrailingTrivia(_identifierName.GetTrailingTrivia());
                }

                return (TExpressionSyntax)invocation;
            }

            private bool ShouldReadFromBackingField()
                => _propertyBackingField != null && _property.GetMethod == null;

            private SyntaxNode GetSetInvocationExpression(
                TExpressionSyntax writeValue, bool keepTrivia, string? conflictMessage)
            {
                return GetInvocationExpression(_desiredSetMethodName,
                    argument: Generator.Argument(writeValue),
                    keepTrivia: keepTrivia,
                    conflictMessage: conflictMessage);
            }

            private bool ShouldWriteToBackingField()
                => _propertyBackingField != null && _property.SetMethod == null;

            private static TIdentifierNameSyntax AddConflictAnnotation(TIdentifierNameSyntax name, string conflictMessage)
            {
                return name.ReplaceToken(
                    name.GetFirstToken(),
                    AddConflictAnnotation(name.GetFirstToken(), conflictMessage));
            }

            private static SyntaxToken AddConflictAnnotation(SyntaxToken token, string? conflictMessage)
            {
                if (conflictMessage != null)
                {
                    token = token.WithAdditionalAnnotations(ConflictAnnotation.Create(conflictMessage));
                }

                return token;
            }

            private readonly struct ReplaceParentArgs(ReferenceReplacer replacer, GetWriteValue getWriteValue, bool keepTrivia, string? conflictMessage)
            {
                public readonly ReferenceReplacer Replacer = replacer;
                public readonly GetWriteValue GetWriteValue = getWriteValue;
                public readonly bool KeepTrivia = keepTrivia;
                public readonly string? ConflictMessage = conflictMessage;
            }
        }
    }
}
