Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
Imports Nexamas.UI.General
Imports Nexamas.UI.RuntimeEnvironment
Imports Nexamas.UI.Layout
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    Partial Public NotInheritable Class MASLabel
        Protected Overrides Sub Render(canvas As SKCanvas, ctx As MASThemeContext, pixelBounds As SKRect)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If String.IsNullOrEmpty(_text) Then Return
            If pixelBounds.Width <= 1.0F OrElse pixelBounds.Height <= 1.0F Then Return

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)

            Dim contentBounds As New SKRect(
                pixelBounds.Left + (_paddingLeft * dpi),
                pixelBounds.Top + (_paddingTop * dpi),
                pixelBounds.Right - (_paddingRight * dpi),
                pixelBounds.Bottom - (_paddingBottom * dpi))

            If contentBounds.Width <= 1.0F OrElse contentBounds.Height <= 1.0F Then Return

            Dim saveCount As Integer = canvas.Save()

            Try
                canvas.ClipRect(contentBounds, SKClipOperation.Intersect, True)

                If _multiLine Then
                    DrawMultiLine(canvas, ctx, contentBounds)
                Else
                    DrawSingleLine(canvas, ctx, contentBounds)
                End If
            Finally
                canvas.RestoreToCount(saveCount)
            End Try
        End Sub

        Protected Overrides Function WantsKeyboardFocus() As Boolean
            Return False
        End Function

        Protected Overrides Function WantsPointerFocus() As Boolean
            Return False
        End Function

        Protected Overrides Function ExcludeFromPointerRouting() As Boolean
            Return True
        End Function

        Private Sub DrawSingleLine(canvas As SKCanvas, ctx As MASThemeContext, bounds As SKRect)
            Using paint As SKPaint = CreateResolvedPaint(ctx)
                If paint Is Nothing Then Return

                Dim fitted As String = String.Empty
                Dim textWidth As Single = 0.0F

                ResolveSingleLineLayout(ctx, bounds.Width, paint, fitted, textWidth)
                If fitted.Length = 0 Then Return

                Dim x As Single = ResolveAlignedX(bounds, textWidth, fitted, ctx)
                Dim y As Single = ResolveSingleLineBaseline(bounds, paint)

                DrawResolvedLabelText(canvas, fitted, x, y, paint)
            End Using
        End Sub

        Private Sub DrawMultiLine(canvas As SKCanvas, ctx As MASThemeContext, bounds As SKRect)
            Using paint As SKPaint = CreateResolvedPaint(ctx)
                If paint Is Nothing Then Return

                Dim lines() As String = Nothing
                Dim widths() As Single = Nothing
                Dim lineHeight As Single = 0.0F
                Dim totalHeight As Single = 0.0F
                Dim ascent As Single = 0.0F
                Dim descent As Single = 0.0F

                ResolveMultiLineLayout(ctx, bounds.Width, paint, lines, widths, lineHeight, totalHeight, ascent, descent)
                If lines Is Nothing OrElse lines.Length = 0 Then Return

                Dim y As Single

                Select Case _verticalAlignment
                    Case MASVerticalAlignment.Top
                        y = bounds.Top - ascent

                    Case MASVerticalAlignment.Bottom
                        y = bounds.Bottom - totalHeight - ascent + lineHeight

                    Case Else
                        y = bounds.MidY - (totalHeight * 0.5F) - ascent
                End Select

                For i As Integer = 0 To lines.Length - 1
                    Dim line As String = If(lines(i), String.Empty)

                    If line.Length > 0 Then
                        Dim textWidth As Single = If(widths IsNot Nothing AndAlso i < widths.Length, widths(i), MASShapedText.MeasureWidth(line, paint))
                        Dim x As Single = ResolveAlignedX(bounds, textWidth, line, ctx)
                        DrawResolvedLabelText(canvas, line, x, y, paint)
                    End If

                    y += lineHeight
                Next
            End Using
        End Sub

        Private Sub ResolveSingleLineLayout(ctx As MASThemeContext,
                                            maxWidthPx As Single,
                                            paint As SKPaint,
                                            ByRef fittedText As String,
                                            ByRef textWidthPx As Single)

            fittedText = String.Empty
            textWidthPx = 0.0F

            If ctx Is Nothing OrElse paint Is Nothing Then Return
            If maxWidthPx <= 1.0F Then Return

            Dim renderableText As String = MASLabelTypographyPolicy.ResolveRenderableText(_text, allowMultiline:=False)
            Dim environmentStamp As MASRuntimeEnvironmentStamp =
                MASRuntimeEnvironmentStampBuilder.FromThemeContextAndTextDirection(ctx, MASShapedText.IsRtl(renderableText)).Build()

            If _singleLineLayoutValid AndAlso
               String.Equals(_singleLineLayoutText, renderableText, StringComparison.Ordinal) AndAlso
               NearlyEqual(_singleLineLayoutWidthPx, maxWidthPx) AndAlso
               If(_singleLineLayoutEnvironmentStamp, MASRuntimeEnvironmentStamp.Empty).SameAs(environmentStamp) AndAlso
               _singleLineLayoutThemeStamp = ctx.ThemeStamp AndAlso
               NearlyEqual(_singleLineLayoutDpi, PixelSnap.SafeDpi(ctx.Dpi)) AndAlso
               _singleLineLayoutTextStyle = _textStyle AndAlso
               NearlyEqual(_singleLineLayoutFontSizeDip, _fontSizeDip) AndAlso
               _singleLineLayoutFontWeight = _fontWeight Then

                fittedText = _singleLineLayoutFittedText
                textWidthPx = _singleLineLayoutTextWidthPx
                Return
            End If

            fittedText = MASTextCore.FitEllipsis(renderableText, maxWidthPx, paint)
            If fittedText.Length > 0 Then
                textWidthPx = MASShapedText.MeasureWidth(fittedText, paint)
            End If

            _singleLineLayoutValid = True
            _singleLineLayoutText = renderableText
            _singleLineLayoutWidthPx = maxWidthPx
            _singleLineLayoutThemeStamp = ctx.ThemeStamp
            _singleLineLayoutDpi = PixelSnap.SafeDpi(ctx.Dpi)
            _singleLineLayoutTextStyle = _textStyle
            _singleLineLayoutFontSizeDip = _fontSizeDip
            _singleLineLayoutFontWeight = _fontWeight
            _singleLineLayoutFittedText = fittedText
            _singleLineLayoutTextWidthPx = textWidthPx
            _singleLineLayoutEnvironmentStamp = environmentStamp
        End Sub

        Private Sub ResolveMultiLineLayout(ctx As MASThemeContext,
                                           maxWidthPx As Single,
                                           paint As SKPaint,
                                           ByRef lines() As String,
                                           ByRef widths() As Single,
                                           ByRef lineHeightPx As Single,
                                           ByRef totalHeightPx As Single,
                                           ByRef ascentPx As Single,
                                           ByRef descentPx As Single)

            lines = Nothing
            widths = Nothing
            lineHeightPx = 0.0F
            totalHeightPx = 0.0F
            ascentPx = 0.0F
            descentPx = 0.0F

            If ctx Is Nothing OrElse paint Is Nothing Then Return
            If maxWidthPx <= 1.0F Then Return

            Dim renderableText As String = MASLabelTypographyPolicy.ResolveRenderableText(_text, allowMultiline:=True)
            Dim environmentStamp As MASRuntimeEnvironmentStamp =
                MASRuntimeEnvironmentStampBuilder.FromThemeContextAndTextDirection(ctx, MASShapedText.IsRtl(renderableText)).Build()

            If _multiLineLayoutValid AndAlso
               String.Equals(_multiLineLayoutText, renderableText, StringComparison.Ordinal) AndAlso
               NearlyEqual(_multiLineLayoutWidthPx, maxWidthPx) AndAlso
               If(_multiLineLayoutEnvironmentStamp, MASRuntimeEnvironmentStamp.Empty).SameAs(environmentStamp) AndAlso
               _multiLineLayoutThemeStamp = ctx.ThemeStamp AndAlso
               NearlyEqual(_multiLineLayoutDpi, PixelSnap.SafeDpi(ctx.Dpi)) AndAlso
               _multiLineLayoutTextStyle = _textStyle AndAlso
               NearlyEqual(_multiLineLayoutFontSizeDip, _fontSizeDip) AndAlso
               _multiLineLayoutFontWeight = _fontWeight AndAlso
               _multiLineLayoutWordWrap = _wordWrap AndAlso
               _multiLineLayoutMaxLines = _maxLines AndAlso
               NearlyEqual(_multiLineLayoutLineHeightMultiplier, _lineHeightMultiplier) Then

                lines = _multiLineLayoutLines
                widths = _multiLineLayoutWidthsPx
                lineHeightPx = _multiLineLayoutLineHeightPx
                totalHeightPx = _multiLineLayoutTotalHeightPx
                ascentPx = _multiLineLayoutAscentPx
                descentPx = _multiLineLayoutDescentPx
                Return
            End If

            Dim builtLines As List(Of String) = BuildLines(renderableText, paint, maxWidthPx)
            If builtLines.Count = 0 Then
                _multiLineLayoutValid = True
                _multiLineLayoutText = renderableText
                _multiLineLayoutWidthPx = maxWidthPx
                _multiLineLayoutThemeStamp = ctx.ThemeStamp
                _multiLineLayoutDpi = PixelSnap.SafeDpi(ctx.Dpi)
                _multiLineLayoutTextStyle = _textStyle
                _multiLineLayoutFontSizeDip = _fontSizeDip
                _multiLineLayoutFontWeight = _fontWeight
                _multiLineLayoutWordWrap = _wordWrap
                _multiLineLayoutMaxLines = _maxLines
                _multiLineLayoutLineHeightMultiplier = _lineHeightMultiplier
                _multiLineLayoutLines = Array.Empty(Of String)()
                _multiLineLayoutWidthsPx = Array.Empty(Of Single)()
                _multiLineLayoutLineHeightPx = 0.0F
                _multiLineLayoutTotalHeightPx = 0.0F
                _multiLineLayoutAscentPx = 0.0F
                _multiLineLayoutDescentPx = 0.0F
                _multiLineLayoutEnvironmentStamp = environmentStamp

                lines = _multiLineLayoutLines
                widths = _multiLineLayoutWidthsPx
                Return
            End If

            Dim resolvedLines(builtLines.Count - 1) As String
            Dim resolvedWidths(builtLines.Count - 1) As Single

            For i As Integer = 0 To builtLines.Count - 1
                resolvedLines(i) = If(builtLines(i), String.Empty)
                If resolvedLines(i).Length > 0 Then
                    resolvedWidths(i) = MASShapedText.MeasureWidth(resolvedLines(i), paint)
                Else
                    resolvedWidths(i) = 0.0F
                End If
            Next

            Dim fm As SKFontMetrics = paint.FontMetrics
            lineHeightPx = ResolveLineHeight(paint)
            totalHeightPx = ResolveTotalTextHeight(paint, resolvedLines.Length)
            ascentPx = fm.Ascent
            descentPx = fm.Descent

            _multiLineLayoutValid = True
            _multiLineLayoutText = renderableText
            _multiLineLayoutWidthPx = maxWidthPx
            _multiLineLayoutThemeStamp = ctx.ThemeStamp
            _multiLineLayoutDpi = PixelSnap.SafeDpi(ctx.Dpi)
            _multiLineLayoutTextStyle = _textStyle
            _multiLineLayoutFontSizeDip = _fontSizeDip
            _multiLineLayoutFontWeight = _fontWeight
            _multiLineLayoutWordWrap = _wordWrap
            _multiLineLayoutMaxLines = _maxLines
            _multiLineLayoutLineHeightMultiplier = _lineHeightMultiplier
            _multiLineLayoutLines = resolvedLines
            _multiLineLayoutWidthsPx = resolvedWidths
            _multiLineLayoutLineHeightPx = lineHeightPx
            _multiLineLayoutTotalHeightPx = totalHeightPx
            _multiLineLayoutAscentPx = ascentPx
            _multiLineLayoutDescentPx = descentPx
            _multiLineLayoutEnvironmentStamp = environmentStamp

            lines = _multiLineLayoutLines
            widths = _multiLineLayoutWidthsPx
        End Sub

        Private Function BuildLines(text As String, paint As SKPaint, maxWidth As Single) As List(Of String)
            Return MASLabelTypographyPolicy.BuildWrappedLines(text, paint, maxWidth, _wordWrap, _maxLines)
        End Function


        Private Shared Sub DrawResolvedLabelText(canvas As SKCanvas,
                                                 text As String,
                                                 x As Single,
                                                 baselineY As Single,
                                                 paint As SKPaint)
            If canvas Is Nothing OrElse paint Is Nothing Then Return
            If String.IsNullOrEmpty(text) Then Return

            Dim drawX As Single = SnapLabelTextCoordinate(x)
            Dim drawBaselineY As Single = SnapLabelTextCoordinate(baselineY)

            MASShapedText.Draw(canvas, text, drawX, drawBaselineY, paint)
        End Sub


        Private Function CreateResolvedPaint(ctx As MASThemeContext) As SKPaint
            If ctx Is Nothing OrElse ctx.Typography Is Nothing Then Return Nothing

            Dim color As SKColor = MASLabelTypographyPolicy.ResolveTextColor(Enabled, _textRole, _hasTextColor, _textColor, ctx)
            Dim paint As SKPaint = ctx.Typography.CreatePaint(_textStyle, color)

            If paint Is Nothing Then Return Nothing

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)

            If _fontSizeDip > 0.0F Then
                paint.TextSize = _fontSizeDip * dpi
            End If

            If _fontWeight > 0 Then
                Dim typeface As SKTypeface = ResolveTypeface(_fontWeight)
                If typeface IsNot Nothing Then paint.Typeface = typeface
            End If

            paint.TextSize = SnapLabelTextSize(paint.TextSize)
            ConfigureLabelTextPaint(paint)

            Return paint
        End Function

        Private Function ResolveTextColor(ctx As MASThemeContext) As SKColor
            Return MASLabelTypographyPolicy.ResolveTextColor(Enabled, _textRole, _hasTextColor, _textColor, ctx)
        End Function

        Private Function ResolveAlignedX(bounds As SKRect,
                                         textWidth As Single,
                                         text As String,
                                         ctx As MASThemeContext) As Single
            Select Case MASLabelTypographyPolicy.ResolveEffectiveHorizontalAlignment(_horizontalAlignment, text, ctx)
                Case MASHorizontalAlignment.Center
                    Return bounds.MidX - (textWidth * 0.5F)
                Case MASHorizontalAlignment.Right
                    Return bounds.Right - textWidth
                Case Else
                    Return bounds.Left
            End Select
        End Function

        Private Function ResolveSingleLineBaseline(bounds As SKRect, paint As SKPaint) As Single
            Return MASLabelTypographyPolicy.ResolveBaseline(bounds, paint, _verticalAlignment)
        End Function

        Private Function ResolveLineHeight(paint As SKPaint) As Single
            Dim fm As SKFontMetrics = paint.FontMetrics
            Dim rawLineHeight As Single = Math.Max(1.0F, fm.Descent - fm.Ascent)
            Return Math.Max(rawLineHeight, paint.TextSize * _lineHeightMultiplier)
        End Function

        Private Function ResolveTotalTextHeight(paint As SKPaint, lineCount As Integer) As Single
            If lineCount <= 0 Then Return 0.0F
            Return CSng(lineCount) * ResolveLineHeight(paint)
        End Function


        Private Shared Sub ConfigureLabelTextPaint(paint As SKPaint)
            If paint Is Nothing Then Return

            paint.IsAntialias = True
            paint.IsDither = True
        End Sub

        Private Shared Function SnapLabelTextSize(value As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) Then Return 12.0F
            If value <= 0.0F Then Return 1.0F

            Return CSng(Math.Round(value * 2.0F, MidpointRounding.AwayFromZero) / 2.0F)
        End Function

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

            Return CSng(Math.Round(value, MidpointRounding.AwayFromZero))
        End Function


        Private Shared Function ResolveTypeface(fontWeight As Integer) As SKTypeface
            Dim normalizedWeight As Integer = MASLabelTypographyPolicy.NormalizeFontWeight(fontWeight)

            SyncLock _typefaceLock
                If _typefaceCache.ContainsKey(normalizedWeight) Then
                    Return _typefaceCache(normalizedWeight)
                End If

                Dim typeface As SKTypeface =
                    SKTypeface.FromFamilyName(
                        "Segoe UI Variable",
                        New SKFontStyle(normalizedWeight, 5, SKFontStyleSlant.Upright))

                If typeface Is Nothing Then
                    typeface =
                        SKTypeface.FromFamilyName(
                            "Segoe UI",
                            New SKFontStyle(normalizedWeight, 5, SKFontStyleSlant.Upright))
                End If

                _typefaceCache(normalizedWeight) = typeface
                Return typeface
            End SyncLock
        End Function

        Private Shared Function NormalizeFontWeight(value As Integer) As Integer
            Return MASLabelTypographyPolicy.NormalizeFontWeight(value)
        End Function

        Private Shared Function NormalizeNewLines(value As String) As String
            Return MASLabelTypographyPolicy.NormalizeLabelText(value)
        End Function
    End Class

End Namespace
