Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.Scroll
Imports Nexamas.UI.SkiaScroll
Imports Nexamas.UI.Theming
Imports SkiaSharp

Namespace Nexamas.UI.Composition

    ''' <summary>
    ''' Host-level bridge from Composition scroll requests to the official MAS Skia scroll stack.
    ''' This class is deliberately not a Layout node and not a replacement Scroll runtime. It stores only
    ''' per-host adapter records needed to connect MASLayoutScrollRequest output to SkiaScrollAdapter input,
    ''' and delegates geometry, arrows, track, thumb drag, wheel behavior, and painting to the MAS Scroll subsystem.
    ''' Layout reports scroll requests; the host runtime applies them after Arrange.
    ''' The records are instance-owned to prevent cross-window/host scroll contamination.
    ''' </summary>
    Friend NotInheritable Class MASCompositionScrollState

        Friend Const ScrollbarGutterLogical As Single = 13.0F
        Private Const COMPOSITION_SCROLL_THICKNESS_DIP As Single = 9.5F
        Private Const COMPOSITION_SCROLL_THUMB_MIN_DIP As Single = 36.0F

        Private NotInheritable Class ScrollRecord
            Friend Key As String = String.Empty
            Friend LogicalKey As String = String.Empty
            Friend OwnerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.GlobalOwner
            Friend Viewport As SKRect
            Friend ContentHeight As Single
            Friend ViewportHeight As Single
            Friend Adapter As SkiaScrollAdapter
            Friend LastTouchedUtc As DateTime
            Friend IsCaptured As Boolean
        End Class

        Private NotInheritable Class ScrollLookupIndex
            Friend ReadOnly ViewportBuckets As New Dictionary(Of String, List(Of String))(StringComparer.Ordinal)
            Friend ReadOnly ScrollbarBuckets As New Dictionary(Of String, List(Of String))(StringComparer.Ordinal)
        End Class

        Private Const COMPOSITION_SCROLL_LOOKUP_BUCKET_LOGICAL As Single = 96.0F
        Private Const COMPOSITION_SCROLL_GLOBAL_LOOKUP_OWNER As String = ""

        Private ReadOnly _sync As New Object()
        Private ReadOnly _records As New Dictionary(Of String, ScrollRecord)(StringComparer.Ordinal)
        Private ReadOnly _lookupByOwner As New Dictionary(Of String, ScrollLookupIndex)(StringComparer.Ordinal)
        Private ReadOnly _logicalKeyLookup As New Dictionary(Of String, String)(StringComparer.Ordinal)
        Private ReadOnly _requestScrollUpdate As Action
        Private _capturedKey As String = Nothing
        Private _hoverKey As String = Nothing
        Private _lastHitTestProbeCount As Integer
        Private _lastHitTestCandidateCount As Integer
        Private _suppressAdapterInvalidation As Boolean

        ''' <summary>
        ''' Creates a Composition-owned scroll state store. The optional update callback is used only
        ''' by the official shared MAS scroll adapter when its own auto-repeat timer advances offset
        ''' after the original pointer event; layout still owns measurement, arrangement, and apply.
        ''' </summary>
        Friend Sub New(Optional requestScrollUpdate As Action = Nothing)
            _requestScrollUpdate = requestScrollUpdate
        End Sub

        Friend ReadOnly Property LastHitTestProbeCount As Integer
            Get
                SyncLock _sync
                    Return _lastHitTestProbeCount
                End SyncLock
            End Get
        End Property

        Friend ReadOnly Property LastHitTestCandidateCount As Integer
            Get
                SyncLock _sync
                    Return _lastHitTestCandidateCount
                End SyncLock
            End Get
        End Property

        Friend ReadOnly Property RuntimeRecordCount As Integer
            Get
                SyncLock _sync
                    Return _records.Count
                End SyncLock
            End Get
        End Property

        Friend ReadOnly Property RuntimeLookupOwnerCount As Integer
            Get
                SyncLock _sync
                    Return _lookupByOwner.Count
                End SyncLock
            End Get
        End Property

        Friend Function RuntimeRecordCountForOwner(ownerKeyPrefix As String) As Integer
            SyncLock _sync
                Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)
                Dim count As Integer = 0
                For Each record As ScrollRecord In _records.Values
                    If IsOwnedRecord(record, ownerToken) Then count += 1
                Next
                Return count
            End SyncLock
        End Function

        Friend Function RuntimeCandidateCountAt(ownerKeyPrefix As String,
                                                pointLogical As SKPoint,
                                                scrollbarOnly As Boolean) As Integer
            SyncLock _sync
                Dim candidateKeys As List(Of String) = FindCandidateKeys(MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix), pointLogical, scrollbarOnly)
                If candidateKeys Is Nothing Then Return 0
                Return candidateKeys.Count
            End SyncLock
        End Function

        Friend Function ApplyRequests(requests As MASLayoutScrollRequest(),
                                             ownerKeyPrefix As String,
                                             ctx As MASThemeContext) As Boolean
            Dim changedOffset As Boolean = False
            Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)
            Dim activeKeys As New HashSet(Of String)(StringComparer.Ordinal)

            If requests IsNot Nothing Then
                For Each request As MASLayoutScrollRequest In requests
                    If request Is Nothing Then Continue For
                    Dim logicalKey As String = NormalizeKey(request.Key)
                    Dim storageKey As String = BuildStorageKey(ownerToken, logicalKey)
                    activeKeys.Add(storageKey)
                    If UpdateRegion(storageKey, logicalKey, ownerToken, request.ViewportLogical, request.ContentHeightLogical, request.ViewportHeightLogical, ctx) Then
                        changedOffset = True
                    End If
                Next
            End If

            ClearMissing(ownerToken, activeKeys)
            SyncLock _sync
                RebuildLookupIndex()
            End SyncLock
            Return changedOffset
        End Function

        Private Function UpdateRegion(key As String,
                                             logicalKey As String,
                                             ownerToken As MASCompositionScrollOwnerToken,
                                             viewport As SKRect,
                                             contentHeight As Single,
                                             viewportHeight As Single,
                                             ctx As MASThemeContext) As Boolean
            Dim k As String = NormalizeKey(key)

            SyncLock _sync
                Dim record As ScrollRecord = Nothing
                If Not _records.TryGetValue(k, record) Then
                    record = New ScrollRecord()
                    record.Key = k
                    _records(k) = record
                End If

                record.LogicalKey = NormalizeKey(logicalKey)
                record.OwnerToken = If(ownerToken, MASCompositionScrollOwnerToken.GlobalOwner)

                Dim oldOffset As Single = If(record.Adapter Is Nothing, 0.0F, record.Adapter.Offset)

                record.Viewport = viewport
                record.ContentHeight = SafeNonNegative(contentHeight)
                record.ViewportHeight = SafeNonNegative(viewportHeight)
                record.LastTouchedUtc = Nexamas.UI.Timing.MASTimeProvider.UtcNow()

                If ctx IsNot Nothing Then
                    If record.Adapter Is Nothing Then
                        record.Adapter = ScrollFactory.Create(
                            ctx:=ctx,
                            invalidate:=AddressOf RequestAdapterInvalidation,
                            trackThicknessDip:=COMPOSITION_SCROLL_THICKNESS_DIP,
                            thumbMinSizeDip:=COMPOSITION_SCROLL_THUMB_MIN_DIP)
                    Else
                        record.Adapter.UpdateThemeContext(ctx)
                    End If
                End If

                If record.Adapter IsNot Nothing Then
                    Dim dpi As Single = NormalizeDpi(If(ctx Is Nothing, 1.0F, ctx.Dpi))
                    InvokeAdapterInput(Function()
                                           record.Adapter.Update(viewport, dpi, record.ContentHeight)
                                           Return False
                                       End Function)
                End If

                If Not HasOverflow(record) Then
                    record.IsCaptured = False
                    If String.Equals(_capturedKey, k, StringComparison.Ordinal) Then _capturedKey = Nothing
                    If String.Equals(_hoverKey, k, StringComparison.Ordinal) Then _hoverKey = Nothing
                    If record.Adapter IsNot Nothing Then record.Adapter.CancelInteraction()
                End If

                Dim newOffset As Single = If(record.Adapter Is Nothing, 0.0F, record.Adapter.Offset)
                Return Math.Abs(newOffset - oldOffset) > 0.01F
            End SyncLock
        End Function


        Private Function InvokeAdapterInput(action As Func(Of Boolean)) As Boolean
            If action Is Nothing Then Return False

            Dim previousSuppress As Boolean = _suppressAdapterInvalidation
            _suppressAdapterInvalidation = True

            Try
                Return action.Invoke()
            Finally
                _suppressAdapterInvalidation = previousSuppress
            End Try
        End Function

        Private Sub RequestAdapterInvalidation()
            If _suppressAdapterInvalidation Then Return

            Dim callback As Action = _requestScrollUpdate
            If callback Is Nothing Then Return

            Try
                callback.Invoke()
            Catch masCaughtExceptionScrollInvalidate As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtExceptionScrollInvalidate, "Composition.Scroll.AdapterInvalidation")
            End Try
        End Sub

        Friend Function GetOffset(key As String) As Single
            Dim k As String = NormalizeKey(key)

            SyncLock _sync
                Dim record As ScrollRecord = Nothing
                If _records.TryGetValue(k, record) AndAlso record IsNot Nothing AndAlso record.Adapter IsNot Nothing Then
                    Return record.Adapter.Offset
                End If

                Dim storageKey As String = Nothing
                If _logicalKeyLookup.TryGetValue(k, storageKey) AndAlso
                   _records.TryGetValue(storageKey, record) AndAlso
                   record IsNot Nothing AndAlso
                   record.Adapter IsNot Nothing Then
                    Return record.Adapter.Offset
                End If

                Return 0.0F
            End SyncLock
        End Function

        Friend Function TryWheelAt(ownerKeyPrefix As String, pointLogical As SKPoint, delta As Integer) As Boolean
            SyncLock _sync
                Dim record As ScrollRecord = FindBestRecordAt(MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix), pointLogical, False)
                If record Is Nothing OrElse record.Adapter Is Nothing Then Return False
                Return InvokeAdapterInput(Function() record.Adapter.MouseWheel(delta, 64.0F))
            End SyncLock
        End Function

        Friend Function TryPointerDownAt(ownerKeyPrefix As String, pointLogical As SKPoint) As Boolean
            SyncLock _sync
                Dim record As ScrollRecord = FindBestRecordAt(MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix), pointLogical, True)
                If record Is Nothing OrElse record.Adapter Is Nothing Then Return False

                Dim handled As Boolean = InvokeAdapterInput(Function() record.Adapter.MouseDown(pointLogical, 48.0F, 0.85F))
                If handled Then
                    _capturedKey = record.Key
                    _hoverKey = record.Key
                    record.IsCaptured = True
                    record.LastTouchedUtc = Nexamas.UI.Timing.MASTimeProvider.UtcNow()
                End If

                Return handled
            End SyncLock
        End Function

        Friend Function TryPointerMoveAt(ownerKeyPrefix As String, pointLogical As SKPoint) As Boolean
            SyncLock _sync
                Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)

                If Not String.IsNullOrEmpty(_capturedKey) Then
                    Dim captured As ScrollRecord = Nothing
                    If _records.TryGetValue(_capturedKey, captured) AndAlso
                       captured IsNot Nothing AndAlso
                       captured.Adapter IsNot Nothing AndAlso
                       IsOwnedRecord(captured, ownerToken) Then
                        Return InvokeAdapterInput(Function() captured.Adapter.MouseMove(pointLogical))
                    End If
                End If

                Dim hit As ScrollRecord = FindBestRecordAt(ownerToken, pointLogical, True)
                Dim changed As Boolean = False

                If Not String.IsNullOrEmpty(_hoverKey) AndAlso
                   (hit Is Nothing OrElse Not String.Equals(_hoverKey, hit.Key, StringComparison.Ordinal)) Then
                    Dim previous As ScrollRecord = Nothing
                    If _records.TryGetValue(_hoverKey, previous) AndAlso
                       previous IsNot Nothing AndAlso
                       previous.Adapter IsNot Nothing AndAlso
                       IsOwnedRecord(previous, ownerToken) Then
                        If InvokeAdapterInput(Function()
                                                  previous.Adapter.MouseLeave()
                                                  Return True
                                              End Function) Then changed = True
                    End If
                    _hoverKey = Nothing
                End If

                If hit IsNot Nothing AndAlso hit.Adapter IsNot Nothing Then
                    _hoverKey = hit.Key
                    If InvokeAdapterInput(Function() hit.Adapter.MouseMove(pointLogical)) Then changed = True
                End If

                Return changed
            End SyncLock
        End Function

        Friend Function TryPointerUpAt(ownerKeyPrefix As String, pointLogical As SKPoint) As Boolean
            SyncLock _sync
                Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)
                Dim changed As Boolean = False

                If Not String.IsNullOrEmpty(_capturedKey) Then
                    Dim captured As ScrollRecord = Nothing
                    If _records.TryGetValue(_capturedKey, captured) AndAlso
                       captured IsNot Nothing AndAlso
                       captured.Adapter IsNot Nothing AndAlso
                       IsOwnedRecord(captured, ownerToken) Then
                        changed = InvokeAdapterInput(Function() captured.Adapter.MouseUp())
                        captured.IsCaptured = False
                    End If
                End If

                _capturedKey = Nothing
                Return changed
            End SyncLock
        End Function

        Friend Function PointerLeave(ownerKeyPrefix As String) As Boolean
            SyncLock _sync
                Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)
                Dim changed As Boolean = False

                If Not String.IsNullOrEmpty(_hoverKey) Then
                    Dim hovered As ScrollRecord = Nothing
                    If _records.TryGetValue(_hoverKey, hovered) AndAlso
                       hovered IsNot Nothing AndAlso
                       hovered.Adapter IsNot Nothing AndAlso
                       IsOwnedRecord(hovered, ownerToken) Then
                        If InvokeAdapterInput(Function()
                                                  hovered.Adapter.MouseLeave()
                                                  Return True
                                              End Function) Then changed = True
                    End If
                End If

                _hoverKey = Nothing
                Return changed
            End SyncLock
        End Function

        Friend Function CancelPointerCapture(ownerKeyPrefix As String) As Boolean
            SyncLock _sync
                Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)
                Dim changed As Boolean = False

                If Not String.IsNullOrEmpty(_capturedKey) Then
                    Dim captured As ScrollRecord = Nothing
                    If _records.TryGetValue(_capturedKey, captured) AndAlso
                       captured IsNot Nothing AndAlso
                       captured.Adapter IsNot Nothing AndAlso
                       IsOwnedRecord(captured, ownerToken) Then
                        changed = InvokeAdapterInput(Function()
                                                         captured.Adapter.CancelInteraction()
                                                         Return True
                                                     End Function)
                        captured.IsCaptured = False
                    End If
                End If

                _capturedKey = Nothing
                Return changed
            End SyncLock
        End Function

        Friend Sub RenderIndicators(ownerKeyPrefix As String, canvas As SKCanvas, ctx As MASThemeContext)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return

            Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)
            Dim snapshot As List(Of ScrollRecord)
            SyncLock _sync
                snapshot = New List(Of ScrollRecord)()
                For Each kv As KeyValuePair(Of String, ScrollRecord) In _records
                    If IsOwnedRecord(kv.Value, ownerToken) Then snapshot.Add(kv.Value)
                Next
            End SyncLock

            For Each record As ScrollRecord In snapshot
                If record Is Nothing OrElse record.Adapter Is Nothing Then Continue For
                If Not HasOverflow(record) Then Continue For
                record.Adapter.UpdateThemeContext(ctx)
                record.Adapter.Draw(canvas, True)
            Next
        End Sub

        Friend Sub ClearOwner(ownerKeyPrefix As String)
            SyncLock _sync
                Dim ownerToken As MASCompositionScrollOwnerToken = MASCompositionScrollOwnerToken.FromOwnerKey(ownerKeyPrefix)
                Dim keys As New List(Of String)()
                For Each key As String In _records.Keys
                    If IsOwnedRecord(_records(key), ownerToken) Then keys.Add(key)
                Next

                For Each key As String In keys
                    RemoveRecord(key)
                Next

                RebuildLookupIndex()
            End SyncLock
        End Sub

        Private Sub ClearMissing(ownerToken As MASCompositionScrollOwnerToken, activeKeys As HashSet(Of String))
            SyncLock _sync
                Dim keys As New List(Of String)()
                For Each key As String In _records.Keys
                    If Not IsOwnedRecord(_records(key), ownerToken) Then Continue For
                    If activeKeys Is Nothing OrElse Not activeKeys.Contains(key) Then keys.Add(key)
                Next

                For Each key As String In keys
                    RemoveRecord(key)
                Next
            End SyncLock
        End Sub

        Private Function FindBestRecordAt(ownerToken As MASCompositionScrollOwnerToken, pointLogical As SKPoint, scrollbarOnly As Boolean) As ScrollRecord
            Dim candidateKeys As List(Of String) = FindCandidateKeys(ownerToken, pointLogical, scrollbarOnly)
            _lastHitTestCandidateCount = If(candidateKeys Is Nothing, 0, candidateKeys.Count)
            _lastHitTestProbeCount = 0
            If candidateKeys Is Nothing OrElse candidateKeys.Count = 0 Then Return Nothing

            Dim best As ScrollRecord = Nothing
            Dim bestArea As Single = Single.PositiveInfinity

            For Each key As String In candidateKeys
                _lastHitTestProbeCount += 1

                Dim r As ScrollRecord = Nothing
                If String.IsNullOrEmpty(key) OrElse Not _records.TryGetValue(key, r) Then Continue For
                If Not IsOwnedRecord(r, ownerToken) Then Continue For
                If r Is Nothing OrElse r.Adapter Is Nothing OrElse Not HasOverflow(r) Then Continue For

                Dim contains As Boolean
                If scrollbarOnly Then
                    contains = ScrollbarHitRegion(r).Contains(pointLogical.X, pointLogical.Y)
                Else
                    contains = r.Viewport.Contains(pointLogical.X, pointLogical.Y)
                End If

                If Not contains Then Continue For

                Dim area As Single = Math.Max(1.0F, r.Viewport.Width * r.Viewport.Height)
                If area < bestArea Then
                    bestArea = area
                    best = r
                End If
            Next

            Return best
        End Function

        Private Function FindCandidateKeys(ownerToken As MASCompositionScrollOwnerToken, pointLogical As SKPoint, scrollbarOnly As Boolean) As List(Of String)
            Dim ownerKey As String = LookupOwnerKey(ownerToken)
            Dim index As ScrollLookupIndex = Nothing
            If Not _lookupByOwner.TryGetValue(ownerKey, index) Then Return Nothing

            Dim bucketKey As String = BuildBucketKey(pointLogical)
            Dim buckets As Dictionary(Of String, List(Of String)) = If(scrollbarOnly, index.ScrollbarBuckets, index.ViewportBuckets)
            Dim keys As List(Of String) = Nothing
            If buckets.TryGetValue(bucketKey, keys) Then Return keys
            Return Nothing
        End Function

        Private Sub RebuildLookupIndex()
            _lookupByOwner.Clear()
            _logicalKeyLookup.Clear()

            For Each kv As KeyValuePair(Of String, ScrollRecord) In _records
                Dim record As ScrollRecord = kv.Value
                If record Is Nothing Then Continue For

                If Not String.IsNullOrEmpty(record.LogicalKey) AndAlso record.Adapter IsNot Nothing Then
                    _logicalKeyLookup(record.LogicalKey) = record.Key
                End If

                If record.Adapter Is Nothing OrElse Not HasOverflow(record) Then Continue For

                AddRecordToLookup(COMPOSITION_SCROLL_GLOBAL_LOOKUP_OWNER, record)

                Dim ownerKey As String = LookupOwnerKey(record.OwnerToken)
                If Not String.Equals(ownerKey, COMPOSITION_SCROLL_GLOBAL_LOOKUP_OWNER, StringComparison.Ordinal) Then
                    AddRecordToLookup(ownerKey, record)
                End If
            Next
        End Sub

        Private Sub AddRecordToLookup(ownerKey As String, record As ScrollRecord)
            If record Is Nothing Then Return

            Dim index As ScrollLookupIndex = Nothing
            If Not _lookupByOwner.TryGetValue(ownerKey, index) Then
                index = New ScrollLookupIndex()
                _lookupByOwner(ownerKey) = index
            End If

            AddRegionToBuckets(index.ViewportBuckets, record.Viewport, record.Key)
            AddRegionToBuckets(index.ScrollbarBuckets, ScrollbarHitRegion(record), record.Key)
        End Sub

        Private Sub AddRegionToBuckets(buckets As Dictionary(Of String, List(Of String)), region As SKRect, key As String)
            If buckets Is Nothing OrElse String.IsNullOrEmpty(key) OrElse region.IsEmpty Then Return

            Dim leftBucket As Integer = BucketCoordinate(region.Left)
            Dim rightBucket As Integer = BucketCoordinate(region.Right)
            Dim topBucket As Integer = BucketCoordinate(region.Top)
            Dim bottomBucket As Integer = BucketCoordinate(region.Bottom)

            For y As Integer = topBucket To bottomBucket
                For x As Integer = leftBucket To rightBucket
                    Dim bucketKey As String = BuildBucketKey(x, y)
                    Dim keys As List(Of String) = Nothing
                    If Not buckets.TryGetValue(bucketKey, keys) Then
                        keys = New List(Of String)()
                        buckets(bucketKey) = keys
                    End If

                    keys.Add(key)
                Next
            Next
        End Sub

        Private Shared Function LookupOwnerKey(ownerToken As MASCompositionScrollOwnerToken) As String
            If ownerToken Is Nothing OrElse ownerToken.IsGlobal Then Return COMPOSITION_SCROLL_GLOBAL_LOOKUP_OWNER
            Return ownerToken.Key
        End Function

        Private Shared Function BuildBucketKey(point As SKPoint) As String
            Return BuildBucketKey(BucketCoordinate(point.X), BucketCoordinate(point.Y))
        End Function

        Private Shared Function BuildBucketKey(x As Integer, y As Integer) As String
            Return x.ToString(System.Globalization.CultureInfo.InvariantCulture) & ":" & y.ToString(System.Globalization.CultureInfo.InvariantCulture)
        End Function

        Private Shared Function BucketCoordinate(value As Single) As Integer
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) Then Return 0
            Return CInt(Math.Floor(CDbl(value / COMPOSITION_SCROLL_LOOKUP_BUCKET_LOGICAL)))
        End Function

        Private Function ScrollbarHitRegion(record As ScrollRecord) As SKRect
            If record Is Nothing OrElse record.Viewport.IsEmpty Then Return SKRect.Empty
            Dim w As Single = Math.Max(13.0F, ScrollbarGutterLogical + 2.0F)
            Return New SKRect(record.Viewport.Right - w, record.Viewport.Top, record.Viewport.Right, record.Viewport.Bottom)
        End Function

        Private Function HasOverflow(record As ScrollRecord) As Boolean
            If record Is Nothing Then Return False
            If record.Viewport.IsEmpty Then Return False
            Return record.ContentHeight > record.ViewportHeight + 0.5F
        End Function

        Private Sub RemoveRecord(key As String)
            Dim record As ScrollRecord = Nothing
            If Not _records.TryGetValue(key, record) Then Return

            If record IsNot Nothing AndAlso record.Adapter IsNot Nothing Then
                Try
                    record.Adapter.Dispose()
                Catch masCaughtException1 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException1)
                End Try
            End If

            If String.Equals(_capturedKey, key, StringComparison.Ordinal) Then _capturedKey = Nothing
            If String.Equals(_hoverKey, key, StringComparison.Ordinal) Then _hoverKey = Nothing
            _records.Remove(key)
            _lookupByOwner.Clear()
            _logicalKeyLookup.Clear()
        End Sub

        Private Function IsOwnedRecord(record As ScrollRecord, ownerToken As MASCompositionScrollOwnerToken) As Boolean
            If ownerToken Is Nothing OrElse ownerToken.IsGlobal Then Return True
            If record Is Nothing Then Return False
            Return MASCompositionScrollOwnerToken.Same(record.OwnerToken, ownerToken)
        End Function

        Private Shared Function BuildStorageKey(ownerToken As MASCompositionScrollOwnerToken, logicalKey As String) As String
            Dim ownerPart As String = If(ownerToken Is Nothing, String.Empty, ownerToken.Key)
            Return ownerPart & "::" & NormalizeKey(logicalKey)
        End Function

        Private Shared Function NormalizeKey(value As String) As String
            If String.IsNullOrWhiteSpace(value) Then Return "MASPage.Form.Scroll"
            Return value.Trim()
        End Function

        Private Shared Function SafeNonNegative(value As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) OrElse value < 0.0F Then Return 0.0F
            Return value
        End Function

        Private Shared Function NormalizeDpi(value As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) OrElse value <= 0.0F Then Return 1.0F
            Return value
        End Function

    End Class

End Namespace
