Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
Imports Nexamas.UI.Controls
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    ''' <summary>
    ''' Official Nexamas UI premium same-surface navigation rail. It owns visual rail
    ''' item presentation, selection, keyboard navigation, RTL-aware layout, and MAS
    ''' surface/typography routing. Application routing, PageHost state, drawer/shell
    ''' replacement, content hosting, native menus, wrappers, adapters, and pointer-click
    ''' outer focus rings remain outside this control.
    ''' </summary>
    Public NotInheritable Class MASNavigationRail
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

#Region "Nested state"

        Private NotInheritable Class NavigationRailItemState
            Friend Sub New(key As String,
                           label As String,
                           glyph As String,
                           badge As String)
                Me.Key = key
                Me.Label = label
                Me.Glyph = glyph
                Me.Badge = badge
            End Sub

            Friend ReadOnly Property Key As String
            Friend Property Label As String
            Friend Property Glyph As String
            Friend Property Badge As String
        End Class

        Private NotInheritable Class NavigationRailHit
            Friend Sub New(index As Integer,
                           rect As SKRect)
                Me.Index = index
                Me.Rect = rect
            End Sub

            Friend ReadOnly Property Index As Integer
            Friend ReadOnly Property Rect As SKRect
        End Class

#End Region

#Region "Fields"

        Private Shared ReadOnly _contract As MASResolvedComponentContract =
            MASArchitectureRuntime.ResolveContractOrThrow(GetType(MASNavigationRail))

        Private ReadOnly _primitives As New MASVisualPrimitivesPainter()
        Private ReadOnly _items As New List(Of NavigationRailItemState)()
        Private ReadOnly _hits As New List(Of NavigationRailHit)()
        Private _title As String = NavigationRailTokens.DefaultTitle
        Private _selectedIndex As Integer = -1
        Private _hoverIndex As Integer = -1
        Private _pressedIndex As Integer = -1
        Private _topIndex As Integer
        Private _isCompact As Boolean
        Private _contentRect As SKRect = SKRect.Empty
        Private _itemsRect As SKRect = SKRect.Empty
        Private _lastVisibleCount As Integer = NavigationRailTokens.MaxVisibleItems
        Private _lastDpi As Single = 1.0F
        Private _selectionMotionRunner As MASMotionTimelineRunner
        Private _selectionMotionFromIndex As Integer = -1
        Private _selectionMotionToIndex As Integer = -1
        Private _selectionMotionProgress As Single = 1.0F
        Private _hasSelectionMotion As Boolean
        Private _disposedLocal As Boolean

#End Region

#Region "Constructor / Factory"

        Public Sub New()
            MyBase.New()
        End Sub

        Public Shared Function Create(Optional title As String = Nothing) As MASNavigationRail
            Dim control As New MASNavigationRail()
            control._title = MASNavigationRailPolicy.NormalizeTitle(title)
            Return control
        End Function

#End Region

#Region "Public API"

        Public Event NavigationRailChanged As EventHandler
        Public Event SelectedItemChanged As EventHandler

        Public Property Title As String
            Get
                Return _title
            End Get
            Set(value As String)
                Dim normalized As String = MASNavigationRailPolicy.NormalizeTitle(value)
                If String.Equals(_title, normalized, StringComparison.Ordinal) Then Return
                _title = normalized
                RequestSizeLayoutRefreshForSizeAffectingChange("MASNavigationRail.Title")
                InvalidateVisual()
            End Set
        End Property

        Public Property IsCompact As Boolean
            Get
                Return _isCompact
            End Get
            Set(value As Boolean)
                If _isCompact = value Then Return
                _isCompact = value
                RequestSizeLayoutRefreshForSizeAffectingChange("MASNavigationRail.IsCompact")
                InvalidateVisual()
            End Set
        End Property

        Public ReadOnly Property ItemCount As Integer
            Get
                Return _items.Count
            End Get
        End Property

        Public ReadOnly Property SelectedIndex As Integer
            Get
                Return _selectedIndex
            End Get
        End Property

        Public ReadOnly Property SelectedItemKey As String
            Get
                If _selectedIndex < 0 OrElse _selectedIndex >= _items.Count Then Return String.Empty
                Return _items(_selectedIndex).Key
            End Get
        End Property

        Public ReadOnly Property SummaryText As String
            Get
                Return MASNavigationRailPolicy.BuildSummary(ItemCount, _selectedIndex)
            End Get
        End Property

        Public Function GetItemKeys() As IReadOnlyList(Of String)
            Dim keys As New List(Of String)()
            For Each item As NavigationRailItemState In _items
                keys.Add(item.Key)
            Next
            Return New ReadOnlyCollection(Of String)(keys)
        End Function

        Public Function AddItem(itemKey As String,
                                label As String,
                                Optional badgeText As String = Nothing,
                                Optional glyphText As String = Nothing) As MASNavigationRail
            If _items.Count >= NavigationRailTokens.MaxItems Then Return Me
            Dim normalizedKey As String = MASNavigationRailPolicy.NormalizeKey(itemKey, "nav", _items.Count + 1)
            If FindItemIndex(normalizedKey) >= 0 Then Return Me
            Dim normalizedLabel As String = MASNavigationRailPolicy.NormalizeLabel(label)
            _items.Add(New NavigationRailItemState(normalizedKey,
                                                   normalizedLabel,
                                                   MASNavigationRailPolicy.NormalizeGlyph(glyphText, normalizedLabel),
                                                   MASNavigationRailPolicy.NormalizeBadge(badgeText)))
            If _selectedIndex < 0 Then _selectedIndex = 0
            NormalizeSelection()
            RequestSizeLayoutRefreshForSizeAffectingChange("MASNavigationRail.AddItem")
            InvalidateVisual()
            RaiseNavigationRailChangedSafe()
            Return Me
        End Function

        Public Function ClearItems() As MASNavigationRail
            If _items.Count = 0 Then Return Me
            _items.Clear()
            _selectedIndex = -1
            _hoverIndex = -1
            _pressedIndex = -1
            _topIndex = 0
            StopSelectionMotion()
            RequestSizeLayoutRefreshForSizeAffectingChange("MASNavigationRail.ClearItems")
            InvalidateVisual()
            RaiseNavigationRailChangedSafe()
            RaiseSelectedItemChangedSafe()
            Return Me
        End Function

        Public Function SelectItem(itemKey As String) As Boolean
            Dim index As Integer = FindItemIndex(itemKey)
            If index < 0 Then Return False
            SetSelectedIndex(index, True)
            Return True
        End Function

        Public Function MovePrevious() As MASNavigationRail
            MoveBy(-1)
            Return Me
        End Function

        Public Function MoveNext() As MASNavigationRail
            MoveBy(1)
            Return Me
        End Function

        Public Function WithTitle(value As String) As MASNavigationRail
            Title = value
            Return Me
        End Function

        Public Function WithCompact(value As Boolean) As MASNavigationRail
            IsCompact = value
            Return Me
        End Function

        Public Shadows Function WithSize(sizeIntent As MASSize) As MASNavigationRail
            MyBase.SetSize(sizeIntent)
            Return Me
        End Function

#End Region

#Region "Layout"

        Friend Function GetPreferredLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetPreferredLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).DesiredSize
        End Function

        Friend Function GetMinLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetMinLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).MinSize
        End Function

        Private Shared Function CreateSizeContext(context As MASLayoutMeasureContext) As MASSizeContext
            If context Is Nothing Then Return MASIntrinsicControlSizeMetrics.EnsureContext(Nothing)
            Return context.ToSizeContext()
        End Function

        Friend Function MeasureIntrinsicSize(context As MASSizeContext,
                                             intent As MASSize) As MASSizeResult Implements IMASIntrinsicSizeContract.MeasureIntrinsicSize
            Dim safeContext As MASSizeContext = MASIntrinsicControlSizeMetrics.EnsureContext(context)
            Dim preferredWidth As Single = If(_isCompact, NavigationRailTokens.CompactWidthDip, NavigationRailTokens.PreferredWidthDip)
            Dim minimumWidth As Single = If(_isCompact, NavigationRailTokens.ContractCompactMinWidthDip, NavigationRailTokens.ContractExpandedMinWidthDip)
            Return MASSizeResolver.FromMeasured(
                desiredSize:=New SKSize(preferredWidth, NavigationRailTokens.PreferredHeightDip),
                minSize:=New SKSize(minimumWidth, NavigationRailTokens.ContractMinHeightDip),
                maxSize:=New SKSize(NavigationRailTokens.MaxWidthDip, Single.PositiveInfinity),
                intent:=intent,
                context:=safeContext,
                canGrowWidth:=Not _isCompact,
                canGrowHeight:=True,
                contentInset:=New MASLayoutInset(NavigationRailTokens.PaddingDip,
                                                 NavigationRailTokens.PaddingDip,
                                                 NavigationRailTokens.PaddingDip,
                                                 NavigationRailTokens.PaddingDip),
                visualOverflowInset:=MASLayoutInset.Empty,
                hitOverflowInset:=MASLayoutInset.Empty,
                isFallback:=False)
        End Function

#End Region

#Region "Rendering"

        Protected Overrides Sub Render(canvas As SKCanvas,
                                       ctx As MASThemeContext,
                                       pixelBounds As SKRect)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If pixelBounds.Width <= 1.0F OrElse pixelBounds.Height <= 1.0F Then Return

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            _lastDpi = dpi
            Dim bounds As SKRect = PixelSnap.SnapRect(pixelBounds, dpi)
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return

            Dim responsive As MASResponsiveLayoutProfile = MASResponsiveLayoutResolver.FromTheme(ctx, bounds, SizeIntent, MASNavigationRailLayoutPlan.Thresholds)
            Dim plan As MASNavigationRailLayoutPlan = MASNavigationRailLayoutPlan.Create(responsive, _isCompact)

            _primitives.DrawCardSurface(canvas, ctx, bounds, dpi, _contract, MASCardVisualStyle.Secondary)
            _contentRect = PixelSnap.SnapRect(Inset(bounds, plan.PaddingPx), dpi)
            If _contentRect.Width <= 1.0F OrElse _contentRect.Height <= 1.0F Then Return

            _hits.Clear()
            Dim isRtl As Boolean = MASNavigationRailPolicy.ResolveRightToLeft()
            Dim accent As SKColor = MASNavigationRailPolicy.ResolveAccentColor(ctx)
            DrawHeader(canvas, ctx, _contentRect, plan, isRtl)
            DrawItems(canvas, ctx, _contentRect, plan, accent, isRtl)
            DrawFooter(canvas, ctx, _contentRect, plan, isRtl)
        End Sub

        Private Sub DrawHeader(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               content As SKRect,
                               plan As MASNavigationRailLayoutPlan,
                               isRtl As Boolean)
            If Not plan.ShowHeader OrElse plan.HeaderHeightPx <= 0.0F Then Return
            Dim header As SKRect = PixelSnap.SnapRect(New SKRect(content.Left, content.Top, content.Right, content.Top + plan.HeaderHeightPx), plan.Dpi)
            Dim accent As SKColor = MASNavigationRailPolicy.ResolveAccentColor(ctx)
            Dim chipWidth As Single = Math.Min(74.0F * plan.Dpi * plan.ResponsiveProfile.ChromeScale, header.Width * 0.34F)
            Dim chipHeight As Single = 22.0F * plan.Dpi * plan.ResponsiveProfile.ChromeScale
            Dim chip As SKRect = If(isRtl,
                                    New SKRect(header.Left, header.Top + 4.0F * plan.Dpi, header.Left + chipWidth, header.Top + 4.0F * plan.Dpi + chipHeight),
                                    New SKRect(header.Right - chipWidth, header.Top + 4.0F * plan.Dpi, header.Right, header.Top + 4.0F * plan.Dpi + chipHeight))
            Using chipFill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(NavigationRailTokens.HeaderChipFillAlpha)}
                canvas.DrawRoundRect(chip, 999.0F, 999.0F, chipFill)
            End Using
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, SummaryText, chip, plan.Dpi, MASTypography.MASTextStyle.Micro, MASNavigationRailPolicy.ResolveMutedTextColor(ctx), TypographyTextPrimitives.TextAlign.Center, False)
            Dim titleRect As SKRect = If(isRtl,
                                         New SKRect(chip.Right + 10.0F * plan.Dpi, header.Top, header.Right, header.Top + 32.0F * plan.Dpi * plan.ResponsiveProfile.ContentScale),
                                         New SKRect(header.Left, header.Top, chip.Left - 10.0F * plan.Dpi, header.Top + 32.0F * plan.Dpi * plan.ResponsiveProfile.ContentScale))
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, _title, titleRect, plan.Dpi, MASTypography.MASTextStyle.Title, MASNavigationRailPolicy.ResolvePrimaryTextColor(ctx), If(isRtl, TypographyTextPrimitives.TextAlign.Right, TypographyTextPrimitives.TextAlign.Left), False)
            If Not plan.ShowBoundaryText Then Return
            Dim subRect As New SKRect(header.Left, header.Top + 32.0F * plan.Dpi, header.Right, header.Bottom)
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, NavigationRailTokens.BoundaryText, subRect, plan.Dpi, MASTypography.MASTextStyle.Micro, MASNavigationRailPolicy.ResolveMutedTextColor(ctx).WithAlpha(NavigationRailTokens.MutedTextAlpha), If(isRtl, TypographyTextPrimitives.TextAlign.Right, TypographyTextPrimitives.TextAlign.Left), False)
        End Sub

        Private Sub DrawItems(canvas As SKCanvas,
                              ctx As MASThemeContext,
                              content As SKRect,
                              plan As MASNavigationRailLayoutPlan,
                              accent As SKColor,
                              isRtl As Boolean)
            Dim top As Single = content.Top + plan.HeaderHeightPx + If(plan.ShowHeader, 10.0F * plan.Dpi * plan.ResponsiveProfile.GapScale, 0.0F)
            Dim bottom As Single = content.Bottom - plan.FooterHeightPx - If(plan.ShowFooter, 10.0F * plan.Dpi * plan.ResponsiveProfile.GapScale, 0.0F)
            _itemsRect = PixelSnap.SnapRect(New SKRect(content.Left, top, content.Right, bottom), plan.Dpi)
            If _itemsRect.Width <= 1.0F OrElse _itemsRect.Height <= 1.0F Then Return

            If _items.Count = 0 Then
                TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, NavigationRailTokens.EmptyRailText, _itemsRect, plan.Dpi, MASTypography.MASTextStyle.Body, MASNavigationRailPolicy.ResolveMutedTextColor(ctx).WithAlpha(NavigationRailTokens.EmptyTextAlpha), TypographyTextPrimitives.TextAlign.Center, False)
                Return
            End If

            NormalizeSelection()
            Dim rowHeight As Single = plan.ItemHeightPx
            Dim gap As Single = plan.ItemGapPx
            Dim visibleByHeight As Integer = Math.Max(1, CInt(Math.Floor((_itemsRect.Height + gap) / (rowHeight + gap))))
            _lastVisibleCount = Math.Min(plan.MaxVisibleItems, Math.Min(visibleByHeight, _items.Count))
            _topIndex = MASNavigationRailPolicy.EnsureVisibleIndex(_selectedIndex, _topIndex, _items.Count, _lastVisibleCount)

            For visualIndex As Integer = 0 To _lastVisibleCount - 1
                Dim itemIndex As Integer = _topIndex + visualIndex
                If itemIndex < 0 OrElse itemIndex >= _items.Count Then Continue For
                Dim y As Single = _itemsRect.Top + CSng(visualIndex) * (rowHeight + gap)
                Dim itemRect As SKRect = PixelSnap.SnapRect(New SKRect(_itemsRect.Left, y, _itemsRect.Right, y + rowHeight), plan.Dpi)
                DrawItem(canvas, ctx, itemRect, itemIndex, plan, accent, isRtl)
            Next

            DrawSelectionMotionIndicator(canvas, plan, accent, isRtl)
        End Sub

        Private Sub DrawItem(canvas As SKCanvas,
                             ctx As MASThemeContext,
                             rect As SKRect,
                             index As Integer,
                             plan As MASNavigationRailLayoutPlan,
                             accent As SKColor,
                             isRtl As Boolean)
            Dim item As NavigationRailItemState = _items(index)
            Dim selectionMotionActive As Boolean = IsSelectionMotionActive()
            Dim selected As Boolean = index = _selectedIndex AndAlso Not selectionMotionActive
            Dim hovered As Boolean = index = _hoverIndex
            Dim pressed As Boolean = index = _pressedIndex
            Dim fillColor As SKColor = accent.WithAlpha(NavigationRailTokens.ItemFillAlpha)
            If hovered Then fillColor = accent.WithAlpha(NavigationRailTokens.ItemHoverAlpha)
            If selected Then fillColor = accent.WithAlpha(NavigationRailTokens.ItemSelectedAlpha)
            If pressed Then fillColor = accent.WithAlpha(NavigationRailTokens.ItemPressedAlpha)

            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = fillColor}
                canvas.DrawRoundRect(rect, plan.ItemRadiusPx, plan.ItemRadiusPx, fill)
            End Using
            If selected Then
                Using stroke As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Stroke, .StrokeWidth = Math.Max(1.0F, plan.Dpi), .Color = accent.WithAlpha(NavigationRailTokens.ItemStrokeAlpha)}
                    canvas.DrawRoundRect(rect, plan.ItemRadiusPx, plan.ItemRadiusPx, stroke)
                End Using
                DrawSelectedRail(canvas, rect, plan, accent, isRtl)
            End If

            Dim glyphSize As Single = plan.GlyphBoxSizePx
            Dim glyphLeft As Single = If(isRtl, rect.Right - plan.ItemPaddingPx - glyphSize, rect.Left + plan.ItemPaddingPx)
            Dim glyphRect As New SKRect(glyphLeft, rect.MidY - glyphSize / 2.0F, glyphLeft + glyphSize, rect.MidY + glyphSize / 2.0F)
            DrawGlyph(canvas, ctx, glyphRect, item.Glyph, plan, accent, selected)

            If plan.ShowLabels Then
                Dim labelLeft As Single = If(isRtl, rect.Left + plan.ItemPaddingPx, glyphRect.Right + 10.0F * plan.Dpi)
                Dim labelRight As Single = If(isRtl, glyphRect.Left - 10.0F * plan.Dpi, rect.Right - plan.ItemPaddingPx)
                Dim labelRect As New SKRect(labelLeft, rect.Top, labelRight, rect.Bottom)
                Dim align As TypographyTextPrimitives.TextAlign = If(isRtl, TypographyTextPrimitives.TextAlign.Right, TypographyTextPrimitives.TextAlign.Left)
                If item.Badge.Length > 0 AndAlso plan.ShowBadges Then
                    Dim badgeRect As SKRect = ResolveBadgeRect(rect, plan, isRtl)
                    DrawBadge(canvas, ctx, badgeRect, item.Badge, plan, accent)
                    If isRtl Then
                        labelRect.Left = Math.Max(labelRect.Left, badgeRect.Right + 8.0F * plan.Dpi)
                    Else
                        labelRect.Right = Math.Min(labelRect.Right, badgeRect.Left - 8.0F * plan.Dpi)
                    End If
                End If
                TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, item.Label, labelRect, plan.Dpi, MASTypography.MASTextStyle.Body, MASNavigationRailPolicy.ResolvePrimaryTextColor(ctx), align, False)
            End If

            _hits.Add(New NavigationRailHit(index, rect))
        End Sub

        Private Sub DrawSelectionMotionIndicator(canvas As SKCanvas,
                                                     plan As MASNavigationRailLayoutPlan,
                                                     accent As SKColor,
                                                     isRtl As Boolean)
            If canvas Is Nothing OrElse plan Is Nothing Then Return
            If Not IsSelectionMotionActive() Then Return

            Dim fromRect As SKRect = FindHitRect(_selectionMotionFromIndex)
            Dim toRect As SKRect = FindHitRect(_selectionMotionToIndex)
            If fromRect.IsEmpty AndAlso toRect.IsEmpty Then Return
            If fromRect.IsEmpty Then fromRect = toRect
            If toRect.IsEmpty Then toRect = fromRect

            Dim rect As SKRect = PixelSnap.SnapRect(LerpRect(fromRect, toRect, MASMotionSystem.ClampProgress(_selectionMotionProgress)), plan.Dpi)
            If rect.Width <= 1.0F OrElse rect.Height <= 1.0F Then Return

            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(NavigationRailTokens.ItemSelectedAlpha)}
                canvas.DrawRoundRect(rect, plan.ItemRadiusPx, plan.ItemRadiusPx, fill)
            End Using
            Using stroke As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Stroke, .StrokeWidth = Math.Max(1.0F, plan.Dpi), .Color = accent.WithAlpha(NavigationRailTokens.ItemStrokeAlpha)}
                canvas.DrawRoundRect(rect, plan.ItemRadiusPx, plan.ItemRadiusPx, stroke)
            End Using
            DrawSelectedRail(canvas, rect, plan, accent, isRtl)
        End Sub

        Private Sub DrawSelectedRail(canvas As SKCanvas,
                                     rect As SKRect,
                                     plan As MASNavigationRailLayoutPlan,
                                     accent As SKColor,
                                     isRtl As Boolean)
            Dim width As Single = plan.SelectedRailWidthPx
            Dim rail As SKRect = If(isRtl,
                                    New SKRect(rect.Right - width, rect.Top + 10.0F * plan.Dpi, rect.Right, rect.Bottom - 10.0F * plan.Dpi),
                                    New SKRect(rect.Left, rect.Top + 10.0F * plan.Dpi, rect.Left + width, rect.Bottom - 10.0F * plan.Dpi))
            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(190)}
                canvas.DrawRoundRect(rail, width / 2.0F, width / 2.0F, fill)
            End Using
        End Sub

        Private Sub DrawGlyph(canvas As SKCanvas,
                              ctx As MASThemeContext,
                              rect As SKRect,
                              text As String,
                              plan As MASNavigationRailLayoutPlan,
                              accent As SKColor,
                              selected As Boolean)
            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(If(selected, CByte(86), NavigationRailTokens.GlyphFillAlpha))}
                canvas.DrawRoundRect(rect, plan.GlyphRadiusPx, plan.GlyphRadiusPx, fill)
            End Using
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, text, rect, plan.Dpi, MASTypography.MASTextStyle.Micro, MASNavigationRailPolicy.ResolvePrimaryTextColor(ctx), TypographyTextPrimitives.TextAlign.Center, False)
        End Sub

        Private Function ResolveBadgeRect(rect As SKRect,
                                          plan As MASNavigationRailLayoutPlan,
                                          isRtl As Boolean) As SKRect
            Dim width As Single = plan.BadgeMinWidthPx
            Dim height As Single = plan.BadgeHeightPx
            If isRtl Then Return New SKRect(rect.Left + plan.ItemPaddingPx, rect.MidY - height / 2.0F, rect.Left + plan.ItemPaddingPx + width, rect.MidY + height / 2.0F)
            Return New SKRect(rect.Right - plan.ItemPaddingPx - width, rect.MidY - height / 2.0F, rect.Right - plan.ItemPaddingPx, rect.MidY + height / 2.0F)
        End Function

        Private Sub DrawBadge(canvas As SKCanvas,
                              ctx As MASThemeContext,
                              rect As SKRect,
                              text As String,
                              plan As MASNavigationRailLayoutPlan,
                              accent As SKColor)
            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(NavigationRailTokens.BadgeFillAlpha)}
                canvas.DrawRoundRect(rect, plan.BadgeRadiusPx, plan.BadgeRadiusPx, fill)
            End Using
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, text, rect, plan.Dpi, MASTypography.MASTextStyle.Micro, MASNavigationRailPolicy.ResolvePrimaryTextColor(ctx), TypographyTextPrimitives.TextAlign.Center, False)
        End Sub

        Private Sub DrawFooter(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               content As SKRect,
                               plan As MASNavigationRailLayoutPlan,
                               isRtl As Boolean)
            If Not plan.ShowFooter OrElse plan.FooterHeightPx <= 0.0F Then Return
            Dim footer As SKRect = PixelSnap.SnapRect(New SKRect(content.Left, content.Bottom - plan.FooterHeightPx, content.Right, content.Bottom), plan.Dpi)
            Dim accent As SKColor = MASNavigationRailPolicy.ResolveAccentColor(ctx)
            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(NavigationRailTokens.FooterFillAlpha)}
                canvas.DrawRoundRect(footer, 12.0F * plan.Dpi, 12.0F * plan.Dpi, fill)
            End Using
            Dim status As String = If(_selectedIndex >= 0 AndAlso _selectedIndex < _items.Count, "Selected • " & _items(_selectedIndex).Label, NavigationRailTokens.BoundaryText)
            Dim inner As New SKRect(footer.Left + 10.0F * plan.Dpi, footer.Top, footer.Right - 10.0F * plan.Dpi, footer.Bottom)
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, status, inner, plan.Dpi, MASTypography.MASTextStyle.Micro, MASNavigationRailPolicy.ResolveMutedTextColor(ctx), If(isRtl, TypographyTextPrimitives.TextAlign.Right, TypographyTextPrimitives.TextAlign.Left), False)
        End Sub

#End Region

#Region "Input"

        Protected Overrides Sub OnMouseMove(ctx As MASThemeContext,
                                            ptPx As SKPoint)
            Dim hit As NavigationRailHit = HitTest(ptPx)
            Dim index As Integer = If(hit Is Nothing, -1, hit.Index)
            If index = _hoverIndex Then Return
            _hoverIndex = index
            InvalidateVisual()
        End Sub

        Protected Overrides Function OnMouseDown(ctx As MASThemeContext,
                                                 ptPx As SKPoint,
                                                 button As Integer) As Boolean
            If button <> CInt(MouseButtons.Left) Then Return False
            Dim hit As NavigationRailHit = HitTest(ptPx)
            If hit Is Nothing Then Return _contentRect.Contains(ptPx.X, ptPx.Y)
            _pressedIndex = hit.Index
            InvalidateVisual()
            Return True
        End Function

        Protected Overrides Function OnMouseUp(ctx As MASThemeContext,
                                               ptPx As SKPoint,
                                               button As Integer) As Boolean
            If button <> CInt(MouseButtons.Left) Then Return False
            Dim pressed As Integer = _pressedIndex
            _pressedIndex = -1
            Dim hit As NavigationRailHit = HitTest(ptPx)
            If hit IsNot Nothing AndAlso hit.Index = pressed Then
                SetSelectedIndex(hit.Index, True)
                Return True
            End If
            InvalidateVisual()
            Return False
        End Function

        Protected Overrides Sub OnMouseLeave(ctx As MASThemeContext)
            _hoverIndex = -1
            _pressedIndex = -1
            InvalidateVisual()
        End Sub

        Protected Overrides Sub OnMouseCancel(ctx As MASThemeContext)
            _pressedIndex = -1
            InvalidateVisual()
        End Sub

        Protected Overrides Sub OnHostLostFocus(ctx As MASThemeContext)
            _pressedIndex = -1
            InvalidateVisual()
        End Sub

        Friend Overrides Function WantsPointerWheel(ctx As MASThemeContext,
                                                      ptPx As SKPoint) As Boolean
            Return Visible AndAlso Enabled AndAlso
                   _itemsRect.Contains(ptPx.X, ptPx.Y) AndAlso
                   HasScrollableItemViewport()
        End Function

        Friend Overrides Function CanHandlePointerWheel(ctx As MASThemeContext,
                                                        delta As Integer,
                                                        ptPx As SKPoint) As Boolean
            If delta = 0 OrElse Not WantsPointerWheel(ctx, ptPx) Then Return False
            Return CanScrollItemViewport(delta)
        End Function

        Protected Overrides Sub OnMouseWheel(ctx As MASThemeContext,
                                             delta As Integer,
                                             ptPx As SKPoint)
            If Not CanScrollItemViewport(delta) Then Return
            If delta < 0 Then
                _topIndex = Math.Min(ResolveItemViewportMaxTop(), _topIndex + 1)
            ElseIf delta > 0 Then
                _topIndex = Math.Max(0, _topIndex - 1)
            End If
            InvalidateVisual()
        End Sub

        Protected Overrides Function OnKeyDown(ctx As MASThemeContext,
                                               keyCode As Keys) As Boolean
            If Not Enabled OrElse _items.Count = 0 Then Return False
            Select Case keyCode
                Case Keys.Up
                    MoveBy(-1)
                    Return True
                Case Keys.Down
                    MoveBy(1)
                    Return True
                Case Keys.Home
                    SetSelectedIndex(0, True)
                    Return True
                Case Keys.End
                    SetSelectedIndex(_items.Count - 1, True)
                    Return True
                Case Keys.PageUp
                    MoveBy(-Math.Max(1, _lastVisibleCount))
                    Return True
                Case Keys.PageDown
                    MoveBy(Math.Max(1, _lastVisibleCount))
                    Return True
            End Select
            Return False
        End Function

        Protected Overrides Function WantsKeyboardFocus() As Boolean
            Return Visible AndAlso Enabled
        End Function

        Protected Overrides Function WantsPointerFocus() As Boolean
            Return False
        End Function

#End Region

#Region "Helpers"

        Private Sub MoveBy(delta As Integer)
            If _items.Count <= 0 Then Return
            SetSelectedIndex(MASNavigationRailPolicy.ClampIndex(_selectedIndex + delta, _items.Count), True)
        End Sub

        Private Function HasScrollableItemViewport() As Boolean
            Return _items.Count > Math.Max(0, _lastVisibleCount)
        End Function

        Private Function ResolveItemViewportMaxTop() As Integer
            Return Math.Max(0, _items.Count - Math.Max(1, _lastVisibleCount))
        End Function

        Private Function CanScrollItemViewport(delta As Integer) As Boolean
            If Not HasScrollableItemViewport() Then Return False
            Dim maxTop As Integer = ResolveItemViewportMaxTop()
            If delta < 0 Then Return _topIndex < maxTop
            If delta > 0 Then Return _topIndex > 0
            Return False
        End Function

        Private Sub NormalizeSelection()
            _selectedIndex = MASNavigationRailPolicy.ClampIndex(_selectedIndex, _items.Count)
            _topIndex = MASNavigationRailPolicy.EnsureVisibleIndex(_selectedIndex, _topIndex, _items.Count, _lastVisibleCount)
        End Sub

        Private Sub SetSelectedIndex(index As Integer,
                                     shouldRaiseSelectionEvent As Boolean)
            Dim normalized As Integer = MASNavigationRailPolicy.ClampIndex(index, _items.Count)
            If normalized < 0 Then
                _selectedIndex = -1
                StopSelectionMotion()
                Return
            End If
            Dim changed As Boolean = normalized <> _selectedIndex
            Dim previous As Integer = _selectedIndex
            _selectedIndex = normalized
            _topIndex = MASNavigationRailPolicy.EnsureVisibleIndex(_selectedIndex, _topIndex, _items.Count, _lastVisibleCount)
            If changed Then StartSelectionMotion(previous, normalized)
            InvalidateVisual()
            If changed AndAlso shouldRaiseSelectionEvent Then RaiseSelectedItemChangedSafe()
        End Sub

        Private Sub StartSelectionMotion(previousIndex As Integer,
                                         nextIndex As Integer)
            StopSelectionMotion()

            If previousIndex < 0 OrElse nextIndex < 0 OrElse previousIndex = nextIndex Then
                _hasSelectionMotion = False
                _selectionMotionProgress = 1.0F
                Return
            End If

            _selectionMotionFromIndex = previousIndex
            _selectionMotionToIndex = nextIndex
            _selectionMotionProgress = 0.0F
            _hasSelectionMotion = True

            Dim plan As MASMotionTransitionPlan = MASMotionSystem.CreateTransitionPlan(MASMotionTokenId.IndicatorMove, 0.0F, 1.0F)
            If Not plan.ShouldAnimate Then
                CompleteSelectionMotionFrame(Nothing)
                Return
            End If

            _selectionMotionRunner = MASMotionSystem.CreateTimelineRunner(
                owner:=Me,
                consumerName:="MASNavigationRail.SelectionIndicator",
                plan:=plan,
                requestFrame:=AddressOf InvalidateVisual,
                applyFrame:=AddressOf ApplySelectionMotionFrame,
                completed:=AddressOf CompleteSelectionMotionFrame)
            _selectionMotionRunner.Start()
        End Sub

        Private Sub ApplySelectionMotionFrame(frame As MASMotionFrame)
            If frame Is Nothing Then Return
            _selectionMotionProgress = MASMotionSystem.ClampProgress(frame.Value)
            InvalidateVisual()
        End Sub

        Private Sub CompleteSelectionMotionFrame(frame As MASMotionFrame)
            _selectionMotionProgress = 1.0F
            _hasSelectionMotion = False
            _selectionMotionRunner = Nothing
            InvalidateVisual()
        End Sub

        Private Sub StopSelectionMotion()
            If _selectionMotionRunner IsNot Nothing Then
                _selectionMotionRunner.Dispose()
                _selectionMotionRunner = Nothing
            End If

            _hasSelectionMotion = False
            _selectionMotionProgress = 1.0F
        End Sub

        Private Function IsSelectionMotionActive() As Boolean
            Return _hasSelectionMotion AndAlso _selectionMotionRunner IsNot Nothing AndAlso _selectionMotionRunner.IsRunning
        End Function

        Private Function FindHitRect(index As Integer) As SKRect
            If index < 0 Then Return SKRect.Empty

            For Each hit As NavigationRailHit In _hits
                If hit IsNot Nothing AndAlso hit.Index = index Then Return hit.Rect
            Next

            Return SKRect.Empty
        End Function

        Private Shared Function LerpRect(fromRect As SKRect,
                                         toRect As SKRect,
                                         progress As Single) As SKRect
            progress = MASMotionSystem.ClampProgress(progress)
            Return New SKRect(
                LerpSingle(fromRect.Left, toRect.Left, progress),
                LerpSingle(fromRect.Top, toRect.Top, progress),
                LerpSingle(fromRect.Right, toRect.Right, progress),
                LerpSingle(fromRect.Bottom, toRect.Bottom, progress))
        End Function

        Private Shared Function LerpSingle(fromValue As Single,
                                           toValue As Single,
                                           progress As Single) As Single
            Return fromValue + ((toValue - fromValue) * progress)
        End Function

        Private Function FindItemIndex(itemKey As String) As Integer
            For i As Integer = 0 To _items.Count - 1
                If String.Equals(_items(i).Key, itemKey, StringComparison.Ordinal) Then Return i
            Next
            Return -1
        End Function

        Private Function HitTest(point As SKPoint) As NavigationRailHit
            For Each hit As NavigationRailHit In _hits
                If hit.Rect.Contains(point.X, point.Y) Then Return hit
            Next
            Return Nothing
        End Function

        Private Shared Function Inset(rect As SKRect,
                                      amount As Single) As SKRect
            Return New SKRect(rect.Left + amount, rect.Top + amount, rect.Right - amount, rect.Bottom - amount)
        End Function

        Private Sub RaiseNavigationRailChangedSafe()
            Try
                RaiseEvent NavigationRailChanged(Me, EventArgs.Empty)
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(ex, "MASNavigationRail.NavigationRailChanged")
            End Try
        End Sub

        Private Sub RaiseSelectedItemChangedSafe()
            Try
                RaiseEvent SelectedItemChanged(Me, EventArgs.Empty)
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(ex, "MASNavigationRail.SelectedItemChanged")
            End Try
        End Sub

        Friend Function CreateNavigationRailReadinessManifest() As MASNavigationRailReadinessManifest
            Return MASNavigationRailReadinessManifest.CreateDefault()
        End Function

#End Region

#Region "Dispose"

        Protected Overrides Sub Dispose(disposing As Boolean)
            If _disposedLocal Then Return
            _disposedLocal = True
            StopSelectionMotion()
            MyBase.Dispose(disposing)
        End Sub

#End Region

    End Class

End Namespace
