Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic

Namespace Nexamas.UI.Motion

    ''' <summary>
    ''' Internal host-scoped Motion clock registry driven by one owning frame scheduler.
    ''' </summary>
    ''' <remarks>
    ''' Consumers register a small frame callback and an invalidate callback against the clock
    ''' that belongs to their owning host.  The clock owns no WinForms timers, background
    ''' threads, or dispatcher loops; it is advanced only by its matching MASSkiaFrameScheduler
    ''' after that host completes a native PaintSurface cycle.
    ''' </remarks>
    Friend NotInheritable Class MASMotionFrameClock
        Implements IDisposable

        Private NotInheritable Class RegisteredMotionConsumer

            Friend Sub New(owner As Object,
                           consumerName As String,
                           requestFrame As Action,
                           advanceFrame As Func(Of Integer, Boolean))
                Me.Owner = owner
                Me.ConsumerName = If(consumerName, String.Empty)
                Me.RequestFrame = requestFrame
                Me.AdvanceFrame = advanceFrame
            End Sub

            Friend ReadOnly Property Owner As Object
            Friend ReadOnly Property ConsumerName As String
            Friend ReadOnly Property RequestFrame As Action
            Friend ReadOnly Property AdvanceFrame As Func(Of Integer, Boolean)

        End Class

        Private Shared ReadOnly s_registrySyncRoot As New Object()
        Private Shared ReadOnly s_knownClocks As New List(Of WeakReference(Of MASMotionFrameClock))()

        Private ReadOnly _syncRoot As New Object()
        Private ReadOnly _consumers As New List(Of RegisteredMotionConsumer)()
        Private ReadOnly _clockName As String

        Private _lastTimestamp As Long
        Private _disposed As Boolean

        Friend Sub New(Optional clockName As String = Nothing)
            _clockName = If(String.IsNullOrWhiteSpace(clockName),
                            "MASMotionFrameClock",
                            clockName.Trim())
            RegisterKnownClock(Me)
        End Sub

        Friend ReadOnly Property ClockName As String
            Get
                Return _clockName
            End Get
        End Property

        Friend Sub Register(owner As Object,
                            consumerName As String,
                            requestFrame As Action,
                            advanceFrame As Func(Of Integer, Boolean))
            If owner Is Nothing OrElse requestFrame Is Nothing OrElse advanceFrame Is Nothing Then Return

            Dim shouldRequestFrame As Boolean = False

            SyncLock _syncRoot
                If _disposed Then Return
                RemoveConsumerLocked(owner)
                _consumers.Add(New RegisteredMotionConsumer(owner, consumerName, requestFrame, advanceFrame))
                If _lastTimestamp <= 0L Then _lastTimestamp = Nexamas.UI.Timing.MASVisualClock.Timestamp()
                shouldRequestFrame = True
            End SyncLock

            If shouldRequestFrame AndAlso Not IsDisposedSnapshot() Then
                RequestFrameSafe(requestFrame)
            End If
        End Sub

        Friend Sub Unregister(owner As Object)
            If owner Is Nothing Then Return

            SyncLock _syncRoot
                RemoveConsumerLocked(owner)
                If _consumers.Count = 0 Then _lastTimestamp = 0L
            End SyncLock
        End Sub

        Friend Sub UnregisterAll()
            SyncLock _syncRoot
                _consumers.Clear()
                _lastTimestamp = 0L
            End SyncLock
        End Sub

        Friend Function HasActiveConsumers() As Boolean
            SyncLock _syncRoot
                Return _consumers.Count > 0
            End SyncLock
        End Function

        Friend Function ActiveConsumerCount() As Integer
            SyncLock _syncRoot
                Return _consumers.Count
            End SyncLock
        End Function

        ''' <summary>
        ''' Advances only this host clock's registered Motion consumers once from its owning frame scheduler.
        ''' </summary>
        ''' <returns>True when at least one consumer remains alive and has requested another rendered frame.</returns>
        Friend Function AdvanceScheduledAnimations() As Boolean
            Dim snapshot As List(Of RegisteredMotionConsumer)

            SyncLock _syncRoot
                If _disposed OrElse _consumers.Count = 0 Then Return False
                snapshot = New List(Of RegisteredMotionConsumer)(_consumers)
            End SyncLock

            Dim deltaMs As Integer = ComputeDeltaMs()

            Dim aliveRequests As New List(Of Action)()
            Dim activeAfterAdvance As Integer = 0
            Dim advanceStart As Long = Nexamas.UI.Timing.MASVisualClock.Timestamp()

            For Each consumer As RegisteredMotionConsumer In snapshot
                If consumer Is Nothing Then Continue For
                If IsDisposedSnapshot() Then Exit For
                If Not IsConsumerStillRegistered(consumer.Owner, consumer) Then Continue For

                Dim alive As Boolean = False
                Try
                    alive = consumer.AdvanceFrame.Invoke(deltaMs)
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException1)
                    alive = False
                End Try

                SyncLock _syncRoot
                    If _disposed Then
                        alive = False
                    End If

                    Dim current As RegisteredMotionConsumer = FindConsumerLocked(consumer.Owner)
                    If Not alive Then
                        If current IsNot Nothing Then RemoveConsumerLocked(consumer.Owner)
                    ElseIf current IsNot Nothing AndAlso Object.ReferenceEquals(current, consumer) Then
                        activeAfterAdvance += 1
                        If current.RequestFrame IsNot Nothing Then aliveRequests.Add(current.RequestFrame)
                    End If
                End SyncLock
            Next

            If activeAfterAdvance = 0 Then
                SyncLock _syncRoot
                    If _consumers.Count = 0 Then _lastTimestamp = 0L
                End SyncLock
            End If

            Dim advanceElapsedMs As Double = ComputeElapsedMs(advanceStart)
            Nexamas.UI.Performance.MASPerformanceSystem.RecordMotionFrameClockAdvance(activeAfterAdvance, advanceElapsedMs)

            If Not IsDisposedSnapshot() Then
                For Each requestFrame As Action In aliveRequests
                    If IsDisposedSnapshot() Then Exit For
                    RequestFrameSafe(requestFrame)
                Next
            End If

            Return activeAfterAdvance > 0 AndAlso Not IsDisposedSnapshot()
        End Function

        Public Sub Dispose() Implements IDisposable.Dispose
            SyncLock _syncRoot
                If _disposed Then Return
                _disposed = True
                _consumers.Clear()
                _lastTimestamp = 0L
            End SyncLock
        End Sub

        Friend Shared Function AggregateHasActiveConsumers() As Boolean
            Return AggregateActiveConsumerCount() > 0
        End Function

        Friend Shared Function AggregateActiveConsumerCount() As Integer
            Dim clocks As List(Of MASMotionFrameClock) = SnapshotKnownClocks()
            Dim total As Integer = 0

            For Each clock As MASMotionFrameClock In clocks
                If clock Is Nothing Then Continue For
                total += clock.ActiveConsumerCount()
            Next

            Return total
        End Function

        Friend Shared Sub UnregisterFromAll(owner As Object)
            If owner Is Nothing Then Return

            Dim clocks As List(Of MASMotionFrameClock) = SnapshotKnownClocks()
            For Each clock As MASMotionFrameClock In clocks
                If clock Is Nothing Then Continue For
                clock.Unregister(owner)
            Next
        End Sub

        Friend Shared Function AdvanceAllKnownClocksForDiagnostics() As Boolean
            Dim clocks As List(Of MASMotionFrameClock) = SnapshotKnownClocks()
            Dim anyAlive As Boolean = False

            For Each clock As MASMotionFrameClock In clocks
                If clock Is Nothing Then Continue For
                anyAlive = clock.AdvanceScheduledAnimations() OrElse anyAlive
            Next

            Return anyAlive
        End Function

        Private Function ComputeDeltaMs() As Integer
            Dim nowTick As Long = Nexamas.UI.Timing.MASVisualClock.Timestamp()
            Dim previousTick As Long

            SyncLock _syncRoot
                previousTick = _lastTimestamp
                _lastTimestamp = nowTick
            End SyncLock

            Return Nexamas.UI.Timing.MASVisualClock.TimestampDeltaMilliseconds(previousTick, nowTick)
        End Function

        Private Shared Function ComputeElapsedMs(startTick As Long) As Double
            Return Nexamas.UI.Timing.MASVisualClock.ElapsedMilliseconds(startTick)
        End Function

        Private Sub RemoveConsumerLocked(owner As Object)
            For i As Integer = _consumers.Count - 1 To 0 Step -1
                Dim item As RegisteredMotionConsumer = _consumers(i)
                If item IsNot Nothing AndAlso Object.ReferenceEquals(item.Owner, owner) Then
                    _consumers.RemoveAt(i)
                End If
            Next
        End Sub

        Private Function FindConsumerLocked(owner As Object) As RegisteredMotionConsumer
            For Each item As RegisteredMotionConsumer In _consumers
                If item IsNot Nothing AndAlso Object.ReferenceEquals(item.Owner, owner) Then Return item
            Next

            Return Nothing
        End Function

        Private Function IsDisposedSnapshot() As Boolean
            SyncLock _syncRoot
                Return _disposed
            End SyncLock
        End Function

        Private Function IsConsumerStillRegistered(owner As Object,
                                                   expectedConsumer As RegisteredMotionConsumer) As Boolean
            If owner Is Nothing OrElse expectedConsumer Is Nothing Then Return False

            SyncLock _syncRoot
                If _disposed Then Return False
                Dim current As RegisteredMotionConsumer = FindConsumerLocked(owner)
                Return Object.ReferenceEquals(current, expectedConsumer)
            End SyncLock
        End Function

        Private Shared Sub RequestFrameSafe(requestFrame As Action)
            Try
                requestFrame.Invoke()
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRecoverable(masCaughtException2)
            End Try
        End Sub

        Private Shared Sub RegisterKnownClock(clock As MASMotionFrameClock)
            If clock Is Nothing Then Return

            SyncLock s_registrySyncRoot
                PruneDeadKnownClocksLocked()
                s_knownClocks.Add(New WeakReference(Of MASMotionFrameClock)(clock))
            End SyncLock
        End Sub

        Private Shared Function SnapshotKnownClocks() As List(Of MASMotionFrameClock)
            Dim clocks As New List(Of MASMotionFrameClock)()

            SyncLock s_registrySyncRoot
                For i As Integer = s_knownClocks.Count - 1 To 0 Step -1
                    Dim weak As WeakReference(Of MASMotionFrameClock) = s_knownClocks(i)
                    Dim clock As MASMotionFrameClock = Nothing
                    If weak Is Nothing OrElse Not weak.TryGetTarget(clock) Then
                        s_knownClocks.RemoveAt(i)
                    ElseIf clock IsNot Nothing Then
                        clocks.Add(clock)
                    End If
                Next
            End SyncLock

            Return clocks
        End Function

        Private Shared Sub PruneDeadKnownClocksLocked()
            For i As Integer = s_knownClocks.Count - 1 To 0 Step -1
                Dim weak As WeakReference(Of MASMotionFrameClock) = s_knownClocks(i)
                Dim clock As MASMotionFrameClock = Nothing
                If weak Is Nothing OrElse Not weak.TryGetTarget(clock) Then
                    s_knownClocks.RemoveAt(i)
                End If
            Next
        End Sub

    End Class

End Namespace
