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.Rendering
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    ''' <summary>
    ''' Official Nexamas UI premium dashboard widget grid. It owns visual widget
    ''' card presentation, responsive mosaic placement, selection, keyboard movement,
    ''' RTL-aware layout, and MAS surface/typography routing. Data refresh, analytics
    ''' collection, chart engines, storage, providers, shell layout, wrappers, adapters,
    ''' and pointer-click outer focus rings remain outside this control.
    ''' </summary>
    Partial Public NotInheritable Class MASDashboardGrid
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

#Region "Nested state"

        Private NotInheritable Class DashboardWidgetState
            Friend Sub New(key As String,
                           title As String,
                           valueText As String,
                           caption As String,
                           spanColumns As Integer,
                           spanRows As Integer)
                Me.Key = key
                Me.Title = title
                Me.ValueText = valueText
                Me.Caption = caption
                Me.SpanColumns = spanColumns
                Me.SpanRows = spanRows
            End Sub

            Friend ReadOnly Property Key As String
            Friend Property Title As String
            Friend Property ValueText As String
            Friend Property Caption As String
            Friend Property SpanColumns As Integer
            Friend Property SpanRows As Integer
        End Class

        Private NotInheritable Class DashboardWidgetHit
            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 ReadOnly _primitives As New MASVisualPrimitivesPainter()
        Private ReadOnly _widgets As New List(Of DashboardWidgetState)()
        Private ReadOnly _widgetIndexByKey As New Dictionary(Of String, Integer)(StringComparer.Ordinal)
        Private ReadOnly _hits As New List(Of DashboardWidgetHit)()
        Private _title As String = DashboardGridTokens.DefaultTitle
        Private _selectedIndex As Integer = -1
        Private _hoverIndex As Integer = -1
        Private _pressedIndex As Integer = -1
        Private _topIndex As Integer
        Private _lastVisibleCount As Integer = DashboardGridTokens.MaxVisibleWidgets
        Private _contentRect As SKRect = SKRect.Empty
        Private _widgetsRect As SKRect = SKRect.Empty
        Private _lastDpi As Single = 1.0F

#End Region

#Region "Constructor / Factory"

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

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

#End Region

#Region "Public API"

        Public Event DashboardGridChanged As EventHandler
        Public Event SelectedWidgetChanged As EventHandler

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

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

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

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

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

        Public Function GetWidgetKeys() As IReadOnlyList(Of String)
            Dim keys As New List(Of String)()
            For Each item As DashboardWidgetState In _widgets
                keys.Add(item.Key)
            Next
            Return New ReadOnlyCollection(Of String)(keys)
        End Function

        Public Function AddWidget(widgetKey As String,
                                  title As String,
                                  valueText As String,
                                  Optional caption As String = Nothing,
                                  Optional spanColumns As Integer = 1,
                                  Optional spanRows As Integer = 1) As MASDashboardGrid
            If _widgets.Count >= DashboardGridTokens.MaxWidgets Then Return Me
            Dim normalizedKey As String = MASDashboardGridPolicy.NormalizeKey(widgetKey, "widget", _widgets.Count + 1)
            If FindWidgetIndex(normalizedKey) >= 0 Then Return Me
            _widgetIndexByKey.Add(normalizedKey, _widgets.Count)
            _widgets.Add(New DashboardWidgetState(normalizedKey,
                                                  MASDashboardGridPolicy.NormalizeWidgetTitle(title),
                                                  MASDashboardGridPolicy.NormalizeValue(valueText),
                                                  MASDashboardGridPolicy.NormalizeCaption(caption),
                                                  MASDashboardGridPolicy.ClampSpan(spanColumns, DashboardGridTokens.MaxSpanColumns),
                                                  MASDashboardGridPolicy.ClampSpan(spanRows, DashboardGridTokens.MaxSpanRows)))
            If _selectedIndex < 0 Then _selectedIndex = 0
            StopSelectionMotion() : RequestSizeLayoutRefreshForSizeAffectingChange("MASDashboardGrid.AddWidget")
            InvalidateVisual()
            RaiseDashboardGridChangedSafe()
            Return Me
        End Function

        Public Function ClearWidgets() As MASDashboardGrid
            _widgets.Clear()
            _widgetIndexByKey.Clear()
            _hits.Clear()
            _selectedIndex = -1
            _hoverIndex = -1
            _pressedIndex = -1
            _topIndex = 0
            StopSelectionMotion() : RequestSizeLayoutRefreshForSizeAffectingChange("MASDashboardGrid.ClearWidgets")
            InvalidateVisual()
            RaiseDashboardGridChangedSafe()
            Return Me
        End Function

        Public Function SelectWidget(widgetKey As String) As Boolean
            Dim index As Integer = FindWidgetIndex(widgetKey)
            If index < 0 Then Return False
            SetSelectedIndex(index, True)
            Return True
        End Function

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

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

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

        Public Shadows Function WithSize(sizeIntent As MASSize) As MASDashboardGrid
            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)
            Return MASSizeResolver.FromMeasured(
                desiredSize:=New SKSize(DashboardGridTokens.PreferredWidthDip, DashboardGridTokens.PreferredHeightDip),
                minSize:=New SKSize(DashboardGridTokens.ContractMinWidthDip, DashboardGridTokens.ContractMinHeightDip),
                maxSize:=New SKSize(DashboardGridTokens.MaxWidthDip, Single.PositiveInfinity),
                intent:=intent,
                context:=safeContext,
                canGrowWidth:=True,
                canGrowHeight:=True,
                contentInset:=New MASLayoutInset(DashboardGridTokens.PaddingDip,
                                                 DashboardGridTokens.PaddingDip,
                                                 DashboardGridTokens.PaddingDip,
                                                 DashboardGridTokens.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, MASDashboardGridLayoutPlan.Thresholds)
            Dim plan As MASDashboardGridLayoutPlan = MASDashboardGridLayoutPlan.Create(responsive)

            _primitives.DrawCardSurface(canvas, ctx, bounds, dpi, Me.ArchitectureContract, 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 = MASDashboardGridPolicy.ResolveRightToLeft()
            Dim accent As SKColor = MASDashboardGridPolicy.ResolveAccentColor(ctx)
            DrawHeader(canvas, ctx, _contentRect, plan, isRtl)
            DrawWidgets(canvas, ctx, _contentRect, plan, accent, isRtl)
            DrawWidgetSelectionMotionOverlay(canvas, 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 MASDashboardGridLayoutPlan,
                               isRtl As Boolean)
            If Not plan.ShowHeader OrElse plan.HeaderHeightPx <= 0.0F Then Return
            Dim header As New SKRect(content.Left, content.Top, content.Right, content.Top + plan.HeaderHeightPx)
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, _title, header, plan.Dpi, MASTypography.MASTextStyle.Title, MASDashboardGridPolicy.ResolvePrimaryTextColor(ctx), If(isRtl, TypographyTextPrimitives.TextAlign.Right, TypographyTextPrimitives.TextAlign.Left), False)

            If Not plan.ShowSummary Then Return
            Dim summaryWidth As Single = Math.Min(280.0F * plan.Dpi, header.Width * 0.48F)
            Dim summaryRect As SKRect = If(isRtl,
                                           New SKRect(header.Left, header.Top, header.Left + summaryWidth, header.Bottom),
                                           New SKRect(header.Right - summaryWidth, header.Top, header.Right, header.Bottom))
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, SummaryText, summaryRect, plan.Dpi, MASTypography.MASTextStyle.Micro, MASDashboardGridPolicy.ResolveMutedTextColor(ctx), If(isRtl, TypographyTextPrimitives.TextAlign.Left, TypographyTextPrimitives.TextAlign.Right), False)
        End Sub

        Private Sub DrawWidgets(canvas As SKCanvas,
                                ctx As MASThemeContext,
                                content As SKRect,
                                plan As MASDashboardGridLayoutPlan,
                                accent As SKColor,
                                isRtl As Boolean)
            Dim top As Single = content.Top + plan.HeaderHeightPx + plan.CellGapPx
            Dim bottom As Single = content.Bottom - plan.FooterHeightPx - plan.CellGapPx
            _widgetsRect = PixelSnap.SnapRect(New SKRect(content.Left, top, content.Right, bottom), plan.Dpi)
            If _widgetsRect.Width <= 1.0F OrElse _widgetsRect.Height <= 1.0F Then Return

            If _widgets.Count = 0 Then
                TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, DashboardGridTokens.EmptyDashboardText, _widgetsRect, plan.Dpi, MASTypography.MASTextStyle.Body, MASDashboardGridPolicy.ResolveMutedTextColor(ctx).WithAlpha(DashboardGridTokens.EmptyTextAlpha), TypographyTextPrimitives.TextAlign.Center, False)
                Return
            End If

            Dim columns As Integer = plan.ResolveColumnCount(_widgetsRect.Width)
            Dim gap As Single = plan.CellGapPx
            Dim cellWidth As Single = (_widgetsRect.Width - (CSng(columns - 1) * gap)) / CSng(columns)
            Dim cellHeight As Single = plan.CellHeightPx
            Dim maxRowsByHeight As Integer = Math.Max(1, CInt(Math.Floor((_widgetsRect.Height + gap) / (cellHeight + gap))))
            _lastVisibleCount = Math.Max(1, Math.Min(plan.MaxVisibleWidgets, maxRowsByHeight * columns))
            _topIndex = MASDashboardGridPolicy.EnsureVisibleIndex(_selectedIndex, _topIndex, _widgets.Count, _lastVisibleCount)

            Dim cursorColumn As Integer = 0
            Dim cursorRow As Integer = 0
            Dim endIndex As Integer = Math.Min(_widgets.Count - 1, _topIndex + _lastVisibleCount - 1)
            For index As Integer = _topIndex To endIndex
                Dim item As DashboardWidgetState = _widgets(index)
                Dim spanColumns As Integer = Math.Min(item.SpanColumns, columns)
                If cursorColumn + spanColumns > columns Then
                    cursorColumn = 0
                    cursorRow += 1
                End If
                If cursorRow >= maxRowsByHeight Then Exit For

                Dim visualColumn As Integer = If(isRtl, columns - cursorColumn - spanColumns, cursorColumn)
                Dim left As Single = _widgetsRect.Left + CSng(visualColumn) * (cellWidth + gap)
                Dim topRow As Single = _widgetsRect.Top + CSng(cursorRow) * (cellHeight + gap)
                Dim width As Single = CSng(spanColumns) * cellWidth + CSng(spanColumns - 1) * gap
                Dim height As Single = CSng(item.SpanRows) * cellHeight + CSng(item.SpanRows - 1) * gap
                Dim rect As SKRect = PixelSnap.SnapRect(New SKRect(left, topRow, Math.Min(_widgetsRect.Right, left + width), Math.Min(_widgetsRect.Bottom, topRow + height)), plan.Dpi)
                If rect.Height > 8.0F * plan.Dpi Then DrawWidget(canvas, ctx, item, index, rect, plan, accent, isRtl)
                cursorColumn += spanColumns
                If cursorColumn >= columns Then
                    cursorColumn = 0
                    cursorRow += 1
                End If
            Next
        End Sub

        Private Sub DrawWidget(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               item As DashboardWidgetState,
                               index As Integer,
                               rect As SKRect,
                               plan As MASDashboardGridLayoutPlan,
                               accent As SKColor,
                               isRtl As Boolean)
            Dim selected As Boolean = index = _selectedIndex AndAlso Not ShouldSuppressWidgetSelectionFill(index)
            Dim hovered As Boolean = index = _hoverIndex
            Dim pressed As Boolean = index = _pressedIndex
            Dim fillColor As SKColor = accent.WithAlpha(DashboardGridTokens.WidgetFillAlpha)
            If selected Then fillColor = accent.WithAlpha(DashboardGridTokens.WidgetSelectedAlpha)
            If hovered Then fillColor = accent.WithAlpha(DashboardGridTokens.WidgetHoverAlpha)
            If pressed Then fillColor = accent.WithAlpha(DashboardGridTokens.WidgetPressedAlpha)

            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = fillColor}
                canvas.DrawRoundRect(rect, plan.WidgetRadiusPx, plan.WidgetRadiusPx, fill)
            End Using
            Using stroke As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Stroke, .StrokeWidth = Math.Max(1.0F, plan.Dpi), .Color = accent.WithAlpha(DashboardGridTokens.WidgetStrokeAlpha)}
                canvas.DrawRoundRect(rect, plan.WidgetRadiusPx, plan.WidgetRadiusPx, stroke)
            End Using
            If plan.ShowAccentRail Then DrawAccentRail(canvas, rect, plan, accent, isRtl)

            Dim inner As SKRect = Inset(rect, plan.WidgetPaddingPx)
            Dim align As TypographyTextPrimitives.TextAlign = If(isRtl, TypographyTextPrimitives.TextAlign.Right, TypographyTextPrimitives.TextAlign.Left)
            Dim titleRect As New SKRect(inner.Left, inner.Top, inner.Right, inner.Top + 22.0F * plan.Dpi * plan.ResponsiveProfile.ContentScale)
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, item.Title, titleRect, plan.Dpi, MASTypography.MASTextStyle.Micro, MASDashboardGridPolicy.ResolveMutedTextColor(ctx), align, False)

            Dim valueRect As New SKRect(inner.Left, titleRect.Bottom + 4.0F * plan.Dpi, inner.Right, titleRect.Bottom + 36.0F * plan.Dpi * plan.ResponsiveProfile.ContentScale)
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, ResolveProviderWidgetValueText(item), valueRect, plan.Dpi, MASTypography.MASTextStyle.Title, MASDashboardGridPolicy.ResolvePrimaryTextColor(ctx), align, False)

            If plan.ShowCaption Then
                Dim captionRect As New SKRect(inner.Left, valueRect.Bottom + 4.0F * plan.Dpi, inner.Right, inner.Bottom)
                TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, ResolveProviderWidgetCaption(item), captionRect, plan.Dpi, MASTypography.MASTextStyle.Micro, MASDashboardGridPolicy.ResolveMutedTextColor(ctx), align, False)
            End If
            _hits.Add(New DashboardWidgetHit(index, rect))
            PlaceHostedWidgetContent(item.Key, inner, plan.Dpi)
        End Sub

        Private Sub DrawAccentRail(canvas As SKCanvas,
                                   rect As SKRect,
                                   plan As MASDashboardGridLayoutPlan,
                                   accent As SKColor,
                                   isRtl As Boolean)
            Dim width As Single = plan.AccentRailWidthPx
            Dim rail As SKRect = If(isRtl,
                                    New SKRect(rect.Right - width, rect.Top + 12.0F * plan.Dpi, rect.Right, rect.Bottom - 12.0F * plan.Dpi),
                                    New SKRect(rect.Left, rect.Top + 12.0F * plan.Dpi, rect.Left + width, rect.Bottom - 12.0F * plan.Dpi))
            Using fill As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(DashboardGridTokens.AccentFillAlpha)}
                canvas.DrawRoundRect(rail, width, width, fill)
            End Using
        End Sub

        Private Sub DrawFooter(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               content As SKRect,
                               plan As MASDashboardGridLayoutPlan,
                               isRtl As Boolean)
            If Not plan.ShowFooter OrElse plan.FooterHeightPx <= 0.0F Then Return
            Dim footer As New SKRect(content.Left, content.Bottom - plan.FooterHeightPx, content.Right, content.Bottom)
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, DashboardGridTokens.BoundaryText, footer, plan.Dpi, MASTypography.MASTextStyle.Micro, MASDashboardGridPolicy.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 DashboardWidgetHit = 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 DashboardWidgetHit = 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 DashboardWidgetHit = 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
                   _widgetsRect.Contains(ptPx.X, ptPx.Y) AndAlso
                   HasScrollableWidgetViewport()
        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 CanScrollWidgetViewport(delta)
        End Function

        Protected Overrides Sub OnMouseWheel(ctx As MASThemeContext,
                                             delta As Integer,
                                             ptPx As SKPoint)
            If Not CanScrollWidgetViewport(delta) Then Return
            If delta < 0 Then
                _topIndex = Math.Min(ResolveWidgetViewportMaxTop(), _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 _widgets.Count = 0 Then Return False
            Select Case keyCode
                Case Keys.Left
                    MoveBy(If(MASDashboardGridPolicy.ResolveRightToLeft(), 1, -1))
                    Return True
                Case Keys.Right
                    MoveBy(If(MASDashboardGridPolicy.ResolveRightToLeft(), -1, 1))
                    Return True
                Case Keys.Up
                    MoveBy(-Math.Max(1, DashboardGridTokens.MinColumns))
                    Return True
                Case Keys.Down
                    MoveBy(Math.Max(1, DashboardGridTokens.MinColumns))
                    Return True
                Case Keys.Home
                    SetSelectedIndex(0, True)
                    Return True
                Case Keys.End
                    SetSelectedIndex(_widgets.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 _widgets.Count <= 0 Then Return
            SetSelectedIndex(MASDashboardGridPolicy.ClampIndex(_selectedIndex + delta, _widgets.Count), True)
        End Sub

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

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

        Private Function CanScrollWidgetViewport(delta As Integer) As Boolean
            If Not HasScrollableWidgetViewport() Then Return False
            Dim maxTop As Integer = ResolveWidgetViewportMaxTop()
            If delta < 0 Then Return _topIndex < maxTop
            If delta > 0 Then Return _topIndex > 0
            Return False
        End Function

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

        Private Function FindWidgetIndex(widgetKey As String) As Integer
            Dim normalized As String = MASDashboardGridPolicy.NormalizeKey(widgetKey, "widget", 1)
            Dim index As Integer = -1
            If _widgetIndexByKey.TryGetValue(normalized, index) Then Return index
            Return -1
        End Function

        Private Function HitTest(point As SKPoint) As DashboardWidgetHit
            For Each hit As DashboardWidgetHit 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 RaiseDashboardGridChangedSafe()
            Try
                RaiseEvent DashboardGridChanged(Me, EventArgs.Empty)
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(ex, "MASDashboardGrid.DashboardGridChanged")
            End Try
        End Sub

        Private Sub RaiseSelectedWidgetChangedSafe()
            Try
                RaiseEvent SelectedWidgetChanged(Me, EventArgs.Empty)
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(ex, "MASDashboardGrid.SelectedWidgetChanged")
            End Try
        End Sub

        Friend Function CreateDashboardGridReadinessManifest() As MASDashboardGridReadinessManifest
            Return MASDashboardGridReadinessManifest.CreateDefault()
        End Function

#End Region

    End Class

End Namespace
