Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Composition
Imports Nexamas.UI.General
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    ''' <summary>
    ''' Public MAS radio-button control. External applications use this control
    ''' directly for exclusive choices, while grouping, painting, hit testing,
    ''' and peer synchronization remain Nexamas UI-owned internals.
    ''' </summary>
    Public NotInheritable Class MASRadioButton
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract


        Private Shared ReadOnly _contract As MASResolvedComponentContract =
    MASArchitectureRuntime.ResolveContractOrThrow(GetType(MASRadioButton))

        Private Shared ReadOnly _groupSync As New Object()
        Private Shared ReadOnly _registry As New List(Of WeakReference(Of MASRadioButton))()

        Private ReadOnly _painter As New RadioButtonPainter()

        Private _text As String = String.Empty
        Private _groupName As String = String.Empty
        Private _checked As Boolean

        Private _isHover As Boolean
        Private _isPressed As Boolean
        Private _hasFocus As Boolean
        Private _pressPulseRunner As MASMotionTimelineRunner
        Private _motionPressActive As Boolean

        Private _lastRenderBoundsPx As SKRect = SKRect.Empty

        Public Event CheckedChanged As EventHandler

        Public Sub New()
            MyBase.New()
        End Sub

        Public Property [Text] As String
            Get
                Return _text
            End Get
            Set(value As String)
                Dim s As String = MASRadioButtonGroupPolicy.NormalizeText(value)
                If _text = s Then Return

                _text = s
                RaiseTextChanged()
                InvalidateVisual()
            End Set
        End Property

        Public Property GroupName As String
            Get
                Return _groupName
            End Get
            Set(value As String)
                Dim s As String = MASRadioButtonGroupPolicy.NormalizeGroupName(value)
                If String.Equals(_groupName, s, StringComparison.Ordinal) Then Return

                _groupName = s

                If _checked Then
                    UncheckPeersInSameGroup()
                End If

                InvalidateVisual()
            End Set
        End Property

        Public Property Checked As Boolean
            Get
                Return _checked
            End Get
            Set(value As Boolean)
                If _checked = value Then Return

                _checked = value

                If _checked Then
                    UncheckPeersInSameGroup()
                End If

                RaiseEvent CheckedChanged(Me, EventArgs.Empty)
                InvalidateVisual()
            End Set
        End Property

        Public Sub SelectThis()
            If Not MASRadioButtonGroupPolicy.CanInteract(Enabled, Visible) Then Return
            If _checked Then Return
            Checked = True
        End Sub
#Region "SDK Fluent API"

        Public Shared Function Create() As MASRadioButton
            Return New MASRadioButton()
        End Function

        Friend Shared Function [Default]() As MASRadioButton
            Return Create()
        End Function

        Friend Function SetBounds(bounds As SKRect) As MASRadioButton
            SetLocalLayoutBoundsInternal(SafeBounds(bounds))
            Return Me
        End Function

        Friend Function WithBounds(bounds As SKRect) As MASRadioButton
            Return SetBounds(bounds)
        End Function

        Friend Function WithBounds(x As Single,
                                   y As Single,
                                   width As Single,
                                   height As Single) As MASRadioButton

             SetLocalLayoutBoundsInternal(New SKRect(
                SafeCoordinate(x),
                SafeCoordinate(y),
                SafeCoordinate(x) + SafeExtent(width),
                SafeCoordinate(y) + SafeExtent(height)
            ))

            Return Me
        End Function

        Public Function WithText(text As String) As MASRadioButton
            Me.Text = If(text, String.Empty)
            Return Me
        End Function

        Public Function WithGroup(name As String) As MASRadioButton
            GroupName = If(name, String.Empty)
            Return Me
        End Function

        Public Function CheckedState(Optional value As Boolean = True) As MASRadioButton
            Checked = value
            Return Me
        End Function

        Public Function Unchecked() As MASRadioButton
            Checked = False
            Return Me
        End Function

        Public Function SelectRadio() As MASRadioButton
            SelectThis()
            Return Me
        End Function

        Friend Function OnCheckedChanged(handler As EventHandler) As MASRadioButton
            If handler IsNot Nothing Then
                AddHandler CheckedChanged, handler
            End If

            Return Me
        End Function

        Friend Function OnCheckedChanged(action As Action(Of MASRadioButton, Boolean)) As MASRadioButton
            If action Is Nothing Then Return Me

            AddHandler CheckedChanged,
                Sub(sender As Object, e As EventArgs)
                    action.Invoke(Me, Checked)
                End Sub

            Return Me
        End Function

#Region "Size/Layout Contract"

        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 hasText As Boolean = Not String.IsNullOrWhiteSpace(_text)
            Dim maxTextWidth As Single = MASIntrinsicControlSizeMetrics.ResolveSelectionLabelAvailableWidth(
                safeContext.AvailableSize.Width,
                RadioButtonTokens.OuterSize,
                RadioButtonTokens.LabelGap,
                RadioButtonTokens.TextPadRight)

            Dim textSize As SKSize = MASLayoutTextMeasureGateway.Measure(
                safeContext.IntegrationContext,
                _text,
                MASTypography.MASTextStyle.Body,
                maxTextWidth,
                allowWrap:=False).Size

            Dim desired As SKSize = MASIntrinsicControlSizeMetrics.ResolveChoiceControlDesiredSize(
                RadioButtonTokens.OuterSize,
                RadioButtonTokens.OuterSize,
                RadioButtonTokens.MinHeight,
                RadioButtonTokens.LabelGap,
                RadioButtonTokens.TextPadRight,
                RadioButtonTokens.VerticalTextSafetyPad,
                textSize,
                hasText,
                safeContext.AvailableSize.Width)

            Dim minimum As SKSize = MASIntrinsicControlSizeMetrics.ResolveChoiceControlMinimumSize(
                RadioButtonTokens.OuterSize,
                RadioButtonTokens.OuterSize,
                RadioButtonTokens.MinHeight,
                RadioButtonTokens.LabelGap,
                RadioButtonTokens.TextPadRight,
                RadioButtonTokens.MinLabelWidth,
                RadioButtonTokens.VerticalTextSafetyPad,
                textSize,
                hasText)

            Return MASSizeResolver.FromMeasured(
                desiredSize:=desired,
                minSize:=minimum,
                maxSize:=New SKSize(Single.PositiveInfinity, Single.PositiveInfinity),
                intent:=intent,
                context:=safeContext,
                canGrowWidth:=True,
                canGrowHeight:=False,
                contentInset:=MASLayoutInset.Empty,
                visualOverflowInset:=MASIntrinsicControlSizeMetrics.DefaultVisualOverflowInset,
                hitOverflowInset:=MASIntrinsicControlSizeMetrics.CreateTouchHitOverflowInset(desired, safeContext.Density),
                isFallback:=False)
        End Function

#End Region
#End Region
        Protected Overrides Function WantsKeyboardFocus() As Boolean
            Return MASRadioButtonGroupPolicy.WantsKeyboardFocus(Enabled, Visible)
        End Function

        Protected Overrides Function OnKeyDown(ctx As MASThemeContext, keyCode As Keys) As Boolean
            If Not MASRadioButtonGroupPolicy.CanInteract(Enabled, Visible) Then Return False

            If MASRadioButtonGroupPolicy.ShouldSelectFromKey(keyCode) Then
                SelectThis()
                PulsePressReleaseMotion()
                Return True
            End If

            If MASRadioButtonGroupPolicy.ShouldMovePreviousFromKey(keyCode) Then
                Return MoveSelectionWithinGroup(forward:=False)
            End If

            If MASRadioButtonGroupPolicy.ShouldMoveNextFromKey(keyCode) Then
                Return MoveSelectionWithinGroup(forward:=True)
            End If

            If MASRadioButtonGroupPolicy.ShouldCancelFromKey(keyCode) Then
                If _isPressed OrElse _motionPressActive Then
                    _isPressed = False
                    StopPressMotion(clearPressed:=True)
                    InvalidateVisual()
                    Return True
                End If
            End If

            Return False
        End Function

        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

            _lastRenderBoundsPx = pixelBounds

            _painter.Draw(
    canvas:=canvas,
    ctx:=ctx,
    boundsPx:=pixelBounds,
    text:=_text,
    dpi:=If(ctx IsNot Nothing, ctx.Dpi, 1.0F),
    isHover:=_isHover,
    isPressed:=(_isPressed OrElse _motionPressActive),
    hasFocus:=_hasFocus,
    isEnabled:=Enabled,
    isChecked:=_checked,
    contract:=_contract)
        End Sub

        Protected Overrides Function OnMouseDown(ctx As MASThemeContext, ptPx As SKPoint, button As Integer) As Boolean
            If button <> CInt(MouseButtons.Left) Then
                Return False
            End If

            If Not MASRadioButtonGroupPolicy.CanInteract(Enabled, Visible) Then
                Return False
            End If

            If Not PreciseHitTest(ctx, ptPx) Then
                Return False
            End If

            RequestFocus()

            _isPressed = True
            InvalidateVisual()
            Return True
        End Function

        Protected Overrides Function OnMouseUp(ctx As MASThemeContext, ptPx As SKPoint, button As Integer) As Boolean
            If button <> CInt(MouseButtons.Left) Then
                Return False
            End If

            Dim wasPressed As Boolean = _isPressed
            _isPressed = False

            Dim inside As Boolean = PreciseHitTest(ctx, ptPx)

            If wasPressed AndAlso inside Then
                If Not _checked Then
                    Checked = True
                Else
                    InvalidateVisual()
                End If
                PulsePressReleaseMotion()
                Return True
            End If

            If wasPressed Then
                InvalidateVisual()
                Return True
            End If

            Return False
        End Function

        Protected Overrides Sub OnMouseMove(ctx As MASThemeContext, ptPx As SKPoint)
            Dim overNow As Boolean = PreciseHitTest(ctx, ptPx)
            If _isHover = overNow Then Return

            _isHover = overNow
            InvalidateVisual()
        End Sub

        Protected Overrides Sub OnMouseLeave(ctx As MASThemeContext)
            ResetTransientState(clearFocus:=False)
        End Sub

        Protected Overrides Sub OnMouseCancel(ctx As MASThemeContext)
            ResetTransientState(clearFocus:=False)
        End Sub

        Protected Overrides Sub OnHostLostFocus(ctx As MASThemeContext)
            ResetTransientState(clearFocus:=False)
        End Sub

        Protected Overrides Sub OnGotKeyboardFocus()
            If _hasFocus Then Return
            _hasFocus = True
            InvalidateVisual()
        End Sub

        Protected Overrides Sub OnLostKeyboardFocus()
            Dim needInvalidate As Boolean = (_hasFocus OrElse _isPressed OrElse _motionPressActive)
            _hasFocus = False
            _isPressed = False
            StopPressMotion(clearPressed:=True)

            If needInvalidate Then
                InvalidateVisual()
            End If
        End Sub

        Protected Overrides Sub OnEnabledChanged()
            If Not Enabled Then
                ResetTransientState(clearFocus:=True)
            End If

            InvalidateVisual()
        End Sub

        Protected Overrides Sub OnAttached()
            RegisterSelf()

            If _checked Then
                UncheckPeersInSameGroup()
            End If
        End Sub

        Protected Overrides Sub OnDetached()
            ResetTransientState(clearFocus:=True)
            _lastRenderBoundsPx = SKRect.Empty

            UnregisterSelf()
            MyBase.OnDetached()
        End Sub

        Protected Overrides Sub Dispose(disposing As Boolean)
            If disposing Then
                UnregisterSelf()
                _lastRenderBoundsPx = SKRect.Empty
                StopPressMotion(clearPressed:=True)

                Try
                    _painter.Dispose()
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException1)
                End Try
            End If

            MyBase.Dispose(disposing)
        End Sub

        Private Sub ResetTransientState(clearFocus As Boolean)
            Dim needInvalidate As Boolean = (_isHover OrElse _isPressed OrElse _motionPressActive OrElse (clearFocus AndAlso _hasFocus))

            _isHover = False
            _isPressed = False
            StopPressMotion(clearPressed:=True)

            If clearFocus Then
                _hasFocus = False
            End If

            If needInvalidate Then
                InvalidateVisual()
            End If
        End Sub

        Private Sub PulsePressReleaseMotion()
            If Not MASRadioButtonGroupPolicy.CanInteract(Enabled, Visible) Then Return

            _motionPressActive = True
            StopPressMotion(clearPressed:=False)
            _motionPressActive = True

            Dim plan As MASMotionTransitionPlan = MASMotionSystem.CreateTransitionPlan(MASMotionTokenId.PressRelease, 0.0F, 1.0F)

            _pressPulseRunner = MASMotionSystem.CreateTimelineRunner(
                owner:=Me,
                consumerName:="MASRadioButton.PressReleasePulse",
                plan:=plan,
                requestFrame:=AddressOf InvalidateVisual,
                applyFrame:=AddressOf ApplyPressMotionFrame,
                completed:=AddressOf CompletePressMotionFrame)
            _pressPulseRunner.Start()

            InvalidateVisual()
        End Sub

        Private Sub ApplyPressMotionFrame(frame As MASMotionFrame)
            If frame Is Nothing Then Return
            InvalidateVisual()
        End Sub

        Private Sub CompletePressMotionFrame(frame As MASMotionFrame)
            StopPressMotion(clearPressed:=True)
            InvalidateVisual()
        End Sub

        Private Sub StopPressMotion(clearPressed As Boolean)
            If _pressPulseRunner IsNot Nothing Then
                _pressPulseRunner.Dispose()
                _pressPulseRunner = Nothing
            End If

            If clearPressed Then
                _motionPressActive = False
            End If
        End Sub

        Private Function MoveSelectionWithinGroup(forward As Boolean) As Boolean
            Dim peers As MASRadioButton() = SnapshotPeers()
            Dim eligible As New List(Of MASRadioButton)()

            For Each peer As MASRadioButton In peers
                If peer Is Nothing Then Continue For
                If Not peer.Visible OrElse Not peer.Enabled Then Continue For
                If Not peer.IsSameGroupAs(Me) Then Continue For
                eligible.Add(peer)
            Next

            If eligible.Count = 0 Then Return False

            Dim currentIndex As Integer = -1
            For i As Integer = 0 To eligible.Count - 1
                If Object.ReferenceEquals(eligible(i), Me) Then
                    currentIndex = i
                    Exit For
                End If
            Next

            If currentIndex < 0 Then Return False

            Dim nextIndex As Integer = MASRadioButtonGroupPolicy.ResolveWrappedIndex(currentIndex, eligible.Count, forward)
            If nextIndex < 0 OrElse nextIndex >= eligible.Count Then Return False

            Dim target As MASRadioButton = eligible(nextIndex)
            If target Is Nothing Then Return False

            target.Checked = True
            RequestFocus(target)
            Return True
        End Function

        Private Function PreciseHitTest(ctx As MASThemeContext, ptPx As SKPoint) As Boolean
            If Not MASRadioButtonGroupPolicy.CanInteract(Enabled, Visible) Then Return False

            Dim boundsPx As SKRect = _lastRenderBoundsPx
            If boundsPx.Width <= 0.0F OrElse boundsPx.Height <= 0.0F Then
                boundsPx = GetPixelBounds(ctx)
            End If

            If boundsPx.Width <= 0.0F OrElse boundsPx.Height <= 0.0F Then Return False

            Dim layout As RadioButtonLayoutHelper.LayoutInfo =
                RadioButtonLayoutHelper.BuildLayout(
                    ctx:=ctx,
                    boundsPx:=boundsPx,
                    text:=_text
                )

            If layout.InteractiveRect.Width <= 0.0F OrElse layout.InteractiveRect.Height <= 0.0F Then
                Return False
            End If

            Return layout.InteractiveRect.Contains(ptPx)
        End Function

        Private Sub UncheckPeersInSameGroup()
            Dim peers As MASRadioButton() = SnapshotPeers()

            For Each peer As MASRadioButton In peers
                If peer Is Nothing Then Continue For
                If Object.ReferenceEquals(peer, Me) Then Continue For
                If Not peer.IsSameGroupAs(Me) Then Continue For
                If Not peer._checked Then Continue For

                peer.SetCheckedFromGroup(False)
            Next
        End Sub

        Private Function IsSameGroupAs(other As MASRadioButton) As Boolean
            If other Is Nothing Then Return False

            Return MASRadioButtonGroupPolicy.AreSameGroup(
                Me.Host,
                other.Host,
                Me._groupName,
                other._groupName)
        End Function

        Private Sub SetCheckedFromGroup(value As Boolean)
            If _checked = value Then Return

            _checked = value
            RaiseEvent CheckedChanged(Me, EventArgs.Empty)
            InvalidateVisual()
        End Sub

        Private Shared Sub RegisterSelfInstance(rb As MASRadioButton)
            If rb Is Nothing Then Return

            SyncLock _groupSync
                CleanupDeadEntries_NoLock()

                For Each wr As WeakReference(Of MASRadioButton) In _registry
                    If wr Is Nothing Then Continue For

                    Dim existing As MASRadioButton = Nothing
                    If wr.TryGetTarget(existing) Then
                        If Object.ReferenceEquals(existing, rb) Then
                            Return
                        End If
                    End If
                Next

                _registry.Add(New WeakReference(Of MASRadioButton)(rb))
            End SyncLock
        End Sub

        Private Shared Sub UnregisterSelfInstance(rb As MASRadioButton)
            If rb Is Nothing Then Return

            SyncLock _groupSync
                For i As Integer = _registry.Count - 1 To 0 Step -1
                    Dim wr As WeakReference(Of MASRadioButton) = _registry(i)

                    If wr Is Nothing Then
                        _registry.RemoveAt(i)
                        Continue For
                    End If

                    Dim existing As MASRadioButton = Nothing
                    If Not wr.TryGetTarget(existing) Then
                        _registry.RemoveAt(i)
                        Continue For
                    End If

                    If Object.ReferenceEquals(existing, rb) Then
                        _registry.RemoveAt(i)
                    End If
                Next
            End SyncLock
        End Sub

        Private Function SnapshotPeers() As MASRadioButton()
            Dim result As New List(Of MASRadioButton)()

            SyncLock _groupSync
                CleanupDeadEntries_NoLock()

                For Each wr As WeakReference(Of MASRadioButton) In _registry
                    If wr Is Nothing Then Continue For

                    Dim item As MASRadioButton = Nothing
                    If wr.TryGetTarget(item) AndAlso item IsNot Nothing Then
                        result.Add(item)
                    End If
                Next
            End SyncLock

            Return result.ToArray()
        End Function

        Private Shared Sub CleanupDeadEntries_NoLock()
            For i As Integer = _registry.Count - 1 To 0 Step -1
                Dim wr As WeakReference(Of MASRadioButton) = _registry(i)

                If wr Is Nothing Then
                    _registry.RemoveAt(i)
                    Continue For
                End If

                Dim item As MASRadioButton = Nothing
                If Not wr.TryGetTarget(item) OrElse item Is Nothing Then
                    _registry.RemoveAt(i)
                End If
            Next
        End Sub

        Private Sub RegisterSelf()
            RegisterSelfInstance(Me)
        End Sub

        Private Sub UnregisterSelf()
            UnregisterSelfInstance(Me)
        End Sub

        Private Shared Function CreateSizeContext(context As MASLayoutMeasureContext) As MASSizeContext
            If context Is Nothing Then Return New MASSizeContext(New SKSize(Single.PositiveInfinity, Single.PositiveInfinity))
            Return context.ToSizeContext()
        End Function

        Private Shared Function SafeBounds(bounds As SKRect) As SKRect
            If bounds.IsEmpty Then Return SKRect.Empty

            Dim left As Single = SafeCoordinate(bounds.Left)
            Dim top As Single = SafeCoordinate(bounds.Top)
            Dim width As Single = SafeExtent(bounds.Width)
            Dim height As Single = SafeExtent(bounds.Height)

            Return New SKRect(left, top, left + width, top + height)
        End Function

        Private Shared Function SafeCoordinate(value As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) Then Return 0.0F
            Return value
        End Function

        Private Shared Function SafeExtent(value As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) OrElse value < 0.0F Then Return 0.0F
            Return value
        End Function


#Region "Unified MASSize Fluent API"

        ''' <summary>
        ''' Strongly typed entry into the shared MASSize intent API.
        ''' This method keeps fluent chains on the concrete element while delegating all sizing decisions to MASControlBase/MASSize.
        ''' </summary>
        Public Shadows Function WithSize(sizeIntent As Nexamas.UI.Layout.MASSize) As MASRadioButton
            MyBase.SetSize(sizeIntent)
            Return Me
        End Function






#End Region
    End Class

End Namespace
