Option Strict On
Option Explicit On

Imports System
Imports System.Threading

Namespace Nexamas.UI.SkiaScroll

    ''' <summary>
    ''' Internal, host-independent repeat clock for scrollbar arrow hold gestures.
    ''' This is a behavioral repeat clock for input hold semantics, not a visual Motion owner.
    ''' It deliberately does not read Control.MouseButtons and does not use
    ''' System.Windows.Forms.Timer.  The clock runs from a Threading.Timer and
    ''' marshals each repeat pulse through the root-owned UI dispatcher when one
    ''' is supplied.  When an official dispatcher is supplied it is authoritative:
    ''' a rejected post stops the clock instead of falling back to a captured
    ''' SynchronizationContext that may belong to a detached host.  Only adapter
    ''' instances created without an official dispatcher use the captured context
    ''' as a legacy host-independent fallback.  Repeat callbacks are treated as an
    ''' input boundary: callback failures stop the clock and are reported through the
    ''' diagnostics channel instead of escaping from the timer/dispatcher callback.  The
    ''' interval is intentionally close to a 60 Hz frame cadence, while the controller
    ''' owns the actual velocity curve.
    ''' </summary>
    Friend NotInheritable Class MASScrollRepeatClock
        Implements IDisposable

        Private Const DEFAULT_INTERVAL_MS As Integer = 15

        Private ReadOnly _context As SynchronizationContext
        Private ReadOnly _intervalMs As Integer
        Private ReadOnly _callback As Action
        Private ReadOnly _postToUiThread As Func(Of Action, Boolean)
        Private ReadOnly _officialDispatcherRequired As Boolean
        Private ReadOnly _gate As New Object()

        Private _timer As Timer
        Private _running As Boolean
        Private _callbackPending As Boolean
        Private _disposed As Boolean

        Friend Sub New(callback As Action,
                       Optional intervalMs As Integer = DEFAULT_INTERVAL_MS,
                       Optional postToUiThread As Func(Of Action, Boolean) = Nothing)
            If callback Is Nothing Then Throw New ArgumentNullException(NameOf(callback))

            _callback = callback
            _intervalMs = Nexamas.UI.Timing.MASVisualClock.NormalizeVisualPulseIntervalMs(intervalMs)
            _postToUiThread = postToUiThread
            _officialDispatcherRequired = postToUiThread IsNot Nothing
            _context = If(_officialDispatcherRequired, Nothing, SynchronizationContext.Current)
        End Sub

        Friend ReadOnly Property IsRunning As Boolean
            Get
                SyncLock _gate
                    Return _running
                End SyncLock
            End Get
        End Property

        Friend Sub Start()
            SyncLock _gate
                If _disposed OrElse _running Then Return

                _running = True
                Nexamas.UI.Performance.MASPerformanceSystem.RecordConsumerPressure(
                    Nexamas.UI.Performance.MASPerformanceConsumerKind.ScrollRepeatClock,
                    "MASScrollRepeatClock.Start")

                If _timer Is Nothing Then
                    _timer = New Timer(AddressOf OnTimer, Nothing, _intervalMs, _intervalMs)
                Else
                    _timer.Change(_intervalMs, _intervalMs)
                End If
            End SyncLock
        End Sub

        Friend Sub [Stop]()
            SyncLock _gate
                If Not _running Then Return

                _running = False
                If _timer IsNot Nothing Then
                    _timer.Change(Timeout.Infinite, Timeout.Infinite)
                End If
            End SyncLock
        End Sub

        Private Sub OnTimer(state As Object)
            SyncLock _gate
                If _disposed OrElse Not _running OrElse _callbackPending Then Return
                _callbackPending = True
            End SyncLock

            If TryPostToOfficialUiDispatcher() Then Return

            If _officialDispatcherRequired Then
                StopAfterOfficialDispatcherUnavailable()
                Return
            End If

            If TryPostToCapturedContextFallback() Then Return

            StopAfterCapturedContextUnavailable()
        End Sub

        Private Function TryPostToOfficialUiDispatcher() As Boolean
            If _postToUiThread Is Nothing Then Return False

            Try
                Return _postToUiThread.Invoke(AddressOf RunCallbackOnOfficialUiDispatcher)
            Catch masCaughtExceptionPostToUi As Exception
                StopAfterDispatcherUnavailableCore()
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowLifecycleBreaking(
                    masCaughtExceptionPostToUi,
                    "MASScrollRepeatClock.PostToUiThread")
                Return True
            End Try
        End Function

        Private Function TryPostToCapturedContextFallback() As Boolean
            If _context Is Nothing Then Return False

            Try
                _context.Post(AddressOf RunCallbackOnCapturedContext, Nothing)
                Return True
            Catch masCaughtExceptionCapturedContext As Exception
                StopAfterDispatcherUnavailableCore()
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowLifecycleBreaking(
                    masCaughtExceptionCapturedContext,
                    "MASScrollRepeatClock.CapturedContextFallback")
                Return True
            End Try
        End Function

        Private Sub StopAfterOfficialDispatcherUnavailable()
            StopAfterDispatcherUnavailableCore()
            Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowLifecycleBreaking(
                New InvalidOperationException("Scroll repeat pulse was rejected by the official UI dispatcher. The clock was stopped to avoid posting through a stale captured SynchronizationContext."),
                "MASScrollRepeatClock.OfficialDispatcherUnavailable")
        End Sub

        Private Sub StopAfterCapturedContextUnavailable()
            StopAfterDispatcherUnavailableCore()
        End Sub

        Private Sub StopAfterDispatcherUnavailableCore()
            Dim timerToStop As Timer = Nothing

            SyncLock _gate
                If _disposed Then
                    _callbackPending = False
                    Return
                End If

                _running = False
                _callbackPending = False
                timerToStop = _timer
            End SyncLock

            If timerToStop IsNot Nothing Then
                Try
                    timerToStop.Change(Timeout.Infinite, Timeout.Infinite)
                Catch masCaughtExceptionTimer As ObjectDisposedException
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtExceptionTimer)
                End Try
            End If
        End Sub

        Private Sub RunCallbackOnOfficialUiDispatcher()
            RunCallbackOnCapturedContext(Nothing)
        End Sub

        Private Sub RunCallbackOnCapturedContext(state As Object)
            SyncLock _gate
                If _disposed OrElse Not _running Then
                    _callbackPending = False
                    Return
                End If
            End SyncLock

            Try
                Nexamas.UI.Performance.MASPerformanceSystem.RecordConsumerInput(
                    Nexamas.UI.Performance.MASPerformanceConsumerKind.ScrollRepeatClock,
                    "MASScrollRepeatClock.RunCallback")
                _callback.Invoke()
            Catch masCaughtExceptionCallback As Exception
                StopAfterCallbackFailure()
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(
                    masCaughtExceptionCallback,
                    "MASScrollRepeatClock.RunCallback")
            Finally
                SyncLock _gate
                    _callbackPending = False
                End SyncLock
            End Try
        End Sub

        Private Sub StopAfterCallbackFailure()
            Dim timerToStop As Timer = Nothing

            SyncLock _gate
                If _disposed Then
                    _callbackPending = False
                    Return
                End If

                _running = False
                _callbackPending = False
                timerToStop = _timer
            End SyncLock

            If timerToStop IsNot Nothing Then
                Try
                    timerToStop.Change(Timeout.Infinite, Timeout.Infinite)
                Catch masCaughtExceptionTimer As ObjectDisposedException
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtExceptionTimer)
                End Try
            End If
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            Dim timerToDispose As Timer = Nothing

            SyncLock _gate
                If _disposed Then Return

                _disposed = True
                _running = False
                _callbackPending = False
                timerToDispose = _timer
                _timer = Nothing
            End SyncLock

            If timerToDispose IsNot Nothing Then
                timerToDispose.Dispose()
            End If
        End Sub

    End Class

End Namespace
