'========================================================
' File: MASTileBox.vb  (SSOT + Fixes + Fluent SDK API)
'========================================================
Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports Nexamas.UI.Controls
Imports Nexamas.UI.Composition
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.Scroll
Imports Nexamas.UI.SkiaScroll
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Values
Imports Nexamas.UI.Virtualization
Imports SkiaSharp


Namespace Nexamas.UI.Components

    Partial Public NotInheritable Class MASTileBox

        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 OrElse pixelBounds.Height <= 1 Then Return

            If Not UseOffscreenFrameCache Then
                RenderCore(canvas, ctx, pixelBounds)
                Return
            End If

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)

            If CanUseFrameCache(ctx, pixelBounds, dpi) Then
                canvas.DrawBitmap(_cacheBitmap, pixelBounds.Left, pixelBounds.Top)
                Return
            End If

            RebuildFrameCache(ctx, pixelBounds, dpi)

            If _cacheBitmap IsNot Nothing Then
                canvas.DrawBitmap(_cacheBitmap, pixelBounds.Left, pixelBounds.Top)
            End If

        End Sub

        Private Sub RenderCore(canvas As SKCanvas, ctx As MASThemeContext, pixelBounds As SKRect)

            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If pixelBounds.Width <= 1 OrElse pixelBounds.Height <= 1 Then Return

            _contract.AssertConsumable()
            _tileContract.AssertConsumable()

            EnsureScroll(ctx)

            If _tilePainter Is Nothing Then
                _tilePainter = New MASTileVisualPainter(_primitives, _shadow)
            End If

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            Dim total As Integer = _items.Count
            _lastLayoutPlan = MASTileBoxLayoutPlan.Create(ctx, pixelBounds, SizeIntent)

            Dim host As SKRect = pixelBounds
            Dim railM As ScrollRailPolicy.Metrics = ScrollRailPolicy.Build(host, dpi)
            Dim content As SKRect = railM.ContentRect

            _lastHost = host
            _lastContent = content

            If total <= 0 Then
                _hoverIndex = -1
                _pressedIndex = -1
                ResetLayoutCache()
                MarkFrameCacheDirty()

                If _scroll IsNot Nothing Then
                    _scrollHost = host
                    _scroll.Update(_scrollHost, dpi, contentHeight:=0.0F)
                    _scroll.Draw(canvas, False)
                End If

                Return
            End If

            Dim m As IconGridLayout_Size80_EqualSpace.Metrics =
                IconGridLayout_Size80_EqualSpace.Compute(content, dpi, total, _lastLayoutPlan)

            If m.WorkRect.IsEmpty OrElse m.WorkRect.Width < 20 OrElse m.WorkRect.Height < 20 Then
                If _scroll IsNot Nothing Then
                    _scrollHost = host
                    _scroll.Update(_scrollHost, dpi, contentHeight:=0.0F)
                    _scroll.Draw(canvas, True)
                End If

                Return
            End If

            Dim nextCols As Integer = Math.Max(1, m.Cols)
            Dim nextRowStepPx As Single = Math.Max(1.0F, m.Footprint + m.GapY)

            If _lastWork.IsEmpty OrElse
               Not _lastWork.Equals(m.WorkRect) OrElse
               _lastCols <> nextCols OrElse
               Math.Abs(_rowStepPx - nextRowStepPx) >= 0.001F Then
                MarkTileLayoutChanged()
            End If

            _lastWork = m.WorkRect
            _lastCols = nextCols
            _lastRows = Math.Max(1, m.Rows)
            _lastTileSize = m.TileSize
            _lastShadowPad = m.ShadowPad
            _lastFoot = m.Footprint
            _lastGapX = m.GapX
            _lastGapY = m.GapY
            _lastContentH = m.ContentH

            _rowStepPx = nextRowStepPx
            _contentTopPadPx = Math.Max(0.0F, m.GapY + m.ShadowPad)
            _contentLeftPadPx = Math.Max(0.0F, m.GapX + m.ShadowPad)
            _workTopPx = m.WorkRect.Top
            _workLeftPx = m.WorkRect.Left

            If _hoverIndex >= total Then _hoverIndex = -1
            If _pressedIndex >= total Then _pressedIndex = -1

            _scrollHost = New SKRect(host.Left, m.WorkRect.Top, host.Right, m.WorkRect.Bottom)

            Dim offsetY As Single = If(_scroll Is Nothing, 0.0F, _scroll.Offset)

            If Single.IsNaN(offsetY) OrElse Single.IsInfinity(offsetY) OrElse offsetY < 0 Then
                offsetY = 0.0F
            End If

            Dim contentH As Single = ResolveTileGridVirtualScrollExtentPx(total, _lastCols, _rowStepPx)

            If _scroll IsNot Nothing Then
                _scroll.Update(_scrollHost, dpi, contentHeight:=contentH)
                RestorePendingScrollOffset()
            End If

            offsetY = If(_scroll Is Nothing, 0.0F, _scroll.Offset)

            If Single.IsNaN(offsetY) OrElse Single.IsInfinity(offsetY) OrElse offsetY < 0 Then
                offsetY = 0.0F
            End If

            Dim hostH As Single = _scrollHost.Height

            If hostH < 0 OrElse Single.IsNaN(hostH) OrElse Single.IsInfinity(hostH) Then
                hostH = 0.0F
            End If

            Dim maxOff As Single = Math.Max(0.0F, contentH - hostH)

            If offsetY > maxOff Then offsetY = maxOff

            _lastVirtualizationPlan = _virtualization.PrepareGridPlan(
                ctx:=ctx,
                itemCount:=total,
                columnCount:=_lastCols,
                rowExtentPx:=_rowStepPx,
                viewportRectPx:=_scrollHost,
                viewportOffsetPx:=offsetY,
                itemCollectionRevision:=_itemCollectionRevision,
                layoutRevision:=_layoutRevision)

            Dim save As Integer = canvas.Save()

            Try
                canvas.ClipRect(m.WorkRect, SKClipOperation.Intersect, True)

                Dim plan As MASVirtualizationRealizationPlan = _lastVirtualizationPlan

                If plan IsNot Nothing Then
                    For Each rowRequest As MASVirtualizationItemRequest In plan.Requests
                        Dim idxStart As Integer = MASTileGridVirtualizationConsumerCoordinator.ResolveFirstItemIndexForRow(rowRequest.Index, _lastCols)
                        Dim idxEnd As Integer = MASTileGridVirtualizationConsumerCoordinator.ResolveLastItemIndexForRow(rowRequest.Index, _lastCols, total)

                        If idxEnd < idxStart Then Continue For

                        For idx As Integer = idxStart To idxEnd
                            If idx < 0 OrElse idx >= total Then Continue For

                            Dim rr As SKRect = ComputeTileRect(idx, offsetY)
                            If rr.IsEmpty Then Continue For
                            If rr.Bottom < m.WorkRect.Top OrElse rr.Top > m.WorkRect.Bottom Then Continue For

                            Dim item As MASTileItem = _items(idx)
                            Dim bitmapSnapshot As MASTileBitmapRenderSnapshot =
                                If(item IsNot Nothing, item.GetBitmapRenderSnapshot(), MASTileBitmapRenderSnapshot.Empty)
                            Dim bmp As SKBitmap = bitmapSnapshot.TryGetDrawableBitmap()
                            Dim isSel As Boolean = IsTileSelected(item)

                            _tilePainter.DrawTile(
                                contract:=_tileContract,
                                canvas:=canvas,
                                ctx:=ctx,
                                r:=rr,
                                dpi:=dpi,
                                radiusPx:=0.0F,
                                isHover:=(idx = _hoverIndex),
                                isPressed:=(idx = _pressedIndex),
                                isSel:=isSel,
                                bmp:=bmp,
                                enabled:=Me.Enabled)
                        Next
                    Next

                    DrawSelectionMotionTile(canvas, ctx, dpi, offsetY)
                End If

            Finally
                canvas.RestoreToCount(save)
            End Try

            If _scroll IsNot Nothing Then
                _scroll.Draw(canvas, _isOverScrollbar)
            End If

        End Sub

        Private Shared Function ResolveTileGridVirtualScrollExtentPx(itemCount As Integer,
                                                                  columnCount As Integer,
                                                                  rowExtentPx As Single) As Single
            Dim rowCount As Integer = MASTileGridVirtualizationConsumerCoordinator.ResolveRowCount(itemCount, columnCount)
            If rowCount <= 0 Then Return 0.0F

            Dim safeRowExtent As Single = Math.Max(1.0F, MASVirtualizationNumericGuard.NormalizeNonNegative(rowExtentPx))
            Dim extent As Single = CSng(rowCount) * safeRowExtent

            If Single.IsNaN(extent) OrElse Single.IsInfinity(extent) OrElse extent < 0.0F Then
                Return 0.0F
            End If

            Return extent
        End Function

        Private Sub DrawSelectionMotionTile(canvas As SKCanvas,
                                             ctx As MASThemeContext,
                                             dpi As Single,
                                             offsetY As Single)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If _tilePainter Is Nothing Then Return
            If Not IsSelectionMotionActive() Then Return

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

            Dim p As Single = MASMotionSystem.ClampProgress(_selectionMotionProgress)
            Dim rr As New SKRect(
                fromRect.Left + ((toRect.Left - fromRect.Left) * p),
                fromRect.Top + ((toRect.Top - fromRect.Top) * p),
                fromRect.Right + ((toRect.Right - fromRect.Right) * p),
                fromRect.Bottom + ((toRect.Bottom - fromRect.Bottom) * p))

            If rr.IsEmpty OrElse rr.Width <= 1.0F OrElse rr.Height <= 1.0F Then Return
            If rr.Bottom < _lastWork.Top OrElse rr.Top > _lastWork.Bottom Then Return

            Dim item As MASTileItem = Nothing
            If _selectionMotionToIndex >= 0 AndAlso _selectionMotionToIndex < _items.Count Then
                item = _items(_selectionMotionToIndex)
            End If

            Dim bitmapSnapshot As MASTileBitmapRenderSnapshot =
                If(item IsNot Nothing, item.GetBitmapRenderSnapshot(), MASTileBitmapRenderSnapshot.Empty)
            Dim bmp As SKBitmap = bitmapSnapshot.TryGetDrawableBitmap()

            _tilePainter.DrawTile(
                contract:=_tileContract,
                canvas:=canvas,
                ctx:=ctx,
                r:=rr,
                dpi:=dpi,
                radiusPx:=0.0F,
                isHover:=False,
                isPressed:=False,
                isSel:=True,
                bmp:=bmp,
                enabled:=Me.Enabled)
        End Sub

        Private Function CanUseFrameCache(ctx As MASThemeContext, pixelBounds As SKRect, dpi As Single) As Boolean

            If _cacheDirty Then Return False
            If _cacheBitmap Is Nothing Then Return False
            If ctx Is Nothing Then Return False

            If Not _cacheBoundsPx.Equals(pixelBounds) Then Return False
            If Math.Abs(_cacheDpi - dpi) > 0.001F Then Return False
            If _cacheItemCount <> _items.Count Then Return False
            If _cacheHoverIndex <> _hoverIndex Then Return False
            If _cachePressedIndex <> _pressedIndex Then Return False
            If _cacheSelectedIndex <> Me.SelectedIndex Then Return False
            If _cacheIsOverScrollbar <> _isOverScrollbar Then Return False

            Dim off100 As Integer = GetScrollOffset100()

            If _cacheScrollOffset100 <> off100 Then Return False

            Dim theme As IMASAppTheme = ctx.Theme
            Dim themeId As String = Nothing
            Dim themeVersion As Integer = Integer.MinValue
            Dim themeStamp As Integer = ctx.ThemeStamp

            If theme IsNot Nothing Then
                themeId = If(theme.Id, String.Empty)
                themeVersion = theme.Version
            End If

            If _cacheThemeRef Is Nothing AndAlso theme IsNot Nothing Then Return False
            If Not Object.ReferenceEquals(_cacheThemeRef, theme) Then Return False
            If Not String.Equals(_cacheThemeId, themeId, StringComparison.Ordinal) Then Return False
            If _cacheThemeVersion <> themeVersion Then Return False
            If _cacheThemeStamp <> themeStamp Then Return False

            Return True

        End Function

        Private Sub RebuildFrameCache(ctx As MASThemeContext, pixelBounds As SKRect, dpi As Single)

            Dim w As Integer = Math.Max(1, CInt(Math.Ceiling(pixelBounds.Width)))
            Dim h As Integer = Math.Max(1, CInt(Math.Ceiling(pixelBounds.Height)))

            DisposeFrameCache()

            Try
                _cacheBitmap = New SKBitmap(w, h, SKColorType.Bgra8888, SKAlphaType.Premul)

                Using offscreen As New SKCanvas(_cacheBitmap)
                    offscreen.Clear(SKColors.Transparent)
                    offscreen.Translate(-pixelBounds.Left, -pixelBounds.Top)
                    RenderCore(offscreen, ctx, pixelBounds)
                End Using

                _cacheBoundsPx = pixelBounds
                _cacheDpi = dpi
                _cacheItemCount = _items.Count
                _cacheHoverIndex = _hoverIndex
                _cachePressedIndex = _pressedIndex
                _cacheSelectedIndex = Me.SelectedIndex
                _cacheScrollOffset100 = GetScrollOffset100()
                _cacheIsOverScrollbar = _isOverScrollbar

                Dim theme As IMASAppTheme = ctx.Theme

                _cacheThemeRef = theme
                _cacheThemeId = If(theme Is Nothing, Nothing, If(theme.Id, String.Empty))
                _cacheThemeVersion = If(theme Is Nothing, Integer.MinValue, theme.Version)
                _cacheThemeStamp = ctx.ThemeStamp

                _cacheDirty = False

            Catch masCaughtException20 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowRenderFallback(masCaughtException20)
                DisposeFrameCache()
                _cacheDirty = True
            End Try

        End Sub

        Private Function GetScrollOffset100() As Integer

            Dim off As Single = If(_scroll Is Nothing, 0.0F, _scroll.Offset)

            If Single.IsNaN(off) OrElse Single.IsInfinity(off) Then
                off = 0.0F
            End If

            Return CInt(Math.Round(off * 100.0F))

        End Function

        Private Sub MarkFrameCacheDirty()
            _cacheDirty = True
        End Sub

        Private Sub DisposeFrameCache()

            If _cacheBitmap IsNot Nothing Then
                Try
                    _cacheBitmap.Dispose()
                Catch masCaughtException9 As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException9)
                End Try

                _cacheBitmap = Nothing
            End If

            _cacheBoundsPx = SKRect.Empty
            _cacheDpi = -1.0F
            _cacheItemCount = -1
            _cacheHoverIndex = -2
            _cachePressedIndex = -2
            _cacheSelectedIndex = -2
            _cacheScrollOffset100 = -1
            _cacheIsOverScrollbar = False

            _cacheThemeRef = Nothing
            _cacheThemeId = Nothing
            _cacheThemeVersion = Integer.MinValue
            _cacheThemeStamp = Integer.MinValue

        End Sub

    End Class

End Namespace
