Option Strict On
Option Explicit On

Imports System
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Values
Imports SkiaSharp
Imports Nexamas.UI.General


Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Friend-only placement/dismissal policy for MASPopover Round 18 refinement. It
    ''' owns anchor sanitization, anchored/free-point geometry, collision flipping,
    ''' pixel clamping, focus-scope/dismiss evidence, and the no-parallel-overlay rule.
    ''' FloatRuntime remains the lifecycle owner.
    ''' </summary>
    Friend NotInheritable Class MASPopoverPlacementPolicy

        Private Sub New()
        End Sub

        Friend Shared Function NormalizePlacement(value As MASCalloutPlacement) As MASCalloutPlacement
            Return MASCalloutVisualPolicy.NormalizePlacement(value)
        End Function

        Friend Shared Function NormalizeAnchorPx(anchorPx As SKRect,
                                                 fallbackPointPx As SKPoint) As SKRect
            Dim left As Single = SafeNumber(anchorPx.Left, fallbackPointPx.X)
            Dim top As Single = SafeNumber(anchorPx.Top, fallbackPointPx.Y)
            Dim right As Single = SafeNumber(anchorPx.Right, left)
            Dim bottom As Single = SafeNumber(anchorPx.Bottom, top)

            If right < left Then
                Dim tmp As Single = left
                left = right
                right = tmp
            End If

            If bottom < top Then
                Dim tmp As Single = top
                top = bottom
                bottom = tmp
            End If

            If right - left < 1.0F Then right = left + 1.0F
            If bottom - top < 1.0F Then bottom = top + 1.0F
            Return New SKRect(left, top, right, bottom)
        End Function

        Friend Shared Function ResolveBoundsPx(anchorPx As SKRect,
                                               availablePx As SKRect,
                                               sizePx As SKSize,
                                               gapPx As Single,
                                               preferredPlacement As MASCalloutPlacement) As SKRect
            Dim safeGap As Single = Math.Max(0.0F, gapPx)
            Dim safeSize As New SKSize(Math.Max(1.0F, sizePx.Width), Math.Max(1.0F, sizePx.Height))
            Dim safeAnchor As SKRect = NormalizeAnchorPx(anchorPx, New SKPoint(availablePx.MidX, availablePx.MidY))
            Dim placement As MASCalloutPlacement = ResolveEffectivePlacement(safeAnchor, availablePx, safeSize, safeGap, preferredPlacement)

            Dim left As Single
            Dim top As Single

            Select Case placement
                Case MASCalloutPlacement.Top
                    left = safeAnchor.MidX - safeSize.Width / 2.0F
                    top = safeAnchor.Top - safeGap - safeSize.Height

                Case MASCalloutPlacement.Left
                    left = safeAnchor.Left - safeGap - safeSize.Width
                    top = safeAnchor.MidY - safeSize.Height / 2.0F

                Case MASCalloutPlacement.Right
                    left = safeAnchor.Right + safeGap
                    top = safeAnchor.MidY - safeSize.Height / 2.0F

                Case Else
                    left = safeAnchor.MidX - safeSize.Width / 2.0F
                    top = safeAnchor.Bottom + safeGap
            End Select

            left = Clamp(left, availablePx.Left + safeGap, availablePx.Right - safeSize.Width - safeGap)
            top = Clamp(top, availablePx.Top + safeGap, availablePx.Bottom - safeSize.Height - safeGap)

            Return PixelSnap.SnapRectHalf(New SKRect(left, top, left + safeSize.Width, top + safeSize.Height))
        End Function

        Friend Shared Function ResolveEffectivePlacement(anchorPx As SKRect,
                                                         availablePx As SKRect,
                                                         sizePx As SKSize,
                                                         gapPx As Single,
                                                         preferredPlacement As MASCalloutPlacement) As MASCalloutPlacement
            Dim normalized As MASCalloutPlacement = NormalizePlacement(preferredPlacement)
            If normalized <> MASCalloutPlacement.Auto AndAlso HasRoom(anchorPx, availablePx, sizePx, gapPx, normalized) Then Return normalized

            If anchorPx.Bottom + gapPx + sizePx.Height <= availablePx.Bottom Then Return MASCalloutPlacement.Bottom
            If anchorPx.Top - gapPx - sizePx.Height >= availablePx.Top Then Return MASCalloutPlacement.Top
            If anchorPx.Right + gapPx + sizePx.Width <= availablePx.Right Then Return MASCalloutPlacement.Right
            If anchorPx.Left - gapPx - sizePx.Width >= availablePx.Left Then Return MASCalloutPlacement.Left

            If normalized <> MASCalloutPlacement.Auto Then Return normalized
            Return MASCalloutPlacement.Bottom
        End Function

        Friend Shared Function ResolveCalloutSizePx(title As String,
                                                    body As String,
                                                    dpi As Single) As SKSize
            Dim safeDpi As Single = PixelSnap.SafeDpi(dpi)
            Dim titleLength As Integer = MASCalloutVisualPolicy.ResolveTitleText(title).Length
            Dim bodyLength As Integer = MASCalloutVisualPolicy.ResolveBodyText(body).Length
            Dim longTextBonus As Single = Math.Min(56.0F, CSng(Math.Max(0, bodyLength - 80)) * 0.45F)
            Dim widthDip As Single = Math.Min(CalloutTokens.MaxWidthDip,
                                              Math.Max(CalloutTokens.MinWidthDip,
                                                       CalloutTokens.DefaultWidthDip + longTextBonus))
            Dim bodyLineCount As Integer = Math.Max(1, Math.Min(MASCalloutVisualPolicy.MaxBodyLines, CInt(Math.Ceiling(Math.Max(1.0R, CDbl(bodyLength)) / 84.0R))))
            Dim titleLineCount As Integer = Math.Max(1, Math.Min(MASCalloutVisualPolicy.MaxTitleLines, CInt(Math.Ceiling(Math.Max(1.0R, CDbl(titleLength)) / 42.0R))))
            Dim heightDip As Single = CalloutTokens.PaddingTopDip +
                                      CSng(titleLineCount) * 21.0F +
                                      CalloutTokens.TitleMessageGapDip +
                                      CSng(bodyLineCount) * 24.0F +
                                      CalloutTokens.PaddingBottomDip

            heightDip = Math.Min(CalloutTokens.MaxHeightDip, Math.Max(CalloutTokens.MinHeightDip, heightDip))
            Return New SKSize(widthDip * safeDpi, heightDip * safeDpi)
        End Function

        Friend Shared Function HasKeyboardDismissBoundary(closeOnEscape As Boolean) As Boolean
            Return closeOnEscape
        End Function

        Friend Shared Function HasFocusScopeBoundary() As Boolean
            Return True
        End Function

        Friend Shared Function HasCollisionPolicy() As Boolean
            Return True
        End Function

        Friend Shared Function HasNoParallelOverlayServiceRoute() As Boolean
            Return True
        End Function

        Private Shared Function HasRoom(anchorPx As SKRect,
                                        availablePx As SKRect,
                                        sizePx As SKSize,
                                        gapPx As Single,
                                        placement As MASCalloutPlacement) As Boolean
            Select Case placement
                Case MASCalloutPlacement.Top
                    Return anchorPx.Top - gapPx - sizePx.Height >= availablePx.Top
                Case MASCalloutPlacement.Bottom
                    Return anchorPx.Bottom + gapPx + sizePx.Height <= availablePx.Bottom
                Case MASCalloutPlacement.Left
                    Return anchorPx.Left - gapPx - sizePx.Width >= availablePx.Left
                Case MASCalloutPlacement.Right
                    Return anchorPx.Right + gapPx + sizePx.Width <= availablePx.Right
                Case Else
                    Return False
            End Select
        End Function

        Private Shared Function SafeNumber(value As Single,
                                           fallback As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) Then Return fallback
            Return value
        End Function

        Private Shared Function Clamp(value As Single,
                                      minimum As Single,
                                      maximum As Single) As Single
            If maximum < minimum Then Return minimum
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) Then Return minimum
            If value < minimum Then Return minimum
            If value > maximum Then Return maximum
            Return value
        End Function

    End Class

End Namespace
