Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Globalization
Imports System.Collections.ObjectModel
Imports System.Linq
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Layout
Imports Nexamas.UI.PropertyGrid
Imports Nexamas.UI.Theming
Imports Nexamas.UI.General
Imports Nexamas.UI.Motion
Imports SkiaSharp

Namespace Nexamas.UI.Components

    ''' <summary>
    ''' Public Nexamas UI property grid control for developer tools, settings screens, and future designers.
    ''' It inspects an object or accepts manual property rows, renders them as categorized MAS rows, and commits simple edits through MAS-owned property conversion.
    ''' </summary>
    Public NotInheritable Class MASPropertyGrid
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

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

        Private ReadOnly _properties As New List(Of MASPropertyGridPropertyState)()
        Private ReadOnly _collapsedCategories As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
        Private ReadOnly _renderer As New MASPropertyGridRenderer()
        Private _title As String = "Properties"
        Private _filterText As String = String.Empty
        Private _selectedIndex As Integer = -1
        Private _hoverIndex As Integer = -1
        Private _scrollOffsetPx As Single
        Private _lastRenderBoundsPx As SKRect = SKRect.Empty
        Private _lastRows As IReadOnlyList(Of MASPropertyGridRenderRow) = New ReadOnlyCollection(Of MASPropertyGridRenderRow)(New List(Of MASPropertyGridRenderRow)())
        Private _lastDpi As Single = 1.0F
        Private _sourceObject As Object
        Private _selectionMotionRunner As MASMotionTimelineRunner
        Private _selectionMotionFromIndex As Integer = -1
        Private _selectionMotionToIndex As Integer = -1
        Private _selectionMotionProgress As Single = 1.0F
        Private _hasSelectionMotion As Boolean

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

        Public Event PropertyValueChanged As EventHandler
        Public Event SelectionChanged As EventHandler

        Public Shared Function Create(Optional source As Object = Nothing) As MASPropertyGrid
            Dim control As New MASPropertyGrid()
            If source IsNot Nothing Then control.SetObject(source)
            Return control
        End Function

        Public Property Title As String
            Get
                Return _title
            End Get
            Set(value As String)
                Dim normalized As String = MASPropertyGridText.Normalize(value, "Properties")
                If String.Equals(_title, normalized, StringComparison.Ordinal) Then Return
                _title = normalized
                InvalidateVisual()
            End Set
        End Property

        Public Property FilterText As String
            Get
                Return _filterText
            End Get
            Set(value As String)
                Dim normalized As String = MASPropertyGridText.Normalize(value, String.Empty)
                If String.Equals(_filterText, normalized, StringComparison.Ordinal) Then Return
                _filterText = normalized
                StopSelectionMotion()
                _scrollOffsetPx = 0.0F
                InvalidateVisual()
            End Set
        End Property

        Public ReadOnly Property PropertyCount As Integer
            Get
                Return _properties.Count
            End Get
        End Property

        Public ReadOnly Property CategoryCount As Integer
            Get
                Return _properties.Select(Function(item) item.Category).Distinct(StringComparer.OrdinalIgnoreCase).Count()
            End Get
        End Property

        Public ReadOnly Property HasSelection As Boolean
            Get
                Return _selectedIndex >= 0 AndAlso _selectedIndex < _properties.Count
            End Get
        End Property

        Public ReadOnly Property SelectedPropertyName As String
            Get
                Dim item As MASPropertyGridPropertyState = GetSelectedProperty()
                If item Is Nothing Then Return String.Empty
                Return item.Name
            End Get
        End Property

        Public ReadOnly Property SelectedValueText As String
            Get
                Dim item As MASPropertyGridPropertyState = GetSelectedProperty()
                If item Is Nothing Then Return String.Empty
                Return item.ValueText
            End Get
        End Property

        Public ReadOnly Property IsObjectBound As Boolean
            Get
                Return _sourceObject IsNot Nothing
            End Get
        End Property

        Public Function WithTitle(title As String) As MASPropertyGrid
            Me.Title = title
            Return Me
        End Function

        Public Function WithObject(source As Object) As MASPropertyGrid
            Return SetObject(source)
        End Function

        Public Function SetObject(source As Object) As MASPropertyGrid
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            _sourceObject = source
            ReplaceProperties(MASPropertyGridObjectInspector.Inspect(source))
            Return Me
        End Function

        Public Function ClearProperties() As MASPropertyGrid
            StopSelectionMotion()
            _sourceObject = Nothing
            _properties.Clear()
            _collapsedCategories.Clear()
            _filterText = String.Empty
            _selectedIndex = -1
            _hoverIndex = -1
            _scrollOffsetPx = 0.0F
            _lastRows = New ReadOnlyCollection(Of MASPropertyGridRenderRow)(New List(Of MASPropertyGridRenderRow)())
            InvalidateVisual()
            RaiseEvent SelectionChanged(Me, EventArgs.Empty)
            Return Me
        End Function

        Public Function AddProperty(category As String,
                                    name As String,
                                    value As Object,
                                    Optional editable As Boolean = True) As MASPropertyGrid
            _sourceObject = Nothing
            Dim entry As MASPropertyGridPropertyState = MASPropertyGridPropertyState.CreateManual(category, name, value, editable)
            _properties.Add(entry)
            If _selectedIndex < 0 Then _selectedIndex = 0
            InvalidateVisual()
            Return Me
        End Function

        Public Function WithFilterText(value As String) As MASPropertyGrid
            FilterText = value
            Return Me
        End Function

        Public Function ClearFilter() As MASPropertyGrid
            FilterText = String.Empty
            Return Me
        End Function

        Public Function ToggleCategory(category As String) As MASPropertyGrid
            Dim normalized As String = MASPropertyGridText.Normalize(category, String.Empty)
            If normalized.Length = 0 Then Return Me
            StopSelectionMotion()
            If _collapsedCategories.Contains(normalized) Then
                _collapsedCategories.Remove(normalized)
            Else
                _collapsedCategories.Add(normalized)
            End If
            _scrollOffsetPx = MASPropertyGridTokens.NormalizeScrollOffset(_scrollOffsetPx, CalculateTotalContentHeightPx(Math.Max(1.0F, _lastDpi)), MASPropertyGridTokens.ResolveGridRect(_lastRenderBoundsPx, Math.Max(1.0F, _lastDpi)).Height)
            InvalidateVisual()
            Return Me
        End Function

        Public Function ExpandAllCategories() As MASPropertyGrid
            If _collapsedCategories.Count = 0 Then Return Me
            StopSelectionMotion()
            _collapsedCategories.Clear()
            InvalidateVisual()
            Return Me
        End Function

        Public Function CollapseAllCategories() As MASPropertyGrid
            StopSelectionMotion()
            _collapsedCategories.Clear()
            For Each entry As MASPropertyGridPropertyState In _properties
                If entry IsNot Nothing AndAlso MatchFilter(entry) Then _collapsedCategories.Add(entry.Category)
            Next
            InvalidateVisual()
            Return Me
        End Function

        Public Function SelectProperty(propertyName As String) As Boolean
            Dim normalized As String = MASPropertyGridText.Normalize(propertyName, String.Empty)
            If normalized.Length = 0 Then Return False

            For index As Integer = 0 To _properties.Count - 1
                If String.Equals(_properties(index).Name, normalized, StringComparison.OrdinalIgnoreCase) Then
                    SelectIndex(index)
                    Return True
                End If
            Next

            Return False
        End Function

        Public Function TrySetValue(propertyName As String, value As Object) As Boolean
            Dim entry As MASPropertyGridPropertyState = FindProperty(propertyName)
            If entry Is Nothing Then Return False
            Return ApplyEdit(entry, entry.TrySetValue(value))
        End Function

        Public Function TrySetValueText(propertyName As String, valueText As String) As Boolean
            Dim entry As MASPropertyGridPropertyState = FindProperty(propertyName)
            If entry Is Nothing Then Return False
            Return ApplyEdit(entry, entry.TrySetValueText(valueText))
        End Function

        Friend Function CreateSnapshot() As MASPropertyGridSnapshot
            Return New MASPropertyGridSnapshot(_properties)
        End Function

        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

        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:=MASPropertyGridTokens.DesiredSizeDip,
                minSize:=MASPropertyGridTokens.MinimumSizeDip,
                maxSize:=New SKSize(Single.PositiveInfinity, Single.PositiveInfinity),
                intent:=intent,
                context:=safeContext,
                canGrowWidth:=True,
                canGrowHeight:=True,
                contentInset:=MASLayoutInset.Empty,
                visualOverflowInset:=MASLayoutInset.Empty,
                hitOverflowInset:=MASLayoutInset.Empty,
                isFallback:=False)
        End Function

        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

            _contract.AssertConsumable()
            _lastRenderBoundsPx = pixelBounds
            Dim plan As MASPropertyGridLayoutPlan = MASPropertyGridLayoutPlan.Create(ctx, pixelBounds, SizeIntent)
            Dim dpi As Single = Math.Max(0.25F, plan.MetricDpi)
            _lastDpi = dpi

            If plan.GridRectPx.IsEmpty Then
                _lastRows = New ReadOnlyCollection(Of MASPropertyGridRenderRow)(New List(Of MASPropertyGridRenderRow)())
                _scrollOffsetPx = 0.0F
                _renderer.Draw(canvas, ctx, pixelBounds, _title, _lastRows, _selectedIndex, _hoverIndex, _contract, dpi, IsSelectionMotionActive(), _selectionMotionFromIndex, _selectionMotionToIndex, _selectionMotionProgress)
                Return
            End If

            Dim gridRect As SKRect = plan.GridRectPx
            Dim totalHeight As Single = CalculateTotalContentHeightPx(dpi)
            _scrollOffsetPx = MASPropertyGridTokens.NormalizeScrollOffset(_scrollOffsetPx, totalHeight, gridRect.Height)
            _lastRows = BuildRenderRows(pixelBounds, dpi, _scrollOffsetPx)

            _renderer.Draw(canvas, ctx, pixelBounds, _title, _lastRows, _selectedIndex, _hoverIndex, _contract, dpi, IsSelectionMotionActive(), _selectionMotionFromIndex, _selectionMotionToIndex, _selectionMotionProgress)
        End Sub

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

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

        Protected Overrides Function OnKeyDown(ctx As MASThemeContext, keyCode As Keys) As Boolean
            If Not Enabled OrElse Not Visible Then Return False

            Select Case keyCode
                Case Keys.Down
                    MoveSelection(1)
                    Return True
                Case Keys.Up
                    MoveSelection(-1)
                    Return True
                Case Keys.Space, Keys.Enter
                    Return ToggleSelectedBoolean()
                Case Keys.Left
                    Return SetSelectedCategoryCollapsed(True)
                Case Keys.Right
                    Return SetSelectedCategoryCollapsed(False)
                Case Keys.Back
                    If _filterText.Length > 0 Then
                        FilterText = _filterText.Substring(0, _filterText.Length - 1)
                        Return True
                    End If
                Case Keys.Delete, Keys.Escape
                    If _filterText.Length > 0 Then
                        FilterText = String.Empty
                        Return True
                    End If
            End Select

            Return False
        End Function

        Protected Overrides Function OnMouseDown(ctx As MASThemeContext, ptPx As SKPoint, button As Integer) As Boolean
            If button <> CInt(MouseButtons.Left) Then Return False
            If Not Enabled OrElse Not Visible Then Return False
            RequestFocus()
            Dim category As String = ResolveCategoryAt(ptPx)
            If category.Length > 0 Then
                ToggleCategory(category)
                Return True
            End If
            Dim index As Integer = ResolveEntryIndexAt(ptPx)
            If index >= 0 Then
                SelectIndex(index)
                Return True
            End If

            Return False
        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
            If Not Enabled OrElse Not Visible Then Return False
            Dim index As Integer = ResolveEntryIndexAt(ptPx)
            If index >= 0 AndAlso index = _selectedIndex Then
                Dim entry As MASPropertyGridPropertyState = _properties(index)
                If entry.Kind = MASPropertyGridValueKind.BooleanValue AndAlso entry.IsEditable Then
                    Return ApplyEdit(entry, entry.TrySetValue(Not CBool(entry.Value)))
                End If
            End If

            Return index >= 0
        End Function

        Protected Overrides Function OnTextInput(ctx As MASThemeContext, text As String) As Boolean
            If String.IsNullOrEmpty(text) Then Return False
            Dim appended As Boolean = False
            For Each ch As Char In text
                If Not Char.IsControl(ch) Then
                    _filterText &= ch
                    appended = True
                End If
            Next
            If appended Then
                _filterText = MASPropertyGridText.Normalize(_filterText, String.Empty)
                StopSelectionMotion()
                _scrollOffsetPx = 0.0F
                InvalidateVisual()
            End If
            Return appended
        End Function

        Protected Overrides Sub OnMouseMove(ctx As MASThemeContext, ptPx As SKPoint)
            Dim index As Integer = ResolveEntryIndexAt(ptPx)
            If index = _hoverIndex Then Return
            _hoverIndex = index
            InvalidateVisual()
        End Sub

        Protected Overrides Sub OnMouseLeave(ctx As MASThemeContext)
            If _hoverIndex <> -1 Then
                _hoverIndex = -1
                InvalidateVisual()
            End If
        End Sub

        Friend Overrides Function WantsPointerWheel(ctx As MASThemeContext, ptPx As SKPoint) As Boolean
            If Not Enabled OrElse Not Visible Then Return False
            If ctx Is Nothing Then Return False
            Dim boundsPx As SKRect = ResolveLastBounds(ctx)
            Dim dpi As Single = Math.Max(0.25F, _lastDpi)
            Dim gridRect As SKRect = MASPropertyGridTokens.ResolveGridRect(boundsPx, dpi)
            Return gridRect.Width > 1.0F AndAlso gridRect.Height > 1.0F AndAlso gridRect.Contains(ptPx.X, ptPx.Y)
        End Function

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

            Dim boundsPx As SKRect = ResolveLastBounds(ctx)
            Dim dpi As Single = Math.Max(0.25F, _lastDpi)
            Dim gridRect As SKRect = MASPropertyGridTokens.ResolveGridRect(boundsPx, dpi)
            If gridRect.Width <= 1.0F OrElse gridRect.Height <= 1.0F Then Return False

            Dim totalHeight As Single = CalculateTotalContentHeightPx(dpi)
            Dim stepPx As Single = MASPropertyGridTokens.ToPx(MASPropertyGridTokens.RowHeightDip * 3.0F, dpi)
            Dim targetOffset As Single = MASPropertyGridTokens.NormalizeScrollOffset(_scrollOffsetPx - (Math.Sign(delta) * stepPx), totalHeight, gridRect.Height)
            Return Math.Abs(targetOffset - _scrollOffsetPx) > 0.01F
        End Function

        Protected Overrides Sub OnMouseWheel(ctx As MASThemeContext, delta As Integer, ptPx As SKPoint)
            If Not Enabled OrElse Not Visible Then Return
            Dim boundsPx As SKRect = ResolveLastBounds(ctx)
            Dim dpi As Single = Math.Max(0.25F, _lastDpi)
            Dim gridRect As SKRect = MASPropertyGridTokens.ResolveGridRect(boundsPx, dpi)
            If gridRect.Width <= 1.0F OrElse gridRect.Height <= 1.0F Then Return

            Dim totalHeight As Single = CalculateTotalContentHeightPx(dpi)
            Dim stepPx As Single = MASPropertyGridTokens.ToPx(MASPropertyGridTokens.RowHeightDip * 3.0F, dpi)
            _scrollOffsetPx = MASPropertyGridTokens.NormalizeScrollOffset(_scrollOffsetPx - (Math.Sign(delta) * stepPx), totalHeight, gridRect.Height)
            InvalidateVisual()
        End Sub

        Protected Overrides Sub OnDetached()
            StopSelectionMotion()
            _hoverIndex = -1
            _lastRenderBoundsPx = SKRect.Empty
            _lastRows = New ReadOnlyCollection(Of MASPropertyGridRenderRow)(New List(Of MASPropertyGridRenderRow)())
            MyBase.OnDetached()
        End Sub

        Private Sub ReplaceProperties(properties As IEnumerable(Of MASPropertyGridPropertyState))
            StopSelectionMotion()
            _properties.Clear()
            If properties IsNot Nothing Then
                For Each entry As MASPropertyGridPropertyState In properties
                    If entry IsNot Nothing Then _properties.Add(entry)
                Next
            End If

            _selectedIndex = If(_properties.Count > 0, 0, -1)
            _hoverIndex = -1
            _scrollOffsetPx = 0.0F
            InvalidateVisual()
            RaiseEvent SelectionChanged(Me, EventArgs.Empty)
        End Sub

        Private Function BuildRenderRows(boundsPx As SKRect, dpi As Single, scrollOffsetPx As Single) As IReadOnlyList(Of MASPropertyGridRenderRow)
            Dim rows As New List(Of MASPropertyGridRenderRow)()
            Dim gridRect As SKRect = MASPropertyGridTokens.ResolveGridRect(boundsPx, dpi)
            If gridRect.Width <= 1.0F OrElse gridRect.Height <= 1.0F Then Return New ReadOnlyCollection(Of MASPropertyGridRenderRow)(rows)

            Dim y As Single = gridRect.Top - scrollOffsetPx
            Dim lastCategory As String = Nothing
            Dim categoryHeight As Single = MASPropertyGridTokens.ToPx(MASPropertyGridTokens.CategoryHeightDip, dpi)
            Dim rowHeight As Single = MASPropertyGridTokens.ToPx(MASPropertyGridTokens.RowHeightDip, dpi)
            Dim nameWidth As Single = Math.Min(MASPropertyGridTokens.ToPx(MASPropertyGridTokens.NameColumnWidthDip, dpi), gridRect.Width * 0.55F)

            For index As Integer = 0 To _properties.Count - 1
                Dim entry As MASPropertyGridPropertyState = _properties(index)
                If entry Is Nothing OrElse Not MatchFilter(entry) Then Continue For

                If Not String.Equals(lastCategory, entry.Category, StringComparison.OrdinalIgnoreCase) Then
                    Dim categoryRect As New SKRect(gridRect.Left, y, gridRect.Right, y + categoryHeight)
                    Dim collapsed As Boolean = _collapsedCategories.Contains(entry.Category)
                    Dim visibleCount As Integer = CountVisiblePropertiesInCategory(entry.Category)
                    Dim caption As String = If(collapsed, "▸ ", "▾ ") & entry.Category & "  " & visibleCount.ToString(CultureInfo.CurrentCulture)
                    rows.Add(New MASPropertyGridRenderRow(True, -1, entry.Category, caption, String.Empty, String.Empty, False, categoryRect, categoryRect, categoryRect, visibleCount, collapsed))
                    y += categoryHeight
                    lastCategory = entry.Category
                End If

                If _collapsedCategories.Contains(entry.Category) Then Continue For

                Dim rowRect As New SKRect(gridRect.Left, y, gridRect.Right, y + rowHeight)
                Dim nameRect As New SKRect(gridRect.Left, y, gridRect.Left + nameWidth, y + rowHeight)
                Dim valueRect As New SKRect(nameRect.Right, y, gridRect.Right, y + rowHeight)
                rows.Add(New MASPropertyGridRenderRow(False, index, entry.Category, entry.DisplayName, entry.ValueText, entry.KindName, entry.IsEditable, rowRect, nameRect, valueRect))
                y += rowHeight
            Next

            Return New ReadOnlyCollection(Of MASPropertyGridRenderRow)(rows)
        End Function

        Private Function CalculateTotalContentHeightPx(dpi As Single) As Single
            If _properties.Count = 0 Then Return 0.0F
            Dim categories As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
            Dim visiblePropertyCount As Integer = 0
            For Each entry As MASPropertyGridPropertyState In _properties
                If entry Is Nothing OrElse Not MatchFilter(entry) Then Continue For
                categories.Add(entry.Category)
                If Not _collapsedCategories.Contains(entry.Category) Then visiblePropertyCount += 1
            Next
            Return (CSng(categories.Count) * MASPropertyGridTokens.ToPx(MASPropertyGridTokens.CategoryHeightDip, dpi)) +
                   (CSng(visiblePropertyCount) * MASPropertyGridTokens.ToPx(MASPropertyGridTokens.RowHeightDip, dpi))
        End Function

        Private Function ResolveEntryIndexAt(ptPx As SKPoint) As Integer
            If _lastRows Is Nothing Then Return -1
            For Each row As MASPropertyGridRenderRow In _lastRows
                If row Is Nothing OrElse row.IsCategory Then Continue For
                If row.RowRectPx.Contains(ptPx) Then Return row.EntryIndex
            Next
            Return -1
        End Function

        Private Function ResolveCategoryAt(ptPx As SKPoint) As String
            If _lastRows Is Nothing Then Return String.Empty
            For Each row As MASPropertyGridRenderRow In _lastRows
                If row Is Nothing OrElse Not row.IsCategory Then Continue For
                If row.RowRectPx.Contains(ptPx) Then Return row.Category
            Next
            Return String.Empty
        End Function

        Private Sub SelectIndex(index As Integer)
            If index < -1 OrElse index >= _properties.Count Then Return
            If _selectedIndex = index Then Return
            Dim previousIndex As Integer = _selectedIndex
            _selectedIndex = index
            EnsureSelectionVisible()
            StartSelectionMotion(previousIndex, _selectedIndex)
            InvalidateVisual()
            RaiseEvent SelectionChanged(Me, EventArgs.Empty)
        End Sub

        Private Sub MoveSelection(delta As Integer)
            If _properties.Count = 0 Then Return
            Dim nextIndex As Integer = _selectedIndex
            If nextIndex < 0 Then nextIndex = 0 Else nextIndex += delta
            If nextIndex < 0 Then nextIndex = 0
            If nextIndex >= _properties.Count Then nextIndex = _properties.Count - 1
            SelectIndex(nextIndex)
        End Sub

        Private Sub EnsureSelectionVisible()
            If _selectedIndex < 0 Then Return
            Dim boundsPx As SKRect = _lastRenderBoundsPx
            If boundsPx.Width <= 1.0F OrElse boundsPx.Height <= 1.0F Then Return
            Dim dpi As Single = Math.Max(1.0F, _lastDpi)
            Dim gridRect As SKRect = MASPropertyGridTokens.ResolveGridRect(boundsPx, dpi)
            If gridRect.Height <= 1.0F Then Return

            Dim rows As IReadOnlyList(Of MASPropertyGridRenderRow) = BuildRenderRows(boundsPx, dpi, _scrollOffsetPx)
            For Each row As MASPropertyGridRenderRow In rows
                If row IsNot Nothing AndAlso Not row.IsCategory AndAlso row.EntryIndex = _selectedIndex Then
                    If row.RowRectPx.Top < gridRect.Top Then
                        _scrollOffsetPx = Math.Max(0.0F, _scrollOffsetPx - (gridRect.Top - row.RowRectPx.Top))
                    ElseIf row.RowRectPx.Bottom > gridRect.Bottom Then
                        _scrollOffsetPx += row.RowRectPx.Bottom - gridRect.Bottom
                    End If
                    Return
                End If
            Next
        End Sub

        Private Function SetSelectedCategoryCollapsed(collapsed As Boolean) As Boolean
            Dim entry As MASPropertyGridPropertyState = GetSelectedProperty()
            If entry Is Nothing Then Return False
            Dim changed As Boolean
            If collapsed Then
                changed = Not _collapsedCategories.Contains(entry.Category)
                _collapsedCategories.Add(entry.Category)
            Else
                changed = _collapsedCategories.Contains(entry.Category)
                _collapsedCategories.Remove(entry.Category)
            End If
            If Not changed Then Return False
            StopSelectionMotion()
            InvalidateVisual()
            Return True
        End Function


        Private Sub StartSelectionMotion(previousIndex As Integer, nextIndex As Integer)
            StopSelectionMotion()
            If previousIndex < 0 OrElse nextIndex < 0 Then Return
            If previousIndex = nextIndex Then Return

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

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

            _selectionMotionRunner = MASMotionSystem.CreateTimelineRunner(
                owner:=Me,
                consumerName:="MASPropertyGrid.Selection",
                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
            _selectionMotionFromIndex = -1
            _selectionMotionToIndex = -1
        End Sub

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

        Private Function MatchFilter(entry As MASPropertyGridPropertyState) As Boolean
            If entry Is Nothing Then Return False
            Dim q As String = If(_filterText, String.Empty).Trim()
            If q.Length = 0 Then Return True
            Return ContainsIgnoreCase(entry.Category, q) OrElse
                   ContainsIgnoreCase(entry.DisplayName, q) OrElse
                   ContainsIgnoreCase(entry.Name, q) OrElse
                   ContainsIgnoreCase(entry.ValueText, q) OrElse
                   ContainsIgnoreCase(entry.KindName, q)
        End Function

        Private Function CountVisiblePropertiesInCategory(category As String) As Integer
            Dim count As Integer = 0
            For Each entry As MASPropertyGridPropertyState In _properties
                If entry IsNot Nothing AndAlso String.Equals(entry.Category, category, StringComparison.OrdinalIgnoreCase) AndAlso MatchFilter(entry) Then count += 1
            Next
            Return count
        End Function

        Private Shared Function ContainsIgnoreCase(value As String, query As String) As Boolean
            Return If(value, String.Empty).IndexOf(If(query, String.Empty), StringComparison.OrdinalIgnoreCase) >= 0
        End Function

        Private Function ToggleSelectedBoolean() As Boolean
            Dim entry As MASPropertyGridPropertyState = GetSelectedProperty()
            If entry Is Nothing OrElse Not entry.IsEditable OrElse entry.Kind <> MASPropertyGridValueKind.BooleanValue Then Return False
            Return ApplyEdit(entry, entry.TrySetValue(Not CBool(entry.Value)))
        End Function

        Private Function ApplyEdit(entry As MASPropertyGridPropertyState, result As MASPropertyGridEditResult) As Boolean
            If entry Is Nothing OrElse result Is Nothing OrElse Not result.Success Then Return False
            InvalidateVisual()
            If Not Object.Equals(result.OldValue, result.NewValue) Then RaiseEvent PropertyValueChanged(Me, EventArgs.Empty)
            Return True
        End Function

        Private Function FindProperty(propertyName As String) As MASPropertyGridPropertyState
            Dim normalized As String = MASPropertyGridText.Normalize(propertyName, String.Empty)
            If normalized.Length = 0 Then Return Nothing
            Return _properties.FirstOrDefault(Function(item) String.Equals(item.Name, normalized, StringComparison.OrdinalIgnoreCase))
        End Function

        Private Function GetSelectedProperty() As MASPropertyGridPropertyState
            If _selectedIndex < 0 OrElse _selectedIndex >= _properties.Count Then Return Nothing
            Return _properties(_selectedIndex)
        End Function

        Private Function ResolveLastBounds(ctx As MASThemeContext) As SKRect
            If _lastRenderBoundsPx.Width > 0.0F AndAlso _lastRenderBoundsPx.Height > 0.0F Then Return _lastRenderBoundsPx
            Return GetPixelBounds(ctx)
        End Function

        Private Shared Function CreateSizeContext(context As MASLayoutMeasureContext) As MASSizeContext
            If context Is Nothing Then Return New MASSizeContext(New SKSize(Single.PositiveInfinity, Single.PositiveInfinity))
            Return context.ToSizeContext()
        End Function

    End Class

End Namespace
