Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports Nexamas.UI.Components
Imports Nexamas.UI.Controls
Imports Nexamas.UI.General
Imports Nexamas.UI.Icons
Imports Nexamas.UI.Theming
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Values
Imports Nexamas.UI.Architecture
Imports SkiaSharp

Namespace Nexamas.UI.Rendering

    Partial Friend NotInheritable Class MASToastRenderer
        Private Structure ToastMeasure
            Friend WidthPx As Single
            Friend HeightPx As Single
            Friend HasTitle As Boolean
            Friend HasAction As Boolean
            Friend HasUndo As Boolean
            Friend HasClose As Boolean
            Friend TitleHeightPx As Single
            Friend MessageHeightPx As Single
            Friend ActionWidthPx As Single
        End Structure

        Friend Function Arrange(ctx As MASThemeContext,
                                viewportPx As SKRect,
                                items As IList(Of MASToastItem),
                                placement As MASToastPlacement) As Dictionary(Of Integer, MASToastLayoutInfo)

            Dim result As New Dictionary(Of Integer, MASToastLayoutInfo)(Math.Max(0, If(items Is Nothing, 0, items.Count)))

            If _disposed Then Return result
            If ctx Is Nothing Then Return result
            If items Is Nothing OrElse items.Count = 0 Then Return result
            If viewportPx.Width <= 1.0F OrElse viewportPx.Height <= 1.0F Then Return result

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

            Dim marginPx As Single = ToastTokens.OuterMargin * dpi
            Dim gapPx As Single = ToastTokens.StackGap * dpi

            Dim maxWidthPx As Single =
                Math.Min(ToastTokens.MaxWidth * dpi,
                         Math.Max(160.0F, viewportPx.Width * 0.42F))

            Dim minWidthPx As Single =
                Math.Min(ToastTokens.MinWidth * dpi, maxWidthPx)

            Dim yCursor As Single

            Select Case placement
                Case MASToastPlacement.BottomRight, MASToastPlacement.BottomLeft
                    yCursor = viewportPx.Bottom - marginPx
                Case Else
                    yCursor = viewportPx.Top + marginPx
            End Select

            For i As Integer = items.Count - 1 To 0 Step -1
                Dim item As MASToastItem = items(i)
                If item Is Nothing Then Continue For

                Dim visibility As Single = item.OpenProgress * (1.0F - item.CloseProgress)
                If visibility <= 0.001F Then Continue For

                Dim opacity As Single = Math.Max(0.0F, Math.Min(1.0F, visibility))
                Dim slidePx As Single = (1.0F - visibility) * (ToastTokens.EnterSlide * dpi)

                Dim measured As ToastMeasure =
                    MeasureToast(
                        ctx:=ctx,
                        title:=item.Title,
                        message:=item.Message,
                        actionText:=item.ActionText,
                        hasUndo:=item.UndoCallback IsNot Nothing,
                        dismissible:=item.Dismissible,
                        showLifetimeBar:=item.ShowLifetimeBar,
                        hasTimedLifetime:=item.DurationMs > 0,
                        maxWidthPx:=maxWidthPx,
                        minWidthPx:=minWidthPx)

                Dim panelRect As SKRect

                Select Case placement
                    Case MASToastPlacement.BottomRight
                        panelRect = New SKRect(
                            viewportPx.Right - marginPx - measured.WidthPx,
                            yCursor - measured.HeightPx - slidePx,
                            viewportPx.Right - marginPx,
                            yCursor - slidePx)

                        yCursor = panelRect.Top - gapPx

                    Case MASToastPlacement.BottomLeft
                        panelRect = New SKRect(
                            viewportPx.Left + marginPx,
                            yCursor - measured.HeightPx - slidePx,
                            viewportPx.Left + marginPx + measured.WidthPx,
                            yCursor - slidePx)

                        yCursor = panelRect.Top - gapPx

                    Case MASToastPlacement.TopRight
                        panelRect = New SKRect(
                            viewportPx.Right - marginPx - measured.WidthPx,
                            yCursor + slidePx,
                            viewportPx.Right - marginPx,
                            yCursor + measured.HeightPx + slidePx)

                        yCursor = panelRect.Bottom + gapPx

                    Case Else
                        panelRect = New SKRect(
                            viewportPx.Left + marginPx,
                            yCursor + slidePx,
                            viewportPx.Left + marginPx + measured.WidthPx,
                            yCursor + measured.HeightPx + slidePx)

                        yCursor = panelRect.Bottom + gapPx
                End Select

                Dim remainingRatio As Single = 0.0F

                If item.DurationMs > 0 Then
                    remainingRatio =
                        CSng(Math.Max(0.0R,
                                      Math.Min(1.0R,
                                               item.RemainingMs / Math.Max(1, item.DurationMs))))
                End If

                result(item.Id) =
                    BuildLayout(
                        ctx:=ctx,
                        toastId:=item.Id,
                        panelRectPx:=PixelSnap.SnapRectHalf(panelRect),
                        measure:=measured,
                        opacity:=opacity,
                        remainingRatio:=remainingRatio,
                        dismissible:=item.Dismissible,
                        showLifetimeBar:=item.ShowLifetimeBar)
            Next

            Return result
        End Function


        Private Function MeasureToast(ctx As MASThemeContext,
                                      title As String,
                                      message As String,
                                      actionText As String,
                                      hasUndo As Boolean,
                                      dismissible As Boolean,
                                      showLifetimeBar As Boolean,
                                      hasTimedLifetime As Boolean,
                                      maxWidthPx As Single,
                                      minWidthPx As Single) As ToastMeasure

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

            Dim padX As Single = ToastTokens.PaddingX * dpi
            Dim padY As Single = ToastTokens.PaddingY * dpi
            Dim titleGap As Single = ToastTokens.TitleGap * dpi
            Dim bodyGap As Single = ToastTokens.BodyGap * dpi
            Dim accentW As Single = ToastTokens.AccentStripWidth * dpi
            Dim compactButtonSize As Single = ResolveCompactGlyphButtonSizePx(dpi)

            Dim titlePaint As SKPaint =
                ctx.Typography.GetPaint(MASTypography.MASTextStyle.Title, SKColors.White)

            Dim bodyPaint As SKPaint =
                ctx.Typography.GetPaint(MASTypography.MASTextStyle.Body, SKColors.White)

            Dim actionPaint As SKPaint =
                ctx.Typography.GetPaint(MASTypography.MASTextStyle.Button, SKColors.White)

            Dim hasTitle As Boolean = Not String.IsNullOrWhiteSpace(title)
            Dim hasAction As Boolean = Not String.IsNullOrWhiteSpace(actionText)
            Dim hasClose As Boolean = dismissible

            Dim compactButtonCount As Integer = 0
            If hasUndo Then compactButtonCount += 1
            If hasClose Then compactButtonCount += 1

            Dim reserveRight As Single = ResolveCompactGlyphGroupReservePx(dpi, compactButtonCount)

            Dim usableTextWidth As Single =
                maxWidthPx - accentW - (padX * 2.0F) - reserveRight

            If usableTextWidth < 80.0F Then usableTextWidth = 80.0F

            Dim titleLines As List(Of String) =
                WrapLines(title, titlePaint, usableTextWidth, 2)

            Dim bodyLines As List(Of String) =
                WrapLines(message, bodyPaint, usableTextWidth, ToastTokens.MaxBodyLines)

            Dim titleLineH As Single = titlePaint.TextSize * MASTypography.WrapLineHeightMul
            Dim bodyLineH As Single = bodyPaint.TextSize * MASTypography.WrapLineHeightMul

            Dim titleH As Single = 0.0F

            If hasTitle AndAlso titleLines.Count > 0 Then
                titleH = titleLines.Count * titleLineH
            End If

            Dim bodyH As Single = Math.Max(bodyLineH, bodyLines.Count * bodyLineH)

            Dim actionW As Single = 0.0F

            If hasAction Then
                Dim buttonHorizontalPad As Single = ButtonTokens.PaddingX * dpi
                Dim actionMeasureSession As MASTextWidthMeasureSession = MASTextWidthMeasureSession.ForPaint(actionPaint)

                actionW =
                    Math.Max(
                        ToastTokens.ActionMinWidth * dpi,
                        actionMeasureSession.Measure(actionText) + (buttonHorizontalPad * 2.0F))
            End If

            Dim widthPx As Single = Math.Max(minWidthPx, maxWidthPx)

            Dim heightPx As Single = padY + titleH

            If titleH > 0.0F Then
                heightPx += titleGap
            End If

            heightPx += bodyH

            If hasAction Then
                heightPx += bodyGap + (ToastTokens.ActionHeight * dpi)
            End If

            If showLifetimeBar AndAlso hasTimedLifetime Then
                heightPx += Math.Max(4.0F, LifetimeBarVerticalReserveDip * dpi)
            End If

            If hasClose OrElse hasUndo Then
                heightPx = Math.Max(heightPx, (padY * 2.0F) + compactButtonSize)
            End If

            heightPx += padY

            If heightPx < ToastTokens.MinHeight * dpi Then
                heightPx = ToastTokens.MinHeight * dpi
            End If

            Return New ToastMeasure With {
                .WidthPx = PixelSnap.SnapHalf(widthPx),
                .HeightPx = PixelSnap.SnapHalf(heightPx),
                .HasTitle = hasTitle,
                .HasAction = hasAction,
                .HasUndo = hasUndo,
                .HasClose = hasClose,
                .TitleHeightPx = PixelSnap.SnapHalf(titleH),
                .MessageHeightPx = PixelSnap.SnapHalf(bodyH),
                .ActionWidthPx = PixelSnap.SnapHalf(actionW)
            }
        End Function

        Private Function BuildLayout(ctx As MASThemeContext,
                                     toastId As Integer,
                                     panelRectPx As SKRect,
                                     measure As ToastMeasure,
                                     opacity As Single,
                                     remainingRatio As Single,
                                     dismissible As Boolean,
                                     showLifetimeBar As Boolean) As MASToastLayoutInfo

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

            Dim padX As Single = ToastTokens.PaddingX * dpi
            Dim padY As Single = ToastTokens.PaddingY * dpi
            Dim accentW As Single = ToastTokens.AccentStripWidth * dpi
            Dim compactButtonSize As Single = ResolveCompactGlyphButtonSizePx(dpi)
            Dim contentGap As Single = ResolveCompactGlyphContentGapPx(dpi)
            Dim buttonGap As Single = ResolveCompactGlyphButtonGapPx(dpi)
            Dim titleGap As Single = ToastTokens.TitleGap * dpi
            Dim bodyGap As Single = ToastTokens.BodyGap * dpi
            Dim actionH As Single = ToastTokens.ActionHeight * dpi

            Dim contentLeft As Single = panelRectPx.Left + accentW + padX
            Dim contentTop As Single = panelRectPx.Top + padY
            Dim contentRight As Single = panelRectPx.Right - padX

            Dim closeRect As SKRect = SKRect.Empty
            Dim undoRect As SKRect = SKRect.Empty
            Dim buttonTop As Single = panelRectPx.Top + padY
            Dim nextButtonRight As Single = panelRectPx.Right - padX

            If dismissible Then
                closeRect =
                    PixelSnap.SnapRectHalf(
                        New SKRect(
                            nextButtonRight - compactButtonSize,
                            buttonTop,
                            nextButtonRight,
                            buttonTop + compactButtonSize))

                nextButtonRight = closeRect.Left - buttonGap
                contentRight = closeRect.Left - contentGap
            End If

            If measure.HasUndo Then
                undoRect =
                    PixelSnap.SnapRectHalf(
                        New SKRect(
                            nextButtonRight - compactButtonSize,
                            buttonTop,
                            nextButtonRight,
                            buttonTop + compactButtonSize))

                contentRight = undoRect.Left - contentGap
            End If

            Dim titleRect As SKRect = SKRect.Empty
            Dim messageRect As SKRect = SKRect.Empty
            Dim actionRect As SKRect = SKRect.Empty
            Dim lifetimeRect As SKRect = SKRect.Empty

            Dim y As Single = contentTop

            If measure.HasTitle Then
                titleRect =
                    PixelSnap.SnapRectHalf(
                        New SKRect(contentLeft, y, contentRight, y + measure.TitleHeightPx))

                y = titleRect.Bottom + titleGap
            End If

            messageRect =
                PixelSnap.SnapRectHalf(
                    New SKRect(contentLeft, y, contentRight, y + measure.MessageHeightPx))

            y = messageRect.Bottom

            If measure.HasAction Then
                y += bodyGap

                actionRect =
                    PixelSnap.SnapRectHalf(
                        New SKRect(
                            contentLeft,
                            y,
                            contentLeft + measure.ActionWidthPx,
                            y + actionH))
            End If

            If showLifetimeBar AndAlso remainingRatio > 0.0F Then
                Dim horizontalInsetPx As Single = LifetimeBarHorizontalInsetDip * dpi
                Dim bottomInsetPx As Single = Math.Max(5.0F, LifetimeBarBottomInsetDip * dpi)
                Dim barH As Single = Math.Max(2.0F, ToastTokens.LifetimeBarHeight * dpi)

                Dim barLeft As Single = contentLeft + horizontalInsetPx
                Dim barRight As Single = contentRight - horizontalInsetPx

                If barRight > barLeft + 2.0F Then
                    lifetimeRect =
                        PixelSnap.SnapRectHalf(
                            New SKRect(
                                barLeft,
                                panelRectPx.Bottom - bottomInsetPx - barH,
                                barRight,
                                panelRectPx.Bottom - bottomInsetPx))
                End If
            End If

            Dim accentRect As SKRect =
                PixelSnap.SnapRectHalf(
                    New SKRect(
                        panelRectPx.Left,
                        panelRectPx.Top,
                        panelRectPx.Left + accentW,
                        panelRectPx.Bottom))

            Return New MASToastLayoutInfo With {
                .ToastId = toastId,
                .PanelRectPx = panelRectPx,
                .AccentRectPx = accentRect,
                .TitleRectPx = titleRect,
                .MessageRectPx = messageRect,
                .ActionRectPx = actionRect,
                .UndoRectPx = undoRect,
                .CloseRectPx = closeRect,
                .LifetimeBarRectPx = lifetimeRect,
                .Opacity = opacity,
                .RemainingRatio = remainingRatio
            }
        End Function
    End Class

End Namespace
