' 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.

Imports System.Collections.Concurrent
Imports System.Threading
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.LanguageService
Imports Microsoft.CodeAnalysis.UseAutoProperty
Imports Microsoft.CodeAnalysis.VisualBasic.LanguageService
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax

Namespace Microsoft.CodeAnalysis.VisualBasic.UseAutoProperty
    <DiagnosticAnalyzer(LanguageNames.VisualBasic)>
    Friend Class VisualBasicUseAutoPropertyAnalyzer
        Inherits AbstractUseAutoPropertyAnalyzer(Of
            SyntaxKind,
            PropertyBlockSyntax,
            FieldDeclarationSyntax,
            ModifiedIdentifierSyntax,
            ExpressionSyntax)

        Protected Overrides ReadOnly Property PropertyDeclarationKind As SyntaxKind = SyntaxKind.PropertyBlock

        Protected Overrides ReadOnly Property SyntaxFacts As ISyntaxFacts = VisualBasicSyntaxFacts.Instance

        Protected Overrides Function SupportsReadOnlyProperties(compilation As Compilation) As Boolean
            Return DirectCast(compilation, VisualBasicCompilation).LanguageVersion >= LanguageVersion.VisualBasic14
        End Function

        Protected Overrides Function SupportsPropertyInitializer(compilation As Compilation) As Boolean
            Return DirectCast(compilation, VisualBasicCompilation).LanguageVersion >= LanguageVersion.VisualBasic10
        End Function

        Protected Overrides Function CanExplicitInterfaceImplementationsBeFixed() As Boolean
            Return True
        End Function

        Protected Overrides Sub RegisterIneligibleFieldsAction(fieldNames As HashSet(Of String), ineligibleFields As ConcurrentSet(Of IFieldSymbol), semanticModel As SemanticModel, codeBlock As SyntaxNode, cancellationToken As CancellationToken)
            ' There are no syntactic constructs that make a field ineligible to be replaced with 
            ' a property.  In C# you can't use a property in a ref/out position.  But that restriction
            ' doesn't apply to VB.
        End Sub

        Protected Overrides Function CanConvert(prop As IPropertySymbol) As Boolean
            ' VB auto props cannot specify accessibility for accessors.  So if the original
            ' code had different accessibility between the accessors and property then we 
            ' can't convert it.

            If prop.GetMethod IsNot Nothing AndAlso
               prop.GetMethod.DeclaredAccessibility <> prop.DeclaredAccessibility Then
                Return False
            End If

            If prop.SetMethod IsNot Nothing AndAlso
               prop.SetMethod.DeclaredAccessibility <> prop.DeclaredAccessibility Then
                Return False
            End If

            Return True
        End Function

        Protected Overrides Function GetFieldInitializer(variable As ModifiedIdentifierSyntax, cancellationToken As CancellationToken) As ExpressionSyntax
            Dim declarator = TryCast(variable.Parent, VariableDeclaratorSyntax)
            Return declarator?.Initializer?.Value
        End Function

        Private Shared Function CheckExpressionSyntactically(expression As ExpressionSyntax) As Boolean
            If expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then
                Dim memberAccessExpression = DirectCast(expression, MemberAccessExpressionSyntax)
                Return memberAccessExpression.Expression.Kind() = SyntaxKind.MeExpression AndAlso
                    memberAccessExpression.Name.Kind() = SyntaxKind.IdentifierName
            ElseIf expression.IsKind(SyntaxKind.IdentifierName) Then
                Return True
            End If

            Return False
        End Function

        Protected Overrides Function GetGetterExpression(getMethod As IMethodSymbol, cancellationToken As CancellationToken) As ExpressionSyntax
            ' Getter has to be of the form:
            '
            '     Get
            '         Return field
            '     End Get
            ' or
            '     Get
            '         Return Me.field
            '     End Get
            Dim accessor = TryCast(TryCast(getMethod.DeclaringSyntaxReferences(0).GetSyntax(cancellationToken), AccessorStatementSyntax)?.Parent, AccessorBlockSyntax)
            Dim statements = accessor?.Statements
            If statements?.Count = 1 Then
                Dim statement = statements.Value(0)
                If statement.Kind() = SyntaxKind.ReturnStatement Then
                    Dim expr = DirectCast(statement, ReturnStatementSyntax).Expression
                    Return If(CheckExpressionSyntactically(expr), expr, Nothing)
                End If
            End If

            Return Nothing
        End Function

        Protected Overrides Function GetSetterExpression(setMethod As IMethodSymbol, semanticModel As SemanticModel, cancellationToken As CancellationToken) As ExpressionSyntax
            ' Setter has to be of the form:
            '
            '     Set(value)
            '         field = value
            '     End Set
            ' or
            '     Set(value)
            '         Me.field = value
            '     End Set
            Dim setAccessor = TryCast(TryCast(setMethod.DeclaringSyntaxReferences(0).GetSyntax(cancellationToken), AccessorStatementSyntax)?.Parent, AccessorBlockSyntax)
            Dim statements = setAccessor?.Statements
            If statements?.Count = 1 Then
                Dim statement = statements.Value(0)
                If statement.IsKind(SyntaxKind.SimpleAssignmentStatement) Then
                    Dim assignmentStatement = DirectCast(statement, AssignmentStatementSyntax)
                    If assignmentStatement.Right.Kind() = SyntaxKind.IdentifierName Then
                        Dim identifier = DirectCast(assignmentStatement.Right, IdentifierNameSyntax)
                        Dim symbol = semanticModel.GetSymbolInfo(identifier, cancellationToken).Symbol
                        If setMethod.Parameters.Contains(TryCast(symbol, IParameterSymbol)) Then
                            Return If(CheckExpressionSyntactically(assignmentStatement.Left), assignmentStatement.Left, Nothing)
                        End If
                    End If
                End If
            End If

            Return Nothing
        End Function

        Protected Overrides Function GetFieldNode(fieldDeclaration As FieldDeclarationSyntax, identifier As ModifiedIdentifierSyntax) As SyntaxNode
            Return GetNodeToRemove(identifier)
        End Function

        Protected Overrides Sub RegisterNonConstructorFieldWrites(
                fieldNames As HashSet(Of String),
                fieldWrites As ConcurrentDictionary(Of IFieldSymbol, ConcurrentSet(Of SyntaxNode)),
                semanticModel As SemanticModel,
                codeBlock As SyntaxNode,
                cancellationToken As CancellationToken)

            ' the property doesn't have a setter currently. check all the types the field is 
            ' declared in.  If the field is written to outside of a constructor, then this 
            ' field Is Not eligible for replacement with an auto prop.  We'd have to make 
            ' the autoprop read/write, And that could be opening up the property widely 
            ' (in accessibility terms) in a way the user would not want.

            ' Always fine to convert an prop to an auto-prop if its field was only written in a constructor.
            If codeBlock.AncestorsAndSelf().Contains(Function(node) node.Kind() = SyntaxKind.ConstructorBlock) Then
                Return
            End If

            For Each node In codeBlock.DescendantNodesAndSelf().OfType(Of IdentifierNameSyntax)
                If Not fieldNames.Contains(node.Identifier.ValueText) Then
                    Continue For
                End If

                Dim field = TryCast(semanticModel.GetSymbolInfo(node, cancellationToken).Symbol, IFieldSymbol)
                If field IsNot Nothing AndAlso
                   node.IsWrittenTo(semanticModel, cancellationToken) Then

                    AddFieldWrite(fieldWrites, field, node)
                End If
            Next
        End Sub
    End Class
End Namespace
