Option Strict On
Option Explicit On

Imports System
Imports System.Windows.Forms
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Theming
Imports SkiaSharp

Namespace Nexamas.UI.FloatRuntime

    ' ============================================================
    ' MASFloatRuntime Contract
    ' Class: MASFloatControlsContent
    '
    ' Purpose:
    '   Provides a ControlsLayer-backed Float content adapter.
    '
    ' Responsibility:
    '   Hosts Nexamas UI MASControlBase roots inside a Float, renders them using
    '   ControlsLayer, and routes input to the hosted ControlsLayer.
    '
    ' Owns:
    '   A local ControlsLayer.
    '   Hosted MASControlBase roots.
    '   Attached Float session reference.
    '
    ' Does Not Own:
    '   MASFloatRuntime lifecycle.
    '   Show/close policy.
    '   Queueing.
    '   Host ordering.
    '   Result delivery.
    '   Nexamas UI RootHost.
    '   ThemeContext.
    '
    ' Allowed Dependencies:
    '   ControlsLayer.
    '   MASControlBase.
    '   ThemeContext.
    '   IMASFloatContent.
    '   IMASFloatInputPolicy.
    '   IMASFloatRenderableContent.
    '   IMASFloatRoutedInputContent.
    '
    ' Forbidden Dependencies:
    '   Legacy Overlay classes.
    '   OverlayHost.
    '   OverlayManager.
    '   OverlayContent.
    '   OverlayLayer.
    '   OverlayService.
    '   Feature-owned lifecycle services.
    '
    ' Lifecycle Role:
    '   Attached/detached by MASFloatCoordinator through IMASFloatContent.
    '
    ' Threading:
    '   Used on Nexamas UI UI/render/input paths.
    '
    ' Mutation Rules:
    '   Controls may be added before or after session attach.
    '   Session may only be used to request lifecycle actions.
    '
    ' Invariants:
    '   Float bounds are pixel-space.
    '   MASControlBase BoundsLogical are logical-space and must be derived
    '   from boundsPx using ctx.Dpi.
    '   Content must not close itself except through IMASFloatSession.
    '
    ' Failure Rules:
    '   Missing ThemeContext prevents render/input routing.
    ' ============================================================
    Friend NotInheritable Class MASFloatControlsContent
        Implements IMASFloatContent
        Implements IMASFloatInputPolicy
        Implements IMASFloatRenderableContent
        Implements IMASFloatRoutedInputContent
        Implements IMASFloatPointerMoveChangeAwareContent
        Implements IDisposable

        Private ReadOnly _layer As ControlsLayer
        Private ReadOnly _name As String
        Private ReadOnly _invalidate As Action
        Private ReadOnly _themeHost As ThemeHost
        Private ReadOnly _panelShellDrag As New MASPanelShellDragController()
        Private _session As IMASFloatSession
        Private _panelShellClosePressed As Boolean
        Private _panelShellCloseHovered As Boolean
        Private _disposed As Boolean

        Friend Sub New(
    name As String,
    themeHost As ThemeHost,
    invalidate As Action
)
            If themeHost Is Nothing Then
                Throw New ArgumentNullException(NameOf(themeHost))
            End If

            _name = If(name, String.Empty)
            _themeHost = themeHost
            _invalidate = invalidate

            _layer = New ControlsLayer(AddressOf RequestInvalidate)
            _layer.SetHost(_themeHost)
        End Sub

        Friend ReadOnly Property Layer As ControlsLayer
            Get
                Return _layer
            End Get
        End Property

        Public ReadOnly Property Name As String Implements IMASFloatContent.Name
            Get
                Return _name
            End Get
        End Property

        Friend ReadOnly Property Session As IMASFloatSession
            Get
                Return _session
            End Get
        End Property

        Public Property AcceptsPointerInput As Boolean = True Implements IMASFloatInputPolicy.AcceptsPointerInput

        Public Property AcceptsKeyboardInput As Boolean = True Implements IMASFloatInputPolicy.AcceptsKeyboardInput

        Public Property AcceptsTextInput As Boolean = False Implements IMASFloatInputPolicy.AcceptsTextInput

        Public Property CloseOnOutsidePointer As Boolean = True Implements IMASFloatInputPolicy.CloseOnOutsidePointer

        Public Property SwallowOutsidePointer As Boolean = True Implements IMASFloatInputPolicy.SwallowOutsidePointer

        Public Property CloseOnEscape As Boolean = True Implements IMASFloatInputPolicy.CloseOnEscape

        Friend Sub Add(root As MASControlBase)
            If _disposed Then Return
            If root Is Nothing Then Throw New ArgumentNullException(NameOf(root))

            _layer.Add(root)
        End Sub

        Public Sub AttachSession(session As IMASFloatSession) Implements IMASFloatContent.AttachSession
            If _disposed Then Return
            If session Is Nothing Then Throw New ArgumentNullException(NameOf(session))

            _session = session
        End Sub

        Public Sub DetachSession() Implements IMASFloatContent.DetachSession
            ResetPanelShellCloseInteraction()
            _session = Nothing
        End Sub

        Public Sub RenderFloat(
    canvas As SKCanvas,
    ctx As MASThemeContext,
    boundsPx As SKRect
) Implements IMASFloatRenderableContent.RenderFloat

            If _disposed Then Return
            If canvas Is Nothing OrElse ctx Is Nothing Then Return

            UpdateRootBoundsLogical(ctx, boundsPx)

            _layer.Render(canvas, ctx)
        End Sub

        Public Function HitTestFloat(
            ctx As MASThemeContext,
            boundsPx As SKRect,
            ptPx As SKPoint
        ) As Boolean Implements IMASFloatRoutedInputContent.HitTestFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            Return boundsPx.Contains(ptPx.X, ptPx.Y)
        End Function

        Public Sub PointerMoveFloat(
            ctx As MASThemeContext,
            ptPx As SKPoint
        ) Implements IMASFloatRoutedInputContent.PointerMoveFloat

            If PointerMoveFloatVisualChanged(ctx, ptPx) Then
                RequestInvalidate()
            End If
        End Sub

        Public Function PointerMoveFloatVisualChanged(
            ctx As MASThemeContext,
            ptPx As SKPoint
        ) As Boolean Implements IMASFloatPointerMoveChangeAwareContent.PointerMoveFloatVisualChanged

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            If TryRoutePanelShellScrollablePointerMove(ctx, ptPx) Then
                Return True
            End If

            If _panelShellDrag.IsDragging Then
                SetPanelShellCloseInteractionAtPoint(ptPx, False)

                Dim targetBoundsPx As SKRect =
                    _panelShellDrag.DragTo(ptPx, ResolveAvailableBoundsPx())

                If _session IsNot Nothing Then
                    _session.TrySetBoundsPx(targetBoundsPx)
                End If

                Return True
            End If

            Dim closeChanged As Boolean = SetPanelShellCloseInteractionAtPoint(ptPx, _panelShellClosePressed)
            If _panelShellCloseHovered OrElse _panelShellClosePressed Then
                Return closeChanged
            End If

            Dim controlsChanged As Boolean = _layer.MouseMoveSurface(ctx, ptPx)

            Return closeChanged OrElse controlsChanged
        End Function

        Public Function PointerDownFloat(
            ctx As MASThemeContext,
            ptPx As SKPoint,
            button As MouseButtons
        ) As Boolean Implements IMASFloatRoutedInputContent.PointerDownFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            If TryRoutePanelShellScrollablePointerDown(ctx, ptPx, button) Then
                Return True
            End If

            If TryRoutePanelShellPointerDown(ptPx, button) Then
                Return True
            End If

            _layer.MouseDownSurface(ctx, ptPx, CInt(button))
            Return True
        End Function

        Public Function PointerUpFloat(
            ctx As MASThemeContext,
            ptPx As SKPoint,
            button As MouseButtons
        ) As Boolean Implements IMASFloatRoutedInputContent.PointerUpFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            If TryRoutePanelShellScrollablePointerUp(ctx, ptPx, button) Then
                Return True
            End If

            If _panelShellDrag.IsDragging Then
                _panelShellDrag.EndDrag()
                RequestInvalidate()
                Return True
            End If

            If _panelShellClosePressed Then
                Return CompletePanelShellCloseButtonPress(ptPx)
            End If

            _layer.MouseUpSurface(ctx, ptPx, CInt(button))
            Return True
        End Function

        Public Function PointerWheelFloat(
            ctx As MASThemeContext,
            ptPx As SKPoint,
            delta As Integer
        ) As Boolean Implements IMASFloatRoutedInputContent.PointerWheelFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            If TryRoutePanelShellScrollableWheel(ctx, ptPx, delta) Then
                Return True
            End If

            _layer.MouseWheelSurface(ctx, ptPx, delta)
            Return True
        End Function

        Public Function KeyDownFloat(
            ctx As MASThemeContext,
            keyCode As Keys
        ) As Boolean Implements IMASFloatRoutedInputContent.KeyDownFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            Return _layer.KeyDown(ctx, keyCode)
        End Function

        Public Function KeyUpFloat(
            ctx As MASThemeContext,
            keyCode As Keys
        ) As Boolean Implements IMASFloatRoutedInputContent.KeyUpFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            Return _layer.KeyUp(ctx, keyCode)
        End Function

        Public Function TextInputFloat(
            ctx As MASThemeContext,
            text As String
        ) As Boolean Implements IMASFloatRoutedInputContent.TextInputFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False
            If String.IsNullOrEmpty(text) Then Return False

            Return _layer.TextInput(ctx, text)
        End Function

        Public Function Win32ImeMessageFloat(
            ctx As MASThemeContext,
            hwnd As IntPtr,
            message As Integer,
            wParam As IntPtr,
            lParam As IntPtr
        ) As Boolean Implements IMASFloatRoutedInputContent.Win32ImeMessageFloat

            If _disposed Then Return False
            If ctx Is Nothing Then Return False

            Return _layer.Win32ImeMessage(ctx, hwnd, message, wParam, lParam)
        End Function

        Private Function TryRoutePanelShellPointerDown(ptPx As SKPoint,
                                                           button As MouseButtons) As Boolean
            If button <> MouseButtons.Left Then Return False
            If _session Is Nothing Then Return False

            Dim hosted As IMASPanelShellHostedContent = Nothing
            If Not TryGetPanelShellHostedContent(hosted) Then Return False
            If hosted Is Nothing Then Return False

            Dim shellResult As MASPanelShellResult = hosted.LastPanelShellResult
            If shellResult Is Nothing Then Return False

            Dim hit As MASPanelShellHitTarget = shellResult.HitTest(ptPx)

            If hit = MASPanelShellHitTarget.CloseButton AndAlso hosted.PanelShellCloseEnabled Then
                _panelShellClosePressed = True
                hosted.SetPanelShellCloseButtonInteraction(True, True)
                RequestInvalidate()
                Return True
            End If

            If hit <> MASPanelShellHitTarget.Header Then Return False
            If Not hosted.PanelShellDragEnabled Then Return False

            ResetPanelShellCloseInteraction()

            Dim currentBoundsPx As SKRect = SKRect.Empty
            If Not _session.TryGetBoundsPx(currentBoundsPx) Then Return False

            Return _panelShellDrag.BeginDrag(
                pointerPx:=ptPx,
                shellResult:=shellResult,
                currentBoundsPx:=currentBoundsPx,
                button:=button)
        End Function

        Private Function SetPanelShellCloseInteractionAtPoint(ptPx As SKPoint,
                                                              keepPressed As Boolean) As Boolean
            Dim hosted As IMASPanelShellHostedContent = Nothing
            If Not TryGetPanelShellHostedContent(hosted) OrElse hosted Is Nothing Then
                Dim changedWithoutHost As Boolean = _panelShellCloseHovered OrElse _panelShellClosePressed
                _panelShellCloseHovered = False
                _panelShellClosePressed = False
                Return changedWithoutHost
            End If

            Dim shellResult As MASPanelShellResult = hosted.LastPanelShellResult
            If shellResult Is Nothing OrElse Not hosted.PanelShellCloseEnabled Then
                Dim changedDisabled As Boolean = _panelShellCloseHovered OrElse _panelShellClosePressed
                hosted.SetPanelShellCloseButtonInteraction(False, False)
                _panelShellCloseHovered = False
                _panelShellClosePressed = False
                Return changedDisabled
            End If

            Dim hover As Boolean = (shellResult.HitTest(ptPx) = MASPanelShellHitTarget.CloseButton)
            Dim pressed As Boolean = keepPressed AndAlso _panelShellClosePressed AndAlso hover
            Dim changed As Boolean = (_panelShellCloseHovered <> hover) OrElse (_panelShellClosePressed <> pressed)

            _panelShellCloseHovered = hover
            _panelShellClosePressed = pressed
            hosted.SetPanelShellCloseButtonInteraction(hover, pressed)

            Return changed
        End Function

        Private Function CompletePanelShellCloseButtonPress(ptPx As SKPoint) As Boolean
            Dim hosted As IMASPanelShellHostedContent = Nothing
            If Not TryGetPanelShellHostedContent(hosted) OrElse hosted Is Nothing Then
                _panelShellClosePressed = False
                Return True
            End If

            Dim closeNow As Boolean = False

            Dim shellResult As MASPanelShellResult = hosted.LastPanelShellResult
            If shellResult IsNot Nothing AndAlso hosted.PanelShellCloseEnabled Then
                closeNow = (shellResult.HitTest(ptPx) = MASPanelShellHitTarget.CloseButton)
            End If

            _panelShellClosePressed = False
            hosted.SetPanelShellCloseButtonInteraction(closeNow, False)
            RequestInvalidate()

            If closeNow Then
                If hosted.RequestPanelShellClose() Then Return True

                If _session IsNot Nothing Then
                    Try
                        _session.Cancel()
                    Catch masCaughtException1 As Exception
                        Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException1)
                    End Try
                End If
            End If

            Return True
        End Function

        Private Sub ResetPanelShellCloseInteraction()
            _panelShellClosePressed = False
            _panelShellCloseHovered = False

            Dim scrollable As IMASPanelShellScrollableHostedContent = Nothing
            If TryGetPanelShellScrollableHostedContent(scrollable) AndAlso scrollable IsNot Nothing Then
                scrollable.CancelPanelShellScrollInteraction()
            End If

            Dim hosted As IMASPanelShellHostedContent = Nothing
            If Not TryGetPanelShellHostedContent(hosted) OrElse hosted Is Nothing Then Return

            hosted.SetPanelShellCloseButtonInteraction(False, False)
        End Sub

        Private Function TryRoutePanelShellScrollablePointerMove(ctx As MASThemeContext, ptPx As SKPoint) As Boolean
            Dim scrollable As IMASPanelShellScrollableHostedContent = Nothing
            If Not TryGetPanelShellScrollableHostedContent(scrollable) OrElse scrollable Is Nothing Then Return False
            Return scrollable.TryHandlePanelShellScrollPointerMove(ctx, ptPx)
        End Function

        Private Function TryRoutePanelShellScrollablePointerDown(ctx As MASThemeContext, ptPx As SKPoint, button As MouseButtons) As Boolean
            Dim scrollable As IMASPanelShellScrollableHostedContent = Nothing
            If Not TryGetPanelShellScrollableHostedContent(scrollable) OrElse scrollable Is Nothing Then Return False
            Return scrollable.TryHandlePanelShellScrollPointerDown(ctx, ptPx, button)
        End Function

        Private Function TryRoutePanelShellScrollablePointerUp(ctx As MASThemeContext, ptPx As SKPoint, button As MouseButtons) As Boolean
            Dim scrollable As IMASPanelShellScrollableHostedContent = Nothing
            If Not TryGetPanelShellScrollableHostedContent(scrollable) OrElse scrollable Is Nothing Then Return False
            Return scrollable.TryHandlePanelShellScrollPointerUp(ctx, ptPx, button)
        End Function

        Private Function TryRoutePanelShellScrollableWheel(ctx As MASThemeContext, ptPx As SKPoint, delta As Integer) As Boolean
            Dim scrollable As IMASPanelShellScrollableHostedContent = Nothing
            If Not TryGetPanelShellScrollableHostedContent(scrollable) OrElse scrollable Is Nothing Then Return False
            Return scrollable.TryHandlePanelShellScrollWheel(ctx, ptPx, delta)
        End Function

        Private Function TryGetPanelShellScrollableHostedContent(ByRef scrollable As IMASPanelShellScrollableHostedContent) As Boolean
            scrollable = Nothing

            For Each root As MASControlBase In _layer.GetSnapshot()
                If root Is Nothing Then Continue For

                scrollable = TryCast(root, IMASPanelShellScrollableHostedContent)
                If scrollable IsNot Nothing Then Return True
            Next

            Return False
        End Function

        Private Function TryGetPanelShellHostedContent(ByRef hosted As IMASPanelShellHostedContent) As Boolean
            hosted = Nothing

            For Each root As MASControlBase In _layer.GetSnapshot()
                If root Is Nothing Then Continue For

                hosted = TryCast(root, IMASPanelShellHostedContent)
                If hosted IsNot Nothing Then Return True
            Next

            Return False
        End Function

        Private Function ResolveAvailableBoundsPx() As SKRect
            If _session Is Nothing Then Return SKRect.Empty

            Try
                Return _session.AvailableBoundsPx
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException2)
            End Try

            Return SKRect.Empty
        End Function

        Private Sub UpdateRootBoundsLogical(ctx As MASThemeContext, boundsPx As SKRect)
            If ctx Is Nothing Then Return

            Dim dpi As Single = ctx.Dpi
            If dpi <= 0.0F OrElse Single.IsNaN(dpi) OrElse Single.IsInfinity(dpi) Then
                dpi = 1.0F
            End If

            Dim logical As New SKRect(
                boundsPx.Left / dpi,
                boundsPx.Top / dpi,
                boundsPx.Right / dpi,
                boundsPx.Bottom / dpi
            )

            For Each root As MASControlBase In _layer.GetSnapshot()
                If root Is Nothing Then Continue For
                If SameRectLogical(root.BoundsLogical, logical) Then Continue For

                root.SetLocalLayoutBoundsInternal(logical)
            Next
        End Sub

        Private Shared Function SameRectLogical(left As SKRect, right As SKRect) As Boolean
            Const epsilon As Single = 0.001F

            Return Math.Abs(left.Left - right.Left) <= epsilon AndAlso
                   Math.Abs(left.Top - right.Top) <= epsilon AndAlso
                   Math.Abs(left.Right - right.Right) <= epsilon AndAlso
                   Math.Abs(left.Bottom - right.Bottom) <= epsilon
        End Function

        Private Sub RequestInvalidate()
            If _disposed Then Return

            Try
                If _invalidate IsNot Nothing Then
                    _invalidate.Invoke()
                End If
            Catch masCaughtException3 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException3)
            End Try
        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose
            If _disposed Then Return
            _disposed = True

            ResetPanelShellCloseInteraction()
            _session = Nothing
            _panelShellDrag.Cancel()

            Try
                _layer.Dispose()
            Catch masCaughtException4 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowBoundary(masCaughtException4)
            End Try
        End Sub

    End Class

End Namespace
