Option Strict On
Option Explicit On

Imports System
Imports Nexamas.UI.TextRendering
Imports SkiaSharp

Namespace Nexamas.UI.TextInput

    Friend NotInheritable Class MASTextInlineLayout

#Region "Fields"

        Private ReadOnly _caretXs As Single()

#End Region

#Region "Properties"

        Friend ReadOnly Property Text As String
        Friend ReadOnly Property IsRtl As Boolean
        Friend ReadOnly Property ContentRect As SKRect
        Friend ReadOnly Property ScrollOffsetPx As Single
        Friend ReadOnly Property FullWidth As Single
        Friend ReadOnly Property TextX As Single
        Friend ReadOnly Property ShapedRun As MASTextShapedRun

#End Region

#Region "Factory"

        Friend Shared Function Create(contentR As SKRect,
                                      paint As SKPaint,
                                      scrollOffsetPx As Single,
                                      displayText As String,
                                      isRtl As Boolean,
                                      Optional crispRendering As Boolean = True) As MASTextInlineLayout

            Return New MASTextInlineLayout(
                contentR:=contentR,
                paint:=paint,
                scrollOffsetPx:=scrollOffsetPx,
                displayText:=displayText,
                isRtl:=isRtl,
                crispRendering:=crispRendering
            )
        End Function

#End Region

#Region "Constructor"

        Private Sub New(contentR As SKRect,
                        paint As SKPaint,
                        scrollOffsetPx As Single,
                        displayText As String,
                        isRtl As Boolean,
                        crispRendering As Boolean)

            Text = If(displayText, String.Empty)
            Me.IsRtl = isRtl
            Me.ContentRect = contentR
            Me.ScrollOffsetPx = NormalizeScroll(scrollOffsetPx)

            If paint Is Nothing OrElse Text.Length = 0 Then
                FullWidth = 0.0F
                TextX = ResolveTextOriginX(
                    If(isRtl,
                       contentR.Right + Me.ScrollOffsetPx,
                       contentR.Left - Me.ScrollOffsetPx),
                    crispRendering)

                ShapedRun = MASTextShapedRun.Empty(Text)
                _caretXs = New Single() {TextX}
                Return
            End If

            ShapedRun = MASShapedText.GetShapedRun(Text, paint)

            Dim trailingStart As Integer = GetTrailingWhitespaceStart(Text)
            Dim visibleText As String = Text.Substring(0, trailingStart)
            Dim trailingText As String = Text.Substring(trailingStart)

            Dim visibleWidth As Single = MeasureVisibleWidth(visibleText, paint)
            Dim trailingWidth As Single = MeasureWhitespaceAdvance(trailingText, paint)

            FullWidth = visibleWidth + trailingWidth

            If isRtl Then
                ' مهم جداً:
                ' لا تجعل trailing spaces تغيّر موضع رسم الكلمة.
                TextX = ResolveTextOriginX(contentR.Right - visibleWidth + Me.ScrollOffsetPx, crispRendering)
            Else
                TextX = ResolveTextOriginX(contentR.Left - Me.ScrollOffsetPx, crispRendering)
            End If

            _caretXs = BuildStableCaretMap(
                text:=Text,
                paint:=paint,
                textX:=TextX,
                visibleWidth:=visibleWidth,
                trailingStart:=trailingStart,
                isRtl:=isRtl
            )
        End Sub

#End Region

