Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Globalization
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
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

    Partial Public NotInheritable Class MASPagination

#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 plan As MASPaginationLayoutPlan = MASPaginationLayoutPlan.Create(ctx, pixelBounds, SizeIntent)
            _lastLayoutPlan = plan
            Dim dpi As Single = plan.Dpi
            Dim bounds As SKRect = PixelSnap.SnapRect(pixelBounds, dpi)
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return

            DrawSurface(canvas, ctx, bounds, plan)

            Dim content As New SKRect(bounds.Left + plan.PaddingHorizontalPx,
                                      bounds.Top + plan.PaddingVerticalPx,
                                      bounds.Right - plan.PaddingHorizontalPx,
                                      bounds.Bottom - plan.PaddingVerticalPx)
            If content.Width <= 1.0F OrElse content.Height <= 1.0F Then Return

            Dim isRtl As Boolean = ResolveRightToLeft()
            _lastRenderedRtl = isRtl

            Dim nodes As List(Of PageNode) = BuildRenderNodes(ctx, content.Width, plan, isRtl)
            DrawNodes(canvas, ctx, content, nodes, plan, isRtl)
            DrawFocusRing(canvas, ctx, plan)
        End Sub

        Private Sub DrawSurface(canvas As SKCanvas,
                                ctx As MASThemeContext,
                                bounds As SKRect,
                                plan As MASPaginationLayoutPlan)
            If plan Is Nothing Then Return
            Dim dpi As Single = plan.Dpi
            Dim radius As Single = PixelSnap.SafeRadius(plan.NodeRadiusPx + 2.0F * dpi)
            Dim fillColor As SKColor = ResolvePanelColor(ctx).WithAlpha(PaginationTokens.SurfaceFillAlpha)
            Dim borderColor As SKColor = ResolveMutedTextColor(ctx).WithAlpha(PaginationTokens.SurfaceBorderAlpha)

            Using fill As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Fill,
                .Color = fillColor
            }
                canvas.DrawRoundRect(bounds, radius, radius, fill)
            End Using

            Dim strokeRect As SKRect = bounds
            strokeRect.Inflate(-0.5F * dpi, -0.5F * dpi)
            Using stroke As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = Math.Max(1.0F, 1.0F * dpi),
                .Color = borderColor
            }
                canvas.DrawRoundRect(strokeRect, radius, radius, stroke)
            End Using
        End Sub

        Private Function BuildRenderNodes(ctx As MASThemeContext,
                                          maxWidthPx As Single,
                                          plan As MASPaginationLayoutPlan,
                                          isRtl As Boolean) As List(Of PageNode)
            If plan Is Nothing OrElse plan.CollapseNodes Then Return BuildCompactNodes(ctx, If(plan Is Nothing, PixelSnap.SafeDpi(If(ctx Is Nothing, 1.0F, ctx.Dpi)), plan.MetricDpi), isRtl, plan)
            Dim nodes As List(Of PageNode) = If(plan.PreferCompactNodes, BuildCompactNodes(ctx, plan.MetricDpi, isRtl, plan), BuildFullNodes(ctx, plan.MetricDpi, isRtl, plan))
            If CalculateNodesWidth(nodes, plan) <= maxWidthPx Then Return nodes
            Return BuildCompactNodes(ctx, plan.MetricDpi, isRtl, plan)
        End Function

        Private Function BuildFullNodes(ctx As MASThemeContext,
                                        dpi As Single,
                                        isRtl As Boolean,
                                        plan As MASPaginationLayoutPlan) As List(Of PageNode)
            Dim nodes As New List(Of PageNode)()
            nodes.Add(CreateNode(PaginationTokens.FirstText, PageNodeAction.FirstPage, 1, CanMovePrevious, ctx, dpi, True, plan))
            nodes.Add(CreateNode(ResolvePreviousText(isRtl), PageNodeAction.PreviousPage, Math.Max(1, _currentPage - 1), CanMovePrevious, ctx, dpi, True, plan))

            For Each page As Integer In BuildVisiblePages()
                If page = 0 Then
                    nodes.Add(CreateNode(PaginationTokens.EllipsisText, PageNodeAction.None, 0, False, ctx, dpi, False, plan))
                Else
                    nodes.Add(CreateNode(page.ToString(CultureInfo.InvariantCulture), PageNodeAction.PageNumber, page, True, ctx, dpi, False, plan))
                End If
            Next

            nodes.Add(CreateNode(ResolveNextText(isRtl), PageNodeAction.NextPage, Math.Min(_totalPages, _currentPage + 1), CanMoveNext, ctx, dpi, True, plan))
            nodes.Add(CreateNode(PaginationTokens.LastText, PageNodeAction.LastPage, _totalPages, CanMoveNext, ctx, dpi, True, plan))
            Return nodes
        End Function

        Private Function BuildCompactNodes(ctx As MASThemeContext,
                                           dpi As Single,
                                           isRtl As Boolean,
                                           plan As MASPaginationLayoutPlan) As List(Of PageNode)
            Dim nodes As New List(Of PageNode)()
            nodes.Add(CreateNode(ResolvePreviousText(isRtl), PageNodeAction.PreviousPage, Math.Max(1, _currentPage - 1), CanMovePrevious, ctx, dpi, True, plan))
            nodes.Add(CreateNode(_currentPage.ToString(CultureInfo.InvariantCulture) & " / " & _totalPages.ToString(CultureInfo.InvariantCulture), PageNodeAction.None, _currentPage, False, ctx, dpi, False, plan, If(plan Is Nothing, PaginationTokens.SummaryNodeMinWidthDip * dpi, plan.SummaryNodeMinWidthPx)))
            nodes.Add(CreateNode(ResolveNextText(isRtl), PageNodeAction.NextPage, Math.Min(_totalPages, _currentPage + 1), CanMoveNext, ctx, dpi, True, plan))
            Return nodes
        End Function

        Private Function BuildVisiblePages() As List(Of Integer)
            Dim pages As New List(Of Integer)()
            If _totalPages <= _pageWindow + 2 Then
                For page As Integer = 1 To _totalPages
                    pages.Add(page)
                Next
                Return pages
            End If

            Dim middleWindow As Integer = Math.Max(PaginationTokens.MinimumPageWindow, _pageWindow)
            Dim half As Integer = middleWindow \ 2
            Dim startPage As Integer = Math.Max(2, _currentPage - half)
            Dim endPage As Integer = Math.Min(_totalPages - 1, startPage + middleWindow - 1)
            startPage = Math.Max(2, endPage - middleWindow + 1)

            pages.Add(1)
            If startPage > 2 Then pages.Add(0)

            For page As Integer = startPage To endPage
                pages.Add(page)
            Next

            If endPage < _totalPages - 1 Then pages.Add(0)
            pages.Add(_totalPages)
            Return pages
        End Function

        Private Function CreateNode(text As String,
                                    action As PageNodeAction,
                                    page As Integer,
                                    enabled As Boolean,
                                    ctx As MASThemeContext,
                                    dpi As Single,
                                    navigation As Boolean,
                                    plan As MASPaginationLayoutPlan,
                                    Optional minimumWidthPx As Single = 0.0F) As PageNode
            Dim width As Single = If(plan Is Nothing,
                                     If(navigation, PaginationTokens.NavigationNodeWidthDip * dpi, PaginationTokens.NodeMinWidthDip * dpi),
                                     If(navigation, plan.NavigationNodeWidthPx, plan.NodeMinWidthPx))
            Dim padX As Single = If(plan Is Nothing, PaginationTokens.NodePaddingHorizontalDip * dpi, plan.NodePaddingHorizontalPx)
            width = Math.Max(width, minimumWidthPx)
            width = Math.Max(width, MeasureTextWidthPx(ctx, text) + padX * 2.0F)

            Return New PageNode With {
                .Text = If(text, String.Empty),
                .Action = action,
                .Page = page,
                .Enabled = enabled,
                .WidthPx = width,
                .Rect = SKRect.Empty
            }
        End Function

        Private Shared Function CalculateNodesWidth(nodes As List(Of PageNode),
                                                    plan As MASPaginationLayoutPlan) As Single
            If nodes Is Nothing OrElse nodes.Count = 0 Then Return 0.0F

            Dim width As Single = 0.0F
            For i As Integer = 0 To nodes.Count - 1
                width += nodes(i).WidthPx
                If i < nodes.Count - 1 Then width += If(plan Is Nothing, 0.0F, plan.NodeGapPx)
            Next
            Return width
        End Function

        Private Sub DrawNodes(canvas As SKCanvas,
                              ctx As MASThemeContext,
                              content As SKRect,
                              nodes As List(Of PageNode),
                              plan As MASPaginationLayoutPlan,
                              isRtl As Boolean)
            _lastNodes = Array.Empty(Of PageNode)()
            If nodes Is Nothing OrElse nodes.Count = 0 Then Return

            Dim arranged As New List(Of PageNode)()
            If plan Is Nothing Then Return
            Dim dpi As Single = plan.Dpi
            Dim totalWidth As Single = CalculateNodesWidth(nodes, plan)
            Dim startX As Single = If(isRtl, content.Right, content.Left + Math.Max(0.0F, (content.Width - totalWidth) * 0.5F))
            If isRtl Then startX = content.Right - Math.Max(0.0F, (content.Width - totalWidth) * 0.5F)

            If isRtl Then
                Dim x As Single = startX
                For i As Integer = 0 To nodes.Count - 1
                    Dim node As PageNode = nodes(i)
                    node.Rect = New SKRect(x - node.WidthPx, content.Top, x, content.Bottom)
                    DrawNode(canvas, ctx, node, arranged.Count, plan)
                    arranged.Add(node)
                    x = node.Rect.Left - plan.NodeGapPx
                Next
            Else
                Dim x As Single = startX
                For i As Integer = 0 To nodes.Count - 1
                    Dim node As PageNode = nodes(i)
                    node.Rect = New SKRect(x, content.Top, x + node.WidthPx, content.Bottom)
                    DrawNode(canvas, ctx, node, arranged.Count, plan)
                    arranged.Add(node)
                    x = node.Rect.Right + plan.NodeGapPx
                Next
            End If

            DrawSelectionMotionIndicator(canvas, ctx, arranged, plan)

            _lastNodes = arranged.ToArray()
        End Sub

        Private Sub DrawNode(canvas As SKCanvas,
                             ctx As MASThemeContext,
                             node As PageNode,
                             nodeIndex As Integer,
                             plan As MASPaginationLayoutPlan)
            If node.Rect.Width <= 1.0F OrElse node.Rect.Height <= 1.0F Then Return

            If plan Is Nothing Then Return
            Dim dpi As Single = plan.Dpi
            Dim rect As SKRect = PixelSnap.SnapRect(node.Rect, dpi)
            Dim selectionMotionActive As Boolean = IsSelectionMotionActive()
            Dim active As Boolean = node.Action = PageNodeAction.PageNumber AndAlso node.Page = _currentPage AndAlso Not selectionMotionActive
            Dim hovered As Boolean = node.Enabled AndAlso nodeIndex = _hoverNodeIndex
            Dim pressed As Boolean = node.Enabled AndAlso nodeIndex = _pressedNodeIndex
            Dim radius As Single = PixelSnap.SafeRadius(plan.NodeRadiusPx)

            Dim fillAlpha As Byte = 0
            Dim fillColor As SKColor = ResolveAccentColor(ctx)
            If active Then
                fillAlpha = PaginationTokens.NodeSelectedAlpha
            ElseIf pressed Then
                fillAlpha = PaginationTokens.NodePressedAlpha
            ElseIf hovered Then
                fillAlpha = PaginationTokens.NodeHoverAlpha
            End If

            If fillAlpha > 0 Then
                Using fill As New SKPaint With {
                    .IsAntialias = True,
                    .Style = SKPaintStyle.Fill,
                    .Color = fillColor.WithAlpha(fillAlpha)
                }
                    canvas.DrawRoundRect(rect, radius, radius, fill)
                End Using
            End If

            Dim textRect As New SKRect(rect.Left + plan.NodePaddingHorizontalPx,
                                       rect.Top,
                                       rect.Right - plan.NodePaddingHorizontalPx,
                                       rect.Bottom)
            If textRect.Width <= 1.0F Then Return

            TypographyTextPrimitives.DrawSingleLineText(
                canvas:=canvas,
                ctx:=ctx,
                text:=node.Text,
                bounds:=textRect,
                dpi:=dpi,
                style:=MASTypography.MASTextStyle.Small,
                color:=ResolveNodeTextColor(ctx, active, node.Enabled),
                align:=TypographyTextPrimitives.TextAlign.Center,
                drawShadow:=False)
        End Sub

        Private Sub DrawSelectionMotionIndicator(canvas As SKCanvas,
                                                     ctx As MASThemeContext,
                                                     nodes As List(Of PageNode),
                                                     plan As MASPaginationLayoutPlan)
            If canvas Is Nothing OrElse ctx Is Nothing OrElse plan Is Nothing Then Return
            If Not IsSelectionMotionActive() Then Return
            If nodes Is Nothing OrElse nodes.Count = 0 Then Return

            Dim fromRect As SKRect = FindPageNodeRect(nodes, _selectionMotionFromPage)
            Dim toRect As SKRect = FindPageNodeRect(nodes, _selectionMotionToPage)
            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 = ResolveAccentColor(ctx).WithAlpha(PaginationTokens.NodeSelectedAlpha)
            }
                canvas.DrawRoundRect(rect, PixelSnap.SafeRadius(plan.NodeRadiusPx), PixelSnap.SafeRadius(plan.NodeRadiusPx), fill)
            End Using
        End Sub

        Private Shared Function FindPageNodeRect(nodes As List(Of PageNode),
                                                   page As Integer) As SKRect
            If nodes Is Nothing OrElse page < 1 Then Return SKRect.Empty

            For Each node As PageNode In nodes
                If node.Action = PageNodeAction.PageNumber AndAlso node.Page = page Then Return node.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 Sub DrawFocusRing(canvas As SKCanvas,
                                  ctx As MASThemeContext,
                                  plan As MASPaginationLayoutPlan)
            If plan Is Nothing Then Return
            Dim dpi As Single = plan.Dpi
            If Not HasKeyboardFocus Then Return
            If _lastNodes Is Nothing OrElse _lastNodes.Length = 0 Then Return

            For i As Integer = 0 To _lastNodes.Length - 1
                If Not _lastNodes(i).Enabled Then Continue For
                If _lastNodes(i).Action <> PageNodeAction.PageNumber Then Continue For
                If _lastNodes(i).Page <> _currentPage Then Continue For

                Dim rect As SKRect = PixelSnap.SnapRect(_lastNodes(i).Rect, dpi)
                rect.Inflate(plan.FocusInflatePx, plan.FocusInflatePx)
                Dim radius As Single = PixelSnap.SafeRadius(plan.NodeRadiusPx + plan.FocusInflatePx)

                Using stroke As New SKPaint With {
                    .IsAntialias = True,
                    .Style = SKPaintStyle.Stroke,
                    .StrokeWidth = plan.FocusStrokePx,
                    .Color = ResolveAccentColor(ctx).WithAlpha(PaginationTokens.FocusAlpha)
                }
                    canvas.DrawRoundRect(rect, radius, radius, stroke)
                End Using
                Exit For
            Next
        End Sub

#End Region

    End Class

End Namespace
