Option Strict On
Option Explicit On

Imports System
Imports System.Windows.Forms
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.TextInput
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    Partial Public MustInherit Class MASInputBoxSkiaBase

#Region "Caret + hit testing in text"

        Private Function GetIndexFromPoint(ctx As MASThemeContext, ptPx As SKPoint) As Integer
            If ctx Is Nothing Then Return 0

            Dim contentR As SKRect = GetContentRectPx(ctx)
            Dim pText As SKPaint = CreateTextPaint(ctx)
            If pText Is Nothing Then Return 0

            Dim drawText As String = GetDisplayText()

            If _multilineController.Enabled Then
                Return _layoutEngine.GetMultilineIndexFromPoint(
                    contentR:=contentR,
                    paint:=pText,
                    verticalScrollOffsetPx:=_multilineController.VerticalScrollOffsetPx,
                    horizontalScrollOffsetPx:=_multilineController.ResolveHorizontalScrollOffset(_controller.ScrollOffsetPx),
                    ptPx:=ptPx,
                    displayText:=drawText,
                    wordWrap:=_multilineController.WordWrap,
                    crispRendering:=_textCrispRendering
                )
            End If

            Dim isRtl As Boolean = ResolveIsRtl(drawText)

            Return _layoutEngine.GetIndexFromPoint(
                contentR:=contentR,
                paint:=pText,
                scrollOffsetPx:=_controller.ScrollOffsetPx,
                ptPx:=ptPx,
                isRtl:=isRtl,
                displayText:=drawText,
                crispRendering:=_textCrispRendering
            )
        End Function

        Private Function GetCaretX(contentR As SKRect,
                                   pText As SKPaint,
                                   isRtl As Boolean,
                                   drawText As String) As Single

            Return _layoutEngine.GetCaretX(
                contentR:=contentR,
                paint:=pText,
                scrollOffsetPx:=_controller.ScrollOffsetPx,
                caretIndex:=_state.CaretIndex,
                isRtl:=isRtl,
                displayText:=drawText,
                crispRendering:=_textCrispRendering
            )
        End Function

        Private Function GetCaretXForIndex(contentR As SKRect,
                                           pText As SKPaint,
                                           index As Integer,
                                           isRtl As Boolean,
                                           drawText As String) As Single

            Return _layoutEngine.GetCaretXForIndex(
                contentR:=contentR,
                paint:=pText,
                scrollOffsetPx:=_controller.ScrollOffsetPx,
                index:=index,
                isRtl:=isRtl,
                displayText:=drawText,
                crispRendering:=_textCrispRendering
            )
        End Function

#End Region

#Region "Scroll"

        Private Sub EnsureCaretVisible(ctx As MASThemeContext)
            If ctx Is Nothing Then Return

            Dim contentR As SKRect = GetContentRectPx(ctx)
            If contentR.Width <= 2.0F Then
                _controller.ResetScroll()
                Return
            End If

            Dim pText As SKPaint = CreateTextPaint(ctx)
            If pText Is Nothing Then Return

            Dim drawText As String = GetDisplayText()

            If _multilineController.Enabled Then
                Dim paragraph As MASTextParagraphLayout =
                    _layoutEngine.BuildParagraphLayout(
                        contentR:=contentR,
                        paint:=pText,
                        displayText:=drawText,
                        wordWrap:=_multilineController.WordWrap,
                        verticalScrollOffsetPx:=_multilineController.VerticalScrollOffsetPx,
                        horizontalScrollOffsetPx:=_multilineController.ResolveHorizontalScrollOffset(_controller.ScrollOffsetPx),
                        crispRendering:=_textCrispRendering,
                        caretIndex:=_state.CaretIndex
                    )

                Dim newScroll As Single =
                    _multilineViewport.EnsureCaretVisible(
                        contentR:=contentR,
                        paragraph:=paragraph,
                        caretIndex:=_state.CaretIndex,
                        currentVerticalScrollPx:=_multilineController.VerticalScrollOffsetPx
                    )

                If _multilineController.SetVerticalScrollOffset(newScroll) Then
                    InvalidateTextLayout()
                End If

                Return
            End If

            Dim isRtl As Boolean = ResolveIsRtl(drawText)

            _controller.EnsureCaretVisible(
                layoutEngine:=_layoutEngine,
                contentR:=contentR,
                paint:=pText,
                isRtl:=isRtl,
                displayText:=drawText,
                crispRendering:=_textCrispRendering
            )
        End Sub

        Private Sub EnsureScrollInRange(contentR As SKRect,
                                        pText As SKPaint,
                                        drawText As String,
                                        isRtl As Boolean)

            If _multilineController.Enabled Then
                Dim paragraph As MASTextParagraphLayout =
                    _layoutEngine.BuildParagraphLayout(
                        contentR:=contentR,
                        paint:=pText,
                        displayText:=drawText,
                        wordWrap:=_multilineController.WordWrap,
                        verticalScrollOffsetPx:=_multilineController.VerticalScrollOffsetPx,
                        horizontalScrollOffsetPx:=_multilineController.ResolveHorizontalScrollOffset(_controller.ScrollOffsetPx),
                        crispRendering:=_textCrispRendering,
                        caretIndex:=_state.CaretIndex
                    )

                Dim newScroll As Single =
                    _multilineViewport.EnsureScrollInRange(
                        contentR:=contentR,
                        paragraph:=paragraph,
                        currentVerticalScrollPx:=_multilineController.VerticalScrollOffsetPx
                    )

                If _multilineController.SetVerticalScrollOffset(newScroll) Then
                    InvalidateTextLayout()
                End If

                Return
            End If

            _controller.EnsureScrollInRange(
                layoutEngine:=_layoutEngine,
                contentR:=contentR,
                paint:=pText,
                isRtl:=isRtl,
                displayText:=drawText
            )
        End Sub

