Option Strict On
Option Explicit On

Imports System.Collections.Generic
Imports Nexamas.UI.Performance

Namespace Nexamas.UI.Virtualization

    ''' <summary>
    ''' Owned measurement cache for virtualized item extents. It stores only deterministic item measurement facts and provides
    ''' prefix-delta lookups so range planning can remain fast with large data sets and sparse measured overrides. Prefix math is
    ''' performed in Double to avoid precision loss at very large scroll extents while preserving Single compatibility shims.
    ''' </summary>
    Friend NotInheritable Class MASVirtualizationMeasurementCache

        Private ReadOnly _extentsByIndex As Dictionary(Of Integer, Single)
        Private ReadOnly _orderedMeasuredIndexes As List(Of Integer)
        Private ReadOnly _prefixMeasuredDeltas As List(Of Double)
        Private _prefixDirty As Boolean
        Private _prefixDefaultExtentPx As Double
        Private _prefixDirtyStartIndex As Integer
        Private _lastPrefixRebuildStartIndexForTests As Integer
        Private _lastPrefixRebuildCountForTests As Integer
        Private _revision As Integer
        Private _lastInvalidationReason As MASVirtualizationInvalidationReason

        Friend Sub New()
            _extentsByIndex = New Dictionary(Of Integer, Single)()
            _orderedMeasuredIndexes = New List(Of Integer)()
            _prefixMeasuredDeltas = New List(Of Double)()
            _prefixDirty = True
            _prefixDefaultExtentPx = 0.0R
            _prefixDirtyStartIndex = 0
            _lastPrefixRebuildStartIndexForTests = -1
            _lastPrefixRebuildCountForTests = 0
            _revision = 0
            _lastInvalidationReason = MASVirtualizationInvalidationReason.None
        End Sub

        Friend ReadOnly Property Count As Integer
            Get
                Return _extentsByIndex.Count
            End Get
        End Property

        Friend ReadOnly Property Revision As Integer
            Get
                Return _revision
            End Get
        End Property

        Friend ReadOnly Property LastInvalidationReason As MASVirtualizationInvalidationReason
            Get
                Return _lastInvalidationReason
            End Get
        End Property

        Friend ReadOnly Property LastPrefixRebuildStartIndexForTests As Integer
            Get
                Return _lastPrefixRebuildStartIndexForTests
            End Get
        End Property

        Friend ReadOnly Property LastPrefixRebuildCountForTests As Integer
            Get
                Return _lastPrefixRebuildCountForTests
            End Get
        End Property

        Friend Sub SetMeasuredExtent(index As Integer, extentPx As Single)
            If index < 0 OrElse Not MASVirtualizationNumericGuard.IsFinitePositive(extentPx) Then
                Return
            End If

            Dim existing As Single = 0.0F
            Dim alreadyMeasured As Boolean = _extentsByIndex.TryGetValue(index, existing)
            If alreadyMeasured AndAlso Math.Abs(existing - extentPx) < 0.0001F Then
                Return
            End If

            Dim orderedPosition As Integer = LowerBound(_orderedMeasuredIndexes, index)
            If Not alreadyMeasured Then
                _orderedMeasuredIndexes.Insert(orderedPosition, index)
            End If

            _extentsByIndex(index) = extentPx
            MarkChanged(MASVirtualizationInvalidationReason.ItemExtentChanged, orderedPosition)
            MASPerformanceSystem.RecordVirtualizationMeasurementWritten(_extentsByIndex.Count, _revision)
        End Sub

        Friend Function TryGetMeasuredExtent(index As Integer, ByRef extentPx As Single) As Boolean
            If index < 0 Then
                extentPx = 0.0F
                Return False
            End If

            Dim found As Boolean = _extentsByIndex.TryGetValue(index, extentPx)
            If found Then
                MASPerformanceSystem.RecordCacheHit(MASPerformanceCacheKind.VirtualizationMeasurement)
            Else
                MASPerformanceSystem.RecordCacheMiss(MASPerformanceCacheKind.VirtualizationMeasurement)
            End If

            Return found
        End Function

        Friend Function ResolveExtent(index As Integer, fallbackExtentPx As Single) As Single
            Return MASVirtualizationNumericGuard.ToSingleSaturated(ResolveExtentDouble(index, fallbackExtentPx))
        End Function

        Friend Function ResolveExtentDouble(index As Integer, fallbackExtentPx As Single) As Double
            Dim measured As Single = 0.0F
            If TryGetMeasuredExtent(index, measured) Then
                Return CDbl(measured)
            End If

            Return MASVirtualizationNumericGuard.NormalizeNonNegative(CDbl(fallbackExtentPx))
        End Function

        Friend Function ResolveOffset(index As Integer, itemCount As Integer, fallbackExtentPx As Single) As Single
            Return MASVirtualizationNumericGuard.ToSingleSaturated(ResolveOffsetDouble(index, itemCount, fallbackExtentPx))
        End Function

        Friend Function ResolveOffsetDouble(index As Integer, itemCount As Integer, fallbackExtentPx As Single) As Double
            Dim safeIndex = Math.Max(0, Math.Min(Math.Max(0, itemCount), index))
            If safeIndex = 0 Then
                Return 0.0R
            End If

            Dim safeFallback As Double = MASVirtualizationNumericGuard.NormalizeNonNegative(CDbl(fallbackExtentPx))
            EnsurePrefixIndex(safeFallback)
            Dim measuredBefore = LowerBound(_orderedMeasuredIndexes, safeIndex)
            Dim delta As Double = 0.0R
            If measuredBefore > 0 Then
                delta = _prefixMeasuredDeltas(measuredBefore - 1)
            End If

            Return (CDbl(safeIndex) * safeFallback) + delta
        End Function

        Friend Function ResolveTotalExtent(itemCount As Integer, fallbackExtentPx As Single) As Single
            Return MASVirtualizationNumericGuard.ToSingleSaturated(ResolveTotalExtentDouble(itemCount, fallbackExtentPx))
        End Function

        Friend Function ResolveTotalExtentDouble(itemCount As Integer, fallbackExtentPx As Single) As Double
            Dim safeCount = Math.Max(0, itemCount)
            If safeCount = 0 Then
                Return 0.0R
            End If

            Dim safeFallback As Double = MASVirtualizationNumericGuard.NormalizeNonNegative(CDbl(fallbackExtentPx))
            EnsurePrefixIndex(safeFallback)
            Dim measuredInsideCount = LowerBound(_orderedMeasuredIndexes, safeCount)
            Dim delta As Double = 0.0R
            If measuredInsideCount > 0 Then
                delta = _prefixMeasuredDeltas(measuredInsideCount - 1)
            End If

            Return (CDbl(safeCount) * safeFallback) + delta
        End Function

        Friend Function EnumerateMeasurements() As IReadOnlyList(Of MASVirtualizationMeasurement)
            Dim measurements As New List(Of MASVirtualizationMeasurement)(_orderedMeasuredIndexes.Count)

            For Each index As Integer In _orderedMeasuredIndexes
                Dim extent As Single = 0.0F
                If _extentsByIndex.TryGetValue(index, extent) Then
                    measurements.Add(New MASVirtualizationMeasurement(index, extent))
                End If
            Next

            Return measurements.ToArray()
        End Function

        Friend Sub Clear(Optional reason As MASVirtualizationInvalidationReason = MASVirtualizationInvalidationReason.ManualReset)
            If _extentsByIndex.Count = 0 Then
                _lastInvalidationReason = reason
                MASPerformanceSystem.RecordVirtualizationMeasurementInvalidated(_extentsByIndex.Count, _revision)
                Return
            End If

            _extentsByIndex.Clear()
            _orderedMeasuredIndexes.Clear()
            _prefixMeasuredDeltas.Clear()
            MarkChanged(reason)
        End Sub

        Friend Sub TrimToItemCount(itemCount As Integer, Optional reason As MASVirtualizationInvalidationReason = MASVirtualizationInvalidationReason.ItemCollectionChanged)
            Dim safeCount = Math.Max(0, itemCount)
            Dim removed = False
            Dim firstRemovedPosition As Integer = LowerBound(_orderedMeasuredIndexes, safeCount)

            For position As Integer = _orderedMeasuredIndexes.Count - 1 To firstRemovedPosition Step -1
                _extentsByIndex.Remove(_orderedMeasuredIndexes(position))
                _orderedMeasuredIndexes.RemoveAt(position)
                removed = True
            Next

            If removed Then
                MarkChanged(reason, firstRemovedPosition)
            End If
        End Sub

        Private Sub MarkChanged(reason As MASVirtualizationInvalidationReason,
                                Optional dirtyStartIndex As Integer = 0)
            Dim safeDirtyStart As Integer = Math.Max(0, Math.Min(dirtyStartIndex, _orderedMeasuredIndexes.Count))
            If _prefixDirty Then
                _prefixDirtyStartIndex = Math.Min(_prefixDirtyStartIndex, safeDirtyStart)
            Else
                _prefixDirtyStartIndex = safeDirtyStart
            End If

            _prefixDirty = True
            _revision += 1
            _lastInvalidationReason = reason
            MASPerformanceSystem.RecordVirtualizationMeasurementInvalidated(_extentsByIndex.Count, _revision)
        End Sub

        Private Sub EnsurePrefixIndex(defaultExtentPx As Double)
            Dim defaultExtentChanged As Boolean = Math.Abs(_prefixDefaultExtentPx - defaultExtentPx) >= 0.000001R
            If Not _prefixDirty AndAlso Not defaultExtentChanged Then
                Return
            End If

            Dim startIndex As Integer = If(defaultExtentChanged, 0, Math.Max(0, Math.Min(_prefixDirtyStartIndex, _orderedMeasuredIndexes.Count)))
            If startIndex > _prefixMeasuredDeltas.Count Then
                startIndex = 0
            End If

            Dim accumulatedDelta As Double = 0.0R
            If startIndex <= 0 Then
                _prefixMeasuredDeltas.Clear()
            Else
                accumulatedDelta = _prefixMeasuredDeltas(startIndex - 1)
                If _prefixMeasuredDeltas.Count > startIndex Then
                    _prefixMeasuredDeltas.RemoveRange(startIndex, _prefixMeasuredDeltas.Count - startIndex)
                End If
            End If

            For position As Integer = startIndex To _orderedMeasuredIndexes.Count - 1
                Dim extentPx As Single = 0.0F
                If _extentsByIndex.TryGetValue(_orderedMeasuredIndexes(position), extentPx) Then
                    accumulatedDelta += CDbl(extentPx) - defaultExtentPx
                    _prefixMeasuredDeltas.Add(accumulatedDelta)
                End If
            Next

            _prefixDefaultExtentPx = defaultExtentPx
            _prefixDirtyStartIndex = _orderedMeasuredIndexes.Count
            _prefixDirty = False
            _lastPrefixRebuildStartIndexForTests = startIndex
            _lastPrefixRebuildCountForTests = Math.Max(0, _orderedMeasuredIndexes.Count - startIndex)
            MASPerformanceSystem.RecordVirtualizationPrefixRebuilt(_extentsByIndex.Count, _revision)
        End Sub

        Private Shared Function LowerBound(values As IList(Of Integer), target As Integer) As Integer
            Dim low = 0
            Dim high = values.Count

            While low < high
                Dim middle = low + ((high - low) \ 2)
                If values(middle) < target Then
                    low = middle + 1
                Else
                    high = middle
                End If
            End While

            Return low
        End Function

    End Class

End Namespace
