Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Globalization
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Controls
Imports Nexamas.UI.General
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    Friend NotInheritable Class UnifiedButtonRenderer

        Private Sub New()
        End Sub

#Region "Paint cache"

        Friend Structure PaintKey
            Public ReadOnly StyleId As Integer
            Public ReadOnly ColorArgb As UInteger
            Public ReadOnly Dpi100 As Integer

            Friend Sub New(styleId As Integer, color As SKColor, dpi As Single)
                Me.StyleId = styleId
                Me.ColorArgb = PackColorArgb(color)
                Me.Dpi100 = CInt(Math.Round(dpi * 100.0F))
            End Sub

            Private Shared Function PackColorArgb(c As SKColor) As UInteger
                Return (CUInt(c.Alpha) << 24) Or
                       (CUInt(c.Red) << 16) Or
                       (CUInt(c.Green) << 8) Or
                       CUInt(c.Blue)
            End Function
        End Structure

        Friend NotInheritable Class PaintCacheEntry
            Friend Property Paint As SKPaint
            Friend Property Metrics As SKFontMetrics
            Friend Property HasMetrics As Boolean
            Friend Property TypefaceKey As String
        End Class

        Friend NotInheritable Class RenderResources
            Implements IDisposable

            Public ReadOnly MetricsCache As New Dictionary(Of PaintKey, PaintCacheEntry)()
            Public ReadOnly MeasureCache As New Dictionary(Of String, Single)()

            Friend Const MeasureCacheLimit As Integer = 256

            Private _disposed As Boolean

            Public Sub Dispose() Implements IDisposable.Dispose
                If _disposed Then Return
                _disposed = True

                MetricsCache.Clear()
                MeasureCache.Clear()
            End Sub

        End Class

#End Region

#Region "Draw entry"

        Friend Shared Sub Draw(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               primitives As MASVisualPrimitivesPainter,
                               shadow As MASShadowPainter,
                               rPx As SKRect,
                               personality As MASButtonPersonality,
                               text As String,
                               state As MASButtonState,
                               contract As MASResolvedComponentContract,
                               isSelected As Boolean,
                               isDefault As Boolean,
                               intent As MASButtonIntent,
                               drawIcon As Action(Of SKCanvas, SKRect, Single, SKColor),
                               drawIconEmphasis As Action(Of SKCanvas, SKRect, Single, SKColor),
                               iconPlacement As MASButtonIconPlacement,
                               dpi As Single,
                               res As RenderResources)

            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If primitives Is Nothing OrElse shadow Is Nothing OrElse res Is Nothing Then Return
            If contract Is Nothing Then Throw New ArgumentNullException(NameOf(contract))
            If rPx.Width <= 1.0F OrElse rPx.Height <= 1.0F Then Return

            Dim scope As MASContractRenderScope =
    CreateDrawScope(contract)

            scope.RequireAnyLayer(
    MASRenderLayer.Surface,
    MASRenderLayer.Shadow,
    MASRenderLayer.Text,
    MASRenderLayer.Hover,
    MASRenderLayer.Pressed,
    MASRenderLayer.Focus)

            dpi = PixelSnap.SafeDpi(dpi)

            Dim app As IMASAppTheme = ctx.Theme
            If app Is Nothing Then Return

            Dim m As MASSurfaceInteractionRing.Metrics = MASSurfaceInteractionRing.GetMetrics(dpi)
            Dim baseRect As SKRect = MASSurfaceInteractionRing.SnapBaseRect(rPx, m)

            If Not PixelSnap.HasArea(baseRect) Then Return

            Dim stateSpec As MASButtonStateResolver.ButtonStateRenderSpec =
                MASButtonStateResolver.ResolveRenderSpec(
                    ctx:=ctx,
                    personality:=personality,
                    intent:=intent,
                    isDefault:=isDefault,
                    state:=state,
                    contract:=contract,
                    isSelected:=isSelected,
                    baseRect:=baseRect,
                    dpi:=dpi)

            shadow.DrawLayeredShadow(
                canvas:=canvas,
                r:=stateSpec.ShadowRect,
                contract:=contract,
                dpi:=dpi,
                surfaceTheme:=MASAppThemeContracts.Foundation(app).Surface)

            Dim buttonSpec As MASVisualPrimitivesPainter.ButtonChromeSpec =
                MASVisualPrimitivesPainter.BuildButtonChromeSpec(
                    theme:=stateSpec.Surface)

            primitives.DrawButtonChrome(
                canvas:=canvas,
                ctx:=ctx,
                r:=baseRect,
                dpi:=dpi,
                contract:=contract,
                spec:=buttonSpec)

            If stateSpec.MASVisualInteractionState <> MASInteractionState.None Then
                If stateSpec.Overlay.FillEnabled OrElse stateSpec.Overlay.RingEnabled Then

                    MASSurfaceInteractionRing.DrawOverlay(
    canvas:=canvas,
    baseRectPx:=baseRect,
    contract:=contract,
    spec:=stateSpec.Overlay,
    dpi:=dpi)
                End If
            End If

            DrawContent(
                canvas:=canvas,
                ctx:=ctx,
                baseRect:=baseRect,
                contentOffsetY:=stateSpec.ContentOffsetY,
                interactionState:=stateSpec.MASVisualInteractionState,
                text:=text,
                textColor:=stateSpec.TextColor,
                iconColor:=stateSpec.IconColor,
                drawIcon:=drawIcon,
                drawIconEmphasis:=drawIconEmphasis,
                iconPlacement:=iconPlacement,
                dpi:=dpi,
                res:=res)

        End Sub

