Imports System.Collections.Generic
Imports System.Drawing
Imports System.Windows.Forms
Imports DevExpress.Office.Utils
Imports DevExpress.XtraRichEdit.API.Native
Imports DevExpress.XtraRichEdit.Services
Imports System.Linq
Imports System.Text.RegularExpressions

Public Class SQLSyntaxHighlightService
    Implements ISyntaxHighlightService

    Private ReadOnly document As Document

    Private _keywords As Regex
    Private _quotedString As New Regex("'([^']|'')*'")
    Private _commentedString As New Regex("(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)")
    Private _placeholderString As New Regex("{#(\w+)#([\:\.\w\s_/-]+)}")

    Private ReadOnly keywords As New List(Of String) From {
        "INSERT",
        "SELECT",
        "CREATE",
        "TABLE",
        "USE",
        "IDENTITY",
        "JOIN",
        "ON",
        "OFF",
        "NOT",
        "NULL",
        "WITH",
        "SET",
        "GO",
        "DECLARE",
        "EXECUTE",
        "NVARCHAR",
        "FROM",
        "INTO",
        "VALUES",
        "WHERE",
        "AND"
    }

    Public Sub New(ByVal document As Document)
        Me.document = document

        Me._keywords = New Regex("\b(" & String.Join("|", keywords.Select(Function(w) Regex.Escape(w))) & ")\b")
    End Sub
    Public Sub ForceExecute() Implements ISyntaxHighlightService.ForceExecute
        Execute()
    End Sub
    Public Sub Execute() Implements ISyntaxHighlightService.Execute
        Dim tSqltokens As List(Of SyntaxHighlightToken) = ParseTokens()
        document.ApplySyntaxHighlight(tSqltokens)
    End Sub

    Private Function ParseTokens() As List(Of SyntaxHighlightToken)
        Dim tokens As New List(Of SyntaxHighlightToken)()
        Dim ranges() As DocumentRange = Nothing

        ' search for quoted strings
        ranges = TryCast(document.FindAll(_quotedString).GetAsFrozen(), DocumentRange())
        For i As Integer = 0 To ranges.Length - 1
            tokens.Add(CreateToken(ranges(i).Start.ToInt(), ranges(i).End.ToInt(), Color.Red))
        Next i

        'Extract all keywords
        ranges = TryCast(document.FindAll(_keywords).GetAsFrozen(), DocumentRange())
        For j As Integer = 0 To ranges.Length - 1
            If Not IsRangeInTokens(ranges(j), tokens) Then
                tokens.Add(CreateToken(ranges(j).Start.ToInt(), ranges(j).End.ToInt(), Color.Blue))
            End If
        Next j

        'Find all placeholders
        ranges = TryCast(document.FindAll(_placeholderString).GetAsFrozen(), DocumentRange())
        For j As Integer = 0 To ranges.Length - 1
            If Not IsRangeInTokens(ranges(j), tokens) Then
                tokens.Add(CreateToken(ranges(j).Start.ToInt(), ranges(j).End.ToInt(), Color.DarkTurquoise))
            End If
        Next j

        'Find all comments
        ranges = TryCast(document.FindAll(_commentedString).GetAsFrozen(), DocumentRange())
        For j As Integer = 0 To ranges.Length - 1
            If Not IsRangeInTokens(ranges(j), tokens) Then
                tokens.Add(CreateToken(ranges(j).Start.ToInt(), ranges(j).End.ToInt(), Color.Green))
            End If
        Next j

        ' order tokens by their start position
        tokens.Sort(New SyntaxHighlightTokenComparer())

        ' fill in gaps in document coverage
        tokens = CombineWithPlainTextTokens(tokens)
        Return tokens
    End Function

    'Parse the remaining text into tokens:
    Private Function CombineWithPlainTextTokens(ByVal tokens As List(Of SyntaxHighlightToken)) As List(Of SyntaxHighlightToken)
        Dim result As New List(Of SyntaxHighlightToken)(tokens.Count * 2 + 1)
        Dim documentStart As Integer = Me.document.Range.Start.ToInt()
        Dim documentEnd As Integer = Me.document.Range.End.ToInt()
        If tokens.Count = 0 Then
            result.Add(CreateToken(documentStart, documentEnd, Color.Black))
        Else
            Dim firstToken As SyntaxHighlightToken = tokens(0)
            If documentStart < firstToken.Start Then
                result.Add(CreateToken(documentStart, firstToken.Start, Color.Black))
            End If
            result.Add(firstToken)
            For i As Integer = 1 To tokens.Count - 1
                Dim token As SyntaxHighlightToken = tokens(i)
                Dim prevToken As SyntaxHighlightToken = tokens(i - 1)
                If prevToken.End <> token.Start Then
                    result.Add(CreateToken(prevToken.End, token.Start, Color.Black))
                End If
                result.Add(token)
            Next i
            Dim lastToken As SyntaxHighlightToken = tokens(tokens.Count - 1)
            If documentEnd > lastToken.End Then
                result.Add(CreateToken(lastToken.End, documentEnd, Color.Black))
            End If
        End If
        Return result
    End Function

    'Create a token from the retrieved range and specify its forecolor
    Private Function CreateToken(ByVal start As Integer, ByVal [end] As Integer, ByVal foreColor As Color) As SyntaxHighlightToken
        Dim properties As New SyntaxHighlightProperties()
        properties.ForeColor = foreColor
        Return New SyntaxHighlightToken(start, [end] - start, properties)
    End Function

    'Check whether tokens intersect each other
    Private Function IsRangeInTokens(ByVal range As DocumentRange, ByVal tokens As List(Of SyntaxHighlightToken)) As Boolean
        Return tokens.Any(Function(t) IsIntersect(range, t))
    End Function
    Private Function IsIntersect(ByVal range As DocumentRange, ByVal token As SyntaxHighlightToken) As Boolean
        Dim start As Integer = range.Start.ToInt()
        If start >= token.Start AndAlso start < token.End Then
            Return True
        End If
        Dim [end] As Integer = range.End.ToInt() - 1
        If [end] >= token.Start AndAlso [end] < token.End Then
            Return True
        End If
        If start < token.Start AndAlso [end] >= token.End Then
            Return True
        End If
        Return False
    End Function
End Class

'Compare token's initial positions to sort them
Public Class SyntaxHighlightTokenComparer
    Implements IComparer(Of SyntaxHighlightToken)

    Public Function Compare(ByVal x As SyntaxHighlightToken, ByVal y As SyntaxHighlightToken) As Integer Implements IComparer(Of SyntaxHighlightToken).Compare
        Return x.Start - y.Start
    End Function
End Class