Option Strict On
Option Explicit On

Imports System
Imports System.Diagnostics
Imports System.Globalization
Imports Nexamas.UI.TextRendering
Imports SkiaSharp

Namespace Nexamas.UI.TextInput

    ' CONTRACT:
    ' MASTextInputLayoutSnapshotBuilder is the only component that creates
    ' MASTextInputLayoutSnapshot instances.
    '
    ' Stage 7B:
    ' - Keeps the current single-line InlineLayout as the active render path.
    ' - Does not build ParagraphLayout by default.
    ' - ParagraphLayout is attached only when Multiline=True.
    Friend NotInheritable Class MASTextInputLayoutSnapshotBuilder

        Friend Function Build(state As MASTextInputState,
                              layout As MASTextInputLayoutEngine,
                              paint As SKPaint,
                              contentR As SKRect,
                              scrollOffsetPx As Single,
                              Optional crispRendering As Boolean = True) As MASTextInputLayoutSnapshot

            Dim snap As New MASTextInputLayoutSnapshot()

            If state Is Nothing Then Return snap
            If layout Is Nothing Then Return snap
            If paint Is Nothing Then Return snap

            ValidateBuilderInputs(state, paint)

            Dim text As String = If(state.DisplayText, String.Empty)
            Dim isRtl As Boolean = DetectRtl(text)
            Dim safeScroll As Single = NormalizeScroll(scrollOffsetPx)

            Dim inlineLayout As MASTextInlineLayout =
                layout.BuildInlineLayout(
                    contentR:=contentR,
                    paint:=paint,
                    scrollOffsetPx:=safeScroll,
                    isRtl:=isRtl,
                    displayText:=text,
                    crispRendering:=crispRendering
                )

            ' Stage 7B performance:
            ' Do not build ParagraphLayout for the default single-line snapshot.
            ' MASInputBoxSkiaBase attaches ParagraphLayout only when Multiline=True.

            Dim baselineY As Single = MASTextMetrics.ResolveCenteredBaselineY(contentR, paint)

            snap.ContentRect = contentR
            snap.DisplayText = text
            snap.IsRtl = isRtl
            snap.Paint = paint
            snap.PaintSignature = CreatePaintSignature(paint)
            snap.ScrollOffsetPx = safeScroll
            snap.VerticalScrollOffsetPx = 0.0F
            snap.CaretIndex = ClampIndex(state.CaretIndex, text.Length)
            snap.SelectionStart = ClampIndex(state.SelectionStart, text.Length)
            snap.SelectionLength = ClampSelectionLength(snap.SelectionStart, state.SelectionLength, text.Length)
            snap.InlineLayout = inlineLayout
            snap.ParagraphLayout = Nothing
            snap.BaselineY = baselineY
            snap.CaretX = inlineLayout.GetCaretX(snap.CaretIndex)
            snap.TextVersion = state.Version

            ValidateSnapshot(snap)

            Return snap
        End Function

        Friend Shared Function CreatePaintSignature(paint As SKPaint) As String
            If paint Is Nothing Then Return "paint:null"

            Dim typefaceKey As String = "tf:null"
            If paint.Typeface IsNot Nothing Then
                Try
                    typefaceKey = "tf:" & paint.Typeface.Handle.ToInt64().ToString(CultureInfo.InvariantCulture)
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowTextShapingFallback(masCaughtException1)
                    typefaceKey = "tf:" & paint.Typeface.ToString()
                End Try
            End If

            Return typefaceKey & "|" &
                   paint.TextSize.ToString("0.###", CultureInfo.InvariantCulture) & "|" &
                   CUInt(paint.Color).ToString(CultureInfo.InvariantCulture) & "|" &
                   paint.IsAntialias.ToString(CultureInfo.InvariantCulture)
        End Function

        Private Shared Function DetectRtl(text As String) As Boolean
            Try
                Return MASShapedText.IsRtl(If(text, String.Empty))
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowTextShapingFallback(masCaughtException2)
                Return False
            End Try
        End Function

        Private Shared Function ClampIndex(value As Integer, maxLen As Integer) As Integer
            If value < 0 Then Return 0
            If value > maxLen Then Return maxLen
            Return value
        End Function

        Private Shared Function ClampSelectionLength(start As Integer,
                                                     length As Integer,
                                                     maxLen As Integer) As Integer
            If length <= 0 Then Return 0
            If start < 0 Then start = 0
            If start > maxLen Then Return 0

            Dim maxLength As Integer = maxLen - start
            If length > maxLength Then Return maxLength

            Return length
        End Function

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

        <Conditional("DEBUG")>
        Private Shared Sub ValidateBuilderInputs(state As MASTextInputState,
                                                 paint As SKPaint)

            Debug.Assert(state IsNot Nothing,
                         "SnapshotBuilder contract failed: state is Nothing.")

            Debug.Assert(paint IsNot Nothing,
                         "SnapshotBuilder contract failed: paint is Nothing.")

            If state Is Nothing Then Return

            Debug.Assert(state.DisplayText IsNot Nothing,
                         "SnapshotBuilder contract failed: DisplayText must never be Nothing.")
        End Sub

        <Conditional("DEBUG")>
        Private Shared Sub ValidateSnapshot(snap As MASTextInputLayoutSnapshot)
            Debug.Assert(snap IsNot Nothing,
                         "SnapshotBuilder contract failed: snapshot is Nothing.")

            If snap Is Nothing Then Return

            Dim textLength As Integer = If(snap.DisplayText, String.Empty).Length

            Debug.Assert(snap.DisplayText IsNot Nothing,
                         "SnapshotBuilder contract failed: snapshot DisplayText is Nothing.")

            Debug.Assert(snap.CaretIndex >= 0 AndAlso snap.CaretIndex <= textLength,
                         "SnapshotBuilder contract failed: CaretIndex outside DisplayText bounds.")

            Debug.Assert(snap.SelectionStart >= 0 AndAlso snap.SelectionStart <= textLength,
                         "SnapshotBuilder contract failed: SelectionStart outside DisplayText bounds.")

            Debug.Assert(snap.SelectionLength >= 0,
                         "SnapshotBuilder contract failed: SelectionLength must be >= 0.")

            Debug.Assert(snap.SelectionStart + snap.SelectionLength <= textLength,
                         "SnapshotBuilder contract failed: selection range exceeds DisplayText length.")

            Debug.Assert(snap.InlineLayout IsNot Nothing,
                         "SnapshotBuilder contract failed: InlineLayout is Nothing.")

            ' ParagraphLayout is optional here. It is attached only when Multiline=True.
        End Sub

    End Class

End Namespace
