Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports SkiaSharp

Namespace Nexamas.UI.TextInput

    ''' <summary>
    ''' Optional syntax-colored renderer for multiline text snapshots.
    ''' It keeps selection and caret behavior compatible with MASTextMultilineRenderer.
    ''' </summary>
    Friend NotInheritable Class MASTextMultilineStyledRenderer

        Private Const CARET_WIDTH_PX As Single = 1.0F
        Private Const SELECTION_RADIUS_PX As Single = 2.0F

        Private ReadOnly _fallbackRenderer As New MASTextMultilineRenderer()
        Private ReadOnly _selectionGeometry As New MASTextMultilineSelectionGeometry()

        Friend Sub DrawFromSnapshot(canvas As SKCanvas,
                                    snap As MASTextInputLayoutSnapshot,
                                    theme As IMASInputTheme,
                                    hasFocus As Boolean,
                                    highlighter As IMASTextSyntaxHighlighter,
                                    fontProfile As MASTextInputFontProfile)

            If canvas Is Nothing OrElse snap Is Nothing Then Return
            If highlighter Is Nothing OrElse snap.Paint Is Nothing OrElse snap.ParagraphLayout Is Nothing Then
                _fallbackRenderer.DrawFromSnapshot(canvas, snap, theme, hasFocus)
                Return
            End If

            If snap.ContentRect.Width <= 0.0F OrElse snap.ContentRect.Height <= 0.0F Then Return

            DrawSelection(canvas, snap, theme)
            DrawStyledText(canvas, snap, highlighter, fontProfile)

            If hasFocus Then
                DrawCaret(canvas, snap)
            End If
        End Sub

        Private Sub DrawSelection(canvas As SKCanvas,
                                  snap As MASTextInputLayoutSnapshot,
                                  theme As IMASInputTheme)

            If snap.SelectionLength <= 0 Then Return

            Dim rects As IReadOnlyList(Of SKRect) =
                _selectionGeometry.BuildRects(
                    layout:=snap.ParagraphLayout,
                    selectionStart:=snap.SelectionStart,
                    selectionLength:=snap.SelectionLength,
                    paint:=snap.Paint)

            If rects Is Nothing OrElse rects.Count = 0 Then Return

            Using pSel As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Fill,
                .Color = ResolveSelectionColor(theme)
            }
                For Each rect As SKRect In rects
                    If rect.IsEmpty OrElse rect.Right <= rect.Left Then Continue For
                    If rect.Bottom < snap.ContentRect.Top OrElse rect.Top > snap.ContentRect.Bottom Then Continue For

                    Dim clipped As SKRect = ClipToContent(rect, snap.ContentRect)
                    If clipped.IsEmpty OrElse clipped.Right <= clipped.Left Then Continue For

                    canvas.DrawRoundRect(clipped, SELECTION_RADIUS_PX, SELECTION_RADIUS_PX, pSel)
                Next
            End Using
        End Sub

        Private Shared Sub DrawStyledText(canvas As SKCanvas,
                                          snap As MASTextInputLayoutSnapshot,
                                          highlighter As IMASTextSyntaxHighlighter,
                                          fontProfile As MASTextInputFontProfile)

            Dim paragraph As MASTextParagraphLayout = snap.ParagraphLayout
            If paragraph Is Nothing Then Return

            Dim text As String = If(snap.DisplayText, String.Empty)
            Dim spans As IReadOnlyList(Of MASTextSyntaxSpan) = Nothing

            Try
                spans = highlighter.GetSpans(text)
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRenderFallback(masCaughtException1)
                spans = Nothing
            End Try

            Dim ordered As List(Of MASTextSyntaxSpan) = Nothing
            If spans IsNot Nothing AndAlso spans.Count > 0 Then
                ordered = New List(Of MASTextSyntaxSpan)(spans)
                ordered.Sort(Function(a, b) a.Start.CompareTo(b.Start))
            End If

            For i As Integer = 0 To paragraph.LineCount - 1
                Dim line As MASTextWrappedLine = paragraph.Lines(i)
                If line Is Nothing Then Continue For
                If String.IsNullOrEmpty(line.Text) Then Continue For
                If line.Bounds.Bottom < snap.ContentRect.Top OrElse line.Bounds.Top > snap.ContentRect.Bottom Then Continue For

                ' Keep RTL visual correctness stable. Highlighting can be enabled later with a bidi-aware styled renderer.
                If line.IsRtl OrElse ordered Is Nothing OrElse ordered.Count = 0 Then
                    DrawPlainLine(canvas, snap, line, fontProfile)
                Else
                    DrawStyledLine(canvas, snap, line, ordered, fontProfile)
                End If
            Next
        End Sub

        Private Shared Sub DrawPlainLine(canvas As SKCanvas,
                                         snap As MASTextInputLayoutSnapshot,
                                         line As MASTextWrappedLine,
                                         fontProfile As MASTextInputFontProfile)

            If line Is Nothing OrElse String.IsNullOrEmpty(line.Text) Then Return

            Dim x As Single = If(line.InlineLayout IsNot Nothing, line.InlineLayout.TextX, line.Bounds.Left)
            If snap.CrispRendering Then
                x = MASTextCrispRendering.SnapTextX(x)
            End If

            Using p As SKPaint = snap.Paint.Clone()
                If fontProfile IsNot Nothing Then
                    fontProfile.ApplyToPaint(p, line.Text)
                End If

                If snap.CrispRendering Then
                    MASTextCrispRendering.ConfigurePaint(p)
                End If
                MASShapedText.Draw(canvas, line.Text, x, If(snap.CrispRendering, MASTextCrispRendering.SnapBaseline(line.BaselineY), line.BaselineY), p)
            End Using
        End Sub

        Private Shared Sub DrawStyledLine(canvas As SKCanvas,
                                          snap As MASTextInputLayoutSnapshot,
                                          line As MASTextWrappedLine,
                                          spans As IReadOnlyList(Of MASTextSyntaxSpan),
                                          fontProfile As MASTextInputFontProfile)

            Dim lineStart As Integer = line.StartIndex
            Dim lineEnd As Integer = line.StartIndex + line.Length
            Dim index As Integer = lineStart

            For Each span As MASTextSyntaxSpan In spans
                If span Is Nothing OrElse span.Length <= 0 Then Continue For

                Dim spanStart As Integer = Math.Max(span.Start, lineStart)
                Dim spanEnd As Integer = Math.Min(span.Start + span.Length, lineEnd)

                If spanEnd <= lineStart Then Continue For
                If spanStart >= lineEnd Then Exit For

                If spanStart > index Then
                    DrawLineRun(canvas, snap, line, index, spanStart - index, Nothing, fontProfile)
                End If

                DrawLineRun(canvas, snap, line, spanStart, spanEnd - spanStart, span.Style, fontProfile)
                index = Math.Max(index, spanEnd)
            Next

            If index < lineEnd Then
                DrawLineRun(canvas, snap, line, index, lineEnd - index, Nothing, fontProfile)
            End If
        End Sub

        Private Shared Sub DrawLineRun(canvas As SKCanvas,
                                       snap As MASTextInputLayoutSnapshot,
                                       line As MASTextWrappedLine,
                                       globalStart As Integer,
                                       length As Integer,
                                       style As MASTextSyntaxStyle,
                                       fontProfile As MASTextInputFontProfile)

            If line Is Nothing OrElse line.InlineLayout Is Nothing Then Return
            If length <= 0 Then Return

            Dim localStart As Integer = globalStart - line.StartIndex
            If localStart < 0 OrElse localStart >= line.Text.Length Then Return

            Dim safeLen As Integer = Math.Min(length, line.Text.Length - localStart)
            If safeLen <= 0 Then Return

            Dim runText As String = line.Text.Substring(localStart, safeLen)
            Dim runX As Single = line.InlineLayout.GetCaretX(localStart)

            Using p As SKPaint = snap.Paint.Clone()
                If style IsNot Nothing AndAlso style.Foreground.HasValue Then
                    p.Color = style.Foreground.Value
                End If

                If style IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(style.FontFamily) Then
                    Try
                        Dim tf As SKTypeface = SKTypeface.FromFamilyName(style.FontFamily)
                        If tf IsNot Nothing Then p.Typeface = tf
                    Catch masCaughtException2 As Exception
                        Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRenderFallback(masCaughtException2)
                    End Try
                ElseIf fontProfile IsNot Nothing Then
                    fontProfile.ApplyToPaint(p, runText)
                End If

                If snap.CrispRendering Then
                    MASTextCrispRendering.ConfigurePaint(p)
                End If
                If snap.CrispRendering Then
                    runX = MASTextCrispRendering.SnapTextX(runX)
                End If
                MASShapedText.Draw(canvas, runText, runX, If(snap.CrispRendering, MASTextCrispRendering.SnapBaseline(line.BaselineY), line.BaselineY), p)
            End Using
        End Sub

        Private Shared Sub DrawCaret(canvas As SKCanvas,
                                     snap As MASTextInputLayoutSnapshot)

            Dim paragraph As MASTextParagraphLayout = snap.ParagraphLayout
            If paragraph Is Nothing OrElse snap.Paint Is Nothing Then Return

            Dim caretPoint As SKPoint = paragraph.GetCaretPoint(snap.CaretIndex)
            Dim line As MASTextWrappedLine = paragraph.GetLineFromTextIndex(snap.CaretIndex)
            If line Is Nothing Then Return

            Dim caretX As Single = caretPoint.X
            If line.IsRtl Then caretX -= CARET_WIDTH_PX

            caretX = Math.Max(snap.ContentRect.Left, Math.Min(snap.ContentRect.Right - CARET_WIDTH_PX, caretX))
            caretX = If(snap.CrispRendering, MASTextCrispRendering.SnapCaretX(caretX), CSng(Math.Round(caretX, MidpointRounding.AwayFromZero)))

            Dim topY As Single = Math.Max(snap.ContentRect.Top + 1.0F, line.Bounds.Top + 1.0F)
            Dim botY As Single = Math.Min(snap.ContentRect.Bottom - 1.0F, line.Bounds.Bottom - 1.0F)

            If botY <= topY Then Return

            Using pCaret As New SKPaint With {
                .IsAntialias = False,
                .Style = SKPaintStyle.Fill,
                .Color = snap.Paint.Color.WithAlpha(235)
            }
                canvas.DrawRect(New SKRect(caretX, topY, caretX + CARET_WIDTH_PX, botY), pCaret)
            End Using
        End Sub

        Private Shared Function ClipToContent(rect As SKRect,
                                              contentR As SKRect) As SKRect
            Return New SKRect(
                Math.Max(rect.Left, contentR.Left),
                Math.Max(rect.Top, contentR.Top),
                Math.Min(rect.Right, contentR.Right),
                Math.Min(rect.Bottom, contentR.Bottom))
        End Function

        Private Shared Function ResolveSelectionColor(theme As IMASInputTheme) As SKColor
            If theme IsNot Nothing AndAlso theme.States.Focused.Ring.Enabled Then
                Return theme.States.Focused.Ring.Color.WithAlpha(72)
            End If

            Return SKColors.DeepSkyBlue.WithAlpha(72)
        End Function

    End Class

End Namespace
