Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports Nexamas.UI.Theming
Imports SkiaSharp

Namespace Nexamas.UI.FloatRuntime

    ' <summary>
    ' Hit-testing, ordering, capture, and visual-state helpers for Float input routing.
    ' Kept separate from pointer/keyboard route methods and outside-pointer command preparation.
    ' </summary>
    Partial Friend NotInheritable Class MASFloatInputRouter

        ''' <summary>
        ''' Finds the top open Float that owns outside-pointer behavior at the given point.
        ''' This is required for transient child menus: when a popup is visually above a
        ''' parent Float, pointer move/up/wheel outside the popup must not leak to the
        ''' parent, and pointer down outside the popup must give CloseOnOutsidePointer a
        ''' chance before the parent receives the click.
        ''' </summary>
        Private Function FindTopOutsidePointerPolicyRecord(ctx As MASThemeContext,
                                                           ptPx As SKPoint,
                                                           includeCloseOnOutside As Boolean) As MASFloatLifecycleRecord
            If ctx Is Nothing Then Return Nothing

            Dim best As MASFloatLifecycleRecord = Nothing

            For Each current As MASFloatLifecycleRecord In _host.State.Records
                If current Is Nothing OrElse current.Handle Is Nothing Then
                    Continue For
                End If

                If current.State <> MASFloatLifecycleState.Open Then
                    Continue For
                End If

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(current)
                If policy Is Nothing OrElse
                   Not HasCapability(current, MASFloatCapabilities.PointerInput) OrElse
                   Not policy.AcceptsPointerInput Then

                    Continue For
                End If

                Dim ownsOutsidePointer As Boolean =
                    policy.SwallowOutsidePointer OrElse
                    (includeCloseOnOutside AndAlso
                     policy.CloseOnOutsidePointer AndAlso
                     HasCapability(current, MASFloatCapabilities.OutsideDismiss))

                If Not ownsOutsidePointer Then
                    Continue For
                End If

                Dim boundsPx As SKRect = SKRect.Empty
                If Not _host.State.TryGetBoundsPx(current.Handle, boundsPx) Then
                    Continue For
                End If

                If IsRecordHit(ctx, current, boundsPx, ptPx) Then
                    Continue For
                End If

                If IsAboveForRouting(current, best) Then
                    best = current
                End If
            Next

            Return best
        End Function

        Private Function IsRecordHit(ctx As MASThemeContext,
                                     record As MASFloatLifecycleRecord,
                                     boundsPx As SKRect,
                                     ptPx As SKPoint) As Boolean
            If record Is Nothing Then Return False

            If boundsPx.IsEmpty OrElse boundsPx.Width <= 0.0F OrElse boundsPx.Height <= 0.0F Then
                Return False
            End If

            If Not boundsPx.Contains(ptPx.X, ptPx.Y) Then
                Return False
            End If

            Dim routed As IMASFloatRoutedInputContent = GetRoutedInputContent(record)

            If routed Is Nothing Then
                Return True
            End If

            Try
                Return routed.HitTestFloat(ctx, boundsPx, ptPx)
            Catch masCaughtException9 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException9, "MASFloatInputRouter.HitTesting.IsRecordHit")
                Return False
            End Try
        End Function

        'Private Function FindTopPointerRecordAt(
        '    ctx As MASThemeContext,
        '    ptPx As SKPoint
        ') As MASFloatLifecycleRecord
        '    Dim best As MASFloatLifecycleRecord = Nothing

        '    For Each slotState As MASFloatSlotState In _state.GetAllSlotStates()
        '        If slotState Is Nothing OrElse slotState.Current Is Nothing Then
        '            Continue For
        '        End If

        '        Dim current As MASFloatLifecycleRecord = slotState.Current

        '        If current.State <> MASFloatLifecycleState.Open Then
        '            Continue For
        '        End If

        '        Dim policy As IMASFloatInputPolicy = GetInputPolicy(current)
        '        If policy Is Nothing OrElse
        '           Not HasCapability(current, MASFloatCapabilities.PointerInput) OrElse
        '           Not policy.AcceptsPointerInput Then
        '            Continue For
        '        End If

        '        Dim boundsPx As SKRect = SKRect.Empty
        '        If Not _host.State.TryGetBoundsPx(current.Handle, boundsPx) Then
        '            Continue For
        '        End If

        '        If Not boundsPx.Contains(ptPx.X, ptPx.Y) Then Continue For

        '        If best Is Nothing OrElse current.Handle.Layer > best.Handle.Layer Then
        '            best = current
        '        End If
        '    Next

        '    Return best
        'End Function
        ''' <summary>
        ''' Finds the top pointer-eligible record at a point using layer first, then host/render order.
        ''' </summary>
        Private Function FindTopPointerRecordAt(ctx As MASThemeContext,
                                        ptPx As SKPoint) As MASFloatLifecycleRecord

            Dim best As MASFloatLifecycleRecord = Nothing

            For Each current As MASFloatLifecycleRecord In _host.State.Records
                If current Is Nothing OrElse current.Handle Is Nothing Then
                    Continue For
                End If

                If current.State <> MASFloatLifecycleState.Open Then
                    Continue For
                End If

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(current)

                If policy Is Nothing OrElse
           Not HasCapability(current, MASFloatCapabilities.PointerInput) OrElse
           Not policy.AcceptsPointerInput Then

                    Continue For
                End If

                Dim boundsPx As SKRect = SKRect.Empty

                If Not _host.State.TryGetBoundsPx(current.Handle, boundsPx) Then
                    Continue For
                End If

                If Not IsRecordHit(ctx, current, boundsPx, ptPx) Then
                    Continue For
                End If

                If IsAboveForRouting(current, best) Then
                    best = current
                End If
            Next

            Return best

        End Function
        ''' <summary>
        ''' Finds the top pointer-eligible record for outside-pointer decisions without a hit point.
        ''' </summary>
        Private Function FindTopPointerRecord() As MASFloatLifecycleRecord
            Dim best As MASFloatLifecycleRecord = Nothing

            For Each current As MASFloatLifecycleRecord In _host.State.Records
                If current Is Nothing OrElse current.Handle Is Nothing Then
                    Continue For
                End If

                If current.State <> MASFloatLifecycleState.Open Then
                    Continue For
                End If

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(current)
                If policy Is Nothing OrElse
                   Not HasCapability(current, MASFloatCapabilities.PointerInput) OrElse
                   Not policy.AcceptsPointerInput Then
                    Continue For
                End If

                If IsAboveForRouting(current, best) Then
                    best = current
                End If
            Next

            Return best
        End Function

        ''' <summary>
        ''' Finds the top modal-blocking pointer record even when the pointer is over the dim/empty area.
        ''' This keeps modal floats owning hover, wheel and button-up outside their visual bounds.
        ''' </summary>
        Private Function FindTopBlockingPointerRecord() As MASFloatLifecycleRecord
            Dim best As MASFloatLifecycleRecord = Nothing

            For Each current As MASFloatLifecycleRecord In _host.State.Records
                If current Is Nothing OrElse current.Handle Is Nothing Then
                    Continue For
                End If

                If Not IsVisualLifecycleState(current.State) Then
                    Continue For
                End If

                If Not HasCapability(current, MASFloatCapabilities.ModalBlocking) Then
                    Continue For
                End If

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(current)
                If policy Is Nothing OrElse
                   Not HasCapability(current, MASFloatCapabilities.PointerInput) OrElse
                   Not policy.AcceptsPointerInput Then

                    Continue For
                End If

                If IsAboveForRouting(current, best) Then
                    best = current
                End If
            Next

            Return best
        End Function

        ''' <summary>
        ''' Finds the keyboard target, preferring the focus owner before top-record fallback.
        ''' </summary>
        Private Function FindTopKeyboardRecord() As MASFloatLifecycleRecord
            Dim focusOwner As MASFloatHandle = _focusManager.CurrentFocusOwner
            If focusOwner IsNot Nothing Then
                Dim focusRecord As MASFloatLifecycleRecord = FindCurrentRecordByHandle(focusOwner)
                If focusRecord IsNot Nothing Then
                    Dim focusPolicy As IMASFloatInputPolicy = GetInputPolicy(focusRecord)
                    If focusPolicy IsNot Nothing AndAlso
                       HasCapability(focusRecord, MASFloatCapabilities.KeyboardInput) AndAlso
                       focusPolicy.AcceptsKeyboardInput AndAlso
                       _focusManager.ShouldRouteKeyboardTo(focusRecord) Then

                        Return focusRecord
                    End If
                End If
            End If

            Dim best As MASFloatLifecycleRecord = Nothing

            For Each current As MASFloatLifecycleRecord In _host.State.Records
                If current Is Nothing OrElse current.Handle Is Nothing Then
                    Continue For
                End If

                If current.State <> MASFloatLifecycleState.Open Then
                    Continue For
                End If

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(current)
                If policy Is Nothing OrElse
                   Not HasCapability(current, MASFloatCapabilities.KeyboardInput) OrElse
                   Not policy.AcceptsKeyboardInput Then
                    Continue For
                End If

                If IsAboveForRouting(current, best) Then
                    best = current
                End If
            Next

            Return best
        End Function

        ''' <summary>
        ''' Finds the text-input target, preferring the focus owner before top-record fallback.
        ''' </summary>
        Private Function FindTopTextRecord() As MASFloatLifecycleRecord
            Dim focusOwner As MASFloatHandle = _focusManager.CurrentFocusOwner
            If focusOwner IsNot Nothing Then
                Dim focusRecord As MASFloatLifecycleRecord = FindCurrentRecordByHandle(focusOwner)
                If focusRecord IsNot Nothing Then
                    Dim focusPolicy As IMASFloatInputPolicy = GetInputPolicy(focusRecord)
                    If focusPolicy IsNot Nothing AndAlso
                       HasCapability(focusRecord, MASFloatCapabilities.TextInput) AndAlso
                       focusPolicy.AcceptsTextInput AndAlso
                       _focusManager.ShouldRouteKeyboardTo(focusRecord) Then

                        Return focusRecord
                    End If
                End If
            End If

            Dim best As MASFloatLifecycleRecord = Nothing

            For Each current As MASFloatLifecycleRecord In _host.State.Records
                If current Is Nothing OrElse current.Handle Is Nothing Then
                    Continue For
                End If

                If current.State <> MASFloatLifecycleState.Open Then
                    Continue For
                End If

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(current)
                If policy Is Nothing OrElse
                   Not HasCapability(current, MASFloatCapabilities.TextInput) OrElse
                   Not policy.AcceptsTextInput Then
                    Continue For
                End If

                If IsAboveForRouting(current, best) Then
                    best = current
                End If
            Next

            Return best
        End Function

        Private Function FindCapturedPointerRecord() As MASFloatLifecycleRecord
            If _capturedPointerHandle Is Nothing Then
                Return Nothing
            End If

            Dim record As MASFloatLifecycleRecord =
                FindCurrentRecordByHandle(_capturedPointerHandle)

            If record Is Nothing Then
                ReleasePointerCapture()
                Return Nothing
            End If

            Return record
        End Function

        Private Function FindCurrentRecordByHandle(
            handle As MASFloatHandle
        ) As MASFloatLifecycleRecord
            If handle Is Nothing Then
                Return Nothing
            End If

            For Each slotState As MASFloatSlotState In _state.GetAllSlotStates()
                If slotState Is Nothing OrElse slotState.Current Is Nothing Then
                    Continue For
                End If

                Dim current As MASFloatLifecycleRecord = slotState.Current

                If current.State <> MASFloatLifecycleState.Open AndAlso
                   current.State <> MASFloatLifecycleState.Opening Then
                    Continue For
                End If

                If IsSameHandle(current.Handle, handle) Then
                    Return current
                End If
            Next

            Return Nothing
        End Function

        Private Sub CapturePointer(handle As MASFloatHandle)
            If handle Is Nothing Then
                _capturedPointerHandle = Nothing
                Return
            End If

            _capturedPointerHandle = handle
        End Sub

        Private Sub ReleasePointerCapture()
            _capturedPointerHandle = Nothing
        End Sub

        Private Sub ReleasePointerCaptureIfSame(handle As MASFloatHandle)
            If _capturedPointerHandle Is Nothing OrElse handle Is Nothing Then
                Return
            End If

            If IsSameHandle(_capturedPointerHandle, handle) Then
                _capturedPointerHandle = Nothing
            End If
        End Sub

        Private Shared Function GetInputPolicy(
            record As MASFloatLifecycleRecord
        ) As IMASFloatInputPolicy
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing OrElse
               record.ResolvedRequest.Request Is Nothing Then
                Return Nothing
            End If

            Return TryCast(record.ResolvedRequest.Request.Content, IMASFloatInputPolicy)
        End Function

        Private Shared Function GetRoutedInputContent(
            record As MASFloatLifecycleRecord
        ) As IMASFloatRoutedInputContent
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing OrElse
               record.ResolvedRequest.Request Is Nothing Then
                Return Nothing
            End If

            Return TryCast(record.ResolvedRequest.Request.Content, IMASFloatRoutedInputContent)
        End Function

        Private Shared Function HasCapability(
            record As MASFloatLifecycleRecord,
            capability As MASFloatCapabilities
        ) As Boolean
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing OrElse
               record.ResolvedRequest.Descriptor Is Nothing Then
                Return False
            End If

            Return record.ResolvedRequest.Descriptor.Capabilities.HasFlag(capability)
        End Function

        ''' <summary>
        ''' Compares routing priority: higher layer wins; same-layer ties follow host/render order.
        ''' </summary>
        Private Function IsAboveForRouting(
            candidate As MASFloatLifecycleRecord,
            currentBest As MASFloatLifecycleRecord
        ) As Boolean
            If candidate Is Nothing OrElse candidate.Handle Is Nothing Then
                Return False
            End If

            If currentBest Is Nothing OrElse currentBest.Handle Is Nothing Then
                Return True
            End If

            Dim candidateIsChildOfBest As Boolean = IsDescendantOf(candidate, currentBest)
            Dim bestIsChildOfCandidate As Boolean = IsDescendantOf(currentBest, candidate)

            If candidateIsChildOfBest <> bestIsChildOfCandidate Then
                Return candidateIsChildOfBest
            End If

            If candidate.Handle.Layer <> currentBest.Handle.Layer Then
                Return candidate.Handle.Layer > currentBest.Handle.Layer
            End If

            Return GetHostOrderIndex(candidate.Handle) > GetHostOrderIndex(currentBest.Handle)
        End Function

        Private Function IsDescendantOf(candidate As MASFloatLifecycleRecord,
                                        possibleAncestor As MASFloatLifecycleRecord) As Boolean
            If candidate Is Nothing OrElse candidate.Handle Is Nothing Then Return False
            If possibleAncestor Is Nothing OrElse possibleAncestor.Handle Is Nothing Then Return False
            If Object.ReferenceEquals(candidate, possibleAncestor) Then Return False

            Dim parentHandle As MASFloatHandle = GetParentHandle(candidate)
            Dim guard As Integer = 0

            While parentHandle IsNot Nothing AndAlso guard < 16
                If IsSameHandle(parentHandle, possibleAncestor.Handle) Then
                    Return True
                End If

                Dim parentRecord As MASFloatLifecycleRecord = FindHostedRecordByHandle(parentHandle)
                If parentRecord Is Nothing Then
                    Exit While
                End If

                parentHandle = GetParentHandle(parentRecord)
                guard += 1
            End While

            Return False
        End Function

        Private Function FindHostedRecordByHandle(handle As MASFloatHandle) As MASFloatLifecycleRecord
            If handle Is Nothing Then Return Nothing

            For Each record As MASFloatLifecycleRecord In _host.State.Records
                If record Is Nothing OrElse record.Handle Is Nothing Then
                    Continue For
                End If

                If IsSameHandle(record.Handle, handle) Then
                    Return record
                End If
            Next

            Return Nothing
        End Function

        Private Shared Function GetParentHandle(record As MASFloatLifecycleRecord) As MASFloatHandle
            If record Is Nothing OrElse
               record.ResolvedRequest Is Nothing OrElse
               record.ResolvedRequest.Request Is Nothing Then

                Return Nothing
            End If

            If record.ResolvedRequest.Scope <> MASFloatScope.ParentFloat Then
                Return Nothing
            End If

            Return record.ResolvedRequest.Request.ParentHandle
        End Function

        ''' <summary>
        ''' Returns the hosted record index used as the same-layer routing tie-breaker.
        ''' </summary>
        Private Function GetHostOrderIndex(handle As MASFloatHandle) As Integer
            If handle Is Nothing Then
                Return -1
            End If

            Dim records As IReadOnlyList(Of MASFloatLifecycleRecord) = _host.State.Records

            For index As Integer = 0 To records.Count - 1
                Dim record As MASFloatLifecycleRecord = records(index)
                If record Is Nothing OrElse record.Handle Is Nothing Then
                    Continue For
                End If

                If IsSameHandle(record.Handle, handle) Then
                    Return index
                End If
            Next

            Return -1
        End Function
        Friend Function HasAnyFloatVisual() As Boolean
            SyncLock _state.SyncRoot
                For Each slotState As MASFloatSlotState In _state.GetAllSlotStates()
                    If slotState Is Nothing OrElse slotState.Current Is Nothing Then Continue For

                    Dim record As MASFloatLifecycleRecord = slotState.Current

                    If IsVisualLifecycleState(record.State) Then
                        Return True
                    End If
                Next
            End SyncLock

            Return False
        End Function

        Friend Function HasBlockingFloat() As Boolean
            SyncLock _state.SyncRoot
                For Each slotState As MASFloatSlotState In _state.GetAllSlotStates()
                    If slotState Is Nothing OrElse slotState.Current Is Nothing Then Continue For

                    Dim record As MASFloatLifecycleRecord = slotState.Current

                    If Not IsVisualLifecycleState(record.State) Then Continue For

                    If HasCapability(record, MASFloatCapabilities.ModalBlocking) Then
                        Return True
                    End If
                Next
            End SyncLock

            Return False
        End Function

        Private Shared Function IsVisualLifecycleState(state As MASFloatLifecycleState) As Boolean
            Return state = MASFloatLifecycleState.Open OrElse
           state = MASFloatLifecycleState.Opening
        End Function
        Private Shared Function IsSameHandle(
            left As MASFloatHandle,
            right As MASFloatHandle
        ) As Boolean
            If left Is Nothing OrElse right Is Nothing Then
                Return False
            End If

            Return left.Id = right.Id AndAlso left.Generation = right.Generation
        End Function

    End Class

End Namespace