#End Region

#Region "Word helpers"

        Private Function IsDoubleClick(ptPx As SKPoint, idx As Integer) As Boolean
            Return _selectionController.IsDoubleClick(ptPx, idx)
        End Function

        Private Sub RememberClick(ptPx As SKPoint, idx As Integer)
            _selectionController.RememberClick(ptPx, idx)
        End Sub

#End Region

#Region "Text normalization + display"

        Private Function NormalizeAssignedText(value As String) As String
            Dim s As String = NormalizeTextForInputMode(FilterIncomingText(If(value, String.Empty)))

            If _state.MaxLength > 0 AndAlso s.Length > _state.MaxLength Then
                s = s.Substring(0, _state.MaxLength)
            End If

            Return s
        End Function

        Private Function NormalizeIncomingText(value As String, allowedSpace As Integer) As String
            Dim s As String = NormalizeTextForInputMode(FilterIncomingText(If(value, String.Empty)))

            If allowedSpace <= 0 Then Return ""

            If s.Length > allowedSpace Then
                s = s.Substring(0, allowedSpace)
            End If

            Return s
        End Function

        Private Function GetRemainingCapacity() As Integer
            Return _state.GetRemainingCapacity()
        End Function

        Private Function GetDisplayText() As String
            Return _state.DisplayText
        End Function

#End Region

