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.Icons
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports Nexamas.UI.TextInput
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Public Nexamas UI feedback control for field-level validation text.
    ''' It is a visual message element only: validation rules remain owned by
    ''' FormValidationSystem and inline input indicators remain owned by TextInput controls.
    ''' </summary>
    Public NotInheritable Class MASValidationMessage
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

#Region "Fields"

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

        Private _text As String = String.Empty
        Private _state As MASTextValidationState = MASTextValidationState.Information
        Private _showIcon As Boolean = True
        Private _revealMotionRunner As MASMotionTimelineRunner
        Private _revealProgress As Single = 1.0F
        Private _hasRevealMotion 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) As MASValidationMessage
            Dim control As New MASValidationMessage()
            control._text = MASValidationMessagePolicy.NormalizeMessageText(text)
            Return control
        End Function

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

                _text = normalized
                StartValidationRevealMotion()
                RaiseTextChanged()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASValidationMessage.Text")
                InvalidateVisual()
            End Set
        End Property

        Public Property State As MASTextValidationState
            Get
                Return _state
            End Get
            Set(value As MASTextValidationState)
                Dim normalized As MASTextValidationState = MASValidationMessagePolicy.NormalizeState(value)
                If _state = normalized Then Return

                _state = normalized
                StartValidationRevealMotion()
                InvalidateVisual()
            End Set
        End Property

        Public Property ShowIcon As Boolean
            Get
                Return _showIcon
            End Get
            Set(value As Boolean)
                If _showIcon = value Then Return

                _showIcon = value
                StartValidationRevealMotion()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASValidationMessage.ShowIcon")
                InvalidateVisual()
            End Set
        End Property

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

        Public Function WithState(value As MASTextValidationState) As MASValidationMessage
            State = value
            Return Me
        End Function

        Public Function WithIcon(value As Boolean) As MASValidationMessage
            ShowIcon = value
            Return Me
        End Function

        Public Shadows Function WithSize(sizeIntent As MASSize) As MASValidationMessage
            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)
            Dim maxTextWidth As Single = ResolveMeasureTextWidth(safeContext)
            Dim textSize As SKSize = MeasureText(safeContext, maxTextWidth)

            Dim desired As New SKSize(
                ValidationMessageTokens.PaddingLeftDip +
                ResolveIconSlotWidth() +
                textSize.Width +
                ValidationMessageTokens.PaddingRightDip,
                Math.Max(ValidationMessageTokens.MinHeightDip,
                         ValidationMessageTokens.PaddingTopDip + textSize.Height + ValidationMessageTokens.PaddingBottomDip))

            If Not Single.IsPositiveInfinity(safeContext.AvailableSize.Width) AndAlso safeContext.AvailableSize.Width > 0.0F Then
                desired.Width = Math.Min(desired.Width, safeContext.AvailableSize.Width)
            End If

            Dim minimum As New SKSize(
                ValidationMessageTokens.MinWidthDip,
                ValidationMessageTokens.MinHeightDip)

            Dim contentInset As New MASLayoutInset(
                ValidationMessageTokens.PaddingLeftDip,
                ValidationMessageTokens.PaddingTopDip,
                ValidationMessageTokens.PaddingRightDip,
                ValidationMessageTokens.PaddingBottomDip)

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

        Private Function MeasureText(context As MASSizeContext,
                                     maxTextWidth As Single) As SKSize
            Dim result As MASTextMeasureResult =
                MASLayoutTextMeasureGateway.Measure(
                    If(context Is Nothing, Nothing, context.IntegrationContext),
                    If(String.IsNullOrEmpty(_text), " ", _text),
                    MASTypography.MASTextStyle.Small,
                    Math.Max(12.0F, maxTextWidth),
                    allowWrap:=True)

            Return result.Size
        End Function

        Private Function ResolveMeasureTextWidth(context As MASSizeContext) As Single
            Dim available As Single = MASValidationMessagePolicy.MaxWrappedWidthDip

            If context IsNot Nothing AndAlso
               Not Single.IsPositiveInfinity(context.AvailableSize.Width) AndAlso
               context.AvailableSize.Width > 1.0F Then
                available = context.AvailableSize.Width -
                            ValidationMessageTokens.PaddingLeftDip -
                            ResolveIconSlotWidth() -
                            ValidationMessageTokens.PaddingRightDip
            End If

            Return Math.Max(12.0F, Math.Min(MASValidationMessagePolicy.MaxWrappedWidthDip, available))
        End Function

        Private Function ResolveIconSlotWidth() As Single
            If Not _showIcon OrElse _state = MASTextValidationState.None Then Return 0.0F
            Return ValidationMessageTokens.IconDiameterDip + ValidationMessageTokens.IconGapDip
        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
            If String.IsNullOrEmpty(_text) AndAlso _state = MASTextValidationState.None Then Return

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            Dim color As SKColor = MASValidationMessagePolicy.ResolveStateColor(ctx, _state)
            If _state = MASTextValidationState.None Then color = MASValidationMessagePolicy.ResolveMutedColor(ctx)

            Dim content As New SKRect(
                pixelBounds.Left + ValidationMessageTokens.PaddingLeftDip * dpi,
                pixelBounds.Top + ValidationMessageTokens.PaddingTopDip * dpi,
                pixelBounds.Right - ValidationMessageTokens.PaddingRightDip * dpi,
                pixelBounds.Bottom - ValidationMessageTokens.PaddingBottomDip * dpi)

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

            Dim revealOffsetY As Single = ResolveValidationRevealOffsetPx(dpi)
            If Math.Abs(revealOffsetY) > 0.0001F Then
                content = New SKRect(content.Left, content.Top + revealOffsetY, content.Right, content.Bottom + revealOffsetY)
            End If
            Dim revealAlpha As Single = ResolveValidationRevealAlpha()

            If _showIcon AndAlso _state <> MASTextValidationState.None Then
                Dim iconSize As Single = ValidationMessageTokens.IconDiameterDip * dpi
                Dim iconRect As New SKRect(
                    content.Left,
                    content.Top + Math.Max(0.0F, (ValidationMessageTokens.MinHeightDip * dpi - iconSize) / 2.0F),
                    content.Left + iconSize,
                    content.Top + Math.Max(0.0F, (ValidationMessageTokens.MinHeightDip * dpi - iconSize) / 2.0F) + iconSize)
                DrawIcon(canvas, ctx, iconRect, ApplyValidationRevealAlpha(color, revealAlpha), _state, dpi)
                content.Left = iconRect.Right + ValidationMessageTokens.IconGapDip * dpi
            End If

            If content.Width <= 1.0F Then Return

            DrawWrappedMessageText(
                canvas:=canvas,
                ctx:=ctx,
                text:=_text,
                bounds:=content,
                color:=ApplyValidationRevealAlpha(color.WithAlpha(ValidationMessageTokens.TextAlpha), revealAlpha),
                dpi:=dpi)
        End Sub

        Private Shared Sub DrawIcon(canvas As SKCanvas,
                                    ctx As MASThemeContext,
                                    bounds As SKRect,
                                    color As SKColor,
                                    state As MASTextValidationState,
                                    dpi As Single)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return

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

            Dim iconRect As New SKRect(
                bounds.Left + bounds.Width * 0.19F,
                bounds.Top + bounds.Height * 0.19F,
                bounds.Right - bounds.Width * 0.19F,
                bounds.Bottom - bounds.Height * 0.19F)

            MASIconPainter.Draw(
                canvas:=canvas,
                bounds:=iconRect,
                dpi:=PixelSnap.SafeDpi(dpi),
                kind:=MASValidationMessagePolicy.ResolveIconKind(state),
                color:=SKColors.White.WithAlpha(ValidationMessageTokens.IconGlyphAlpha),
                style:=MASIconStyle.Outline)
        End Sub

        Private Shared Sub DrawWrappedMessageText(canvas As SKCanvas,
                                                  ctx As MASThemeContext,
                                                  text As String,
                                                  bounds As SKRect,
                                                  color As SKColor,
                                                  dpi As Single)
            If canvas Is Nothing OrElse ctx Is Nothing OrElse ctx.Typography Is Nothing Then Return
            If bounds.Width <= 2.0F OrElse bounds.Height <= 2.0F Then Return
            If String.IsNullOrEmpty(text) Then Return

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

            Dim lines As List(Of String) = MASValidationMessagePolicy.BuildWrappedLines(
                text:=text,
                paint:=paint,
                maxWidthPx:=bounds.Width,
                maxLines:=MASValidationMessagePolicy.MaxWrappedLines)

            If lines Is Nothing OrElse lines.Count = 0 Then Return

            Dim lineHeight As Single = paint.TextSize * MASTypography.WrapLineHeightMul
            Dim y As Single = bounds.Top - paint.FontMetrics.Ascent
            Dim isRtl As Boolean = ResolveTextIsRtl(text)

            For i As Integer = 0 To lines.Count - 1
                If y > bounds.Bottom + lineHeight Then Exit For

                Dim line As String = lines(i)
                If line.Length > 0 Then
                    Dim x As Single = bounds.Left
                    If isRtl Then
                        x = bounds.Right - MASShapedText.MeasureWidth(line, paint)
                    End If
                    MASShapedText.Draw(canvas, line, MASTextCrispRendering.SnapTextX(x), MASTextCrispRendering.SnapBaseline(y), paint)
                End If

                y += lineHeight
            Next
        End Sub

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

#End Region


#Region "Motion"

        Private Sub StartValidationRevealMotion()
            If _disposedLocal Then Return

            StopValidationRevealMotion(resetReveal:=False)
            _revealProgress = 0.0F
            _hasRevealMotion = True

            Dim plan As MASMotionTransitionPlan = MASMotionSystem.CreateTransitionPlan(MASMotionTokenId.ValidationReveal, 0.0F, 1.0F)
            _revealMotionRunner = MASMotionSystem.CreateTimelineRunner(
                owner:=Me,
                consumerName:="MASValidationMessage.ValidationReveal",
                plan:=plan,
                requestFrame:=AddressOf InvalidateVisual,
                applyFrame:=AddressOf ApplyValidationRevealMotionFrame,
                completed:=AddressOf CompleteValidationRevealMotionFrame)
            _revealMotionRunner.Start()

            InvalidateVisual()
        End Sub

        Private Sub ApplyValidationRevealMotionFrame(frame As MASMotionFrame)
            If frame Is Nothing Then Return
            _revealProgress = MASMotionSystem.ClampProgress(frame.EasedProgress)
            _hasRevealMotion = True
            InvalidateVisual()
        End Sub

        Private Sub CompleteValidationRevealMotionFrame(frame As MASMotionFrame)
            StopValidationRevealMotion(resetReveal:=True)
            InvalidateVisual()
        End Sub

        Private Sub StopValidationRevealMotion(resetReveal As Boolean)
            If _revealMotionRunner IsNot Nothing Then
                _revealMotionRunner.Dispose()
                _revealMotionRunner = Nothing
            End If

            If resetReveal Then
                _revealProgress = 1.0F
                _hasRevealMotion = False
            End If
        End Sub

        Private Function ResolveValidationRevealAlpha() As Single
            If Not _hasRevealMotion Then Return 1.0F
            Return Math.Max(0.35F, MASMotionSystem.ClampProgress(_revealProgress))
        End Function

        Private Function ResolveValidationRevealOffsetPx(dpi As Single) As Single
            If Not _hasRevealMotion Then Return 0.0F
            Dim progress As Single = MASMotionSystem.ClampProgress(_revealProgress)
            Return (1.0F - progress) * 3.0F * PixelSnap.SafeDpi(dpi)
        End Function

        Private Shared Function ApplyValidationRevealAlpha(color As SKColor, alphaMultiplier As Single) As SKColor
            Dim clamped As Single = Math.Max(0.0F, Math.Min(1.0F, alphaMultiplier))
            Dim alpha As Byte = CByte(Math.Max(0, Math.Min(255, CInt(Math.Round(CDbl(color.Alpha) * CDbl(clamped))))))
            Return color.WithAlpha(alpha)
        End Function

#End Region

#Region "Dispose"

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

            If disposing Then
                Try
                    StopValidationRevealMotion(resetReveal:=True)
                Catch masCaughtException2 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException2)
                End Try
            End If

            MyBase.Dispose(disposing)
        End Sub

#End Region

#Region "Governance"

        Friend Function CreateValidationMessageReadinessManifest() As Nexamas.UI.Controls.Governance.MASValidationMessageReadinessManifest
            Return Nexamas.UI.Controls.Governance.MASValidationMessageReadinessManifest.CreateCurrent()
        End Function

#End Region

    End Class

End Namespace
