Option Strict On
Option Explicit On

Imports System
Imports SkiaSharp
Imports Nexamas.UI.General

Namespace Nexamas.UI.Scroll

    Friend NotInheritable Class ScrollbarController

        Friend Enum Part
            None = 0
            UpArrow = 1
            DownArrow = 2
            Track = 3
            Thumb = 4
        End Enum

        Friend Structure State
            Friend HoverPart As Part
            Friend DownPart As Part
            Friend Dragging As Boolean
        End Structure

        Private ReadOnly _model As ScrollModel
        Private ReadOnly _metrics As ScrollMetrics
        Private ReadOnly _layout As ScrollbarLayout

        Private _hostRect As SKRect = SKRect.Empty
        Private _contentHeight As Single
        Private _viewportHeight As Single

        Private _hover As Part = Part.None
        Private _down As Part = Part.None

        Private _dragging As Boolean
        Private _dragOffsetY As Single

        Private _mouseDown As Boolean
        Private _pressedArrowPart As Part = Part.None
        Private _repeatPart As Part = Part.None
        Private _repeatRowStepPx As Single
        Private _repeatDirection As Integer
        Private _repeatLastTickMs As Long
        Private _repeatCarryPx As Single
        Private _repeatStartedTick As Integer
        Private _repeatClockActive As Boolean

        ' Smooth arrow-hold motion policy.  The first click still performs the official
        ' row/line step, but the held repeat becomes a small, time-based pixel stream
        ' with a short acceleration ramp instead of coarse row-sized jumps.
        Private Const REPEAT_INITIAL_DELAY_MS As Integer = 170
        Private Const REPEAT_TARGET_INTERVAL_MS As Single = 92.0F
        Private Const REPEAT_RAMP_DURATION_MS As Single = 240.0F
        Private Const REPEAT_MIN_VELOCITY_FACTOR As Single = 0.28F
        Private Const REPEAT_MAX_REFERENCE_STEP_PX As Single = 64.0F
        Private Const REPEAT_MIN_STEP_PX As Single = 1.0F
        Private Const REPEAT_MAX_TICK_MS As Long = 34

        Friend Sub New(model As ScrollModel, metrics As ScrollMetrics, layout As ScrollbarLayout)
            If model Is Nothing Then Throw New ArgumentNullException(NameOf(model))
            If metrics Is Nothing Then Throw New ArgumentNullException(NameOf(metrics))
            If layout Is Nothing Then Throw New ArgumentNullException(NameOf(layout))

            _model = model
            _metrics = metrics
            _layout = layout
        End Sub

        Friend Sub Update(hostRect As SKRect, contentHeight As Single, viewportHeight As Single)
            _hostRect = PixelSnap.SnapRectHalf(hostRect)
            _contentHeight = Math.Max(0.0F, PixelSnap.SnapHalf(contentHeight))
            _viewportHeight = Math.Max(0.0F, PixelSnap.SnapHalf(viewportHeight))

            If Not CanScroll() AndAlso Not _mouseDown Then
                ResetInteraction()
            End If
        End Sub

        Friend Function GetState() As State
            Return New State With {
                .HoverPart = _hover,
                .DownPart = _down,
                .Dragging = _dragging
            }
        End Function

        Friend ReadOnly Property RequiresPump As Boolean
            Get
                Return _mouseDown AndAlso
                    (_repeatPart = Part.UpArrow OrElse _repeatPart = Part.DownArrow) AndAlso
                    _repeatDirection <> 0
            End Get
        End Property

        Friend Function Tick() As Boolean
            If Not _mouseDown Then Return False
            If _repeatPart <> Part.UpArrow AndAlso _repeatPart <> Part.DownArrow Then Return False
            Return ProcessArrowAutoRepeat()
        End Function

        Friend Function MouseMove(pt As SKPoint) As Boolean
            Dim oldHover As Part = _hover
            Dim oldPos As Integer = _model.Position

            If Not CanScroll() Then
                _hover = Part.None
                Return (oldHover <> _hover)
            End If

            Dim geo As ScrollbarLayout.Geometry = _layout.Compute(_hostRect, _contentHeight, _viewportHeight, _model)

            If geo.UpRect.Contains(pt) Then
                _hover = Part.UpArrow
            ElseIf geo.DownRect.Contains(pt) Then
                _hover = Part.DownArrow
            ElseIf geo.ThumbRect.Contains(pt) Then
                _hover = Part.Thumb
            ElseIf geo.TrackRect.Contains(pt) Then
                _hover = Part.Track
            Else
                _hover = Part.None
            End If

            If _dragging Then
                Dim newY As Single = PixelSnap.SnapHalf(pt.Y - _dragOffsetY)
                Dim maxTravel As Single = geo.TrackRect.Height - geo.ThumbRect.Height

                If maxTravel <= 0 Then
                    _model.Position = 0
                    Return (_model.Position <> oldPos) OrElse (oldHover <> _hover)
                End If

                If newY < geo.TrackRect.Top Then newY = geo.TrackRect.Top
                If newY > geo.TrackRect.Top + maxTravel Then newY = geo.TrackRect.Top + maxTravel

                Dim ratio As Single = Clamp01((newY - geo.TrackRect.Top) / maxTravel)
                _model.Position = CInt(Math.Round(_model.MaxPosition * ratio))

                Return (_model.Position <> oldPos) OrElse (oldHover <> _hover)
            End If

            If _mouseDown AndAlso (_pressedArrowPart = Part.UpArrow OrElse _pressedArrowPart = Part.DownArrow) Then
                Dim isInsidePressedArrow As Boolean =
                    (_pressedArrowPart = Part.UpArrow AndAlso geo.UpRect.Contains(pt)) OrElse
                    (_pressedArrowPart = Part.DownArrow AndAlso geo.DownRect.Contains(pt))

                If isInsidePressedArrow Then
                    _down = _pressedArrowPart

                    If Not RequiresPump Then
                        StartAutoRepeat(_pressedArrowPart, If(_pressedArrowPart = Part.UpArrow, -1, 1))
                    End If
                Else
                    _down = Part.None
                    StopAutoRepeat()
                End If
            End If

            Return (oldHover <> _hover)
        End Function

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

            _mouseDown = True
            _repeatRowStepPx = Math.Max(1.0F, PixelSnap.SnapHalf(rowStepPx))

            Dim oldPos As Integer = _model.Position
            Dim geo As ScrollbarLayout.Geometry = _layout.Compute(_hostRect, _contentHeight, _viewportHeight, _model)

            If geo.UpRect.Contains(pt) Then
                _pressedArrowPart = Part.UpArrow
                _down = Part.UpArrow
                ApplyArrowStep(-1)
                StartAutoRepeat(Part.UpArrow, -1)
                Return True
            End If

            If geo.DownRect.Contains(pt) Then
                _pressedArrowPart = Part.DownArrow
                _down = Part.DownArrow
                ApplyArrowStep(1)
                StartAutoRepeat(Part.DownArrow, 1)
                Return True
            End If

            If geo.ThumbRect.Contains(pt) Then
                _pressedArrowPart = Part.None
                _down = Part.Thumb
                StopAutoRepeat()
                _dragging = True
                _dragOffsetY = pt.Y - geo.ThumbRect.Top
                Return True
            End If

            StopAutoRepeat()
            _pressedArrowPart = Part.None
            _down = Part.None

            If geo.TrackRect.Contains(pt) Then
                Dim factor As Single = Math.Max(0.1F, Math.Min(1.0F, pageFactor))
                Dim pagePx As Integer = CInt(Math.Round(_viewportHeight * factor))

                If pt.Y < geo.ThumbRect.Top Then
                    _model.ScrollBy(-pagePx)
                ElseIf pt.Y > geo.ThumbRect.Bottom Then
                    _model.ScrollBy(pagePx)
                End If

                If _model.Position <> oldPos Then Return True

                _mouseDown = False
                Return False
            End If

            _mouseDown = False
            Return False
        End Function

        Friend Function MouseUp() As Boolean
            Dim hadInteraction As Boolean =
                _dragging OrElse
                _mouseDown OrElse
                _down <> Part.None OrElse
                _pressedArrowPart <> Part.None OrElse
                _repeatPart <> Part.None

            _dragging = False
            _dragOffsetY = 0.0F
            _mouseDown = False
            _pressedArrowPart = Part.None
            _down = Part.None
            StopAutoRepeat()

            Return hadInteraction
        End Function

        Friend Function MouseLeave() As Boolean
            If _mouseDown Then
                Dim hadHover As Boolean = (_hover <> Part.None)
                _hover = Part.None
                Return hadHover
            End If

            Dim hadInteraction As Boolean = HasInteractionState()
            ResetInteraction()
            Return hadInteraction
        End Function

        Friend Function CanMouseWheel(delta As Integer, stepPx As Single) As Boolean
            If Not CanScroll() Then Return False
            If delta = 0 Then Return False

            stepPx = Math.Max(1.0F, PixelSnap.SnapHalf(stepPx))

            Dim notches As Integer = ResolveWheelNotches(delta)
            If notches = 0 Then Return False

            Dim deltaPx As Integer = CInt(Math.Round(-notches * stepPx))
            If deltaPx < 0 Then Return _model.Position > 0
            If deltaPx > 0 Then Return _model.Position < _model.MaxPosition
            Return False
        End Function

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

            stepPx = Math.Max(1.0F, PixelSnap.SnapHalf(stepPx))

            Dim oldPos As Integer = _model.Position
            Dim deltaPx As Integer = CInt(Math.Round(-ResolveWheelNotches(delta) * stepPx))
            _model.ScrollBy(deltaPx)

            Return (_model.Position <> oldPos)
        End Function

        Friend Function EnsureVisible(top As Single, bottom As Single) As Boolean
            Dim oldPos As Integer = _model.Position

            If Not CanScroll() Then
                _model.Position = 0
                Return (_model.Position <> oldPos)
            End If

            top = PixelSnap.SnapHalf(top)
            bottom = PixelSnap.SnapHalf(bottom)

            Dim pos As Single = _model.Position

            If top < pos Then
                pos = top
            ElseIf bottom > pos + _viewportHeight Then
                pos = bottom - _viewportHeight
            End If

            If pos < 0 Then pos = 0
            If pos > _model.MaxPosition Then pos = _model.MaxPosition

            _model.Position = CInt(Math.Round(pos))
            Return (_model.Position <> oldPos)
        End Function

        Private Sub StartAutoRepeat(p As Part, direction As Integer)
            If p <> Part.UpArrow AndAlso p <> Part.DownArrow Then
                StopAutoRepeat()
                Return
            End If

            _repeatPart = p
            _repeatDirection = Math.Sign(direction)
            _repeatCarryPx = 0.0F
            _repeatLastTickMs = 0
            _repeatStartedTick = Nexamas.UI.Timing.MASInteractionClock.TickCount()
            _repeatClockActive = True
        End Sub

        Private Sub StopAutoRepeat()
            _repeatPart = Part.None
            _repeatDirection = 0
            _repeatCarryPx = 0.0F
            _repeatLastTickMs = 0
            _repeatStartedTick = 0
            _repeatClockActive = False
        End Sub

        Private Function ProcessArrowAutoRepeat() As Boolean
            If _repeatPart <> Part.UpArrow AndAlso _repeatPart <> Part.DownArrow Then Return False
            If Not _mouseDown Then Return False
            If _repeatDirection = 0 Then Return False
            If _repeatRowStepPx <= 0.0F Then Return False

            If Not _repeatClockActive Then
                _repeatStartedTick = Nexamas.UI.Timing.MASInteractionClock.TickCount()
                _repeatClockActive = True
                _repeatLastTickMs = 0
                Return False
            End If

            Dim ms As Long = CLng(Nexamas.UI.Timing.MASInteractionClock.ElapsedMillisecondsSince(_repeatStartedTick))
            If ms < 0L Then
                _repeatStartedTick = Nexamas.UI.Timing.MASInteractionClock.TickCount()
                _repeatLastTickMs = 0
                Return False
            End If
            If ms < REPEAT_INITIAL_DELAY_MS Then Return False

            If _repeatLastTickMs <= 0 Then
                _repeatLastTickMs = REPEAT_INITIAL_DELAY_MS
            End If

            Dim elapsedMs As Long = ms - _repeatLastTickMs
            If elapsedMs <= 0 Then Return False

            ' Drop catch-up time after a busy UI frame.  Smoothness is better served by
            ' one bounded motion pulse than by a large compensating jump.
            If elapsedMs > REPEAT_MAX_TICK_MS Then elapsedMs = REPEAT_MAX_TICK_MS
            _repeatLastTickMs = ms

            Dim activeMs As Single = Math.Max(0.0F, CSng(ms - REPEAT_INITIAL_DELAY_MS))
            Dim rampFactor As Single = ComputeRepeatRamp(activeMs)
            Dim referenceStepPx As Single = Math.Min(_repeatRowStepPx, REPEAT_MAX_REFERENCE_STEP_PX)

            If referenceStepPx < REPEAT_MIN_STEP_PX Then referenceStepPx = REPEAT_MIN_STEP_PX

            Dim velocityPxPerMs As Single = (referenceStepPx / REPEAT_TARGET_INTERVAL_MS) * rampFactor
            Dim desiredPx As Single = (velocityPxPerMs * CSng(elapsedMs)) + _repeatCarryPx
            Dim wholePx As Integer = CInt(Math.Truncate(desiredPx))

            _repeatCarryPx = desiredPx - CSng(wholePx)

            If wholePx < CInt(REPEAT_MIN_STEP_PX) Then
                If desiredPx < REPEAT_MIN_STEP_PX Then Return False
                wholePx = CInt(REPEAT_MIN_STEP_PX)
                _repeatCarryPx = 0.0F
            End If

            Dim oldPos As Integer = _model.Position
            _model.ScrollBy(_repeatDirection * wholePx)

            Return (_model.Position <> oldPos)
        End Function

        Private Sub ApplyArrowStep(direction As Integer)
            If direction = 0 Then Return

            Dim stepPx As Integer = CInt(Math.Round(_repeatRowStepPx))
            If stepPx < CInt(REPEAT_MIN_STEP_PX) Then stepPx = CInt(REPEAT_MIN_STEP_PX)
            _model.ScrollBy(Math.Sign(direction) * stepPx)
        End Sub

        Private Function IsAtRepeatBoundary() As Boolean
            If _repeatDirection < 0 Then Return _model.Position <= 0
            If _repeatDirection > 0 Then Return _model.Position >= _model.MaxPosition
            Return True
        End Function

        Private Shared Function ComputeRepeatRamp(activeMs As Single) As Single
            If activeMs <= 0.0F Then Return REPEAT_MIN_VELOCITY_FACTOR

            Dim t As Single = activeMs / REPEAT_RAMP_DURATION_MS
            If t < 0.0F Then t = 0.0F
            If t > 1.0F Then t = 1.0F

            ' SmoothStep: 0..1 with soft acceleration and no abrupt velocity corner.
            Dim eased As Single = (t * t) * (3.0F - (2.0F * t))
            Return REPEAT_MIN_VELOCITY_FACTOR + ((1.0F - REPEAT_MIN_VELOCITY_FACTOR) * eased)
        End Function

        Private Function CanScroll() As Boolean
            If _hostRect.IsEmpty Then Return False
            If _viewportHeight <= 0 OrElse _contentHeight <= _viewportHeight Then Return False
            If _model Is Nothing OrElse _model.MaxPosition <= 0 Then Return False
            Return True
        End Function

        Private Shared Function ResolveWheelNotches(delta As Integer) As Integer
            If delta = 0 Then Return 0
            If Math.Abs(delta) >= 120 Then Return CInt(Math.Truncate(delta / 120.0R))
            Return Math.Sign(delta)
        End Function

        Private Sub ResetInteraction()
            _dragging = False
            _dragOffsetY = 0.0F
            _mouseDown = False
            _pressedArrowPart = Part.None
            _hover = Part.None
            _down = Part.None
            StopAutoRepeat()
        End Sub

        Private Function HasInteractionState() As Boolean
            Return _dragging OrElse
                _mouseDown OrElse
                _hover <> Part.None OrElse
                _down <> Part.None OrElse
                _pressedArrowPart <> Part.None OrElse
                _repeatPart <> Part.None
        End Function

        Private Shared Function Clamp01(v As Single) As Single
            If v < 0.0F Then Return 0.0F
            If v > 1.0F Then Return 1.0F
            Return v
        End Function

    End Class

End Namespace