Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.Rendering

    Friend NotInheritable Class MessageBoxMeasureEngine

        Private Sub New()
        End Sub

        Friend Shared Function Measure(dpi As Single,
                               request As MessageBoxRequest,
                               ctx As MASThemeContext,
                               metrics As MessageBoxMetrics) As MessageBoxMeasureResult

            Dim result As New MessageBoxMeasureResult()

            If request Is Nothing Then request = New MessageBoxRequest()
            If ctx Is Nothing OrElse ctx.Typography Is Nothing Then Return result

            metrics = ResolveMetrics(request, metrics)
            dpi = PixelSnap.SafeDpi(dpi)

            Dim fixedPanelWidth As Single = Math.Max(1.0F, metrics.FixedPanelWidth * dpi)
            Dim preferredPanelHeight As Single = Math.Max(1.0F, metrics.FixedPanelHeight * dpi)
            Dim maxPanelHeight As Single = Math.Max(preferredPanelHeight, metrics.MaxPanelHeight * dpi)

            Dim buttons As IReadOnlyList(Of MASButtonSpec) =
        MessageBoxLayoutEngine.GetSafeButtons(request)

            Dim titleColor As SKColor = SKColors.Black
            Dim bodyColor As SKColor = SKColors.Black
            Dim detailColor As SKColor = SKColors.Gray

            Dim surfaceTheme As MASSurfaceThemeProfile = MASSurfaceThemeProfileResolver.Resolve(ctx)
            If surfaceTheme IsNot Nothing Then
                bodyColor = surfaceTheme.Chrome.TextTitle
                detailColor = surfaceTheme.Chrome.TextDetail
            End If

            Dim messagePaint As SKPaint =
        ctx.Typography.GetPaint(
            MASTypography.MASTextStyle.Body,
            bodyColor)

            Dim detailPaint As SKPaint =
        ctx.Typography.GetPaint(
            MASTypography.MASTextStyle.Small,
            detailColor)

            Dim buttonPaint As SKPaint =
        ctx.Typography.GetPaint(
            MASTypography.MASTextStyle.Button,
            bodyColor)

            If messagePaint Is Nothing OrElse
       detailPaint Is Nothing OrElse
       buttonPaint Is Nothing Then

                Return result
            End If

            Dim preferredCandidate As MessageBoxMeasureResult =
    MeasureFixedSize(
        panelWidth:=fixedPanelWidth,
        panelHeight:=preferredPanelHeight,
        dpi:=dpi,
        request:=request,
        buttons:=buttons,
        messagePaint:=messagePaint,
        detailPaint:=detailPaint,
        buttonPaint:=buttonPaint,
        metrics:=metrics,
        reserveScrollRail:=False)

            Dim best As MessageBoxMeasureResult = preferredCandidate

            If preferredCandidate.HasScrollableContent AndAlso
       maxPanelHeight > preferredPanelHeight + 0.5F Then

                Dim requiredHeight As Single =
            preferredPanelHeight + preferredCandidate.MaxContentScrollOffsetY

                If requiredHeight < preferredPanelHeight Then
                    requiredHeight = preferredPanelHeight
                End If

                If requiredHeight > maxPanelHeight Then
                    requiredHeight = maxPanelHeight
                End If

                best =
    MeasureFixedSize(
        panelWidth:=fixedPanelWidth,
        panelHeight:=requiredHeight,
        dpi:=dpi,
        request:=request,
        buttons:=buttons,
        messagePaint:=messagePaint,
        detailPaint:=detailPaint,
        buttonPaint:=buttonPaint,
        metrics:=metrics,
        reserveScrollRail:=False)
            End If

            If best.HasScrollableContent Then
                best =
    MeasureFixedSize(
        panelWidth:=fixedPanelWidth,
        panelHeight:=best.PanelHeight,
        dpi:=dpi,
        request:=request,
        buttons:=buttons,
        messagePaint:=messagePaint,
        detailPaint:=detailPaint,
        buttonPaint:=buttonPaint,
        metrics:=metrics,
        reserveScrollRail:=True)
            End If

            Return best
        End Function

        Private Shared Function ResolveMetrics(request As MessageBoxRequest,
                                               metrics As MessageBoxMetrics) As MessageBoxMetrics

            If request IsNot Nothing AndAlso request.MetricsOverride IsNot Nothing Then
                Return request.MetricsOverride
            End If

            If metrics IsNot Nothing Then
                Return metrics
            End If

            Return MessageBoxMetricsDefaults.Create()
        End Function

        Private Shared Function MeasureFixedSize(panelWidth As Single,
                                         panelHeight As Single,
                                         dpi As Single,
                                         request As MessageBoxRequest,
                                         buttons As IReadOnlyList(Of MASButtonSpec),
                                         messagePaint As SKPaint,
                                         detailPaint As SKPaint,
                                         buttonPaint As SKPaint,
                                         metrics As MessageBoxMetrics,
                                         reserveScrollRail As Boolean) As MessageBoxMeasureResult

            Dim result As New MessageBoxMeasureResult()

            result.PanelWidth = panelWidth
            result.PanelHeight = panelHeight

            Dim panelRect As New SKRect(0.0F, 0.0F, panelWidth, panelHeight)

            Dim shellSpec As MASPanelShellSpec =
                MessageBoxShellAdapter.CreateSpec(panelRect, request, New MessageBoxInteractionState())

            Dim shellResult As MASPanelShellResult =
             MASPanelShellRenderer.Measure(
    dpi:=dpi,
    spec:=shellSpec)

            If shellResult Is Nothing OrElse shellResult.ContentRectPx.IsEmpty Then
                Return result
            End If

            Dim hostRect As SKRect = shellResult.ContentRectPx

            Dim contentWidth As Single = Math.Max(0.0F, hostRect.Width)
            If contentWidth < 1.0F Then Return result

            Dim contentTopGap As Single = metrics.ContentTopGap * dpi
            Dim contentBottomGap As Single = metrics.ContentBottomGap * dpi
            Dim detailTopGap As Single = metrics.DetailTopGap * dpi
            Dim buttonsRowGap As Single = metrics.ButtonsRowGap * dpi
            Dim buttonsColumnGap As Single = metrics.ButtonsColumnGap * dpi
            Dim buttonHeight As Single = metrics.ButtonHeight * dpi
            Dim messageLeading As Single = MessageBoxTokens.MessageLeadingDip * dpi
            Dim detailLeading As Single = MessageBoxTokens.DetailLeadingDip * dpi
            Dim contentTopPad As Single = MessageBoxTokens.ContentTopPadDip * dpi

            Dim companyIconSize As Single = 0.0F
            If request.CompanyIcon IsNot Nothing AndAlso request.CompanyIcon.RequiresLayoutReservation() Then
                companyIconSize = ResolveAdaptiveSize(
                    panelWidth,
                    metrics.CompanyIconMinSize * dpi,
                    metrics.CompanyIconPreferredSize * dpi,
                    metrics.CompanyIconMaxSize * dpi,
                    360.0F * dpi,
                    620.0F * dpi)
            End If

            Dim companyBlockHeight As Single = 0.0F
            If companyIconSize > 0.0F Then
                companyBlockHeight = companyIconSize + (metrics.CompanyIconBottomGap * dpi)
            End If

            Dim messageTextWidth As Single =
                Math.Min(contentWidth, metrics.ContentMaxReadableWidth * dpi)

            If reserveScrollRail Then
                Dim railWidth As Single = FensterTokens.ScrollbarWidth * dpi
                Dim railGap As Single = FensterTokens.ScrollbarGapDip * dpi
                messageTextWidth -= (railWidth + railGap)
            End If

            If messageTextWidth < Nexamas.UI.Values.MessageBoxTokens.MessageTextMinWidthDip * dpi Then
                messageTextWidth = Nexamas.UI.Values.MessageBoxTokens.MessageTextMinWidthDip * dpi
            End If

            Dim messageLines As List(Of String) =
                WrapTextUnlimited(If(request.Message, String.Empty), messagePaint, messageTextWidth)

            Dim detailLines As List(Of String) =
                WrapTextUnlimited(If(request.Detail, String.Empty), detailPaint, messageTextWidth)

            Dim messageHeight As Single =
                MeasureParagraphHeight(messagePaint, messageLines, messageLeading)

            Dim detailHeight As Single =
                MeasureParagraphHeight(detailPaint, detailLines, detailLeading)

            Dim hasContent As Boolean =
                (messageHeight > 0.0F OrElse detailHeight > 0.0F)

            Dim effectiveButtonsTopGap As Single
            If detailLines.Count = 0 AndAlso messageLines.Count <= 1 Then
                effectiveButtonsTopGap = metrics.SingleLineButtonsTopGap * dpi
            Else
                effectiveButtonsTopGap = metrics.MultiLineButtonsTopGap * dpi
            End If

            Dim buttonWidths As List(Of Single) =
                MeasureButtonWidths(buttons, buttonPaint, metrics, dpi)

            Dim buttonRows As List(Of List(Of Integer)) =
                MessageBoxButtonLayoutPlanner.ArrangeRowsByWidths(
                    buttonWidths,
                    contentWidth,
                    buttonsColumnGap)

            Dim buttonsBlockWidth As Single =
                MessageBoxButtonLayoutPlanner.ComputeMaxRowWidth(
                    buttonWidths,
                    buttonRows,
                    buttonsColumnGap)

            Dim buttonsBlockHeight As Single = 0.0F
            Dim buttonRowsCount As Integer = buttonRows.Count

            If buttonRowsCount > 0 Then
                buttonsBlockHeight =
                    (buttonRowsCount * buttonHeight) +
                    (Math.Max(0, buttonRowsCount - 1) * buttonsRowGap)
            End If

            Dim contentNaturalHeight As Single = 0.0F

            If hasContent Then
                contentNaturalHeight += contentTopGap

                If messageHeight > 0.0F Then
                    contentNaturalHeight += contentTopPad
                    contentNaturalHeight += (metrics.MessageTopInset * dpi)
                    contentNaturalHeight += messageHeight
                End If

                If detailHeight > 0.0F Then
                    If messageHeight > 0.0F Then
                        contentNaturalHeight += detailTopGap
                    End If

                    contentNaturalHeight += detailHeight
                End If

                If detailHeight > 0.0F OrElse messageLines.Count > 1 Then
                    contentNaturalHeight += contentBottomGap
                Else
                    contentNaturalHeight += Math.Min(contentBottomGap, Nexamas.UI.Values.MessageBoxTokens.ScrollBottomGapCapDip * dpi)
                End If
            End If

            Dim checkBoxBlockHeight As Single = 0.0F

            If request.ShowCheckBox Then
                checkBoxBlockHeight =
                    (metrics.CheckBoxTopGap * dpi) +
                    (metrics.CheckBoxHeight * dpi)
            End If

            Dim buttonsSectionHeight As Single = 0.0F

            If buttonsBlockHeight > 0.0F Then
                buttonsSectionHeight =
                    effectiveButtonsTopGap +
                    buttonsBlockHeight +
                    (metrics.ButtonsBottomGap * dpi)
            End If

            Dim fixedNonScrollableHeight As Single =
                companyBlockHeight +
                checkBoxBlockHeight +
                buttonsSectionHeight

            Dim availableViewportHeight As Single =
                Math.Max(0.0F, hostRect.Height - fixedNonScrollableHeight)

            Dim viewportHeight As Single = 0.0F

            If hasContent Then
                viewportHeight = Math.Min(contentNaturalHeight, availableViewportHeight)

                Dim minViewportHeight As Single =
                    Math.Min(contentNaturalHeight, metrics.ContentMinViewportHeight * dpi)

                If viewportHeight < minViewportHeight AndAlso availableViewportHeight >= minViewportHeight Then
                    viewportHeight = minViewportHeight
                End If
            End If

            result.HeaderHeight = shellResult.HeaderRectPx.Height
            result.CompanyIconSize = companyIconSize
            result.MessageIconSize = shellResult.HeaderIconRectPx.Width
            result.CloseButtonSize = shellResult.HeaderCloseRectPx.Width

            result.TitleHeight = shellResult.HeaderTitleRectPx.Height
            result.MessageHeight = messageHeight
            result.DetailHeight = detailHeight

            result.TitleLineCount = If(String.IsNullOrWhiteSpace(request.Title), 0, 1)
            result.MessageLineCount = messageLines.Count
            result.DetailLineCount = detailLines.Count

            result.ContentWidthUsed = messageTextWidth
            result.ButtonsBlockWidth = buttonsBlockWidth
            result.ButtonsBlockHeight = buttonsBlockHeight
            result.ButtonRowsCount = buttonRowsCount
            result.EffectiveButtonsTopGap = effectiveButtonsTopGap

            result.ContentNaturalHeight = contentNaturalHeight
            result.ContentViewportHeight = viewportHeight
            result.MaxContentScrollOffsetY = Math.Max(0.0F, contentNaturalHeight - viewportHeight)
            result.HasScrollableContent = (result.MaxContentScrollOffsetY > 0.5F)

            result.ContentOverflowY = result.HasScrollableContent
            result.RequiresScroll = result.HasScrollableContent
            result.WasTitleTrimmed = False

            If Not String.IsNullOrWhiteSpace(request.Title) Then
                result.TitleLines.Add(request.Title)
            End If

            result.MessageLines.AddRange(messageLines)
            result.DetailLines.AddRange(detailLines)
            result.ButtonWidths.AddRange(buttonWidths)

            If fixedNonScrollableHeight > hostRect.Height Then
                result.IsHeightClamped = True
                result.ContentOverflowY = True
                result.RequiresScroll = True
            End If

            result.CheckBoxBlockHeight = checkBoxBlockHeight

            Return result
        End Function

        Private Shared Function ResolveAdaptiveSize(panelWidth As Single,
                                                    minSize As Single,
                                                    preferredSize As Single,
                                                    maxSize As Single,
                                                    minPanelWidth As Single,
                                                    maxPanelWidth As Single) As Single

            If preferredSize <= 0.0F AndAlso maxSize <= 0.0F Then Return 0.0F

            If maxPanelWidth <= minPanelWidth Then
                Return Math.Max(minSize, preferredSize)
            End If

            Dim t As Single = (panelWidth - minPanelWidth) / (maxPanelWidth - minPanelWidth)

            If t < 0.0F Then t = 0.0F
            If t > 1.0F Then t = 1.0F

            Dim size As Single = preferredSize + ((maxSize - preferredSize) * t)

            If size < minSize Then size = minSize
            If size > maxSize Then size = maxSize

            Return size
        End Function

        Private Shared Function MeasureButtonWidths(buttons As IReadOnlyList(Of MASButtonSpec),
                                                    buttonPaint As SKPaint,
                                                    metrics As MessageBoxMetrics,
                                                    dpi As Single) As List(Of Single)

            Dim list As New List(Of Single)()

            If buttons Is Nothing Then Return list
            If buttonPaint Is Nothing Then Return list

            Dim minW As Single = metrics.ButtonMinWidth * dpi
            Dim maxW As Single = metrics.ButtonMaxWidth * dpi
            Dim textPad As Single = metrics.ButtonTextHorizontalPadding * dpi

            Dim measureSession As MASTextWidthMeasureSession = MASTextWidthMeasureSession.ForPaint(buttonPaint)

            For Each button As MASButtonSpec In buttons
                Dim text As String = If(button IsNot Nothing, button.Text, String.Empty)
                Dim w As Single = measureSession.Measure(If(text, String.Empty)) + (textPad * 2.0F)

                If w < minW Then w = minW
                If w > maxW Then w = maxW

                list.Add(w)
            Next

            Return list
        End Function

        Private Shared Function MeasureParagraphHeight(paint As SKPaint,
                                                       lines As List(Of String),
                                                       extraLeading As Single) As Single

            If paint Is Nothing OrElse lines Is Nothing OrElse lines.Count = 0 Then
                Return 0.0F
            End If

            Dim fm As SKFontMetrics = paint.FontMetrics
            Dim baseLineHeight As Single = fm.Descent - fm.Ascent
            Dim lineHeight As Single = baseLineHeight + extraLeading

            Return baseLineHeight + ((lines.Count - 1) * lineHeight)
        End Function

        Private Shared Function WrapTextUnlimited(text As String,
                                                  paint As SKPaint,
                                                  maxWidth As Single) As List(Of String)

            Dim lines As New List(Of String)()

            If paint Is Nothing Then Return lines
            If String.IsNullOrWhiteSpace(text) Then Return lines
            If maxWidth <= 1.0F Then Return lines

            Dim measureSession As MASTextWidthMeasureSession = MASTextWidthMeasureSession.ForPaint(paint)

            Dim paragraphs() As String =
                text.Replace(vbCrLf, vbLf).
                     Replace(vbCr, vbLf).
                     Split(ControlChars.Lf)

            For Each paragraph As String In paragraphs
                Dim p As String = If(paragraph, String.Empty).Trim()

                If p.Length = 0 Then
                    lines.Add(String.Empty)
                    Continue For
                End If

                Dim words() As String =
                    p.Split({" "c}, StringSplitOptions.RemoveEmptyEntries)

                Dim current As String = String.Empty

                For Each word As String In words
                    Dim candidate As String =
                        If(current.Length = 0, word, current & " " & word)

                    If measureSession.Measure(candidate) <= maxWidth Then
                        current = candidate
                    Else
                        If current.Length > 0 Then
                            lines.Add(current)
                            current = String.Empty
                        End If

                        If measureSession.Measure(word) <= maxWidth Then
                            current = word
                        Else
                            Dim broken As List(Of String) =
                                BreakLongWord(word, measureSession, maxWidth)

                            For i As Integer = 0 To broken.Count - 2
                                lines.Add(broken(i))
                            Next

                            current = broken(broken.Count - 1)
                        End If
                    End If
                Next

                If current.Length > 0 Then
                    lines.Add(current)
                End If
            Next

            Return lines
        End Function

        Private Shared Function BreakLongWord(word As String,
                                              measureSession As MASTextWidthMeasureSession,
                                              maxWidth As Single) As List(Of String)

            Dim result As New List(Of String)()

            If String.IsNullOrEmpty(word) Then
                result.Add(String.Empty)
                Return result
            End If

            Dim current As String = String.Empty

            For Each ch As Char In word
                Dim candidate As String = current & ch

                If current.Length = 0 OrElse measureSession.Measure(candidate) <= maxWidth Then
                    current = candidate
                Else
                    result.Add(current)
                    current = ch.ToString()
                End If
            Next

            If current.Length > 0 Then
                result.Add(current)
            End If

            Return result
        End Function

    End Class

End Namespace