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

    ' ============================================================
    ' MASFloatRuntime Contract
    ' Class: MASFloatInputRouter
    '
    ' Purpose:
    '   Provides the official runtime-owned routing point for Float input
    '   decisions.
    '
    ' Responsibility:
    '   Routes pointer, wheel, keyboard, and text input to active Float
    '   content when appropriate, evaluates outside pointer and Escape
    '   behavior through IMASFloatInputPolicy, and requests close through
    '   Coordinator.
    '
    ' Owns:
    '   No pointer state.
    '   No keyboard state.
    '   No text input state.
    '
    ' Does Not Own:
    '   Platform event subscription.
    '   WinForms/Skia event dispatch.
    '   Close authorization.
    '   Lifecycle execution.
    '   Focus ownership state.
    '   Host drawing.
    '
    ' Allowed Dependencies:
    '   MASFloatCoordinator.
    '   MASFloatRuntimeState.
    '   MASFloatHost.
    '   MASFloatSlotState.
    '   MASFloatLifecycleRecord.
    '   IMASFloatInputPolicy.
    '   IMASFloatRoutedInputContent.
    '   ThemeContext.
    '   SKPoint.
    '   MouseButtons.
    '   Keys.
    '
    ' Forbidden Dependencies:
    '   Legacy Overlay classes.
    '   OverlayHost.
    '   OverlayManager.
    '   OverlayContent.
    '   OverlayLayer.
    '   OverlayService.
    '   BlockingControlsOverlayContent.
    '   Feature-owned lifecycle services.
    '
    ' Lifecycle Role:
    '   Called by Nexamas UI input integration.
    '   Router requests close through Coordinator; it never closes directly.
    '
    ' Threading:
    '   Must be called on the runtime/UI input path.
    '   Does not perform random UI marshaling.
    '
    ' Mutation Rules:
    '   Must not mutate runtime state directly.
    '   Must not mutate host state.
    '   May request Coordinator.Close only.
    '
    ' Invariants:
    '   No Float may receive input without an explicit input policy.
    '   CloseOnOutsidePointer and CloseOnEscape request closing only.
    '   PolicyEngine still authorizes the actual close through Coordinator.
    '   Input point coordinates are surface pixel-space.
    '
    ' Failure Rules:
    '   Missing policy means input is not accepted.
    '   Missing records are ignored.
    ' ============================================================
    Partial Friend NotInheritable Class MASFloatInputRouter

        Private ReadOnly _coordinator As MASFloatCoordinator
        Private ReadOnly _state As MASFloatRuntimeState
        Private ReadOnly _host As MASFloatHost
        Private ReadOnly _focusManager As MASFloatFocusManager

        Private _capturedPointerHandle As MASFloatHandle

        ''' <summary>
        ''' Captures the already-selected outside-pointer target so close remains handle-specific.
        ''' </summary>
        Private NotInheritable Class OutsidePointerWork
            Friend Property Handle As MASFloatHandle
            Friend Property ShouldClose As Boolean
            Friend Property Swallow As Boolean
            Friend Property AllowPointerThroughAfterClose As Boolean
        End Class

        Friend Sub New(
            coordinator As MASFloatCoordinator,
            state As MASFloatRuntimeState,
            host As MASFloatHost,
            focusManager As MASFloatFocusManager
        )
            If coordinator Is Nothing Then
                Throw New ArgumentNullException(NameOf(coordinator))
            End If

            If state Is Nothing Then
                Throw New ArgumentNullException(NameOf(state))
            End If

            If host Is Nothing Then
                Throw New ArgumentNullException(NameOf(host))
            End If

            If focusManager Is Nothing Then
                Throw New ArgumentNullException(NameOf(focusManager))
            End If

            _coordinator = coordinator
            _state = state
            _host = host
            _focusManager = focusManager
        End Sub

        Friend Function RoutePointerMove(
            ctx As MASThemeContext,
            ptPx As SKPoint
        ) As MASFloatPointerMoveRoutingResult
            If ctx Is Nothing Then Return MASFloatPointerMoveRoutingResult.NotHandled()

            Dim content As IMASFloatRoutedInputContent = Nothing

            SyncLock _state.SyncRoot
                Dim record As MASFloatLifecycleRecord = FindCapturedPointerRecord()

                If record Is Nothing Then
                    Dim hitRecord As MASFloatLifecycleRecord = FindTopPointerRecordAt(ctx, ptPx)
                    Dim outsideRecord As MASFloatLifecycleRecord =
                        FindTopOutsidePointerPolicyRecord(
                            ctx:=ctx,
                            ptPx:=ptPx,
                            includeCloseOnOutside:=False)

                    If outsideRecord IsNot Nothing AndAlso IsAboveForRouting(outsideRecord, hitRecord) Then
                        record = outsideRecord
                    Else
                        record = hitRecord
                    End If
                End If

                If record Is Nothing Then
                    record = FindTopBlockingPointerRecord()
                End If

                If record Is Nothing Then Return MASFloatPointerMoveRoutingResult.NotHandled()

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

                content = GetRoutedInputContent(record)
            End SyncLock

            If content Is Nothing Then Return MASFloatPointerMoveRoutingResult.HandledResult(visualChanged:=False)

            Try
                Dim changeAware As IMASFloatPointerMoveChangeAwareContent =
                    TryCast(content, IMASFloatPointerMoveChangeAwareContent)

                If changeAware IsNot Nothing Then
                    Return MASFloatPointerMoveRoutingResult.HandledResult(
                        visualChanged:=changeAware.PointerMoveFloatVisualChanged(ctx, ptPx))
                End If

                ' Compatibility path for older Float content: ownership is preserved and a repaint is
                ' still requested because the router cannot prove whether the content mutated hover state.
                content.PointerMoveFloat(ctx, ptPx)
                Return MASFloatPointerMoveRoutingResult.HandledResult(visualChanged:=True)

            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException1, "MASFloatInputRouter.RoutePointerMove")
                Return MASFloatPointerMoveRoutingResult.HandledResult(visualChanged:=True)
            End Try
        End Function

        Friend Function RoutePointerDown(
            ctx As MASThemeContext,
            ptPx As SKPoint,
            button As MouseButtons
        ) As Boolean

            If ctx Is Nothing Then Return False

            Dim outsideWork As OutsidePointerWork = Nothing
            Dim content As IMASFloatRoutedInputContent = Nothing

            SyncLock _state.SyncRoot
                Dim record As MASFloatLifecycleRecord = FindTopPointerRecordAt(ctx, ptPx)

                Dim outsideRecord As MASFloatLifecycleRecord =
                    FindTopOutsidePointerPolicyRecord(
                        ctx:=ctx,
                        ptPx:=ptPx,
                        includeCloseOnOutside:=True)

                If outsideRecord IsNot Nothing AndAlso IsAboveForRouting(outsideRecord, record) Then
                    outsideWork = PrepareOutsidePointerToRecord(outsideRecord)
                ElseIf record Is Nothing Then
                    outsideWork = PrepareOutsidePointerToTopRecord()
                Else
                    Dim policy As IMASFloatInputPolicy = GetInputPolicy(record)

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

                        Return False
                    End If

                    CapturePointer(record.Handle)
                    content = GetRoutedInputContent(record)
                End If
            End SyncLock

            If outsideWork IsNot Nothing Then
                Return ExecuteOutsidePointerWork(outsideWork)
            End If

            If content Is Nothing Then Return True

            Try
                Return content.PointerDownFloat(ctx, ptPx, button)
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException2, "MASFloatInputRouter.RoutePointerDown")
                Return True
            End Try
        End Function

        Friend Function RoutePointerUp(
            ctx As MASThemeContext,
            ptPx As SKPoint,
            button As MouseButtons
        ) As Boolean
            If ctx Is Nothing Then Return False

            Dim content As IMASFloatRoutedInputContent = Nothing
            Dim releaseCaptureAfterRoute As Boolean = False

            SyncLock _state.SyncRoot
                Dim record As MASFloatLifecycleRecord = FindCapturedPointerRecord()
                Dim hadCapture As Boolean = record IsNot Nothing

                If record Is Nothing Then
                    Dim hitRecord As MASFloatLifecycleRecord = FindTopPointerRecordAt(ctx, ptPx)
                    Dim outsideRecord As MASFloatLifecycleRecord =
                        FindTopOutsidePointerPolicyRecord(
                            ctx:=ctx,
                            ptPx:=ptPx,
                            includeCloseOnOutside:=False)

                    If outsideRecord IsNot Nothing AndAlso IsAboveForRouting(outsideRecord, hitRecord) Then
                        record = outsideRecord
                    Else
                        record = hitRecord
                    End If
                End If

                If record Is Nothing Then
                    record = FindTopBlockingPointerRecord()
                End If

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

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(record)
                If policy Is Nothing OrElse
                   Not HasCapability(record, MASFloatCapabilities.PointerInput) OrElse
                   Not policy.AcceptsPointerInput Then
                    ReleasePointerCapture()
                    Return False
                End If

                content = GetRoutedInputContent(record)
                releaseCaptureAfterRoute = hadCapture

                If content Is Nothing Then
                    If hadCapture Then ReleasePointerCapture()
                    Return True
                End If
            End SyncLock

            Try
                content.PointerUpFloat(ctx, ptPx, button)
            Catch masCaughtException3 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException3, "MASFloatInputRouter.RoutePointerUp")
            Finally
                If releaseCaptureAfterRoute Then
                    SyncLock _state.SyncRoot
                        ReleasePointerCapture()
                    End SyncLock
                End If
            End Try

            Return True
        End Function

        Friend Function RoutePointerWheel(
            ctx As MASThemeContext,
            ptPx As SKPoint,
            delta As Integer
        ) As Boolean
            If ctx Is Nothing Then Return False

            Dim content As IMASFloatRoutedInputContent = Nothing

            SyncLock _state.SyncRoot
                Dim hitRecord As MASFloatLifecycleRecord = FindTopPointerRecordAt(ctx, ptPx)
                Dim outsideRecord As MASFloatLifecycleRecord =
                    FindTopOutsidePointerPolicyRecord(
                        ctx:=ctx,
                        ptPx:=ptPx,
                        includeCloseOnOutside:=False)

                Dim record As MASFloatLifecycleRecord = Nothing

                If outsideRecord IsNot Nothing AndAlso IsAboveForRouting(outsideRecord, hitRecord) Then
                    record = outsideRecord
                Else
                    record = hitRecord
                End If

                If record Is Nothing Then
                    Return FindTopBlockingPointerRecord() IsNot Nothing
                End If

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

                content = GetRoutedInputContent(record)
            End SyncLock

            If content Is Nothing Then Return True

            Try
                Return content.PointerWheelFloat(ctx, ptPx, delta)
            Catch masCaughtException4 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException4, "MASFloatInputRouter.RoutePointerWheel")
                Return True
            End Try
        End Function

        Friend Function RouteKeyDown(
            ctx As MASThemeContext,
            keyCode As Keys
        ) As Boolean
            If ctx Is Nothing Then Return False

            If keyCode = Keys.Escape Then
                Dim closeHandle As MASFloatHandle = Nothing

                SyncLock _state.SyncRoot
                    closeHandle = FindEscapeCloseHandle()
                End SyncLock

                If closeHandle IsNot Nothing AndAlso
                   _coordinator.Close(
                       closeHandle,
                       MASFloatCloseReason.EscapeKey,
                       requestedBySession:=False
                   ) Then

                    Return True
                End If
            End If

            Dim content As IMASFloatRoutedInputContent = Nothing

            SyncLock _state.SyncRoot
                Dim record As MASFloatLifecycleRecord = FindTopKeyboardRecord()
                If record Is Nothing Then Return False

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(record)
                If policy Is Nothing OrElse
                   Not HasCapability(record, MASFloatCapabilities.KeyboardInput) OrElse
                   Not policy.AcceptsKeyboardInput Then Return False

                content = GetRoutedInputContent(record)
            End SyncLock

            If content Is Nothing Then Return True

            Try
                Return content.KeyDownFloat(ctx, keyCode)
            Catch masCaughtException5 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException5, "MASFloatInputRouter.RouteKeyDown")
                Return False
            End Try
        End Function

        Friend Function RouteKeyUp(
            ctx As MASThemeContext,
            keyCode As Keys
        ) As Boolean
            If ctx Is Nothing Then Return False

            Dim content As IMASFloatRoutedInputContent = Nothing

            SyncLock _state.SyncRoot
                Dim record As MASFloatLifecycleRecord = FindTopKeyboardRecord()
                If record Is Nothing Then Return False

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(record)
                If policy Is Nothing OrElse
                   Not HasCapability(record, MASFloatCapabilities.KeyboardInput) OrElse
                   Not policy.AcceptsKeyboardInput Then Return False

                content = GetRoutedInputContent(record)
            End SyncLock

            If content Is Nothing Then Return True

            Try
                Return content.KeyUpFloat(ctx, keyCode)
            Catch masCaughtException6 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException6, "MASFloatInputRouter.RouteKeyUp")
                Return False
            End Try
        End Function

        Friend Function RouteTextInput(
            ctx As MASThemeContext,
            text As String
        ) As Boolean
            If ctx Is Nothing Then Return False
            If String.IsNullOrEmpty(text) Then Return False

            Dim content As IMASFloatRoutedInputContent = Nothing

            SyncLock _state.SyncRoot
                Dim record As MASFloatLifecycleRecord = FindTopTextRecord()
                If record Is Nothing Then Return False

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(record)
                If policy Is Nothing OrElse
                   Not HasCapability(record, MASFloatCapabilities.TextInput) OrElse
                   Not policy.AcceptsTextInput Then Return False

                content = GetRoutedInputContent(record)
            End SyncLock

            If content Is Nothing Then Return True

            Try
                Return content.TextInputFloat(ctx, text)
            Catch masCaughtException7 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException7, "MASFloatInputRouter.RouteTextInput")
                Return False
            End Try
        End Function

        Friend Function RouteWin32ImeMessage(
            ctx As MASThemeContext,
            hwnd As IntPtr,
            message As Integer,
            wParam As IntPtr,
            lParam As IntPtr
        ) As Boolean
            If ctx Is Nothing Then Return False

            Dim content As IMASFloatRoutedInputContent = Nothing

            SyncLock _state.SyncRoot
                Dim record As MASFloatLifecycleRecord = FindTopTextRecord()
                If record Is Nothing Then Return False

                Dim policy As IMASFloatInputPolicy = GetInputPolicy(record)
                If policy Is Nothing OrElse
                   Not HasCapability(record, MASFloatCapabilities.TextInput) OrElse
                   Not policy.AcceptsTextInput Then Return False

                content = GetRoutedInputContent(record)
            End SyncLock

            If content Is Nothing Then Return False

            Try
                Return content.Win32ImeMessageFloat(ctx, hwnd, message, wParam, lParam)
            Catch masCaughtException8 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException8, "MASFloatInputRouter.RouteWin32ImeMessage")
                Return False
            End Try
        End Function

        Friend Sub HostLostFocus()
            SyncLock _state.SyncRoot
                ReleasePointerCapture()
                _focusManager.Clear()
            End SyncLock
        End Sub

        Friend Function RouteEscapeKey() As Boolean
            Dim closeHandle As MASFloatHandle = Nothing

            SyncLock _state.SyncRoot
                closeHandle = FindEscapeCloseHandle()
            End SyncLock

            If closeHandle Is Nothing Then
                Return False
            End If

            Return _coordinator.Close(
                closeHandle,
                MASFloatCloseReason.EscapeKey,
                requestedBySession:=False
            )
        End Function

        Friend Function RouteOutsidePointerForSlot(slot As MASFloatSlot) As Boolean
            Dim outsideWork As OutsidePointerWork = Nothing

            SyncLock _state.SyncRoot
                Dim slotState As MASFloatSlotState = _state.GetSlotState(slot)

                If slotState Is Nothing OrElse slotState.Current Is Nothing Then
                    Return False
                End If

                outsideWork = PrepareOutsidePointerToRecord(slotState.Current)
            End SyncLock

            Return ExecuteOutsidePointerWork(outsideWork)
        End Function

        Friend Function RouteOutsidePointerToHandle(handle As MASFloatHandle) As Boolean
            If handle Is Nothing Then
                Return False
            End If

            Dim outsideWork As OutsidePointerWork = Nothing

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

                    If IsSameHandle(slotState.Current.Handle, handle) Then
                        outsideWork = PrepareOutsidePointerToRecord(slotState.Current)
                        Exit For
                    End If
                Next
            End SyncLock

            Return ExecuteOutsidePointerWork(outsideWork)
        End Function

        Friend Function HasInteractiveFloat() 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 record.State <> MASFloatLifecycleState.Open Then Continue For

                    Dim policy As IMASFloatInputPolicy = GetInputPolicy(record)
                    If policy Is Nothing Then Continue For

                    If (HasCapability(record, MASFloatCapabilities.PointerInput) AndAlso policy.AcceptsPointerInput) OrElse
                       (HasCapability(record, MASFloatCapabilities.KeyboardInput) AndAlso policy.AcceptsKeyboardInput) OrElse
                       (HasCapability(record, MASFloatCapabilities.TextInput) AndAlso policy.AcceptsTextInput) Then
                        Return True
                    End If
                Next
            End SyncLock

            Return False
        End Function
        Friend Function IsPointerOwnedByFloat(
            ctx As MASThemeContext,
            ptPx As SKPoint
        ) As Boolean
            If ctx Is Nothing Then Return False

            SyncLock _state.SyncRoot
                If FindTopPointerRecordAt(ctx, ptPx) IsNot Nothing Then
                    Return True
                End If

                If FindTopOutsidePointerPolicyRecord(
                    ctx:=ctx,
                    ptPx:=ptPx,
                    includeCloseOnOutside:=False) IsNot Nothing Then

                    Return True
                End If

                Return FindTopBlockingPointerRecord() IsNot Nothing
            End SyncLock
        End Function

    End Class

End Namespace
