' 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.Immutable
Imports System.Composition
Imports System.Diagnostics.CodeAnalysis
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.CodeActions
Imports Microsoft.CodeAnalysis.CodeFixes
Imports Microsoft.CodeAnalysis.VisualBasic.CodeActions
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax

Namespace Microsoft.CodeAnalysis.VisualBasic.CodeFixes.MoveToTopOfFile

    <ExportCodeFixProvider(LanguageNames.VisualBasic, Name:=PredefinedCodeFixProviderNames.MoveToTopOfFile), [Shared]>
    <ExtensionOrder(After:=PredefinedCodeFixProviderNames.ImplementInterface)>
    Partial Friend Class MoveToTopOfFileCodeFixProvider
        Inherits CodeFixProvider

        Friend Const BC30465 As String = "BC30465" ' 'Imports' statements must precede any declarations.
        Friend Const BC30637 As String = "BC30637" ' Assembly or Module attribute statements must precede any declarations in a file.
        Friend Const BC30627 As String = "BC30627" ' 'Option' statements must precede any declarations or 'Imports' statements.

        <ImportingConstructor>
        <SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification:="Used in test code: https://github.com/dotnet/roslyn/issues/42814")>
        Public Sub New()
        End Sub

        Public NotOverridable Overrides ReadOnly Property FixableDiagnosticIds As ImmutableArray(Of String)
            Get
                Return ImmutableArray.Create(BC30465, BC30637, BC30627)
            End Get
        End Property

        Public Overrides Function GetFixAllProvider() As FixAllProvider
            ' Fix All is not supported for this code fix
            ' https://github.com/dotnet/roslyn/issues/34471
            Return Nothing
        End Function

        Public NotOverridable Overrides Async Function RegisterCodeFixesAsync(context As CodeFixContext) As Task
            Dim document = context.Document
            Dim span = context.Span
            Dim cancellationToken = context.CancellationToken
            Dim root = Await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(False)

            Dim token = root.FindToken(span.Start)
            If Not token.Span.IntersectsWith(span) Then
                Return
            End If

            Dim node = token _
                .GetAncestors(Of DeclarationStatementSyntax) _
                .FirstOrDefault(Function(c) c.Span.IntersectsWith(span))

            If node Is Nothing Then
                Return
            End If

            Dim compilationUnit = CType(root, CompilationUnitSyntax)

            Dim result = SpecializedCollections.EmptyEnumerable(Of CodeAction)()
            If node.Kind = SyntaxKind.ImportsStatement Then
                Dim importsStatement = DirectCast(node, ImportsStatementSyntax)
                If Not compilationUnit.Imports.Contains(importsStatement) Then
                    If DeclarationsExistAfterImports(node, compilationUnit) OrElse compilationUnit.Attributes.Any(Function(a) a.SpanStart < node.SpanStart) Then
                        result = CreateActionForImports(document, importsStatement, compilationUnit, cancellationToken)
                    End If
                End If
            End If

            If node.Kind = SyntaxKind.OptionStatement Then
                Dim optionStatement = DirectCast(node, OptionStatementSyntax)
                If Not compilationUnit.Options.Contains(optionStatement) Then
                    result = CreateActionForOptions(document, optionStatement, compilationUnit, cancellationToken)
                End If
            End If

            If node.Kind = SyntaxKind.AttributesStatement Then
                Dim attributesStatement = DirectCast(node, AttributesStatementSyntax)
                If Not compilationUnit.Attributes.Contains(attributesStatement) Then
                    result = CreateActionForAttribute(document, attributesStatement, compilationUnit, cancellationToken)
                End If
            End If

            If result IsNot Nothing Then
                context.RegisterFixes(result, context.Diagnostics)
            End If
        End Function

        Private Shared Function DeclarationsExistAfterImports(node As SyntaxNode, root As CompilationUnitSyntax) As Boolean
            Return root.Members.Any(
                Function(m) m IsNot node AndAlso
                        Not m.IsKind(SyntaxKind.OptionStatement, SyntaxKind.AttributesStatement) AndAlso
                        node.Span.End > m.SpanStart)
        End Function

        Private Shared Function CreateActionForImports(document As Document, node As ImportsStatementSyntax, root As CompilationUnitSyntax, cancellationToken As CancellationToken) As IEnumerable(Of CodeAction)
            Dim destinationLine As Integer = 0
            If root.Imports.Any() Then
                destinationLine = FindLastContiguousStatement(root.Imports, root.GetLeadingBannerAndPreprocessorDirectives())
            ElseIf root.Options.Any() Then
                destinationLine = root.Options.Last().GetLocation().GetLineSpan().EndLinePosition.Line + 1
            End If

            If DestinationPositionIsHidden(root, destinationLine, cancellationToken) Then
                Return Nothing
            End If

            Return {
                New MoveToLineCodeAction(document,
                                         node.ImportsKeyword,
                                         destinationLine,
                                         MoveStatement("Imports", destinationLine)),
                New RemoveStatementCodeAction(document, node, DeleteStatement("Imports"))
            }

        End Function

        Private Shared Function CreateActionForOptions(document As Document, node As OptionStatementSyntax, root As CompilationUnitSyntax, cancellationToken As CancellationToken) As IEnumerable(Of CodeAction)
            Dim destinationLine = FindLastContiguousStatement(root.Options, root.GetLeadingBannerAndPreprocessorDirectives())

            If DestinationPositionIsHidden(root, destinationLine, cancellationToken) Then
                Return Nothing
            End If

            Return {
                New MoveToLineCodeAction(document,
                                         node.OptionKeyword,
                                         destinationLine,
                                         MoveStatement("Option", destinationLine)),
                New RemoveStatementCodeAction(document, node, DeleteStatement("Option"))
            }
        End Function

        Private Shared Function CreateActionForAttribute(document As Document, node As AttributesStatementSyntax, root As CompilationUnitSyntax, cancellationToken As CancellationToken) As IEnumerable(Of CodeAction)
            Dim destinationLine As Integer = 0
            If root.Attributes.Any() Then
                destinationLine = FindLastContiguousStatement(root.Attributes, root.GetLeadingBannerAndPreprocessorDirectives())
            ElseIf root.Imports.Any() Then
                destinationLine = root.Imports.Last().GetLocation().GetLineSpan().EndLinePosition.Line + 1
            ElseIf root.Options.Any() Then
                destinationLine = root.Options.Last().GetLocation().GetLineSpan().EndLinePosition.Line + 1
            End If

            If DestinationPositionIsHidden(root, destinationLine, cancellationToken) Then
                Return Nothing
            End If

            Return {
                New MoveToLineCodeAction(document,
                                         node.GetFirstToken(),
                                         destinationLine,
                                         MoveStatement("Attribute", destinationLine)),
                New RemoveStatementCodeAction(document, node, DeleteStatement("Attribute"))
            }
        End Function

        Private Shared Function FindLastContiguousStatement(nodes As IEnumerable(Of SyntaxNode), trivia As IEnumerable(Of SyntaxTrivia)) As Integer
            If Not nodes.Any() Then
                Dim lastBannerText = trivia.LastOrDefault(Function(t) t.IsKind(SyntaxKind.CommentTrivia))
                If lastBannerText = Nothing Then
                    Return 0
                Else
                    Return lastBannerText.GetLocation().GetLineSpan().StartLinePosition.Line + 1
                End If
            End If

            ' Advance through the list of nodes until we find one that doesn't start on the
            ' line immediately following the one before it.
            Dim expectedLine = nodes.First().GetLocation().GetLineSpan().StartLinePosition.Line
            For Each node In nodes
                Dim actualLine = node.GetLocation().GetLineSpan().StartLinePosition.Line

                If actualLine <> expectedLine Then
                    Exit For
                End If

                expectedLine += 1
            Next

            Return expectedLine
        End Function

        Private Shared Function MoveStatement(kind As String, line As Integer) As String
            Return String.Format(VBFeaturesResources.Move_the_0_statement_to_line_1, kind, line + 1)
        End Function

        Private Shared Function DeleteStatement(kind As String) As String
            Return String.Format(VBFeaturesResources.Delete_the_0_statement2, kind)
        End Function

        Private Shared Function DestinationPositionIsHidden(root As CompilationUnitSyntax, destinationLine As Integer, cancellationToken As CancellationToken) As Boolean
            Dim text = root.GetText()
            Dim position = text.Lines(destinationLine).Start
            Return root.SyntaxTree.IsHiddenPosition(destinationLine, cancellationToken)
        End Function
    End Class
End Namespace