#End Region


#Region "Contract render scope"

        Private Shared Function CreateDrawScope(contract As MASResolvedComponentContract) As MASContractRenderScope
            If contract Is Nothing Then Throw New ArgumentNullException(NameOf(contract))

            Return MASArchitectureRuntime.CreateRenderScopeForComponent(Of MASButton)(
                NameOf(UnifiedButtonRenderer),
                contract,
                MASRenderLayer.Surface,
                MASRenderLayer.Shadow,
                MASRenderLayer.Text,
                MASRenderLayer.Hover,
                MASRenderLayer.Pressed,
                MASRenderLayer.Focus)
        End Function

#End Region

#Region "Text + icon"

        Private Shared Function GetPaintEntry(ctx As MASThemeContext,
                                              style As MASTypography.MASTextStyle,
                                              color As SKColor,
                                              dpi As Single,
                                              res As RenderResources) As PaintCacheEntry

            If ctx Is Nothing OrElse ctx.Typography Is Nothing OrElse res Is Nothing Then Return Nothing

            Dim p As SKPaint = ctx.Typography.CreatePaint(style, color)
            If p Is Nothing Then Return Nothing

            Dim key As New PaintKey(CInt(style), color, dpi)

            Dim entry As PaintCacheEntry = Nothing

            If res.MetricsCache.TryGetValue(key, entry) Then
                If entry IsNot Nothing AndAlso
                   String.Equals(entry.TypefaceKey, SafeTypefaceKey(p), StringComparison.Ordinal) Then

                    entry.Paint = p
                    Return entry
                End If
            End If

            entry = New PaintCacheEntry With {
                .Paint = p,
                .HasMetrics = False,
                .TypefaceKey = SafeTypefaceKey(p)
            }

            res.MetricsCache(key) = entry
            Return entry

        End Function

        Private Shared Function CacheMetrics(entry As PaintCacheEntry) As SKFontMetrics
            entry.Metrics = entry.Paint.FontMetrics
            entry.HasMetrics = True
            Return entry.Metrics
        End Function

        Private Shared Function SafeTypefaceKey(p As SKPaint) As String
            If p Is Nothing Then Return "paint:null"

            Dim tf As SKTypeface = p.Typeface
            If tf Is Nothing Then Return "typeface:null"

            Return "typeface:" & tf.ToString()
        End Function

        Private Shared Function MeasureTextCached(p As SKPaint,
                                                  typefaceKey As String,
                                                  s As String,
                                                  res As RenderResources) As Single

            If String.IsNullOrEmpty(s) OrElse p Is Nothing Then Return 0.0F

            Dim key As String =
                typefaceKey & "|" &
                p.TextSize.ToString("0.###", CultureInfo.InvariantCulture) & "|" &
                s

            Dim w As Single

            If res.MeasureCache.TryGetValue(key, w) Then Return w

            w = MASShapedText.MeasureWidth(s, p)

            If res.MeasureCache.Count >= RenderResources.MeasureCacheLimit Then
                res.MeasureCache.Clear()
            End If

            res.MeasureCache(key) = w
            Return w

        End Function

        Private Shared Function FitTextToWidth(text As String,
                                               maxWidth As Single,
                                               p As SKPaint,
                                               typefaceKey As String,
                                               res As RenderResources) As String

            Dim label As String = If(text, String.Empty)
            If label.Length = 0 Then Return String.Empty
            If p Is Nothing OrElse res Is Nothing Then Return label
            If maxWidth <= 0.0F Then Return String.Empty

            Dim fullWidth As Single = MeasureTextCached(p, typefaceKey, label, res)
            If fullWidth <= maxWidth Then Return label

            Const Ellipsis As String = "…"

            Dim ellipsisWidth As Single = MeasureTextCached(p, typefaceKey, Ellipsis, res)
            If ellipsisWidth > maxWidth Then Return String.Empty

            Dim lo As Integer = 0
            Dim hi As Integer = label.Length

            While lo < hi
                Dim mid As Integer = lo + ((hi - lo + 1) \ 2)
                Dim candidate As String = label.Substring(0, mid).TrimEnd() & Ellipsis
                Dim candidateWidth As Single = MeasureTextCached(p, typefaceKey, candidate, res)

                If candidateWidth <= maxWidth Then
                    lo = mid
                Else
                    hi = mid - 1
                End If
            End While

            If lo <= 0 Then Return Ellipsis

            Return label.Substring(0, lo).TrimEnd() & Ellipsis
        End Function

        Private Shared Function Clamp(v As Single,
                                      lo As Single,
                                      hi As Single) As Single

            If v < lo Then Return lo
            If v > hi Then Return hi

            Return v

        End Function


        Private Shared Sub DrawResolvedButtonText(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

            If IsSimpleLeftToRightText(text) Then
                canvas.DrawText(text, x, baselineY, paint)
            Else
                MASShapedText.Draw(canvas, text, x, baselineY, paint)
            End If
        End Sub

        Private Shared Function IsSimpleLeftToRightText(text As String) As Boolean
            If String.IsNullOrEmpty(text) Then Return True
            If MASShapedText.IsRtl(text) Then Return False

            For Each ch As Char In text
                Dim code As Integer = AscW(ch)
                If code < 0 OrElse code > &H7E Then Return False
            Next

            Return True
        End Function

        Private Shared Sub DrawContent(canvas As SKCanvas,
                                       ctx As MASThemeContext,
                                       baseRect As SKRect,
                                       contentOffsetY As Single,
                                       interactionState As MASInteractionState,
                                       text As String,
                                       textColor As SKColor,
                                       iconColor As SKColor,
                                       drawIcon As Action(Of SKCanvas, SKRect, Single, SKColor),
                                       drawIconEmphasis As Action(Of SKCanvas, SKRect, Single, SKColor),
                                       iconPlacement As MASButtonIconPlacement,
                                       dpi As Single,
                                       res As RenderResources)

            Dim padX As Single = ButtonTokens.PaddingX * dpi
            Dim padY As Single = Math.Max(2.0F * dpi, ButtonTokens.PaddingX * 0.35F * dpi)
            Dim iconSize As Single = ButtonTokens.IconSize * dpi
            Dim gap As Single = ButtonTokens.IconGap * dpi

            Dim label As String = If(text, String.Empty)

            Dim entry As PaintCacheEntry =
                GetPaintEntry(
                    ctx:=ctx,
                    style:=MASTypography.MASTextStyle.Button,
                    color:=textColor,
                    dpi:=dpi,
                    res:=res)

            Dim pText As SKPaint = If(entry Is Nothing, Nothing, entry.Paint)
            If pText Is Nothing Then Return

            Dim fm As SKFontMetrics =
                If(entry.HasMetrics, entry.Metrics, CacheMetrics(entry))

            Dim hasIcon As Boolean =
                drawIcon IsNot Nothing OrElse drawIconEmphasis IsNot Nothing

            Dim horizontal As Boolean =
                iconPlacement = MASButtonIconPlacement.Left OrElse
                iconPlacement = MASButtonIconPlacement.Right

            Dim leftLimit As Single = baseRect.Left + padX
            Dim rightLimit As Single = baseRect.Right - padX
            Dim topLimit As Single = baseRect.Top + padY
            Dim bottomLimit As Single = baseRect.Bottom - padY

            If rightLimit <= leftLimit OrElse bottomLimit <= topLimit Then Return

            Dim availableContentW As Single = Math.Max(0.0F, rightLimit - leftLimit)
            Dim maxTextW As Single = availableContentW

            If hasIcon AndAlso horizontal Then
                maxTextW = Math.Max(0.0F, availableContentW - iconSize - If(String.IsNullOrEmpty(label), 0.0F, gap))
            End If

            label = FitTextToWidth(
                text:=label,
                maxWidth:=maxTextW,
                p:=pText,
                typefaceKey:=entry.TypefaceKey,
                res:=res)

            Dim textW As Single =
                MeasureTextCached(
                    p:=pText,
                    typefaceKey:=entry.TypefaceKey,
                    s:=label,
                    res:=res)

            Dim textH As Single = Math.Abs(fm.Ascent) + Math.Abs(fm.Descent)

            Dim contentW As Single
            Dim contentH As Single

            If hasIcon Then
                If horizontal Then
                    contentW = iconSize + If(textW > 0.0F, gap + textW, 0.0F)
                    contentH = Math.Max(iconSize, textH)
                Else
                    contentW = Math.Max(iconSize, textW)
                    contentH = iconSize + If(textH > 0.0F, gap + textH, 0.0F)
                End If
            Else
                contentW = textW
                contentH = textH
            End If

            contentW = Math.Min(contentW, availableContentW)

            Dim xStart As Single =
                PixelSnap.SnapHalf(
                    Clamp(
                        baseRect.MidX - contentW / 2.0F,
                        leftLimit,
                        rightLimit - contentW))

            Dim yStart As Single =
                PixelSnap.SnapHalf(
                    Clamp(
                        baseRect.MidY - contentH / 2.0F + contentOffsetY,
                        topLimit,
                        bottomLimit - contentH))

            Dim iconPainter As Action(Of SKCanvas, SKRect, Single, SKColor) = drawIcon

            If ButtonPolicyDefaults.IsInteractive(interactionState) Then
                iconPainter = If(drawIconEmphasis, drawIcon)
            End If

            Dim textX As Single = xStart
            Dim textCenterY As Single = yStart + contentH / 2.0F
            Dim iconRect As SKRect = SKRect.Empty

            Dim contentClip As New SKRect(leftLimit, topLimit, rightLimit, bottomLimit)
            canvas.Save()
            Try
                canvas.ClipRect(contentClip, SKClipOperation.Intersect, True)

                If hasIcon Then
                    If horizontal Then
                        Dim iconY As Single = baseRect.MidY + ButtonTokens.IconCenterNudgeY * dpi + contentOffsetY

                        If iconPlacement = MASButtonIconPlacement.Left Then
                            iconRect = New SKRect(xStart, iconY - iconSize / 2.0F, xStart + iconSize, iconY + iconSize / 2.0F)
                            textX = iconRect.Right + If(textW > 0.0F, gap, 0.0F)
                        Else
                            textX = xStart
                            iconRect = New SKRect(xStart + textW + If(textW > 0.0F, gap, 0.0F), iconY - iconSize / 2.0F, xStart + textW + If(textW > 0.0F, gap, 0.0F) + iconSize, iconY + iconSize / 2.0F)
                        End If
                    Else
                        Dim iconX As Single = baseRect.MidX - iconSize / 2.0F

                        If iconPlacement = MASButtonIconPlacement.Top Then
                            iconRect = New SKRect(iconX, yStart, iconX + iconSize, yStart + iconSize)
                            textCenterY = iconRect.Bottom + If(textH > 0.0F, gap, 0.0F) + textH / 2.0F
                        Else
                            textCenterY = yStart + textH / 2.0F
                            iconRect = New SKRect(iconX, yStart + textH + If(textH > 0.0F, gap, 0.0F), iconX + iconSize, yStart + textH + If(textH > 0.0F, gap, 0.0F) + iconSize)
                        End If

                        textX = baseRect.MidX - textW / 2.0F
                    End If

                    If iconPainter IsNot Nothing Then
                        iconPainter.Invoke(canvas, PixelSnap.SnapRectHalf(iconRect), dpi, iconColor)
                    End If
                End If

                If textW > 0.0F Then
                    Dim yText As Single =
                        PixelSnap.SnapHalf(
                            textCenterY -
                            ((fm.Ascent + fm.Descent) / 2.0F) +
                            ButtonTokens.TextBaselineNudge * dpi)

                    DrawResolvedButtonText(canvas, label, PixelSnap.SnapHalf(textX), yText, pText)
                End If
            Finally
                canvas.Restore()
            End Try

        End Sub

#End Region

    End Class

End Namespace