' 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.Runtime.CompilerServices
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.AddImport
Imports Microsoft.CodeAnalysis.LanguageService
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.LanguageService
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Imports Microsoft.CodeAnalysis.VisualBasic.Utilities

Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions

    Friend Module CompilationUnitSyntaxExtensions
        <Extension>
        Public Function CanAddImportsStatements(contextNode As SyntaxNode, allowInHiddenRegions As Boolean, cancellationToken As CancellationToken) As Boolean
            If contextNode.GetAncestor(Of ImportsStatementSyntax)() IsNot Nothing Then
                Return False
            End If

            If allowInHiddenRegions Then
                Return True
            End If

            Dim root = contextNode.GetAncestorOrThis(Of CompilationUnitSyntax)()
            If root.Imports.Count > 0 Then
                Dim start = root.Imports.First.SpanStart
                Dim [end] = root.Imports.Last.Span.End

                Return Not contextNode.SyntaxTree.OverlapsHiddenPosition(TextSpan.FromBounds(start, [end]), cancellationToken)
            Else
                Dim start = 0
                Dim [end] = If(root.Members.Count > 0,
                               root.Members.First.GetFirstToken().Span.End,
                               root.Span.End)

                Return Not contextNode.SyntaxTree.OverlapsHiddenPosition(TextSpan.FromBounds(start, [end]), cancellationToken)
            End If
        End Function

        <Extension()>
        Public Function AddImportsStatement(root As CompilationUnitSyntax,
                                            importStatement As ImportsStatementSyntax,
                                            placeSystemNamespaceFirst As Boolean,
                                            ParamArray annotations As SyntaxAnnotation()) As CompilationUnitSyntax
            Return root.AddImportsStatements({importStatement}, placeSystemNamespaceFirst, annotations)
        End Function

        <Extension()>
        Public Function AddImportsStatements(root As CompilationUnitSyntax,
                                            importsStatements As IList(Of ImportsStatementSyntax),
                                            placeSystemNamespaceFirst As Boolean,
                                            ParamArray annotations As SyntaxAnnotation()) As CompilationUnitSyntax
            If importsStatements.Count = 0 Then
                Return root
            End If

            Dim systemFirstInstance = ImportsStatementComparer.SystemFirstInstance
            Dim normalInstance = ImportsStatementComparer.NormalInstance

            Dim comparers = If(placeSystemNamespaceFirst,
                               (systemFirstInstance, normalInstance),
                               (normalInstance, systemFirstInstance))

            Dim [imports] = AddImportsStatements(root, importsStatements)

            ' First, see if the imports were sorted according to the user's preference.  If so,
            ' keep the same sorting after we add the import.  However, if the imports weren't sorted
            ' according to their preference, then see if they're sorted in the other way.  If so
            ' preserve that sorting as well.  That way if the user is working with a file that 
            ' was written on a machine with a different default, the imports will stay in a 
            ' reasonable order.
            If root.Imports.IsSorted(comparers.Item1) Then
                [imports].Sort(comparers.Item1)
            ElseIf root.Imports.IsSorted(comparers.Item2) Then
                [imports].Sort(comparers.Item2)
            End If

            Dim rootAndAddBlankLine = AddImportHelpers.MoveTrivia(
                VisualBasicSyntaxFacts.Instance, root, root.Imports, [imports])
            root = rootAndAddBlankLine.root
            Dim addBlankLine = rootAndAddBlankLine.addBlankLine

            Dim rootWithNewImports = root.WithImports(
                [imports].Select(Function(u) u.WithAdditionalAnnotations(annotations)).ToSyntaxList())
            If addBlankLine Then
                Dim lastImport = rootWithNewImports.Imports.Last()
                Dim nextToken = lastImport.GetLastToken(includeZeroWidth:=True, includeSkipped:=True).GetNextTokenOrEndOfFile(includeZeroWidth:=True, includeSkipped:=True)
                Dim endOfLine = lastImport.GetTrailingTrivia().LastOrDefault(Function(trivia) VisualBasicSyntaxFacts.Instance.IsEndOfLineTrivia(trivia))
                Debug.Assert(Not endOfLine.IsKind(SyntaxKind.None))
                If Not endOfLine.IsKind(SyntaxKind.None) Then
                    rootWithNewImports = rootWithNewImports.ReplaceToken(nextToken, nextToken.WithPrependedLeadingTrivia(endOfLine))
                End If
            End If

            Return rootWithNewImports
        End Function

        Private Function AddImportsStatements(root As CompilationUnitSyntax, importsStatements As IList(Of ImportsStatementSyntax)) As List(Of ImportsStatementSyntax)
            ' We need to try and not place the using inside of a directive if possible.
            Dim [imports] = New List(Of ImportsStatementSyntax)
            Dim importsLength = root.Imports.Count
            Dim endOfList = importsLength - 1
            Dim startOfLastDirective = -1
            Dim endOfLastDirective = -1
            For index = 0 To endOfList
                If root.Imports(index).GetLeadingTrivia().Any(Function(trivia) trivia.IsKind(SyntaxKind.IfDirectiveTrivia, SyntaxKind.ElseIfDirectiveTrivia, SyntaxKind.ElseDirectiveTrivia)) Then
                    startOfLastDirective = index
                End If

                If root.Imports(index).GetLeadingTrivia().Any(Function(trivia) trivia.IsKind(SyntaxKind.EndIfDirectiveTrivia)) Then
                    endOfLastDirective = index
                End If
            Next

            ' if the entire using Is in a directive Or there Is a using list at the end outside of the directive add the using at the end, 
            ' else place it before the last directive.
            [imports].AddRange(root.Imports)
            If (startOfLastDirective = 0 AndAlso (endOfLastDirective = endOfList OrElse endOfLastDirective = -1)) OrElse
                (startOfLastDirective = -1 AndAlso endOfLastDirective = -1) OrElse
                (endOfLastDirective <> endOfList AndAlso endOfLastDirective <> -1) Then
                [imports].AddRange(importsStatements)
            Else
                [imports].InsertRange(startOfLastDirective, importsStatements)
            End If

            Return [imports]
        End Function
    End Module
End Namespace