#Region "Caret / Hit test"

        Friend Function GetCaretX(index As Integer) As Single
            If _caretXs Is Nothing OrElse _caretXs.Length = 0 Then Return TextX

            If index < 0 Then index = 0
            If index >= _caretXs.Length Then index = _caretXs.Length - 1

            Return _caretXs(index)
        End Function

        Friend Function GetIndexFromX(x As Single) As Integer
            If _caretXs Is Nothing OrElse _caretXs.Length <= 1 Then Return 0

            Dim bestIndex As Integer = 0
            Dim bestDistance As Single = Math.Abs(x - _caretXs(0))

            For i As Integer = 1 To _caretXs.Length - 1
                Dim distance As Single = Math.Abs(x - _caretXs(i))

                If distance < bestDistance Then
                    bestDistance = distance
                    bestIndex = i
                End If
            Next

            Return bestIndex
        End Function

        Friend Function GetNextVisualCaretIndex(currentIndex As Integer,
                                                direction As Integer) As Integer

            If _caretXs Is Nothing OrElse _caretXs.Length <= 1 Then Return 0

            If currentIndex < 0 Then currentIndex = 0
            If currentIndex >= _caretXs.Length Then currentIndex = _caretXs.Length - 1
            If direction = 0 Then Return currentIndex

            Dim currentX As Single = _caretXs(currentIndex)

            Dim bestIndex As Integer = currentIndex
            Dim bestDistance As Single = Single.MaxValue

            For i As Integer = 0 To _caretXs.Length - 1
                If i = currentIndex Then Continue For

                Dim dx As Single = _caretXs(i) - currentX

                If direction > 0 Then
                    If dx > 0.01F AndAlso dx < bestDistance Then
                        bestDistance = dx
                        bestIndex = i
                    End If
                Else
                    If dx < -0.01F AndAlso -dx < bestDistance Then
                        bestDistance = -dx
                        bestIndex = i
                    End If
                End If
            Next

            Return bestIndex
        End Function

#End Region

#Region "Selection"

        Friend Function GetSelectionRect(selectionStart As Integer,
                                         selectionLength As Integer,
                                         baselineY As Single,
                                         paint As SKPaint,
                                         contentR As SKRect) As SKRect

            If selectionLength <= 0 Then Return SKRect.Empty
            If paint Is Nothing Then Return SKRect.Empty

            Dim selStart As Integer = Math.Max(0, selectionStart)
            Dim selEnd As Integer = Math.Max(0, selectionStart + selectionLength)

            If selStart > Text.Length Then selStart = Text.Length
            If selEnd > Text.Length Then selEnd = Text.Length
            If selEnd <= selStart Then Return SKRect.Empty

            Dim x1 As Single = GetCaretX(selStart)
            Dim x2 As Single = GetCaretX(selEnd)

            Dim leftX As Single = Math.Max(contentR.Left, Math.Min(x1, x2))
            Dim rightX As Single = Math.Min(contentR.Right, Math.Max(x1, x2))

            If rightX <= leftX Then Return SKRect.Empty

            Dim fm As SKFontMetrics = MASTextMetrics.ResolveFontMetrics(paint)
            Dim topY As Single = Math.Max(contentR.Top + 1.0F, baselineY + fm.Ascent)
            Dim botY As Single = Math.Min(contentR.Bottom - 1.0F, baselineY + fm.Descent)

            If botY <= topY Then
                botY = Math.Min(
                    contentR.Bottom - 1.0F,
                    topY + Math.Max(8.0F, paint.TextSize)
                )
            End If

            Return New SKRect(leftX, topY, rightX, botY)
        End Function

#End Region