#Region "Helpers"

        Private Function GetContentRectPx(ctx As MASThemeContext) As SKRect
            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            Dim slotContentPx As SKRect = ResolveSlotContentRectPx(ctx, dpi)
            If slotContentPx.Width > 1.0F AndAlso slotContentPx.Height > 1.0F Then
                Return slotContentPx
            End If

            Return ResolveLegacyContentRectPx(ctx, dpi)
        End Function

        Private Function ResolveSlotContentRectPx(ctx As MASThemeContext,
                                                  dpi As Single) As SKRect
            Dim logical As SKRect = GetLayoutContentBoundsLogicalInternal()
            If logical.IsEmpty OrElse logical.Width <= 1.0F OrElse logical.Height <= 1.0F Then Return SKRect.Empty

            Dim contentPx As New SKRect(
                logical.Left * dpi,
                logical.Top * dpi,
                logical.Right * dpi,
                logical.Bottom * dpi)

            Dim ringM As MASSurfaceInteractionRing.Metrics = MASSurfaceInteractionRing.GetMetrics(dpi)
            Dim ringSafe As Single = Math.Max(0.0F, ringM.RingPathInsetPx)

            Dim result As New SKRect(
                contentPx.Left + ringSafe,
                contentPx.Top + ringSafe,
                contentPx.Right - ringSafe,
                contentPx.Bottom - ringSafe)

            If result.Width <= 1.0F OrElse result.Height <= 1.0F Then Return SKRect.Empty
            Return PixelSnap.SnapRectHalf(result)
        End Function

        Private Function ResolveLegacyContentRectPx(ctx As MASThemeContext,
                                                    dpi As Single) As SKRect
            Dim pixelBounds As SKRect = GetPixelBounds(ctx)

            Dim ringM As MASSurfaceInteractionRing.Metrics = MASSurfaceInteractionRing.GetMetrics(dpi)
            Dim rBase As SKRect = MASSurfaceInteractionRing.SnapBaseRect(pixelBounds, ringM)

            Return InputLayoutPrimitives.ComputeInputContentRect(
                r:=rBase,
                dpi:=dpi,
                hasIcon:=HasLeftIcon,
                rightReservedWidthDip:=RightReservedWidthDip
            )
        End Function

        Private Function CreateTextPaint(ctx As MASThemeContext) As SKPaint
            If ctx Is Nothing Then Return Nothing
            If ctx.Typography Is Nothing Then Return Nothing
            If ctx.InputTheme Is Nothing Then Return Nothing

            Dim color As SKColor =
                ResolveTextColor(
                    th:=ctx.InputTheme,
                    isDisabled:=Not Me.Enabled,
                    isReadOnly:=IsReadOnlyBox,
                    rawText:=_state.Text
                )

            Dim paint As SKPaint = ctx.Typography.CreatePaint(MASTypography.MASTextStyle.FieldText, color)
            If paint Is Nothing Then Return Nothing

            Try
                If _fontProfile IsNot Nothing Then
                    _fontProfile.ApplyToPaint(paint, _state.Text)
                End If

                If _textFontSizeDip > 0.0F Then
                    Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
                    paint.TextSize = Math.Max(1.0F, _textFontSizeDip * dpi)
                End If

                If _textCrispRendering Then
                    paint.TextSize = MASTextCrispRendering.SnapTextSize(paint.TextSize)
                    MASTextCrispRendering.ConfigurePaint(paint)
                End If

                Return _renderResources.TextPaintCache.AdoptOrReuse(paint)
            Finally
                paint.Dispose()
            End Try
        End Function

        Private Shared Function ResolveIsRtl(value As String) As Boolean
            Try
                Return MASShapedText.IsRtl(value)
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowTextShapingFallback(masCaughtException1)
                Return False
            End Try
        End Function

        Private Function NormalizeTextForInputMode(value As String) As String
            If _multilineController.Enabled Then
                Return NormalizeLineEndings(value)
            End If

            Return NormalizeSingleLine(value)
        End Function

        Private Shared Function NormalizeLineEndings(value As String) As String
            Dim s As String = If(value, String.Empty)
            s = s.Replace(vbCrLf, vbLf)
            s = s.Replace(vbCr, vbLf)
            Return s
        End Function

        Private Shared Function NormalizeSingleLine(value As String) As String
            Dim s As String = If(value, "")
            s = s.Replace(vbCrLf, "")
            s = s.Replace(vbCr, "")
            s = s.Replace(vbLf, "")
            Return s
        End Function

        Private Sub ClampCaretAndSelection()
            _controller.ClampState()
        End Sub

        Private Function ResolveTextColor(th As IMASInputTheme,
                                          isDisabled As Boolean,
                                          isReadOnly As Boolean,
                                          rawText As String) As SKColor
            If th Is Nothing Then Return SKColors.Black

            If isDisabled Then
                Return th.Text.Disabled
            End If

            If isReadOnly Then
                Return th.Text.ReadOnlyText
            End If

            Return th.Text.Normal
        End Function

        Protected Function TryGetContext() As MASThemeContext
            If Not Me.HasHost OrElse Me.Host Is Nothing Then
                Return Nothing
            End If

            Try
                Return Me.Host.TryGetContext()
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowTextShapingFallback(masCaughtException2)
                Return Nothing
            End Try
        End Function

        Private Sub ResetCaretBlink()
            _renderResources.Caret.Reset()
        End Sub

        Private Sub UpdateCaretBlink()
            Dim changed As Boolean = _renderResources.Caret.Update(Me.HasKeyboardFocus)

            If changed Then
                InvalidateVisual()
            End If
        End Sub

#End Region

    End Class

End Namespace
