Option Strict On
Option Explicit On

Imports System
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Public Nexamas UI compact status badge. It is a visual feedback control only:
    ''' notification queues, toast lifetime, list/data adornment ownership, and validation
    ''' policy remain owned by their existing Nexamas UI systems.
    ''' </summary>
    Public NotInheritable Class MASBadge
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

#Region "Fields"

        Private Shared ReadOnly _contract As MASResolvedComponentContract =
            MASArchitectureRuntime.ResolveContractOrThrow(GetType(MASBadge))

        Private ReadOnly _primitives As New MASVisualPrimitivesPainter()

        Private _text As String = String.Empty
        Private _tone As MASToastTone = MASToastTone.Neutral
        Private _dotOnly As Boolean
        Private _pulseMotionRunner As MASMotionTimelineRunner
        Private _pulseProgress As Single = 1.0F
        Private _hasPulseMotion As Boolean
        Private _disposedLocal As Boolean

#End Region

#Region "Constructor"

        Public Sub New()
            MyBase.New()
        End Sub

#End Region

#Region "Public API"

        Public Shared Function Create(Optional text As String = Nothing,
                                      Optional tone As MASToastTone = MASToastTone.Neutral) As MASBadge
            Dim badge As New MASBadge()
            badge._text = MASBadgeVisualPolicy.NormalizeText(text)
            badge._tone = tone
            Return badge
        End Function

        Public Property Text As String
            Get
                Return _text
            End Get
            Set(value As String)
                Dim normalized As String = MASBadgeVisualPolicy.NormalizeText(value)
                If String.Equals(_text, normalized, StringComparison.Ordinal) Then Return

                _text = normalized
                PulseBadgeChangeMotion()
                RaiseTextChanged()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASBadge.Text")
                InvalidateVisual()
            End Set
        End Property

        Public Property Tone As MASToastTone
            Get
                Return _tone
            End Get
            Set(value As MASToastTone)
                If _tone = value Then Return

                _tone = value
                PulseBadgeChangeMotion()
                InvalidateVisual()
            End Set
        End Property

        Public Property DotOnly As Boolean
            Get
                Return _dotOnly
            End Get
            Set(value As Boolean)
                If _dotOnly = value Then Return

                _dotOnly = value
                PulseBadgeChangeMotion()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASBadge.DotOnly")
                InvalidateVisual()
            End Set
        End Property

        Public Function WithText(value As String) As MASBadge
            Text = value
            Return Me
        End Function

        Public Function WithTone(value As MASToastTone) As MASBadge
            Tone = value
            Return Me
        End Function

        Public Function WithDotOnly(Optional value As Boolean = True) As MASBadge
            DotOnly = value
            Return Me
        End Function

        Public Shadows Function WithSize(sizeIntent As MASSize) As MASBadge
            MyBase.SetSize(sizeIntent)
            Return Me
        End Function

#End Region

#Region "Layout"

        Friend Function GetPreferredLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetPreferredLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).DesiredSize
        End Function

        Friend Function GetMinLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetMinLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).MinSize
        End Function

        Friend Function MeasureIntrinsicSize(context As MASSizeContext,
                                             intent As MASSize) As MASSizeResult Implements IMASIntrinsicSizeContract.MeasureIntrinsicSize
            Dim safeContext As MASSizeContext = MASIntrinsicControlSizeMetrics.EnsureContext(context)

            If _dotOnly Then
                Dim dotSize As New SKSize(BadgeTokens.DotMinSizeDip, BadgeTokens.DotMinSizeDip)
                Return MASSizeResolver.FromMeasured(
                    desiredSize:=dotSize,
                    minSize:=dotSize,
                    maxSize:=dotSize,
                    intent:=intent,
                    context:=safeContext,
                    canGrowWidth:=False,
                    canGrowHeight:=False,
                    contentInset:=MASLayoutInset.Empty,
                    visualOverflowInset:=MASLayoutInset.Empty,
                    hitOverflowInset:=MASLayoutInset.Empty,
                    isFallback:=False)
            End If

            Dim textSize As SKSize = MeasureText(safeContext)
            Dim desired As SKSize = MASBadgeVisualPolicy.ResolveDesiredSize(textSize, _dotOnly)
            Dim minimum As SKSize = MASBadgeVisualPolicy.ResolveMinimumSize(textSize, False)
            Dim contentInset As New MASLayoutInset(
                BadgeTokens.PaddingHorizontalDip,
                BadgeTokens.PaddingVerticalDip,
                BadgeTokens.PaddingHorizontalDip,
                BadgeTokens.PaddingVerticalDip)

            Return MASSizeResolver.FromMeasured(
                desiredSize:=desired,
                minSize:=minimum,
                maxSize:=New SKSize(Single.PositiveInfinity, Single.PositiveInfinity),
                intent:=intent,
                context:=safeContext,
                canGrowWidth:=False,
                canGrowHeight:=False,
                contentInset:=contentInset,
                visualOverflowInset:=MASLayoutInset.Empty,
                hitOverflowInset:=MASLayoutInset.Empty,
                isFallback:=False)
        End Function

        Private Function MeasureText(context As MASSizeContext) As SKSize
            Dim result As MASTextMeasureResult =
                MASLayoutTextMeasureGateway.Measure(
                    If(context Is Nothing, Nothing, context.IntegrationContext),
                    ResolveRenderText(),
                    MASBadgeVisualPolicy.ResolveTextStyle(),
                    Single.PositiveInfinity,
                    allowWrap:=False)

            Return result.Size
        End Function

        Private Shared Function CreateSizeContext(context As MASLayoutMeasureContext) As MASSizeContext
            If context Is Nothing Then Return MASIntrinsicControlSizeMetrics.EnsureContext(Nothing)
            Return context.ToSizeContext()
        End Function

#End Region

#Region "Rendering"

        Protected Overrides Sub Render(canvas As SKCanvas,
                                       ctx As MASThemeContext,
                                       pixelBounds As SKRect)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If pixelBounds.Width <= 1.0F OrElse pixelBounds.Height <= 1.0F Then Return

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            Dim baseColor As SKColor = MASBadgeVisualPolicy.ResolveEffectiveFillColor(ctx, _tone, Enabled)

            If _dotOnly Then
                DrawDot(canvas, pixelBounds, baseColor, dpi)
                Return
            End If

            Dim badgeRect As SKRect = PixelSnap.SnapRect(pixelBounds, dpi)
            If badgeRect.Width <= 1.0F OrElse badgeRect.Height <= 1.0F Then Return
            badgeRect = ResolvePulseRect(badgeRect)

            Dim radius As Single = MASVisualPrimitivesPainter.ResolveSurfaceRadiusPx(_contract, badgeRect, dpi)
            radius = PixelSnap.SafeRadius(radius)

            Dim topColor As SKColor = ColorMath.Mix(baseColor, SKColors.White, BadgeTokens.TopLiftMix)
            Dim bottomColor As SKColor = ColorMath.Mix(baseColor, SKColors.Black, BadgeTokens.BottomDepthMix)
            Dim baseSpec As MASVisualPrimitivesPainter.ChipBaseSpec =
                MASVisualPrimitivesPainter.BuildChipBaseSpec(
                    topColor,
                    bottomColor,
                    0.55F,
                    BadgeTokens.FillTopAlpha,
                    BadgeTokens.FillMidAlpha,
                    BadgeTokens.FillBottomAlpha)

            Dim fxSpec As New MASVisualPrimitivesPainter.ChipFxSpec With {
                .WashColor = baseColor,
                .WashAlpha = BadgeTokens.WashAlpha,
                .InnerGlossColor = SKColors.White,
                .InnerGlossAlpha = BadgeTokens.InnerGlossAlpha,
                .InnerGlossFadeT = 0.44F,
                .InnerInsetMul = 0.09F,
                .InnerRadMul = 0.72F
            }

            _primitives.DrawChipBaseNoShadow(canvas, badgeRect, radius, dpi, baseSpec)
            _primitives.DrawChipFxSSOT(canvas, badgeRect, radius, dpi, baseSpec, fxSpec)
            DrawBorder(canvas, badgeRect, radius, baseColor, dpi)
            DrawText(canvas, ctx, badgeRect, ResolveTextColor(ctx, baseColor), dpi)
        End Sub

        Private Sub DrawDot(canvas As SKCanvas,
                            bounds As SKRect,
                            color As SKColor,
                            dpi As Single)
            Dim rect As SKRect = MASBadgeVisualPolicy.BuildDotRect(bounds, dpi)
            If rect.IsEmpty Then Return
            rect = ResolvePulseRect(rect)

            Using fill As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Fill,
                .Color = color.WithAlpha(BadgeTokens.FillBottomAlpha)
            }
                canvas.DrawOval(rect, fill)
            End Using

            Using border As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = Math.Max(1.0F, dpi),
                .Color = ColorMath.Mix(color, SKColors.White, 0.28F).WithAlpha(BadgeTokens.BorderAlpha)
            }
                canvas.DrawOval(rect, border)
            End Using
        End Sub

        Private Shared Sub DrawBorder(canvas As SKCanvas,
                                      bounds As SKRect,
                                      radius As Single,
                                      color As SKColor,
                                      dpi As Single)
            Using border As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = Math.Max(1.0F, dpi),
                .Color = ColorMath.Mix(color, SKColors.White, 0.22F).WithAlpha(BadgeTokens.BorderAlpha)
            }
                canvas.DrawRoundRect(bounds, radius, radius, border)
            End Using
        End Sub

        Private Sub DrawText(canvas As SKCanvas,
                             ctx As MASThemeContext,
                             bounds As SKRect,
                             color As SKColor,
                             dpi As Single)
            Dim content As SKRect = MASBadgeVisualPolicy.BuildContentRect(bounds, dpi)

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

            TypographyTextPrimitives.DrawSingleLineText(
                canvas:=canvas,
                ctx:=ctx,
                text:=ResolveRenderText(),
                bounds:=content,
                dpi:=dpi,
                style:=MASBadgeVisualPolicy.ResolveTextStyle(),
                color:=color.WithAlpha(BadgeTokens.TextAlpha),
                align:=TypographyTextPrimitives.TextAlign.Center,
                drawShadow:=False)
        End Sub

        Private Function ResolveRenderText() As String
            Return MASBadgeVisualPolicy.ResolveRenderText(_text)
        End Function

        Private Shared Function ResolveToneColor(ctx As MASThemeContext,
                                                 tone As MASToastTone) As SKColor
            Return MASBadgeVisualPolicy.ResolveToneColor(ctx, tone)
        End Function

        Private Shared Function ResolveTextColor(ctx As MASThemeContext,
                                                 fillColor As SKColor) As SKColor
            Return MASBadgeVisualPolicy.ResolveTextColor(ctx, fillColor)
        End Function

        Private Shared Function IsLight(color As SKColor) As Boolean
            Dim luminance As Single = CSng((0.2126R * color.Red) + (0.7152R * color.Green) + (0.0722R * color.Blue))
            Return luminance >= 156.0F
        End Function

#End Region


#Region "Motion"

        Private Sub PulseBadgeChangeMotion()
            If _disposedLocal Then Return
            StopBadgePulseMotion(resetPulse:=False)

            _pulseProgress = 0.0F
            _hasPulseMotion = True

            Dim plan As MASMotionTransitionPlan = MASMotionSystem.CreateTransitionPlan(MASMotionTokenId.BadgePulse, 0.0F, 1.0F)
            _pulseMotionRunner = MASMotionSystem.CreateTimelineRunner(
                owner:=Me,
                consumerName:="MASBadge.BadgePulse",
                plan:=plan,
                requestFrame:=AddressOf InvalidateVisual,
                applyFrame:=AddressOf ApplyBadgePulseMotionFrame,
                completed:=AddressOf CompleteBadgePulseMotionFrame)
            _pulseMotionRunner.Start()

            InvalidateVisual()
        End Sub

        Private Sub ApplyBadgePulseMotionFrame(frame As MASMotionFrame)
            If frame Is Nothing Then Return
            _pulseProgress = MASMotionSystem.ClampProgress(frame.EasedProgress)
            _hasPulseMotion = True
            InvalidateVisual()
        End Sub

        Private Sub CompleteBadgePulseMotionFrame(frame As MASMotionFrame)
            StopBadgePulseMotion(resetPulse:=True)
            InvalidateVisual()
        End Sub

        Private Sub StopBadgePulseMotion(resetPulse As Boolean)
            If _pulseMotionRunner IsNot Nothing Then
                _pulseMotionRunner.Dispose()
                _pulseMotionRunner = Nothing
            End If

            If resetPulse Then
                _pulseProgress = 1.0F
                _hasPulseMotion = False
            End If
        End Sub

        Private Function ResolvePulseRect(bounds As SKRect) As SKRect
            If Not _hasPulseMotion Then Return bounds
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return bounds

            Dim progress As Single = MASMotionSystem.ClampProgress(_pulseProgress)
            Dim wave As Single = CSng(Math.Sin(CDbl(progress) * Math.PI))
            Dim scale As Single = 1.0F + (0.045F * wave)
            If scale <= 1.0001F Then Return bounds

            Dim cx As Single = bounds.MidX
            Dim cy As Single = bounds.MidY
            Dim halfWidth As Single = bounds.Width * scale * 0.5F
            Dim halfHeight As Single = bounds.Height * scale * 0.5F
            Return New SKRect(cx - halfWidth, cy - halfHeight, cx + halfWidth, cy + halfHeight)
        End Function

#End Region

#Region "Dispose"

        Protected Overrides Sub Dispose(disposing As Boolean)
            If _disposedLocal Then Return
            _disposedLocal = True

            If disposing Then
                Try
                    StopBadgePulseMotion(resetPulse:=True)
                    _primitives.Dispose()
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException1)
                End Try
            End If

            MyBase.Dispose(disposing)
        End Sub

#End Region

#Region "Helpers"

        Private Shared Function NormalizeText(value As String) As String
            Return MASBadgeVisualPolicy.NormalizeText(value)
        End Function

#End Region

    End Class

End Namespace