#Region "Stable caret map"

        Private Shared Function BuildStableCaretMap(text As String,
                                                    paint As SKPaint,
                                                    textX As Single,
                                                    visibleWidth As Single,
                                                    trailingStart As Integer,
                                                    isRtl As Boolean) As Single()

            Dim s As String = If(text, String.Empty)
            Dim n As Integer = s.Length
            Dim xs(n) As Single

            If n = 0 OrElse paint Is Nothing Then
                xs(0) = textX
                Return xs
            End If

            Dim shapedCaretXs As Single() = MASShapedText.BuildCaretXs(s, paint, textX, isRtl)
            If shapedCaretXs IsNot Nothing AndAlso shapedCaretXs.Length = n + 1 Then
                Return shapedCaretXs
            End If

            If trailingStart < 0 Then trailingStart = 0
            If trailingStart > n Then trailingStart = n

            Dim visibleText As String = s.Substring(0, trailingStart)

            If isRtl Then
                ' النص المرئي:
                ' index 0 = يمين النص
                ' index trailingStart = يسار النص المرئي
                For i As Integer = 0 To trailingStart
                    Dim suffix As String = visibleText.Substring(i)
                    xs(i) = textX + MeasureVisibleWidth(suffix, paint)
                Next

                ' trailing spaces في RTL تظهر يسار النص، بدون تحريك الكلمة نفسها.
                Dim currentX As Single = textX

                For i As Integer = trailingStart + 1 To n
                    currentX -= MeasureCharAdvance(s(i - 1), paint)
                    xs(i) = currentX
                Next
            Else
                For i As Integer = 0 To trailingStart
                    Dim prefix As String = visibleText.Substring(0, i)
                    xs(i) = textX + MeasureVisibleWidth(prefix, paint)
                Next

                Dim currentX As Single = textX + visibleWidth

                For i As Integer = trailingStart + 1 To n
                    currentX += MeasureCharAdvance(s(i - 1), paint)
                    xs(i) = currentX
                Next
            End If

            Return xs
        End Function

        Private Shared Function GetTrailingWhitespaceStart(value As String) As Integer
            If String.IsNullOrEmpty(value) Then Return 0

            Dim i As Integer = value.Length

            While i > 0 AndAlso Char.IsWhiteSpace(value(i - 1))
                i -= 1
            End While

            Return i
        End Function

        Private Shared Function MeasureVisibleWidth(value As String,
                                                    paint As SKPaint) As Single

            If String.IsNullOrEmpty(value) OrElse paint Is Nothing Then Return 0.0F
            Return Math.Max(0.0F, MASShapedText.MeasureWidth(value, paint))
        End Function

        Private Shared Function MeasureWhitespaceAdvance(value As String,
                                                         paint As SKPaint) As Single

            If String.IsNullOrEmpty(value) OrElse paint Is Nothing Then Return 0.0F

            Dim total As Single = 0.0F

            For Each ch As Char In value
                total += MeasureCharAdvance(ch, paint)
            Next

            Return total
        End Function

        Private Shared Function MeasureCharAdvance(ch As Char,
                                                   paint As SKPaint) As Single

            If paint Is Nothing Then Return 4.0F

            If ch = ControlChars.Tab Then
                Return MeasureSingleSpace(paint) * 4.0F
            End If

            If Char.IsWhiteSpace(ch) Then
                Return MeasureSingleSpace(paint)
            End If

            Dim w As Single = MASTextMetrics.MeasureSimpleTextWidth(ch.ToString(), paint)

            If w > 0.01F AndAlso
               Not Single.IsNaN(w) AndAlso
               Not Single.IsInfinity(w) Then
                Return w
            End If

            Return Math.Max(2.0F, paint.TextSize * 0.25F)
        End Function

        Private Shared Function MeasureSingleSpace(paint As SKPaint) As Single
            If paint Is Nothing Then Return 4.0F

            Dim direct As Single = MASTextMetrics.MeasureSimpleTextWidth(" ", paint)

            If direct > 0.01F AndAlso
               Not Single.IsNaN(direct) AndAlso
               Not Single.IsInfinity(direct) Then
                Return direct
            End If

            Dim diff As Single = MASTextMetrics.MeasureSimpleTextWidth("n n", paint) - MASTextMetrics.MeasureSimpleTextWidth("nn", paint)

            If diff > 0.01F AndAlso
               Not Single.IsNaN(diff) AndAlso
               Not Single.IsInfinity(diff) Then
                Return diff
            End If

            Return Math.Max(3.0F, paint.TextSize * 0.28F)
        End Function

#End Region

#Region "Helpers"

        Private Shared Function ResolveTextOriginX(value As Single,
                                                   crispRendering As Boolean) As Single
            If Not crispRendering Then Return value
            Return MASTextCrispRendering.SnapTextX(value)
        End Function

        Private Shared Function NormalizeScroll(value As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) Then Return 0.0F
            Return value
        End Function

#End Region

    End Class

End Namespace