Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Theming
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Controls

    ''' <summary>
    ''' Internal tooltip layout engine. It keeps tooltip geometry bounded to the active viewport,
    ''' wraps long text deterministically, and limits tooltip copy to a compact product-safe size.
    ''' </summary>
    Friend NotInheritable Class MASTooltipLayoutEngine

        Friend Function GetAnchorPx(ctx As MASThemeContext, s As MASTooltipState) As SKPoint
            If ctx Is Nothing OrElse s Is Nothing Then Return SKPoint.Empty

            Return s.SmoothAnchorClientPx
        End Function

        Friend Function GetArrowSizePx(ctx As MASThemeContext) As Single
            If ctx Is Nothing Then Return 7.0F

            Dim plan As MASTooltipLayoutPlan = MASTooltipLayoutPlan.Create(ctx, SKRect.Empty, MASSize.Default)
            Return plan.ArrowSizePx
        End Function

        Friend Function GetPanelRectPx(ctx As MASThemeContext,
                                       s As MASTooltipState,
                                       ownerPixelBounds As SKRect) As SKRect

            If ctx Is Nothing OrElse s Is Nothing OrElse ctx.Typography Is Nothing Then Return SKRect.Empty

            Dim text As String = s.GetRenderedTooltipText()
            If String.IsNullOrWhiteSpace(text) Then Return SKRect.Empty

            Dim viewportPx As SKRect = ResolveViewportPx(s, ownerPixelBounds)
            Dim plan As MASTooltipLayoutPlan = MASTooltipLayoutPlan.Create(ctx, viewportPx, MASSize.Default)
            Dim dpi As Single = plan.MetricDpi

            If viewportPx.Width <= 1.0F OrElse viewportPx.Height <= 1.0F Then
                Return SKRect.Empty
            End If

            Dim p As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Small, SKColors.White)
            If p Is Nothing Then Return SKRect.Empty

            Dim padX As Single = plan.PaddingXPx
            Dim padY As Single = plan.PaddingYPx
            Dim edgeGap As Single = plan.ScreenGapPx
            Dim minW As Single = plan.MinWidthPx
            Dim maxW As Single = plan.MaxWidthPx
            Dim availableW As Single = Math.Max(minW, viewportPx.Width - (edgeGap * 2.0F))
            Dim maxTextWidthPx As Single = Math.Max(
                plan.MinTextWidthPx,
                Math.Min(maxW - (padX * 2.0F), availableW - (padX * 2.0F)))

            Dim wrapped As List(Of String) = BuildWrappedLines(
                ctx:=ctx,
                dpi:=dpi,
                text:=text,
                maxTextWidthPx:=maxTextWidthPx,
                maxLines:=plan.MaxLines)

            If wrapped.Count = 0 Then Return SKRect.Empty

            s.LastTextMaxWidthPx = maxTextWidthPx

            Dim maxLineWidth As Single = 0.0F

            Dim panelMeasureSession As MASTextWidthMeasureSession = MASTextWidthMeasureSession.ForPaint(p)

            For Each line As String In wrapped
                Dim w As Single = panelMeasureSession.Measure(line)
                If w > maxLineWidth Then maxLineWidth = w
            Next

            Dim lineHeight As Single = ResolveLineHeightPx(p)
            Dim bodyH As Single = lineHeight * wrapped.Count

            Dim widthPx As Single = maxLineWidth + (padX * 2.0F)
            Dim heightPx As Single = bodyH + (padY * 2.0F)

            widthPx = Math.Max(minW, widthPx)
            widthPx = Math.Min(maxW, widthPx)
            widthPx = Math.Min(widthPx, availableW)

            Dim cursorGap As Single = 14.0F * dpi
            Dim anchorPx As SKPoint = GetAnchorPx(ctx, s)

            Dim placement As MASTooltip.TooltipPlacement =
                ResolveStablePlacement(
                    s:=s,
                    viewportPx:=viewportPx,
                    anchorPx:=anchorPx,
                    widthPx:=widthPx,
                    heightPx:=heightPx,
                    gapPx:=cursorGap,
                    edgeGapPx:=edgeGap,
                    dpi:=dpi)

            Dim left As Single
            Dim top As Single

            Select Case placement
                Case MASTooltip.TooltipPlacement.Top
                    left = anchorPx.X - (widthPx * 0.5F)
                    top = anchorPx.Y - cursorGap - heightPx

                Case MASTooltip.TooltipPlacement.Bottom
                    left = anchorPx.X - (widthPx * 0.5F)
                    top = anchorPx.Y + cursorGap

                Case MASTooltip.TooltipPlacement.Right
                    left = anchorPx.X + cursorGap
                    top = anchorPx.Y - (heightPx * 0.5F)

                Case Else
                    left = anchorPx.X - cursorGap - widthPx
                    top = anchorPx.Y - (heightPx * 0.5F)
            End Select

            Dim bounded As SKRect = BoundPanelToViewport(
                left:=left,
                top:=top,
                widthPx:=widthPx,
                heightPx:=heightPx,
                viewportPx:=viewportPx,
                edgeGapPx:=edgeGap)

            s.LastPlacement = placement
            s.LastPanelRectPx = PixelSnap.SnapRectHalf(bounded)

            Return s.LastPanelRectPx
        End Function

        Friend Function GetTextRectPx(ctx As MASThemeContext,
                                      s As MASTooltipState,
                                      ownerPixelBounds As SKRect) As SKRect

            If ctx Is Nothing Then Return SKRect.Empty

            Dim panel As SKRect = GetPanelRectPx(ctx, s, ownerPixelBounds)
            If panel.IsEmpty Then Return SKRect.Empty

            Dim plan As MASTooltipLayoutPlan = MASTooltipLayoutPlan.Create(ctx, ResolveViewportPx(s, ownerPixelBounds), MASSize.Default)

            Return PixelSnap.SnapRectHalf(
                New SKRect(
                    panel.Left + plan.PaddingXPx,
                    panel.Top + plan.PaddingYPx,
                    panel.Right - plan.PaddingXPx,
                    panel.Bottom - plan.PaddingYPx))
        End Function

        Friend Function BuildWrappedLines(ctx As MASThemeContext,
                                          dpi As Single,
                                          text As String,
                                          Optional maxTextWidthPx As Single = 0.0F,
                                          Optional maxLines As Integer = 0) As List(Of String)

            Dim lines As New List(Of String)()

            If ctx Is Nothing OrElse ctx.Typography Is Nothing Then Return lines
            If String.IsNullOrWhiteSpace(text) Then Return lines

            dpi = PixelSnap.SafeDpi(dpi)

            Dim p As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Small, SKColors.White)
            If p Is Nothing Then Return lines

            If maxTextWidthPx <= 1.0F OrElse Single.IsNaN(maxTextWidthPx) OrElse Single.IsInfinity(maxTextWidthPx) Then
                maxTextWidthPx = Math.Max(
                    TooltipTokens.MinTextWidth * dpi,
                    (TooltipTokens.MaxWidth - (TooltipTokens.PaddingX * 2.0F)) * dpi)
            End If

            Dim effectiveMaxLines As Integer = If(maxLines > 0, maxLines, TooltipTokens.MaxLines)
            Dim measureSession As MASTextWidthMeasureSession = MASTextWidthMeasureSession.ForPaint(p)

            Dim paragraphs() As String =
                text.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split({vbLf}, StringSplitOptions.None)

            For Each paraRaw As String In paragraphs
                If lines.Count >= effectiveMaxLines Then Exit For

                Dim para As String = If(paraRaw, String.Empty).Trim()

                If para.Length = 0 Then
                    If lines.Count = 0 OrElse lines(lines.Count - 1) <> String.Empty Then
                        lines.Add(String.Empty)
                    End If
                    Continue For
                End If

                Dim words() As String = para.Split({" "c, ControlChars.Tab}, StringSplitOptions.RemoveEmptyEntries)

                If words.Length = 0 Then
                    lines.Add(para)
                    Continue For
                End If

                Dim current As String = String.Empty

                For Each w As String In words
                    If lines.Count >= effectiveMaxLines Then Exit For

                    Dim candidate As String = If(current.Length = 0, w, current & " " & w)

                    If measureSession.Measure(candidate) <= maxTextWidthPx Then
                        current = candidate
                    Else
                        If current.Length > 0 Then
                            lines.Add(current)
                            current = String.Empty

                            If lines.Count >= effectiveMaxLines Then Exit For
                        End If

                        If measureSession.Measure(w) <= maxTextWidthPx Then
                            current = w
                        Else
                            Dim broken As List(Of String) = BreakLongWord(w, measureSession, maxTextWidthPx)

                            For i As Integer = 0 To broken.Count - 1
                                If lines.Count >= effectiveMaxLines Then Exit For

                                If i < broken.Count - 1 Then
                                    lines.Add(broken(i))
                                Else
                                    current = broken(i)
                                End If
                            Next
                        End If
                    End If
                Next

                If lines.Count < effectiveMaxLines AndAlso current.Length > 0 Then
                    lines.Add(current)
                End If
            Next

            If lines.Count = 0 Then
                lines.Add(text)
            End If

            Dim sourceWasTrimmed As Boolean = HasMoreTextThanLines(text, lines)

            If sourceWasTrimmed AndAlso lines.Count > 0 Then
                lines(lines.Count - 1) = FitEllipsis(lines(lines.Count - 1), measureSession, maxTextWidthPx)
            End If

            Return lines
        End Function

        Friend Function GetLineHeightPx(ctx As MASThemeContext) As Single
            If ctx Is Nothing OrElse ctx.Typography Is Nothing Then Return 14.0F

            Dim p As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Small, SKColors.White)
            If p Is Nothing Then Return 14.0F

            Return ResolveLineHeightPx(p)
        End Function

        Private Shared Function ResolveViewportPx(s As MASTooltipState,
                                                  ownerPixelBounds As SKRect) As SKRect

            If s IsNot Nothing AndAlso
               s.HasViewportBoundsPx AndAlso
               Not s.ViewportBoundsPx.IsEmpty AndAlso
               s.ViewportBoundsPx.Width > 1.0F AndAlso
               s.ViewportBoundsPx.Height > 1.0F Then

                Return s.ViewportBoundsPx
            End If

            Return ownerPixelBounds
        End Function

        Private Shared Function BoundPanelToViewport(left As Single,
                                                     top As Single,
                                                     widthPx As Single,
                                                     heightPx As Single,
                                                     viewportPx As SKRect,
                                                     edgeGapPx As Single) As SKRect

            Dim minLeft As Single = viewportPx.Left + edgeGapPx
            Dim minTop As Single = viewportPx.Top + edgeGapPx
            Dim maxRight As Single = viewportPx.Right - edgeGapPx
            Dim maxBottom As Single = viewportPx.Bottom - edgeGapPx

            If widthPx > maxRight - minLeft Then
                widthPx = Math.Max(1.0F, maxRight - minLeft)
            End If

            If heightPx > maxBottom - minTop Then
                heightPx = Math.Max(1.0F, maxBottom - minTop)
            End If

            If left < minLeft Then left = minLeft
            If top < minTop Then top = minTop

            If left + widthPx > maxRight Then
                left = maxRight - widthPx
            End If

            If top + heightPx > maxBottom Then
                top = maxBottom - heightPx
            End If

            If left < minLeft Then left = minLeft
            If top < minTop Then top = minTop

            Return New SKRect(left, top, left + widthPx, top + heightPx)
        End Function

        Private Shared Function ResolveStablePlacement(s As MASTooltipState,
                                                       viewportPx As SKRect,
                                                       anchorPx As SKPoint,
                                                       widthPx As Single,
                                                       heightPx As Single,
                                                       gapPx As Single,
                                                       edgeGapPx As Single,
                                                       dpi As Single) As MASTooltip.TooltipPlacement

            Dim topSpace As Single = anchorPx.Y - viewportPx.Top
            Dim bottomSpace As Single = viewportPx.Bottom - anchorPx.Y
            Dim rightSpace As Single = viewportPx.Right - anchorPx.X
            Dim leftSpace As Single = anchorPx.X - viewportPx.Left

            Dim keepBiasPx As Single = 18.0F * dpi

            Dim requiredTopBottom As Single = heightPx + gapPx + edgeGapPx
            Dim requiredLeftRight As Single = widthPx + gapPx + edgeGapPx

            Dim canKeep As Boolean = False

            Select Case s.LastPlacement
                Case MASTooltip.TooltipPlacement.Top
                    canKeep = (topSpace + keepBiasPx) >= requiredTopBottom
                Case MASTooltip.TooltipPlacement.Bottom
                    canKeep = (bottomSpace + keepBiasPx) >= requiredTopBottom
                Case MASTooltip.TooltipPlacement.Right
                    canKeep = (rightSpace + keepBiasPx) >= requiredLeftRight
                Case MASTooltip.TooltipPlacement.Left
                    canKeep = (leftSpace + keepBiasPx) >= requiredLeftRight
            End Select

            If s.IsOpen AndAlso canKeep Then
                Return s.LastPlacement
            End If

            Dim fitTop As Boolean = topSpace >= requiredTopBottom
            Dim fitBottom As Boolean = bottomSpace >= requiredTopBottom
            Dim fitRight As Boolean = rightSpace >= requiredLeftRight
            Dim fitLeft As Boolean = leftSpace >= requiredLeftRight

            Dim preferredOrder As MASTooltip.TooltipPlacement() = {
                s.LastPlacement,
                OppositeOf(s.LastPlacement),
                MASTooltip.TooltipPlacement.Top,
                MASTooltip.TooltipPlacement.Bottom,
                MASTooltip.TooltipPlacement.Right,
                MASTooltip.TooltipPlacement.Left
            }

            For Each p As MASTooltip.TooltipPlacement In preferredOrder
                Select Case p
                    Case MASTooltip.TooltipPlacement.Top
                        If fitTop Then Return MASTooltip.TooltipPlacement.Top
                    Case MASTooltip.TooltipPlacement.Bottom
                        If fitBottom Then Return MASTooltip.TooltipPlacement.Bottom
                    Case MASTooltip.TooltipPlacement.Right
                        If fitRight Then Return MASTooltip.TooltipPlacement.Right
                    Case MASTooltip.TooltipPlacement.Left
                        If fitLeft Then Return MASTooltip.TooltipPlacement.Left
                End Select
            Next

            Dim bestPlacement As MASTooltip.TooltipPlacement = MASTooltip.TooltipPlacement.Bottom
            Dim bestScore As Single = Single.MinValue

            Dim scoreTop As Single = topSpace - requiredTopBottom
            If scoreTop > bestScore Then
                bestScore = scoreTop
                bestPlacement = MASTooltip.TooltipPlacement.Top
            End If

            Dim scoreBottom As Single = bottomSpace - requiredTopBottom
            If scoreBottom > bestScore Then
                bestScore = scoreBottom
                bestPlacement = MASTooltip.TooltipPlacement.Bottom
            End If

            Dim scoreRight As Single = rightSpace - requiredLeftRight
            If scoreRight > bestScore Then
                bestScore = scoreRight
                bestPlacement = MASTooltip.TooltipPlacement.Right
            End If

            Dim scoreLeft As Single = leftSpace - requiredLeftRight
            If scoreLeft > bestScore Then
                bestScore = scoreLeft
                bestPlacement = MASTooltip.TooltipPlacement.Left
            End If

            Return bestPlacement
        End Function

        Private Shared Function OppositeOf(p As MASTooltip.TooltipPlacement) As MASTooltip.TooltipPlacement
            Select Case p
                Case MASTooltip.TooltipPlacement.Top
                    Return MASTooltip.TooltipPlacement.Bottom
                Case MASTooltip.TooltipPlacement.Bottom
                    Return MASTooltip.TooltipPlacement.Top
                Case MASTooltip.TooltipPlacement.Right
                    Return MASTooltip.TooltipPlacement.Left
                Case Else
                    Return MASTooltip.TooltipPlacement.Right
            End Select
        End Function

        Private Shared Function BreakLongWord(word As String,
                                              measureSession As MASTextWidthMeasureSession,
                                              maxWidth As Single) As List(Of String)

            Dim result As New List(Of String)()
            If String.IsNullOrEmpty(word) Then Return result

            Dim current As String = String.Empty

            For i As Integer = 0 To word.Length - 1
                Dim ch As String = word(i).ToString()
                Dim candidate As String = current & ch

                If measureSession.Measure(candidate) <= maxWidth OrElse current.Length = 0 Then
                    current = candidate
                Else
                    result.Add(current)
                    current = ch
                End If
            Next

            If current.Length > 0 Then
                result.Add(current)
            End If

            Return result
        End Function

        Private Shared Function FitEllipsis(value As String,
                                            measureSession As MASTextWidthMeasureSession,
                                            maxWidth As Single) As String

            Const ellipsis As String = "…"

            Dim text As String = If(value, String.Empty).TrimEnd()
            If text.Length = 0 Then Return ellipsis

            If measureSession.Measure(text & ellipsis) <= maxWidth Then
                Return text & ellipsis
            End If

            Do While text.Length > 0
                text = text.Substring(0, text.Length - 1).TrimEnd()

                If measureSession.Measure(text & ellipsis) <= maxWidth Then
                    Return text & ellipsis
                End If
            Loop

            Return ellipsis
        End Function

        Private Shared Function HasMoreTextThanLines(sourceText As String,
                                                     lines As List(Of String)) As Boolean

            If String.IsNullOrWhiteSpace(sourceText) Then Return False
            If lines Is Nothing OrElse lines.Count = 0 Then Return True

            Dim visible As String = String.Join(" ", lines).Trim()
            Dim normalizedSource As String = sourceText.Replace(vbCrLf, " ").Replace(vbCr, " ").Replace(vbLf, " ").Trim()

            Return normalizedSource.Length > visible.Length
        End Function

        Private Shared Function ResolveLineHeightPx(p As SKPaint) As Single
            Dim fm As SKFontMetrics = p.FontMetrics
            Dim raw As Single = Math.Abs(fm.Ascent) + Math.Abs(fm.Descent) + Math.Abs(fm.Leading)

            If raw <= 0.0F Then
                raw = p.TextSize * 1.2F
            End If

            Return raw * MASTypography.WrapLineHeightMul
        End Function

    End Class

End Namespace
