Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Internal renderer for the root-owned tooltip visual. It is intentionally not part of the public SDK surface.
    ''' </summary>
    Friend NotInheritable Class TooltipRenderer
        Implements IDisposable

        Private ReadOnly _fillPaint As New SKPaint With {
            .IsAntialias = True,
            .IsDither = True,
            .Style = SKPaintStyle.Fill
        }

        Private ReadOnly _strokePaint As New SKPaint With {
            .IsAntialias = True,
            .IsDither = True,
            .Style = SKPaintStyle.Stroke,
            .StrokeJoin = SKStrokeJoin.Round,
            .StrokeCap = SKStrokeCap.Round
        }

        Private _disposed As Boolean

        Friend Sub Render(canvas As SKCanvas,
                          ctx As MASThemeContext,
                          dpi As Single,
                          owner As MASTooltip)

            If _disposed Then Return
            If canvas Is Nothing OrElse ctx Is Nothing OrElse owner Is Nothing Then Return
            If Not owner.Visible Then Return
            If Not owner.HasVisibleTooltip Then Return

            dpi = PixelSnap.SafeDpi(dpi)

            Dim text As String = owner.GetVisibleTooltipText()
            If String.IsNullOrWhiteSpace(text) Then Return

            Dim panelRect As SKRect = owner.GetVisiblePanelRectPx(ctx)
            If panelRect.IsEmpty OrElse panelRect.Width <= 1.0F OrElse panelRect.Height <= 1.0F Then Return

            Dim textRect As SKRect = owner.GetVisibleTextRectPx(ctx)
            If textRect.IsEmpty OrElse textRect.Width <= 1.0F OrElse textRect.Height <= 1.0F Then Return

            Dim opacity As Single = Clamp01(owner.GetVisibleOpacity())
            If opacity <= 0.001F Then Return

            panelRect = PixelSnap.SnapRectHalf(panelRect)

            Dim radius As Single = PixelSnap.SafeRadius(MASTooltipVisualPolicy.ResolvePanelRadiusPx(panelRect, dpi))

            Dim tooltipTheme As IMASTooltipTheme = ctx.TooltipTheme
            Dim surface As MASTooltipSurfaceTheme = Nothing

            If tooltipTheme IsNot Nothing Then
                surface = tooltipTheme.Surface
            End If

            Dim fillTop As SKColor = ResolveSurfaceTop(surface)
            Dim fillBottom As SKColor = ResolveSurfaceBottom(surface)
            Dim borderOuter As SKColor = ResolveBorderOuter(surface)
            Dim borderInner As SKColor = ResolveBorderInner(surface)
            Dim arrowFill As SKColor = ResolveArrowFill(surface, fillBottom)
            Dim arrowStroke As SKColor = ResolveArrowStroke(surface, borderOuter)

            DrawArrow(
                canvas:=canvas,
                ctx:=ctx,
                owner:=owner,
                panelRect:=panelRect,
                opacity:=opacity,
                arrowFill:=arrowFill,
                arrowStroke:=arrowStroke)

            Dim surfaceMaterialDrawn As Boolean = MASSurfaceMaterialComponentGateway.TryDrawFloatingTooltipSurface(
                canvas:=canvas,
                ctx:=ctx,
                boundsPx:=panelRect,
                materialKey:=owner.SurfaceMaterial,
                dpi:=dpi,
                cornerRadiusPx:=radius,
                opacity:=opacity,
                surfaceStrengthLevel:=4,
                borderStrengthLevel:=3)

            If Not surfaceMaterialDrawn Then
                DrawPanel(
                    canvas:=canvas,
                    panelRect:=panelRect,
                    radius:=radius,
                    opacity:=opacity,
                    topColor:=fillTop,
                    bottomColor:=fillBottom,
                    outerBorder:=borderOuter,
                    innerBorder:=borderInner)
            End If

            DrawWrappedText(
                canvas:=canvas,
                ctx:=ctx,
                dpi:=dpi,
                owner:=owner,
                tooltipTheme:=tooltipTheme,
                textRect:=textRect,
                opacity:=opacity)

        End Sub

        Private Sub DrawPanel(canvas As SKCanvas,
                              panelRect As SKRect,
                              radius As Single,
                              opacity As Single,
                              topColor As SKColor,
                              bottomColor As SKColor,
                              outerBorder As SKColor,
                              innerBorder As SKColor)

            If canvas Is Nothing Then Return

            Using shader As SKShader =
                SKShader.CreateLinearGradient(
                    New SKPoint(panelRect.Left, panelRect.Top),
                    New SKPoint(panelRect.Left, panelRect.Bottom),
                    New SKColor() {
                        ScaleAlpha(topColor, opacity),
                        ScaleAlpha(bottomColor, opacity)
                    },
                    Nothing,
                    SKShaderTileMode.Clamp)

                _fillPaint.Shader = shader
                _fillPaint.Color = SKColors.White

                canvas.DrawRoundRect(panelRect, radius, radius, _fillPaint)

                _fillPaint.Shader = Nothing
            End Using

            _strokePaint.StrokeWidth = 1.0F
            _strokePaint.Color = ScaleAlpha(outerBorder, opacity)
            canvas.DrawRoundRect(panelRect, radius, radius, _strokePaint)

            Dim inner As SKRect = panelRect
            inner.Inflate(-1.0F, -1.0F)

            If inner.Width > 1.0F AndAlso inner.Height > 1.0F Then
                _strokePaint.StrokeWidth = 1.0F
                _strokePaint.Color = ScaleAlpha(innerBorder, opacity * 0.65F)
                canvas.DrawRoundRect(
                    inner,
                    Math.Max(1.0F, radius - 1.0F),
                    Math.Max(1.0F, radius - 1.0F),
                    _strokePaint)
            End If

        End Sub

        Private Sub DrawArrow(canvas As SKCanvas,
                              ctx As MASThemeContext,
                              owner As MASTooltip,
                              panelRect As SKRect,
                              opacity As Single,
                              arrowFill As SKColor,
                              arrowStroke As SKColor)

            If canvas Is Nothing OrElse ctx Is Nothing OrElse owner Is Nothing Then Return

            Dim placement As MASTooltip.TooltipPlacement = owner.GetVisiblePlacement()
            Dim anchorPx As SKPoint = owner.GetVisibleAnchorPx(ctx)

            Dim arrowHalfBase As Single = owner.GetArrowSizePx(ctx)
            Dim arrowDepth As Single = Math.Max(4.0F, arrowHalfBase * 0.9F)

            Using path As SKPath =
                BuildArrowPath(
                    placement:=placement,
                    panelRect:=panelRect,
                    anchorPx:=anchorPx,
                    arrowHalfBase:=arrowHalfBase,
                    arrowDepth:=arrowDepth)

                If path Is Nothing Then Return

                _fillPaint.Shader = Nothing
                _fillPaint.Color = ScaleAlpha(arrowFill, opacity)

                _strokePaint.Color = ScaleAlpha(arrowStroke, opacity)
                _strokePaint.StrokeWidth = 1.0F

                canvas.DrawPath(path, _fillPaint)
                canvas.DrawPath(path, _strokePaint)
            End Using

        End Sub

        Private Shared Function BuildArrowPath(placement As MASTooltip.TooltipPlacement,
                                               panelRect As SKRect,
                                               anchorPx As SKPoint,
                                               arrowHalfBase As Single,
                                               arrowDepth As Single) As SKPath

            Dim p As New SKPath()

            Select Case placement

                Case MASTooltip.TooltipPlacement.Top

                    Dim baseY As Single = panelRect.Bottom - 0.5F
                    Dim cx As Single =
                        Clamp(anchorPx.X,
                              panelRect.Left + 12.0F,
                              panelRect.Right - 12.0F)

                    p.MoveTo(cx - arrowHalfBase, baseY)
                    p.QuadTo(cx, baseY + arrowDepth * 0.55F, cx, baseY + arrowDepth)
                    p.QuadTo(cx, baseY + arrowDepth * 0.55F, cx + arrowHalfBase, baseY)
                    p.Close()

                Case MASTooltip.TooltipPlacement.Bottom

                    Dim baseY As Single = panelRect.Top + 0.5F
                    Dim cx As Single =
                        Clamp(anchorPx.X,
                              panelRect.Left + 12.0F,
                              panelRect.Right - 12.0F)

                    p.MoveTo(cx - arrowHalfBase, baseY)
                    p.QuadTo(cx, baseY - arrowDepth * 0.55F, cx, baseY - arrowDepth)
                    p.QuadTo(cx, baseY - arrowDepth * 0.55F, cx + arrowHalfBase, baseY)
                    p.Close()

                Case MASTooltip.TooltipPlacement.Right

                    Dim baseX As Single = panelRect.Left + 0.5F
                    Dim cy As Single =
                        Clamp(anchorPx.Y,
                              panelRect.Top + 12.0F,
                              panelRect.Bottom - 12.0F)

                    p.MoveTo(baseX, cy - arrowHalfBase)
                    p.QuadTo(baseX - arrowDepth * 0.55F, cy, baseX - arrowDepth, cy)
                    p.QuadTo(baseX - arrowDepth * 0.55F, cy, baseX, cy + arrowHalfBase)
                    p.Close()

                Case Else

                    Dim baseX As Single = panelRect.Right - 0.5F
                    Dim cy As Single =
                        Clamp(anchorPx.Y,
                              panelRect.Top + 12.0F,
                              panelRect.Bottom - 12.0F)

                    p.MoveTo(baseX, cy - arrowHalfBase)
                    p.QuadTo(baseX + arrowDepth * 0.55F, cy, baseX + arrowDepth, cy)
                    p.QuadTo(baseX + arrowDepth * 0.55F, cy, baseX, cy + arrowHalfBase)
                    p.Close()

            End Select

            Return p

        End Function

        Private Sub DrawWrappedText(canvas As SKCanvas,
                                    ctx As MASThemeContext,
                                    dpi As Single,
                                    owner As MASTooltip,
                                    tooltipTheme As IMASTooltipTheme,
                                    textRect As SKRect,
                                    opacity As Single)

            If canvas Is Nothing OrElse ctx Is Nothing OrElse owner Is Nothing Then Return
            If ctx.Typography Is Nothing Then Return

            Dim lines As List(Of String) = owner.GetVisibleWrappedLines(ctx, dpi)
            If lines Is Nothing OrElse lines.Count = 0 Then Return

            Dim lineHeight As Single = owner.GetVisibleLineHeightPx(ctx)
            If lineHeight <= 0.0F Then Return

            Dim textColor As SKColor = MASTooltipVisualPolicy.ResolveTextColor(tooltipTheme)
            textColor = ScaleAlpha(textColor, opacity)

            Dim p As SKPaint =
                ctx.Typography.GetPaint(MASTypography.MASTextStyle.Small, textColor)

            If p Is Nothing Then Return

            Dim y As Single = textRect.Top - p.FontMetrics.Ascent
            Dim isRtl As Boolean = MASTooltipVisualPolicy.ResolveTextIsRtl(lines)

            For i As Integer = 0 To lines.Count - 1

                Dim line As String = lines(i)

                If line.Length > 0 Then
                    Dim x As Single = textRect.Left
                    If isRtl Then x = textRect.Right - MASShapedText.MeasureWidth(line, p)
                    MASShapedText.Draw(canvas, line, x, y, p)
                End If

                y += lineHeight

                If y > textRect.Bottom + lineHeight Then
                    Exit For
                End If

            Next

        End Sub

        Private Shared Function ResolveSurfaceTop(surface As MASTooltipSurfaceTheme) As SKColor

            If surface IsNot Nothing AndAlso surface.SurfaceTop.Alpha > 0 Then
                Return surface.SurfaceTop
            End If

            Return New SKColor(38, 38, 42, 242)

        End Function

        Private Shared Function ResolveSurfaceBottom(surface As MASTooltipSurfaceTheme) As SKColor

            If surface IsNot Nothing AndAlso surface.SurfaceBot.Alpha > 0 Then
                Return surface.SurfaceBot
            End If

            If surface IsNot Nothing AndAlso surface.SurfaceMid.Alpha > 0 Then
                Return surface.SurfaceMid
            End If

            Return New SKColor(22, 22, 26, 246)

        End Function

        Private Shared Function ResolveBorderOuter(surface As MASTooltipSurfaceTheme) As SKColor

            If surface IsNot Nothing AndAlso surface.BorderOuter.Alpha > 0 Then
                Return surface.BorderOuter
            End If

            Return New SKColor(255, 255, 255, 48)

        End Function

        Private Shared Function ResolveBorderInner(surface As MASTooltipSurfaceTheme) As SKColor

            If surface IsNot Nothing AndAlso surface.BorderInner.Alpha > 0 Then
                Return surface.BorderInner
            End If

            Return New SKColor(255, 255, 255, 22)

        End Function

        Private Shared Function ResolveArrowFill(surface As MASTooltipSurfaceTheme,
                                                 fallback As SKColor) As SKColor

            If surface IsNot Nothing AndAlso surface.ArrowFill.Alpha > 0 Then
                Return surface.ArrowFill
            End If

            Return fallback

        End Function

        Private Shared Function ResolveArrowStroke(surface As MASTooltipSurfaceTheme,
                                                   fallback As SKColor) As SKColor

            If surface IsNot Nothing AndAlso surface.ArrowStroke.Alpha > 0 Then
                Return surface.ArrowStroke
            End If

            Return fallback

        End Function

        Private Shared Function ResolveTextColor(tooltipTheme As IMASTooltipTheme) As SKColor

            If tooltipTheme IsNot Nothing AndAlso
               tooltipTheme.Text IsNot Nothing AndAlso
               tooltipTheme.Text.Text.Alpha > 0 Then

                Return tooltipTheme.Text.Text
            End If

            Return New SKColor(255, 255, 255, 245)

        End Function

        Private Shared Function ScaleAlpha(c As SKColor,
                                           factor As Single) As SKColor

            factor = Clamp01(factor)

            Dim a As Integer = CInt(Math.Round(c.Alpha * factor))
            a = Math.Max(0, Math.Min(255, a))

            Return c.WithAlpha(CByte(a))

        End Function

        Private Shared Function Clamp01(v As Single) As Single

            If Single.IsNaN(v) OrElse Single.IsInfinity(v) Then Return 0.0F
            If v < 0.0F Then Return 0.0F
            If v > 1.0F Then Return 1.0F

            Return v

        End Function

        Private Shared Function Clamp(v As Single,
                                      minV As Single,
                                      maxV As Single) As Single

            If v < minV Then Return minV
            If v > maxV Then Return maxV

            Return v

        End Function

        Public Sub Dispose() Implements IDisposable.Dispose

            If _disposed Then Return
            _disposed = True

            Try
                _fillPaint.Dispose()
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(
                    masCaughtException1,
                    "TooltipRenderer.Dispose.FillPaint")
            End Try

            Try
                _strokePaint.Dispose()
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(
                    masCaughtException2,
                    "TooltipRenderer.Dispose.StrokePaint")
            End Try

        End Sub

    End Class

End Namespace