Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.Components.DropDownMenu

    ''' <summary>
    ''' Internal layout and hit-testing engine for dropdown/context menus.
    ''' It owns pixel-space panel placement, submenu bridge geometry, and the
    ''' bounded scrolling contract used when a menu contains more rows than the viewport can safely show.
    ''' </summary>
    Friend NotInheritable Class DropDownMenuController

        Friend Sub UpdateLayout(m As DropDownMenuState, viewportBoundsPx As SKRect, dpi As Single)
            If m Is Nothing Then Return

            dpi = PixelSnap.SafeDpi(dpi)

            m.PanelRects.Clear()
            m.LevelRows.Clear()
            m.LevelContentHeightsPx.Clear()

            If Not m.IsOpen Then
                m.TrimLayoutLevels(0)
                Return
            End If

            If m.Model Is Nothing OrElse m.Model.Root Is Nothing Then
                m.TrimLayoutLevels(0)
                Return
            End If

            Dim anchor As SKRect = m.AnchorRectPx
            If anchor.IsEmpty OrElse anchor.Width <= 1.0F OrElse anchor.Height <= 1.0F Then
                m.TrimLayoutLevels(0)
                Return
            End If

            Dim padY As Single = MenuTokens.DropDownPanelPadY * dpi
            Dim padX As Single = MenuTokens.DropDownPanelPadX * dpi
            Dim gapY As Single = MenuTokens.DropDownPanelGapY * dpi
            Dim gapX As Single = Math.Max(1.0F * dpi, MenuTokens.DropDownPanelGapX * dpi)
            Dim overlap As Single = Math.Max(1.0F * dpi, MenuTokens.DropDownSubMenuOverlap * dpi)

            Dim root As MASDropDownMenuNode = m.Model.Root
            Dim levelNode As MASDropDownMenuNode = root

            Dim prevPanel As SKRect = SKRect.Empty
            Dim prevRows As List(Of MASDropDownMenuNode) = Nothing

            Dim level As Integer = 0

            Do
                Dim rows As List(Of MASDropDownMenuNode) = m.GetRowsForNode(levelNode)
                m.LevelRows.Add(rows)
                m.EnsureScrollLevel(level)

                Dim contentH As Single = MeasurePanelContentHeightPx(rows, dpi)
                Dim visibleContentH As Single = ResolveVisibleContentHeightPx(viewportBoundsPx, contentH, dpi)

                m.LevelContentHeightsPx.Add(contentH)

                Dim panelH As Single = (padY * 2.0F) + visibleContentH
                Dim panelW As Single = MeasurePanelWidthPx(rows, dpi, padX)

                Dim left As Single
                Dim top As Single

                If level = 0 Then
                    left = anchor.Left
                    top = anchor.Bottom + gapY
                Else
                    left = prevPanel.Right - overlap + gapX
                    top = prevPanel.Top

                    Dim parentOpenIdx As Integer = GetOpenIndex(m, level - 1)
                    If prevRows IsNot Nothing AndAlso parentOpenIdx >= 0 AndAlso parentOpenIdx < prevRows.Count Then
                        Dim parentRowTop As Single =
                            prevPanel.Top +
                            padY +
                            GetRowTopOffsetPx(prevRows, parentOpenIdx, dpi) -
                            m.GetScrollOffsetPx(level - 1)

                        top = parentRowTop - padY
                    End If
                End If

                Dim panel As New SKRect(
                    left,
                    top,
                    left + panelW,
                    top + panelH
                )

                panel = ClampPanelToViewport(panel, viewportBoundsPx, anchor, level, prevPanel, panelW, panelH, dpi)
                panel = PixelSnap.SnapRectHalf(panel)

                m.PanelRects.Add(panel)
                ClampScrollForLevel(m, level, panel, dpi)

                Dim openIdx As Integer = GetOpenIndex(m, level)
                If rows Is Nothing OrElse openIdx < 0 OrElse openIdx >= rows.Count Then Exit Do

                Dim openNode As MASDropDownMenuNode = rows(openIdx)
                If openNode Is Nothing OrElse openNode.Kind <> MASDropDownMenuNodeKind.SubMenu Then Exit Do

                prevPanel = panel
                prevRows = rows
                levelNode = openNode
                level += 1

                If level > 12 Then Exit Do
            Loop

            m.TrimLayoutLevels(m.PanelRects.Count)
        End Sub

        Private Shared Function ClampPanelToViewport(panel As SKRect,
                                                     viewport As SKRect,
                                                     anchor As SKRect,
                                                     level As Integer,
                                                     prevPanel As SKRect,
                                                     panelW As Single,
                                                     panelH As Single,
                                                     dpi As Single) As SKRect

            If viewport.IsEmpty OrElse viewport.Width <= 1.0F OrElse viewport.Height <= 1.0F Then
                Return panel
            End If

            Dim margin As Single = ResolveViewportMarginPx(dpi)

            Dim leftMin As Single = viewport.Left + margin
            Dim rightMax As Single = viewport.Right - margin
            Dim topMin As Single = viewport.Top + margin
            Dim bottomMax As Single = viewport.Bottom - margin

            If rightMax <= leftMin Then rightMax = leftMin + Math.Max(1.0F, panelW)
            If bottomMax <= topMin Then bottomMax = topMin + Math.Max(1.0F, panelH)

            Dim left As Single = panel.Left
            Dim top As Single = panel.Top

            If level = 0 Then
                If left + panelW > rightMax Then
                    left = Math.Max(leftMin, anchor.Right - panelW)
                End If

                If top + panelH > bottomMax Then
                    top = Math.Max(topMin, anchor.Top - panelH - (MenuTokens.DropDownPanelGapY * dpi))
                End If
            Else
                If left + panelW > rightMax AndAlso Not prevPanel.IsEmpty Then
                    left = prevPanel.Left - panelW - Math.Max(1.0F, MenuTokens.DropDownPanelGapX * dpi)
                End If
            End If

            If left < leftMin Then left = leftMin
            If left + panelW > rightMax Then left = Math.Max(leftMin, rightMax - panelW)

            If top < topMin Then top = topMin
            If top + panelH > bottomMax Then top = Math.Max(topMin, bottomMax - panelH)

            Return New SKRect(left, top, left + panelW, top + panelH)
        End Function

        Private Shared Function ResolveVisibleContentHeightPx(viewport As SKRect,
                                                              contentHeightPx As Single,
                                                              dpi As Single) As Single

            dpi = PixelSnap.SafeDpi(dpi)

            Dim rowH As Single = MenuTokens.DropDownRowHeight * dpi
            Dim padY As Single = MenuTokens.DropDownPanelPadY * dpi
            Dim margin As Single = ResolveViewportMarginPx(dpi)

            Dim minimumVisible As Single = Math.Max(rowH, rowH * 2.0F)
            Dim maximumVisible As Single = contentHeightPx

            If Not viewport.IsEmpty AndAlso viewport.Height > 1.0F Then
                maximumVisible = Math.Max(minimumVisible, viewport.Height - (margin * 2.0F) - (padY * 2.0F))
            End If

            Return PixelSnap.SnapHalf(Math.Max(rowH, Math.Min(contentHeightPx, maximumVisible)))
        End Function

        Private Shared Function ResolveViewportMarginPx(dpi As Single) As Single
            dpi = PixelSnap.SafeDpi(dpi)
            Return Math.Max(4.0F, 6.0F * dpi)
        End Function

        Private Shared Function MeasurePanelContentHeightPx(rows As List(Of MASDropDownMenuNode), dpi As Single) As Single
            Dim rowH As Single = MenuTokens.DropDownRowHeight * dpi
            Dim sepH As Single = MenuTokens.DropDownSeparatorHeight * dpi

            If rows Is Nothing OrElse rows.Count = 0 Then Return rowH

            Dim h As Single = 0.0F

            For Each n As MASDropDownMenuNode In rows
                If n Is Nothing Then Continue For
                h += If(n.Kind = MASDropDownMenuNodeKind.Separator, sepH, rowH)
            Next

            Return PixelSnap.SnapHalf(Math.Max(rowH, h))
        End Function

        Private Shared Function MeasurePanelWidthPx(rows As List(Of MASDropDownMenuNode),
                                                    dpi As Single,
                                                    padX As Single) As Single

            Dim minW As Single = MenuTokens.DropDownPanelMinWidth * dpi
            Dim maxW As Single = MenuTokens.DropDownPanelMaxWidth * dpi

            Dim textSize As Single = Math.Max(10.5F, MenuTokens.DropDownTextSize * dpi)
            Dim reserve As Single = MenuTokens.DropDownRightGlyphReserve * dpi
            Dim leftSlack As Single = Math.Max(0.0F, 8.0F * dpi)
            Dim textSlack As Single = Math.Max(10.0F, 12.0F * dpi)
            Dim shortcutGap As Single = Math.Max(18.0F, 22.0F * dpi)

            Dim maxText As Single = 0.0F
            Dim maxShortcut As Single = 0.0F

            Using p As New SKPaint With {
                .IsAntialias = True,
                .IsDither = True,
                .Style = SKPaintStyle.Fill,
                .TextEncoding = SKTextEncoding.Utf16,
                .TextSize = textSize
            }
                If rows IsNot Nothing Then
                    For Each n As MASDropDownMenuNode In rows
                        If n Is Nothing Then Continue For
                        If n.Kind = MASDropDownMenuNodeKind.Separator Then Continue For

                        Dim t As String = If(n.Text, String.Empty)
                        If t.Length > 0 Then
                            Dim w As Single = MASShapedText.MeasureWidth(t, p)
                            If w > maxText Then maxText = w
                        End If

                        Dim s As String = If(n.ShortcutText, String.Empty)
                        If s.Length > 0 Then
                            Dim sw As Single = MASShapedText.MeasureWidth(s, p)
                            If sw > maxShortcut Then maxShortcut = sw
                        End If
                    Next
                End If
            End Using

            Dim shortcutPart As Single = If(maxShortcut > 0.0F, shortcutGap + maxShortcut, 0.0F)

            Dim wPx As Single =
                (padX * 2.0F) +
                leftSlack +
                maxText +
                shortcutPart +
                reserve +
                textSlack

            If wPx < minW Then wPx = minW
            If wPx > maxW Then wPx = maxW

            Return PixelSnap.SnapHalf(wPx)
        End Function

        Friend Function HitTestLevel(m As DropDownMenuState, ptPx As SKPoint) As Integer
            If m Is Nothing Then Return -1
            If Not m.IsOpen Then Return -1
            If m.PanelRects Is Nothing OrElse m.PanelRects.Count = 0 Then Return -1

            For i As Integer = m.PanelRects.Count - 1 To 0 Step -1
                Dim r As SKRect = m.PanelRects(i)

                If Not r.IsEmpty AndAlso r.Contains(ptPx.X, ptPx.Y) Then
                    Return i
                End If
            Next

            Return -1
        End Function

        Friend Function HitTestLevelOrBridge(m As DropDownMenuState,
                                             ptPx As SKPoint,
                                             dpi As Single) As Integer

            Dim directLevel As Integer = HitTestLevel(m, ptPx)
            If directLevel >= 0 Then Return directLevel

            If IsPointInsideAnySubMenuBridge(m, ptPx, dpi) Then
                Return -2
            End If

            Return -1
        End Function

        Friend Function HitTestRowAtLevel(m As DropDownMenuState,
                                          level As Integer,
                                          ptPx As SKPoint,
                                          dpi As Single) As Integer

            If m Is Nothing Then Return -1
            If Not m.IsOpen Then Return -1

            dpi = PixelSnap.SafeDpi(dpi)

            If m.PanelRects Is Nothing OrElse level < 0 OrElse level >= m.PanelRects.Count Then Return -1

            Dim pr As SKRect = m.PanelRects(level)
            If pr.IsEmpty OrElse Not pr.Contains(ptPx.X, ptPx.Y) Then Return -1

            Dim rows As List(Of MASDropDownMenuNode) = Nothing
            If m.LevelRows IsNot Nothing AndAlso level < m.LevelRows.Count Then
                rows = m.LevelRows(level)
            End If

            If rows Is Nothing OrElse rows.Count = 0 Then Return -1

            Dim padY As Single = MenuTokens.DropDownPanelPadY * dpi
            Dim y0 As Single = pr.Top + padY
            Dim y1 As Single = pr.Bottom - padY

            If ptPx.Y < y0 OrElse ptPx.Y > y1 Then Return -1

            Dim relY As Single = ptPx.Y - y0 + m.GetScrollOffsetPx(level)

            If relY < 0.0F Then Return -1

            Dim acc As Single = 0.0F

            For i As Integer = 0 To rows.Count - 1
                Dim n As MASDropDownMenuNode = rows(i)
                Dim h As Single = GetRowHeightPx(n, dpi)

                If relY >= acc AndAlso relY < acc + h Then
                    Return i
                End If

                acc += h
            Next

            Return -1
        End Function

        Friend Function ScrollLevel(m As DropDownMenuState,
                                    level As Integer,
                                    deltaPx As Single,
                                    dpi As Single) As Boolean

            If m Is Nothing Then Return False
            If level < 0 OrElse level >= m.PanelRects.Count Then Return False

            Dim oldOffset As Single = m.GetScrollOffsetPx(level)
            Dim target As Single = oldOffset + deltaPx

            m.SetScrollOffsetPx(level, target)
            ClampScrollForLevel(m, level, m.PanelRects(level), dpi)

            Return Math.Abs(m.GetScrollOffsetPx(level) - oldOffset) > 0.01F
        End Function

        Friend Function EnsureRowVisible(m As DropDownMenuState,
                                         level As Integer,
                                         rowIndex As Integer,
                                         dpi As Single) As Boolean

            If m Is Nothing Then Return False
            If level < 0 OrElse level >= m.PanelRects.Count Then Return False
            If level >= m.LevelRows.Count Then Return False

            Dim rows As List(Of MASDropDownMenuNode) = m.LevelRows(level)
            If rows Is Nothing OrElse rowIndex < 0 OrElse rowIndex >= rows.Count Then Return False

            dpi = PixelSnap.SafeDpi(dpi)

            Dim panel As SKRect = m.PanelRects(level)
            If panel.IsEmpty Then Return False

            Dim padY As Single = MenuTokens.DropDownPanelPadY * dpi
            Dim visibleH As Single = Math.Max(0.0F, panel.Height - (padY * 2.0F))
            If visibleH <= 1.0F Then Return False

            Dim rowTop As Single = GetRowTopOffsetPx(rows, rowIndex, dpi)
            Dim rowBottom As Single = rowTop + GetRowHeightPx(rows(rowIndex), dpi)
            Dim oldOffset As Single = m.GetScrollOffsetPx(level)
            Dim newOffset As Single = oldOffset

            If rowTop < newOffset Then
                newOffset = rowTop
            ElseIf rowBottom > newOffset + visibleH Then
                newOffset = rowBottom - visibleH
            End If

            m.SetScrollOffsetPx(level, newOffset)
            ClampScrollForLevel(m, level, panel, dpi)

            Return Math.Abs(m.GetScrollOffsetPx(level) - oldOffset) > 0.01F
        End Function

        Friend Function IsPointInsideAnySubMenuBridge(m As DropDownMenuState,
                                                      ptPx As SKPoint,
                                                      dpi As Single) As Boolean

            If m Is Nothing Then Return False
            If Not m.IsOpen Then Return False
            If m.PanelRects Is Nothing OrElse m.LevelRows Is Nothing Then Return False
            If m.PanelRects.Count <= 1 Then Return False

            dpi = PixelSnap.SafeDpi(dpi)

            For level As Integer = 1 To m.PanelRects.Count - 1
                Dim bridge As SKRect = BuildSubMenuBridgeRect(m, level, dpi)

                If Not bridge.IsEmpty AndAlso bridge.Contains(ptPx.X, ptPx.Y) Then
                    Return True
                End If
            Next

            Return False
        End Function

        Friend Function BuildSubMenuBridgeRect(m As DropDownMenuState,
                                               level As Integer,
                                               dpi As Single) As SKRect

            If m Is Nothing Then Return SKRect.Empty
            If level <= 0 Then Return SKRect.Empty
            If m.PanelRects Is Nothing OrElse m.LevelRows Is Nothing Then Return SKRect.Empty
            If level >= m.PanelRects.Count Then Return SKRect.Empty
            If (level - 1) >= m.LevelRows.Count Then Return SKRect.Empty

            dpi = PixelSnap.SafeDpi(dpi)

            Dim parentPanel As SKRect = m.PanelRects(level - 1)
            Dim childPanel As SKRect = m.PanelRects(level)

            If parentPanel.IsEmpty OrElse childPanel.IsEmpty Then Return SKRect.Empty

            Dim parentRows As List(Of MASDropDownMenuNode) = m.LevelRows(level - 1)
            If parentRows Is Nothing OrElse parentRows.Count = 0 Then Return SKRect.Empty

            Dim openIdx As Integer = GetOpenIndex(m, level - 1)
            If openIdx < 0 OrElse openIdx >= parentRows.Count Then Return SKRect.Empty

            Dim openNode As MASDropDownMenuNode = parentRows(openIdx)
            If openNode Is Nothing OrElse openNode.Kind <> MASDropDownMenuNodeKind.SubMenu Then Return SKRect.Empty

            Dim padY As Single = MenuTokens.DropDownPanelPadY * dpi
            Dim slopX As Single = Math.Max(6.0F, 8.0F * dpi)
            Dim slopY As Single = Math.Max(8.0F, 10.0F * dpi)

            Dim rowTop As Single =
                parentPanel.Top +
                padY +
                GetRowTopOffsetPx(parentRows, openIdx, dpi) -
                m.GetScrollOffsetPx(level - 1)

            Dim rowBottom As Single = rowTop + GetRowHeightPx(openNode, dpi)

            Dim left As Single = Math.Min(parentPanel.Right - slopX, childPanel.Left - slopX)
            Dim right As Single = Math.Max(parentPanel.Right + slopX, childPanel.Left + slopX)

            Dim top As Single = Math.Min(rowTop, childPanel.Top) - slopY
            Dim bottom As Single = Math.Max(rowBottom, childPanel.Bottom) + slopY

            If right <= left OrElse bottom <= top Then Return SKRect.Empty

            Return PixelSnap.SnapRectHalf(New SKRect(left, top, right, bottom))
        End Function

        Private Shared Sub ClampScrollForLevel(m As DropDownMenuState,
                                               level As Integer,
                                               panel As SKRect,
                                               dpi As Single)

            If m Is Nothing Then Return
            If level < 0 Then Return

            m.EnsureScrollLevel(level)

            Dim contentH As Single = 0.0F
            If level < m.LevelContentHeightsPx.Count Then
                contentH = m.LevelContentHeightsPx(level)
            End If

            Dim padY As Single = MenuTokens.DropDownPanelPadY * PixelSnap.SafeDpi(dpi)
            Dim visibleH As Single = Math.Max(0.0F, panel.Height - (padY * 2.0F))
            Dim maxScroll As Single = Math.Max(0.0F, contentH - visibleH)
            Dim current As Single = m.GetScrollOffsetPx(level)

            If current > maxScroll Then current = maxScroll
            If current < 0.0F Then current = 0.0F

            m.SetScrollOffsetPx(level, current)
        End Sub

        Friend Shared Function GetRowTopOffsetPx(rows As List(Of MASDropDownMenuNode),
                                                  idx As Integer,
                                                  dpi As Single) As Single

            If rows Is Nothing OrElse rows.Count = 0 OrElse idx <= 0 Then Return 0.0F

            Dim acc As Single = 0.0F
            Dim maxI As Integer = Math.Min(idx, rows.Count)

            For i As Integer = 0 To maxI - 1
                acc += GetRowHeightPx(rows(i), dpi)
            Next

            Return acc
        End Function

        Friend Shared Function GetRowHeightPx(node As MASDropDownMenuNode, dpi As Single) As Single
            dpi = PixelSnap.SafeDpi(dpi)

            If node IsNot Nothing AndAlso node.Kind = MASDropDownMenuNodeKind.Separator Then
                Return MenuTokens.DropDownSeparatorHeight * dpi
            End If

            Return MenuTokens.DropDownRowHeight * dpi
        End Function

        Friend Function GetOpenIndex(m As DropDownMenuState, level As Integer) As Integer
            If m Is Nothing OrElse m.OpenPath Is Nothing Then Return -1
            If level < 0 OrElse level >= m.OpenPath.Count Then Return -1
            Return m.OpenPath(level)
        End Function

    End Class

End Namespace
