' 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.Runtime.InteropServices
Imports Microsoft.CodeAnalysis.[Shared].Collections
Imports Microsoft.CodeAnalysis.Structure
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax

Namespace Microsoft.CodeAnalysis.VisualBasic.Structure
    Friend Module VisualBasicOutliningHelpers
        Public Const Ellipsis = "..."
        Public Const SpaceEllipsis = " " & Ellipsis
        Public Const MaxXmlDocCommentBannerLength = 120

        Private Function GetNodeBannerText(node As SyntaxNode) As String
            Return node.ConvertToSingleLine().ToString() & SpaceEllipsis
        End Function

        Private Function GetCommentBannerText(comment As SyntaxTrivia) As String
            Return "' " & comment.ToString().Substring(1).Trim() & SpaceEllipsis
        End Function

        Private Function CreateCommentsRegion(startComment As SyntaxTrivia,
                                              endComment As SyntaxTrivia) As BlockSpan?
            Dim span = TextSpan.FromBounds(startComment.SpanStart, endComment.Span.End)
            Return CreateBlockSpan(
                span, span,
                GetCommentBannerText(startComment),
                autoCollapse:=True,
                type:=BlockTypes.Comment,
                isCollapsible:=True,
                isDefaultCollapsed:=False)
        End Function

        ' For testing purposes
        Friend Function CreateCommentsRegions(triviaList As SyntaxTriviaList) As ImmutableArray(Of BlockSpan)
            Dim spans = TemporaryArray(Of BlockSpan).Empty
            Try
                CollectCommentsRegions(triviaList, spans)
                Return spans.ToImmutableAndClear()
            Finally
                spans.Dispose()
            End Try
        End Function

        Friend Sub CollectCommentsRegions(triviaList As SyntaxTriviaList,
                                          ByRef spans As TemporaryArray(Of BlockSpan))
            If triviaList.Count > 0 Then
                Dim startComment As SyntaxTrivia? = Nothing
                Dim endComment As SyntaxTrivia? = Nothing

                ' Iterate through trivia and collect groups of contiguous single-line comments that are only separated by whitespace
                For Each trivia In triviaList
                    If trivia.Kind = SyntaxKind.CommentTrivia Then
                        startComment = If(startComment, trivia)
                        endComment = trivia
                    ElseIf trivia.Kind <> SyntaxKind.WhitespaceTrivia AndAlso
                        trivia.Kind <> SyntaxKind.EndOfLineTrivia AndAlso
                        trivia.Kind <> SyntaxKind.EndOfFileToken Then

                        If startComment IsNot Nothing Then
                            spans.AddIfNotNull(CreateCommentsRegion(startComment.Value, endComment.Value))
                            startComment = Nothing
                            endComment = Nothing
                        End If
                    End If
                Next

                ' Add any final span
                If startComment IsNot Nothing Then
                    spans.AddIfNotNull(CreateCommentsRegion(startComment.Value, endComment.Value))
                End If
            End If
        End Sub

        Friend Sub CollectCommentsRegions(node As SyntaxNode,
                                          ByRef spans As TemporaryArray(Of BlockSpan),
                                          optionProvider As BlockStructureOptionProvider)
            If node Is Nothing Then
                Throw New ArgumentNullException(NameOf(node))
            End If

            Dim span As BlockSpan = Nothing
            If optionProvider.IsMetadataAsSource AndAlso TryGetLeadingCollapsibleSpan(node, span) Then
                spans.Add(span)
            Else
                Dim triviaList = node.GetLeadingTrivia()
                CollectCommentsRegions(triviaList, spans)
            End If
        End Sub

        Private Function TryGetLeadingCollapsibleSpan(node As SyntaxNode, <[Out]> ByRef span As BlockSpan) As Boolean
            Dim startToken = node.GetFirstToken()
            Dim endToken = GetEndToken(node)
            If startToken.IsKind(SyntaxKind.None) OrElse endToken.IsKind(SyntaxKind.None) Then
                ' if valid tokens can't be found then a meaningful span can't be generated
                span = Nothing
                Return False
            End If

            Dim firstComment = startToken.LeadingTrivia.FirstOrNull(Function(t) t.Kind = SyntaxKind.CommentTrivia)

            Dim startPosition = If(firstComment.HasValue,
                                   firstComment.Value.SpanStart,
                                   startToken.SpanStart)

            Dim endPosition = endToken.SpanStart

            ' TODO (tomescht): Mark the regions to be collapsed by default.
            If startPosition <> endPosition Then
                Dim hintTextEndToken = GetHintTextEndToken(node)
                span = New BlockSpan(
                    isCollapsible:=True,
                    type:=BlockTypes.Comment,
                    textSpan:=TextSpan.FromBounds(startPosition, endPosition),
                    hintSpan:=TextSpan.FromBounds(startPosition, hintTextEndToken.Span.End),
                    bannerText:=Ellipsis,
                    autoCollapse:=True)
                Return True
            End If

            span = Nothing
            Return False
        End Function

        Private Function GetEndToken(node As SyntaxNode) As SyntaxToken
            If node.IsKind(SyntaxKind.SubNewStatement) Then
                Dim subNewStatement = DirectCast(node, SubNewStatementSyntax)
                Return If(subNewStatement.Modifiers.FirstOrNull(), subNewStatement.DeclarationKeyword)
            ElseIf node.IsKind(SyntaxKind.DelegateSubStatement, SyntaxKind.DelegateFunctionStatement) Then
                Dim delegateStatement = DirectCast(node, DelegateStatementSyntax)
                Return If(delegateStatement.Modifiers.FirstOrNull(), delegateStatement.DelegateKeyword)
            ElseIf node.IsKind(SyntaxKind.EnumStatement) Then
                Dim enumStatement = DirectCast(node, EnumStatementSyntax)
                Return If(enumStatement.Modifiers.FirstOrNull(), enumStatement.EnumKeyword)
            ElseIf node.IsKind(SyntaxKind.EnumMemberDeclaration) Then
                Dim enumMemberDeclaration = DirectCast(node, EnumMemberDeclarationSyntax)
                Return enumMemberDeclaration.Identifier
            ElseIf node.IsKind(SyntaxKind.EventStatement) Then
                Dim eventStatement = DirectCast(node, EventStatementSyntax)
                Return If(eventStatement.Modifiers.FirstOrNull(),
                    If(eventStatement.CustomKeyword.IsKind(SyntaxKind.None), eventStatement.DeclarationKeyword, eventStatement.CustomKeyword))
            ElseIf node.IsKind(SyntaxKind.FieldDeclaration) Then
                Dim fieldDeclaration = DirectCast(node, FieldDeclarationSyntax)
                Return If(fieldDeclaration.Modifiers.FirstOrNull(), fieldDeclaration.Declarators.First().GetFirstToken())
            ElseIf node.IsKind(SyntaxKind.SubStatement, SyntaxKind.FunctionStatement) Then
                Dim methodStatement = DirectCast(node, MethodStatementSyntax)
                Return If(methodStatement.Modifiers.FirstOrNull(), methodStatement.DeclarationKeyword)
            ElseIf node.IsKind(SyntaxKind.OperatorStatement) Then
                Dim operatorStatement = DirectCast(node, OperatorStatementSyntax)
                Return If(operatorStatement.Modifiers.FirstOrNull(), operatorStatement.DeclarationKeyword)
            ElseIf node.IsKind(SyntaxKind.PropertyStatement) Then
                Dim propertyStatement = DirectCast(node, PropertyStatementSyntax)
                Return If(propertyStatement.Modifiers.FirstOrNull(), propertyStatement.DeclarationKeyword)
            ElseIf node.IsKind(SyntaxKind.ClassStatement, SyntaxKind.StructureStatement, SyntaxKind.InterfaceStatement, SyntaxKind.ModuleStatement) Then
                Dim typeStatement = DirectCast(node, TypeStatementSyntax)
                Return If(typeStatement.Modifiers.FirstOrNull(), typeStatement.DeclarationKeyword)
            Else
                Return Nothing
            End If
        End Function

        Private Function GetHintTextEndToken(node As SyntaxNode) As SyntaxToken
            Return node.GetLastToken()
        End Function

        Friend Function CreateBlockSpan(
                span As TextSpan,
                hintSpan As TextSpan,
                bannerText As String,
                autoCollapse As Boolean,
                type As String,
                isCollapsible As Boolean,
                isDefaultCollapsed As Boolean) As BlockSpan?
            Return New BlockSpan(
                textSpan:=span,
                hintSpan:=hintSpan,
                bannerText:=bannerText,
                autoCollapse:=autoCollapse,
                isDefaultCollapsed:=isDefaultCollapsed,
                type:=type,
                isCollapsible:=isCollapsible)
        End Function

        Friend Function CreateBlockSpanFromBlock(
                blockNode As SyntaxNode,
                bannerText As String,
                autoCollapse As Boolean,
                type As String,
                isCollapsible As Boolean) As BlockSpan?
            Return CreateBlockSpan(
                blockNode.Span, GetHintSpan(blockNode),
                bannerText, autoCollapse,
                type, isCollapsible, isDefaultCollapsed:=False)
        End Function

        Friend Function CreateBlockSpanFromBlock(
                blockNode As SyntaxNode,
                bannerNode As SyntaxNode,
                autoCollapse As Boolean,
                type As String,
                isCollapsible As Boolean) As BlockSpan?
            Return CreateBlockSpan(
                blockNode.Span, GetHintSpan(blockNode),
                GetNodeBannerText(bannerNode),
                autoCollapse, type, isCollapsible, isDefaultCollapsed:=False)
        End Function

        Private Function GetHintSpan(blockNode As SyntaxNode) As TextSpan
            ' Don't include attributes in the hint-span for a block.  We don't want
            ' the attributes to show up when users hover over indent guide lines.
            Dim firstToken = blockNode.GetFirstToken()
            If firstToken.Kind() = SyntaxKind.LessThanToken AndAlso
               firstToken.Parent.IsKind(SyntaxKind.AttributeList) Then

                Dim attributeOwner = firstToken.Parent.Parent
                For Each child In attributeOwner.ChildNodesAndTokens
                    If child.Kind() <> SyntaxKind.AttributeList Then
                        Return TextSpan.FromBounds(child.SpanStart, blockNode.Span.End)
                    End If
                Next
            End If

            Return blockNode.Span
        End Function
    End Module
End Namespace
