Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.Virtualization

    ''' <summary>
    ''' Core controller for the MASVirtualizedItemsSystem. It owns item-range planning, measurement-cache lookup, numeric lease
    ''' recycling, and scroll-to-index facts while staying independent from DataGrid, ListView, TreeView, WinForms controls,
    ''' Skia rendering, and file I/O. Offset and total-extent calculations are routed through the measurement cache prefix index
    ''' with Double precision so huge extents do not collapse into Single rounding while public-compatible Single facts remain
    ''' available to existing internal consumers.
    ''' </summary>
    Friend NotInheritable Class MASVirtualizedItemsController

        Private ReadOnly _options As MASVirtualizationOptions
        Private ReadOnly _measurements As MASVirtualizationMeasurementCache
        Private ReadOnly _recyclePool As MASVirtualizationRecyclePool
        Private ReadOnly _realizedLeaseByIndex As Dictionary(Of Integer, Integer)
        Private ReadOnly _staleIndexesScratch As List(Of Integer)
        Private ReadOnly _requestsScratch As List(Of MASVirtualizationItemRequest)
        Private _lastViewport As MASVirtualizationViewport
        Private _lastPlan As MASVirtualizationRealizationPlan
        Private _lastScrollRequest As MASVirtualizationScrollRequest
        Private _planUpdateCount As Integer
        Private _planMaxRealizedCount As Integer

        Friend Sub New(options As MASVirtualizationOptions)
            Dim report = MASVirtualizationGate.Evaluate(options)
            If Not report.IsReady Then
                Throw New ArgumentException("MASVirtualizedItemsController requires valid virtualization options.", NameOf(options))
            End If

            _options = options
            _measurements = New MASVirtualizationMeasurementCache()
            _recyclePool = New MASVirtualizationRecyclePool()
            _realizedLeaseByIndex = New Dictionary(Of Integer, Integer)()
            _staleIndexesScratch = New List(Of Integer)()
            _requestsScratch = New List(Of MASVirtualizationItemRequest)()
            _lastViewport = New MASVirtualizationViewport(0.0R, 0.0R)
            _lastPlan = New MASVirtualizationRealizationPlan(MASVirtualizationRange.Empty, Array.Empty(Of MASVirtualizationItemRequest)(), 0.0R, 0, 0, 0)
            _lastScrollRequest = Nothing
            _planUpdateCount = 0
            _planMaxRealizedCount = 0
        End Sub

        Friend ReadOnly Property Options As MASVirtualizationOptions
            Get
                Return _options
            End Get
        End Property

        Friend ReadOnly Property CreatedLeaseCount As Integer
            Get
                Return _recyclePool.CreatedLeaseCount
            End Get
        End Property

        Friend ReadOnly Property ReusedLeaseCount As Integer
            Get
                Return _recyclePool.ReusedLeaseCount
            End Get
        End Property

        Friend ReadOnly Property DiagnosticsSnapshot As MASVirtualizationDiagnosticsSnapshot
            Get
                Return CreateDiagnosticsSnapshot()
            End Get
        End Property

        Friend Sub RecordMeasuredExtent(index As Integer, extentPx As Single)
            If index < 0 OrElse index >= _options.ItemCount Then
                Return
            End If

            _measurements.SetMeasuredExtent(index, extentPx)
        End Sub

        Friend Sub InvalidateMeasurements(reason As MASVirtualizationInvalidationReason)
            _measurements.Clear(reason)
        End Sub

        Friend Function ResolveOffsetForIndex(index As Integer) As Single
            Return MASVirtualizationNumericGuard.ToSingleSaturated(ResolveOffsetForIndexDouble(index))
        End Function

        Friend Function ResolveOffsetForIndexDouble(index As Integer) As Double
            If _options.ItemCount <= 0 OrElse index < 0 Then
                Return 0.0R
            End If

            Dim safeIndex = Math.Min(_options.ItemCount - 1, index)
            Return OffsetForIndexDouble(safeIndex)
        End Function

        Friend Function ResolveExtentForIndex(index As Integer) As Single
            Return MASVirtualizationNumericGuard.ToSingleSaturated(ResolveExtentForIndexDouble(index))
        End Function

        Friend Function ResolveExtentForIndexDouble(index As Integer) As Double
            If _options.ItemCount <= 0 OrElse index < 0 OrElse index >= _options.ItemCount Then
                Return 0.0R
            End If

            Return ExtentForIndexDouble(index)
        End Function

        Friend Function ResolveIndexAtOffset(offsetPx As Single) As Integer
            Return ResolveIndexAtOffset(CDbl(offsetPx))
        End Function

        Friend Function ResolveIndexAtOffset(offsetPx As Double) As Integer
            If _options.ItemCount <= 0 Then
                Return -1
            End If

            Return FindFirstVisibleIndex(MASVirtualizationNumericGuard.NormalizeNonNegative(offsetPx))
        End Function

        Friend Function TryGetMeasuredExtent(index As Integer, ByRef extentPx As Single) As Boolean
            Return _measurements.TryGetMeasuredExtent(index, extentPx)
        End Function

        Friend Function Update(viewport As MASVirtualizationViewport) As MASVirtualizationRealizationPlan
            _measurements.TrimToItemCount(_options.ItemCount)

            If viewport Is Nothing OrElse Not viewport.IsValid OrElse _options.ItemCount = 0 Then
                ReleaseAllRealizedLeases()
                _lastViewport = If(viewport, New MASVirtualizationViewport(0.0R, 0.0R))
                _lastPlan = New MASVirtualizationRealizationPlan(MASVirtualizationRange.Empty, Array.Empty(Of MASVirtualizationItemRequest)(), TotalExtentPxDouble(), _recyclePool.CreatedLeaseCount, _recyclePool.ReusedLeaseCount, 0)
                RecordPerformancePlanFacts()
                RecordPlanBudgetFacts(_lastPlan)
                Return _lastPlan
            End If

            _lastViewport = viewport

            Dim range = CalculateRange(viewport)
            Dim firstDesired As Integer = If(range.IsEmpty, Integer.MaxValue, range.FirstIndex)
            Dim lastDesired As Integer = If(range.IsEmpty, Integer.MinValue, range.LastIndex)

            _staleIndexesScratch.Clear()
            For Each realizedIndex As Integer In _realizedLeaseByIndex.Keys
                If realizedIndex < firstDesired OrElse realizedIndex > lastDesired Then
                    _staleIndexesScratch.Add(realizedIndex)
                End If
            Next

            For Each staleIndex As Integer In _staleIndexesScratch
                _recyclePool.Release(_realizedLeaseByIndex(staleIndex))
                _realizedLeaseByIndex.Remove(staleIndex)
            Next

            _requestsScratch.Clear()
            Dim retainedCount As Integer = 0
            If Not range.IsEmpty Then
                For index = range.FirstIndex To range.LastIndex
                    Dim offset = OffsetForIndexDouble(index)
                    Dim extent = ExtentForIndexDouble(index)
                    Dim leaseId As Integer = 0
                    Dim retained = _realizedLeaseByIndex.TryGetValue(index, leaseId)
                    Dim recycled = False

                    If retained Then
                        retainedCount += 1
                    Else
                        leaseId = _recyclePool.Acquire(recycled)
                        _realizedLeaseByIndex(index) = leaseId
                    End If

                    _requestsScratch.Add(New MASVirtualizationItemRequest(index, offset, extent, leaseId, retained, recycled))
                Next
            End If

            _lastPlan = New MASVirtualizationRealizationPlan(range, _requestsScratch, TotalExtentPxDouble(), _recyclePool.CreatedLeaseCount, _recyclePool.ReusedLeaseCount, retainedCount)
            RecordPerformancePlanFacts()
            RecordPlanBudgetFacts(_lastPlan)
            Return _lastPlan
        End Function

        Friend Function CreateScrollToIndexRequest(index As Integer,
                                                   viewport As MASVirtualizationViewport,
                                                   Optional alignment As MASVirtualizationAlignment = MASVirtualizationAlignment.Nearest) As MASVirtualizationScrollRequest
            Dim safeAlignment = NormalizeAlignment(alignment)

            If _options.ItemCount = 0 Then
                _lastScrollRequest = New MASVirtualizationScrollRequest(0, 0.0R, safeAlignment, 0.0R)
                Return _lastScrollRequest
            End If

            Dim safeIndex = Math.Max(0, Math.Min(_options.ItemCount - 1, index))
            Dim safeViewport = If(viewport IsNot Nothing AndAlso viewport.IsValid, viewport, New MASVirtualizationViewport(0.0R, CDbl(_options.EstimatedItemExtentPx)))
            Dim itemStart = OffsetForIndexDouble(safeIndex)
            Dim itemExtent = ExtentForIndexDouble(safeIndex)
            Dim itemEnd = itemStart + itemExtent
            Dim targetOffset As Double = safeViewport.OffsetPxDouble

            Select Case safeAlignment
                Case MASVirtualizationAlignment.Start
                    targetOffset = itemStart
                Case MASVirtualizationAlignment.Center
                    targetOffset = itemStart - ((safeViewport.ExtentPxDouble - itemExtent) / 2.0R)
                Case MASVirtualizationAlignment.End
                    targetOffset = itemEnd - safeViewport.ExtentPxDouble
                Case Else
                    If itemStart < safeViewport.OffsetPxDouble Then
                        targetOffset = itemStart
                    ElseIf itemEnd > safeViewport.EndPxDouble Then
                        targetOffset = itemEnd - safeViewport.ExtentPxDouble
                    End If
            End Select

            Dim maxOffset = Math.Max(0.0R, TotalExtentPxDouble() - safeViewport.ExtentPxDouble)
            targetOffset = Math.Max(0.0R, Math.Min(maxOffset, targetOffset))
            _lastScrollRequest = New MASVirtualizationScrollRequest(safeIndex, targetOffset, safeAlignment, TotalExtentPxDouble())
            Return _lastScrollRequest
        End Function

        Friend Function TotalExtentPx() As Single
            Return MASVirtualizationNumericGuard.ToSingleSaturated(TotalExtentPxDouble())
        End Function

        Friend Function TotalExtentPxDouble() As Double
            Return _measurements.ResolveTotalExtentDouble(_options.ItemCount, _options.EstimatedItemExtentPx)
        End Function

        Friend Function CreatePlanAllocationBudgetSnapshot() As MASVirtualizationPlanAllocationBudgetSnapshot
            Return New MASVirtualizationPlanAllocationBudgetSnapshot(
                _planUpdateCount,
                _planMaxRealizedCount,
                _recyclePool.CreatedLeaseCount,
                _recyclePool.ReusedLeaseCount,
                _staleIndexesScratch.Capacity,
                _requestsScratch.Capacity)
        End Function

        Private Function CreateDiagnosticsSnapshot() As MASVirtualizationDiagnosticsSnapshot
            Dim viewportOffset As Single = 0.0F
            Dim viewportExtent As Single = 0.0F
            If _lastViewport IsNot Nothing Then
                viewportOffset = _lastViewport.OffsetPx
                viewportExtent = _lastViewport.ExtentPx
            End If

            Dim scrollIndex As Integer? = Nothing
            Dim scrollTarget As Single? = Nothing
            Dim scrollAlignment As MASVirtualizationAlignment? = Nothing
            If _lastScrollRequest IsNot Nothing Then
                scrollIndex = _lastScrollRequest.Index
                scrollTarget = _lastScrollRequest.TargetOffsetPx
                scrollAlignment = _lastScrollRequest.Alignment
            End If

            Dim snapshot As New MASVirtualizationDiagnosticsSnapshot(
                _options.ItemCount,
                _options.EstimatedItemExtentPx,
                _options.OverscanItemCount,
                _options.Axis,
                _options.ExtentStrategy,
                If(_lastPlan IsNot Nothing, _lastPlan.Range, MASVirtualizationRange.Empty),
                If(_lastPlan IsNot Nothing, _lastPlan.RealizedCount, 0),
                TotalExtentPx(),
                viewportOffset,
                viewportExtent,
                _measurements.Count,
                _measurements.Revision,
                _recyclePool.CreatedLeaseCount,
                _recyclePool.ReusedLeaseCount,
                _recyclePool.AvailableLeaseCount,
                scrollIndex,
                scrollTarget,
                scrollAlignment,
                _measurements.LastInvalidationReason)

            Return snapshot
        End Function

        Private Sub RecordPerformancePlanFacts()
            MASPerformanceSystem.RecordVirtualizationSnapshot(
                itemCount:=_options.ItemCount,
                realizedCount:=If(_lastPlan IsNot Nothing, _lastPlan.RealizedCount, 0),
                measurementCount:=_measurements.Count,
                measurementRevision:=_measurements.Revision,
                createdLeaseCount:=_recyclePool.CreatedLeaseCount,
                reusedLeaseCount:=_recyclePool.ReusedLeaseCount)
        End Sub

        Private Sub RecordPlanBudgetFacts(plan As MASVirtualizationRealizationPlan)
            _planUpdateCount += 1
            _planMaxRealizedCount = Math.Max(_planMaxRealizedCount, If(plan IsNot Nothing, plan.RealizedCount, 0))
        End Sub

        Private Shared Function NormalizeAlignment(alignment As MASVirtualizationAlignment) As MASVirtualizationAlignment
            If System.Enum.IsDefined(GetType(MASVirtualizationAlignment), alignment) Then
                Return alignment
            End If

            Return MASVirtualizationAlignment.Nearest
        End Function

        Private Function CalculateRange(viewport As MASVirtualizationViewport) As MASVirtualizationRange
            Dim first = FindFirstVisibleIndex(viewport.OffsetPxDouble)
            Dim last = FindLastVisibleIndex(viewport.EndPxDouble, first)
            first = Math.Max(0, first - _options.OverscanItemCount)
            last = Math.Min(_options.ItemCount - 1, last + _options.OverscanItemCount)

            If first > last Then
                Return MASVirtualizationRange.Empty
            End If

            Return New MASVirtualizationRange(first, last)
        End Function

        Private Function FindFirstVisibleIndex(offsetPx As Double) As Integer
            If _options.ItemCount <= 0 Then
                Return 0
            End If

            Dim safeOffset = Math.Max(0.0R, Math.Min(TotalExtentPxDouble(), offsetPx))
            Dim low = 0
            Dim high = _options.ItemCount - 1
            Dim result = high

            While low <= high
                Dim middle = low + ((high - low) \ 2)
                Dim middleEnd = OffsetForIndexDouble(middle) + ExtentForIndexDouble(middle)

                If middleEnd > safeOffset Then
                    result = middle
                    high = middle - 1
                Else
                    low = middle + 1
                End If
            End While

            Return result
        End Function

        Private Function FindLastVisibleIndex(endPx As Double, firstIndex As Integer) As Integer
            If _options.ItemCount <= 0 Then
                Return 0
            End If

            Dim safeEnd = Math.Max(0.0R, Math.Min(TotalExtentPxDouble(), endPx))
            Dim low = Math.Max(0, Math.Min(_options.ItemCount - 1, firstIndex))
            Dim high = _options.ItemCount - 1
            Dim result = high

            While low <= high
                Dim middle = low + ((high - low) \ 2)
                Dim middleEnd = OffsetForIndexDouble(middle) + ExtentForIndexDouble(middle)

                If middleEnd >= safeEnd Then
                    result = middle
                    high = middle - 1
                Else
                    low = middle + 1
                End If
            End While

            Return result
        End Function

        Private Function OffsetForIndexDouble(index As Integer) As Double
            Return _measurements.ResolveOffsetDouble(index, _options.ItemCount, _options.EstimatedItemExtentPx)
        End Function

        Private Function ExtentForIndexDouble(index As Integer) As Double
            Return _measurements.ResolveExtentDouble(index, _options.EstimatedItemExtentPx)
        End Function

        Private Sub ReleaseAllRealizedLeases()
            For Each leaseId In _realizedLeaseByIndex.Values.ToArray()
                _recyclePool.Release(leaseId)
            Next

            _realizedLeaseByIndex.Clear()
        End Sub

    End Class

End Namespace
