Option Strict On
Option Explicit On

Imports System
Imports Nexamas.UI.Scroll
Imports Nexamas.UI.Theming
Imports SkiaSharp

Namespace Nexamas.UI.SkiaScroll

    ''' <summary>
    ''' Internal Skia scrollbar adapter shared by MAS controls and popup content.
    ''' The adapter owns viewport/content metrics, drag/wheel routing, DPI refresh,
    ''' and invalidation coalescing; consumer applications must use the owning
    ''' controls/services instead of constructing scroll infrastructure directly.
    ''' </summary>
    Friend NotInheritable Class SkiaScrollAdapter
        Implements IDisposable

        Private ReadOnly _metrics As ScrollMetrics
        Private ReadOnly _host As SkiaScrollHost
        Private ReadOnly _invalidate As Action
        Private ReadOnly _repeatClock As MASScrollRepeatClock
        Private ReadOnly _hasOfficialDispatcher As Boolean

        Private _hostRect As SKRect = SKRect.Empty
        Private _contentHeight As Single
        Private _viewportHeight As Single
        Private _dpi As Single = 1.0F
        Private _lastCanScroll As Boolean
        Private _leftButtonPressed As Boolean
        Private _pumping As Boolean
        Private _disposed As Boolean

        Friend Sub New(metrics As ScrollMetrics,
               layout As ScrollbarLayout,
               painter As ScrollbarPainter,
               Optional invalidate As Action = Nothing,
               Optional postToUiThread As Func(Of Action, Boolean) = Nothing)

            If metrics Is Nothing Then Throw New ArgumentNullException(NameOf(metrics))
            If layout Is Nothing Then Throw New ArgumentNullException(NameOf(layout))
            If painter Is Nothing Then Throw New ArgumentNullException(NameOf(painter))

            _metrics = metrics
            _host = New SkiaScrollHost(metrics, layout, painter)
            _invalidate = invalidate
            _hasOfficialDispatcher = postToUiThread IsNot Nothing

            _repeatClock = New MASScrollRepeatClock(AddressOf OnPumpTick, postToUiThread:=postToUiThread)
        End Sub
        Friend Sub UpdateThemeContext(ctx As MASThemeContext)
            If _disposed Then Return
            If ctx Is Nothing Then Return

            _host.UpdateThemeContext(ctx)
        End Sub
        Friend NotInheritable Class VisibleRange
            Friend Property FirstIndex As Integer
            Friend Property LastIndex As Integer
            Friend Property RowsToDraw As Integer
            Friend Property StartY As Single
            Friend Property PixelOffset As Single
            Friend Property ContentTop As Single
            Friend Property ContentBottom As Single
            Friend Property ViewportBottom As Single
        End Class

        Friend ReadOnly Property Offset As Single
            Get
                Return _host.Offset
            End Get
        End Property

        Friend ReadOnly Property IsPumping As Boolean
            Get
                Return _pumping
            End Get
        End Property

        Friend Sub Update(hostRect As SKRect, dpi As Single, contentHeight As Single)
            If _disposed Then Return

            Dim oldHost As SKRect = _hostRect
            Dim oldContent As Single = _contentHeight
            Dim oldViewport As Single = _viewportHeight
            Dim oldDpi As Single = _dpi

            Dim d As Single = dpi
            If d <= 0.0F OrElse Single.IsNaN(d) OrElse Single.IsInfinity(d) Then
                d = 1.0F
            End If

            Dim newHost As SKRect = hostRect
            Dim newContent As Single = Math.Max(0.0F, contentHeight)
            Dim newViewport As Single = Math.Max(0.0F, hostRect.Height)
            Dim newCanScroll As Boolean = (newContent > newViewport + 0.5F)

            ' CONTRACT:
            ' A False -> True CanScroll transition is normal during first measured/painted layout
            ' and must not cancel an active arrow auto-repeat.  Even a transient True -> False
            ' layout pulse must not cancel while the pointer is logically held down; MouseUp,
            ' CancelInteraction, or Dispose own the end of the held gesture.
            If _lastCanScroll AndAlso Not newCanScroll AndAlso Not _leftButtonPressed Then
                _leftButtonPressed = False
                StopPump()
                _host.MouseUp()
                _host.MouseLeave()
            End If

            _hostRect = newHost
            _dpi = d
            _contentHeight = newContent
            _viewportHeight = newViewport

            _metrics.UpdateDpi(_dpi)
            _host.Update(_hostRect, _contentHeight, _viewportHeight)
            _lastCanScroll = newCanScroll

            Dim changed As Boolean =
                (oldHost <> _hostRect) OrElse
                (oldContent <> _contentHeight) OrElse
                (oldViewport <> _viewportHeight) OrElse
                (oldDpi <> _dpi)

            If changed Then
                RequestInvalidate()
            End If
        End Sub

        Friend Function GetVisibleRange(rowH As Single, totalItems As Integer) As VisibleRange
            Dim vr As New VisibleRange()

            Dim firstIndex As Integer = 0
            Dim rowsToDraw As Integer = 0
            Dim startY As Single = _hostRect.Top

            _host.GetVisibleRange(rowH, totalItems, firstIndex, rowsToDraw, startY, _hostRect.Top)

            If totalItems <= 0 OrElse rowH <= 0.0F OrElse _viewportHeight <= 0.0F Then
                vr.FirstIndex = 0
                vr.LastIndex = -1
                vr.RowsToDraw = 0
                vr.StartY = startY
                vr.PixelOffset = 0.0F
                vr.ContentTop = 0.0F
                vr.ContentBottom = 0.0F
                vr.ViewportBottom = _hostRect.Bottom
                Return vr
            End If

            Dim scrollOffset As Single = Offset
            Dim yOffset As Single = scrollOffset - (firstIndex * rowH)
            Dim lastIndex As Integer = Math.Min(totalItems - 1, firstIndex + rowsToDraw - 1)

            vr.FirstIndex = firstIndex
            vr.LastIndex = lastIndex
            vr.RowsToDraw = rowsToDraw
            vr.StartY = startY
            vr.PixelOffset = yOffset
            vr.ContentTop = scrollOffset
            vr.ContentBottom = scrollOffset + _viewportHeight
            vr.ViewportBottom = _hostRect.Bottom

            Return vr
        End Function

        Friend Function HitTestIndex(pt As SKPoint, rowH As Single, totalItems As Integer) As Integer
            If _disposed Then Return -1
            Return _host.HitTestIndex(pt, _hostRect, rowH, totalItems)
        End Function

        Friend Sub EnsureVisible(top As Single, bottom As Single)
            If _disposed Then Return
            If _host.EnsureVisible(top, bottom) Then
                RequestInvalidate()
            End If
        End Sub

        Friend Sub ScrollToOffset(offsetPx As Single)
            If _disposed Then Return
            If _host.ScrollToOffset(offsetPx) Then
                RequestInvalidate()
            End If
        End Sub

        Friend Sub Draw(canvas As SKCanvas, isHover As Boolean)
            If _disposed Then Return
            If canvas Is Nothing Then Return

            _host.Paint(canvas, _dpi, isHover)
        End Sub

        Friend Function MouseMove(pt As SKPoint) As Boolean
            If _disposed Then Return False

            Dim handled As Boolean = _host.MouseMove(pt)

            If _host.RequiresPump Then
                StartPump()
            ElseIf _pumping Then
                StopPump()
            End If

            If handled Then
                RequestInvalidate()
            End If

            Return handled
        End Function

        Friend Function MouseDown(pt As SKPoint, rowStepPx As Single, pageFactor As Single) As Boolean
            If _disposed Then Return False

            Dim handled As Boolean = _host.MouseDown(pt, rowStepPx, pageFactor)

            If handled Then
                _leftButtonPressed = True

                If _host.RequiresPump Then
                    StartPump()
                Else
                    StopPump()
                End If

                RequestInvalidate()
            End If

            Return handled
        End Function

        Friend Function MouseUp() As Boolean
            If _disposed Then Return False

            Dim handled As Boolean = _host.MouseUp()
            _leftButtonPressed = False
            StopPump()

            If handled Then
                RequestInvalidate()
            End If

            Return handled
        End Function

        Friend Sub MouseLeave()
            If _disposed Then Return

            Dim changed As Boolean = _host.MouseLeave()
            Dim wasPumping As Boolean = _pumping

            If Not _host.RequiresPump Then
                StopPump()
            End If

            If changed OrElse wasPumping Then
                RequestInvalidate()
            End If
        End Sub

        Friend Function CanMouseWheel(delta As Integer, stepPx As Single) As Boolean
            If _disposed Then Return False
            Return _host.CanMouseWheel(delta, stepPx)
        End Function

        Friend Function MouseWheel(delta As Integer, stepPx As Single) As Boolean
            If _disposed Then Return False

            Dim handled As Boolean = _host.MouseWheel(delta, stepPx)

            If handled Then
                RequestInvalidate()
            End If

            Return handled
        End Function

        Friend Sub CancelInteraction()
            If _disposed Then Return

            Dim wasPumping As Boolean = _pumping

            _leftButtonPressed = False
            StopPump()

            Dim changed As Boolean = _host.MouseUp()
            changed = _host.MouseLeave() OrElse changed

            If changed OrElse wasPumping Then
                RequestInvalidate()
            End If
        End Sub

        Private Sub StartPump()
            If Not _leftButtonPressed Then Return
            If Not _hasOfficialDispatcher Then
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(
                    New InvalidOperationException("Scroll repeat pump was not started because no official UI dispatcher was supplied."),
                    "SkiaScrollAdapter.ScrollRepeatDispatcherMissing")
                Return
            End If
            If _pumping AndAlso Not _repeatClock.IsRunning Then _pumping = False
            If _pumping Then Return
            _pumping = True
            Nexamas.UI.Performance.MASPerformanceSystem.RecordConsumerPressure(
                Nexamas.UI.Performance.MASPerformanceConsumerKind.ScrollAdapter,
                "SkiaScrollAdapter.StartPump")
            _repeatClock.Start()
        End Sub

        Private Sub StopPump()
            If Not _pumping Then Return
            _pumping = False
            _repeatClock.Stop()
        End Sub

        Private Sub OnPumpTick()
            If _disposed OrElse Not _pumping Then Return

            Nexamas.UI.Performance.MASPerformanceSystem.RecordConsumerInput(
                Nexamas.UI.Performance.MASPerformanceConsumerKind.ScrollAdapter,
                "SkiaScrollAdapter.OnPumpTick")

            If Not _leftButtonPressed Then
                Dim changedByCancel As Boolean = _host.MouseUp()
                StopPump()

                If changedByCancel Then
                    RequestInvalidate()
                End If

                Return
            End If

            If Not _host.RequiresPump Then
                StopPump()
                Return
            End If

            Dim changed As Boolean = _host.Tick()
            If changed Then
                RequestInvalidate()
            End If
        End Sub

        Private Sub RequestInvalidate()
            If _disposed Then Return
            Nexamas.UI.Performance.MASPerformanceSystem.RecordConsumerInvalidation(
                Nexamas.UI.Performance.MASPerformanceConsumerKind.ScrollAdapter,
                "SkiaScrollAdapter.RequestInvalidate")
            _invalidate?.Invoke()
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            If _disposed Then Return
            _disposed = True

            _leftButtonPressed = False
            StopPump()
            _repeatClock.Dispose()
            _host.Dispose()
        End Sub

    End Class

End Namespace