Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.Rendering

    Friend NotInheritable Class MessageBoxRenderer
        Implements IDisposable

#Region "Fields"

        Private Shared ReadOnly _buttonContractLazy As New Lazy(Of MASResolvedComponentContract)(
            Function()
                Return MASArchitectureRuntime.ResolveContractOrThrow(GetType(MASButton))
            End Function)

        Private Shared ReadOnly _checkBoxContractLazy As New Lazy(Of MASResolvedComponentContract)(
            Function()
                Return MASArchitectureRuntime.ResolveContractOrThrow(GetType(MASCheckBox))
            End Function)

        Private ReadOnly _buttonRenderer As MASButtonRenderer
        Private ReadOnly _checkBoxPainter As CheckBoxPainter

        Private _disposed As Boolean

#End Region

#Region "Ctor"

        Friend Sub New()
            _buttonRenderer = New MASButtonRenderer()
            _checkBoxPainter = New CheckBoxPainter()
        End Sub

#End Region

#Region "Render"

        Friend Sub Render(canvas As SKCanvas,
                          ctx As MASThemeContext,
                          panelRectPx As SKRect,
                          request As MessageBoxRequest,
                          state As MessageBoxInteractionState,
                          layout As MessageBoxLayoutResult)

            If _disposed Then Return
            If canvas Is Nothing Then Return
            If ctx Is Nothing Then Return
            If ctx.Typography Is Nothing Then Return
            If panelRectPx.IsEmpty OrElse panelRectPx.Width <= 0.0F OrElse panelRectPx.Height <= 0.0F Then Return
            If layout Is Nothing Then Return

            If request Is Nothing Then
                request = New MessageBoxRequest()
            End If

            If state Is Nothing Then
                state = New MessageBoxInteractionState()
            End If

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

            Dim metrics As MessageBoxMetrics =
                If(request.MetricsOverride, MessageBoxMetricsDefaults.Create())

            Dim shellSpec As MASPanelShellSpec =
                MessageBoxShellAdapter.CreateSpec(
                    panelRectPx:=panelRectPx,
                    request:=request,
                    state:=state)

            MASPanelShellRenderer.Draw(
                canvas:=canvas,
                ctx:=ctx,
                dpi:=dpi,
                spec:=shellSpec)

            DrawCompanyIcon(
                canvas:=canvas,
                ctx:=ctx,
                companyIconRect:=layout.CompanyIconRect,
                dpi:=dpi,
                request:=request)

            DrawContent(
                canvas:=canvas,
                ctx:=ctx,
                layout:=layout,
                dpi:=dpi,
                state:=state,
                metrics:=metrics)

            DrawCheckBox(
                canvas:=canvas,
                ctx:=ctx,
                layout:=layout,
                dpi:=dpi,
                request:=request,
                state:=state)

            DrawButtons(
                canvas:=canvas,
                ctx:=ctx,
                layout:=layout,
                dpi:=dpi,
                request:=request,
                state:=state)

        End Sub

#End Region

#Region "Surface Theme"

        Private Shared Function ResolveSurfaceTheme(ctx As MASThemeContext) As MASSurfaceThemeProfile
            If ctx Is Nothing Then Return Nothing
            Return MASSurfaceThemeProfileResolver.Resolve(ctx)
        End Function

#End Region

#Region "Company Icon"

        Private Shared Sub DrawCompanyIcon(canvas As SKCanvas,
                                           ctx As MASThemeContext,
                                           companyIconRect As SKRect,
                                           dpi As Single,
                                           request As MessageBoxRequest)

            If canvas Is Nothing Then Return
            If ctx Is Nothing Then Return
            If companyIconRect.IsEmpty Then Return
            If request Is Nothing Then Return
            If request.CompanyIcon Is Nothing Then Return

            Dim iconSpec As MessageBoxCompanyIconSpec = request.CompanyIcon
            Dim rect As SKRect = PixelSnap.SnapRectHalf(companyIconRect)

            If rect.IsEmpty OrElse rect.Width <= 0.0F OrElse rect.Height <= 0.0F Then
                Return
            End If

            If iconSpec.Mode = MessageBoxCompanyIconMode.ShowImage AndAlso iconSpec.Image IsNot Nothing Then
                canvas.DrawImage(iconSpec.Image, rect)
                Return
            End If

            If iconSpec.Mode <> MessageBoxCompanyIconMode.ShowFallback Then
                Return
            End If

            Dim surfaceTheme As MASSurfaceThemeProfile = ResolveSurfaceTheme(ctx)
            If surfaceTheme Is Nothing Then Return

            Dim fallback As String = If(iconSpec.FallbackText, String.Empty).Trim()
            If fallback.Length = 0 Then Return

            Dim baseColor As SKColor = surfaceTheme.Chrome.TextTitle
            Dim fillColor As SKColor = ColorMath.WithAlpha(baseColor, 28)
            Dim ringColor As SKColor = ColorMath.WithAlpha(baseColor, 92)

            Using fill As New SKPaint With {
                .IsAntialias = True,
                .IsDither = True,
                .Style = SKPaintStyle.Fill,
                .Color = fillColor
            }
                canvas.DrawOval(rect, fill)
            End Using

            Using ring As New SKPaint With {
                .IsAntialias = True,
                .IsDither = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = Math.Max(1.0F, 1.1F * dpi),
                .Color = ringColor
            }
                canvas.DrawOval(rect, ring)
            End Using

            If ctx.Typography Is Nothing Then Return

            Dim glyph As String = fallback.Substring(0, 1).ToUpperInvariant()

            TypographyTextPrimitives.DrawSingleLineText(
                canvas:=canvas,
                typography:=ctx.Typography,
                text:=glyph,
                bounds:=rect,
                dpi:=dpi,
                style:=MASTypography.MASTextStyle.Small,
                color:=surfaceTheme.Chrome.TextTitle,
                align:=TypographyTextPrimitives.TextAlign.Center,
                drawShadow:=False)

        End Sub

#End Region

#Region "Content"

        Private Shared Sub DrawContent(canvas As SKCanvas,
                                       ctx As MASThemeContext,
                                       layout As MessageBoxLayoutResult,
                                       dpi As Single,
                                       state As MessageBoxInteractionState,
                                       metrics As MessageBoxMetrics)

            If canvas Is Nothing Then Return
            If ctx Is Nothing Then Return
            If layout Is Nothing Then Return
            If ctx.Typography Is Nothing Then Return
            If layout.ContentViewportRect.IsEmpty Then Return

            Dim surfaceTheme As MASSurfaceThemeProfile = ResolveSurfaceTheme(ctx)
            If surfaceTheme Is Nothing Then Return

            Dim viewport As SKRect = PixelSnap.SnapRectHalf(layout.ContentViewportRect)
            If viewport.IsEmpty OrElse viewport.Width <= 0.0F OrElse viewport.Height <= 0.0F Then Return

            Dim scrollY As Single = 0.0F

            If state IsNot Nothing Then
                scrollY = state.ContentScrollOffsetY
            End If

            scrollY = Math.Max(0.0F, Math.Min(scrollY, layout.MaxContentScrollOffsetY))

            canvas.Save()

            Try
                canvas.ClipRect(viewport)

                If Not layout.MessageRect.IsEmpty AndAlso
                   layout.MessageLines IsNot Nothing AndAlso
                   layout.MessageLines.Count > 0 Then

                    Dim messagePaint As SKPaint =
                        ctx.Typography.GetPaint(
                            MASTypography.MASTextStyle.Body,
                            surfaceTheme.Chrome.TextTitle)

                    If messagePaint IsNot Nothing Then
                        DrawParagraphLines(
                            canvas:=canvas,
                            rect:=OffsetRect(layout.MessageRect, 0.0F, -scrollY),
                            lines:=layout.MessageLines,
                            paint:=messagePaint,
                            extraLeading:=MessageBoxTokens.MessageLeadingDip * dpi)
                    End If
                End If

                If Not layout.DetailRect.IsEmpty AndAlso
                   layout.DetailLines IsNot Nothing AndAlso
                   layout.DetailLines.Count > 0 Then

                    Dim detailPaint As SKPaint =
                        ctx.Typography.GetPaint(
                            MASTypography.MASTextStyle.Small,
                            surfaceTheme.Chrome.TextDetail)

                    If detailPaint IsNot Nothing Then
                        DrawParagraphLines(
                            canvas:=canvas,
                            rect:=OffsetRect(layout.DetailRect, 0.0F, -scrollY),
                            lines:=layout.DetailLines,
                            paint:=detailPaint,
                            extraLeading:=MessageBoxTokens.DetailLeadingDip * dpi)
                    End If
                End If

            Finally
                canvas.Restore()
            End Try

            DrawContentScrollFades(
                canvas:=canvas,
                ctx:=ctx,
                viewport:=viewport,
                scrollY:=scrollY,
                maxScrollY:=layout.MaxContentScrollOffsetY,
                dpi:=dpi,
                metrics:=metrics)

        End Sub

        Private Shared Sub DrawParagraphLines(canvas As SKCanvas,
                                              rect As SKRect,
                                              lines As List(Of String),
                                              paint As SKPaint,
                                              extraLeading As Single)

            If canvas Is Nothing Then Return
            If rect.IsEmpty Then Return
            If paint Is Nothing Then Return
            If lines Is Nothing OrElse lines.Count = 0 Then Return

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

            For Each line As String In lines
                canvas.DrawText(If(line, String.Empty), rect.Left, baseline, paint)
                baseline += lineHeight
            Next

        End Sub

        Private Shared Function OffsetRect(r As SKRect, dx As Single, dy As Single) As SKRect
            Return New SKRect(
                r.Left + dx,
                r.Top + dy,
                r.Right + dx,
                r.Bottom + dy)
        End Function

        Private Shared Sub DrawContentScrollFades(canvas As SKCanvas,
                                                  ctx As MASThemeContext,
                                                  viewport As SKRect,
                                                  scrollY As Single,
                                                  maxScrollY As Single,
                                                  dpi As Single,
                                                  metrics As MessageBoxMetrics)

            If canvas Is Nothing Then Return
            If ctx Is Nothing Then Return
            If viewport.IsEmpty Then Return
            If maxScrollY <= 0.5F Then Return

            If metrics Is Nothing Then
                metrics = MessageBoxMetricsDefaults.Create()
            End If

            Dim surfaceTheme As MASSurfaceThemeProfile = ResolveSurfaceTheme(ctx)
            If surfaceTheme Is Nothing Then Return

            Dim fadeH As Single =
                Math.Max(4.0F * dpi, metrics.ContentScrollFadeHeightDip * dpi)

            Dim baseColor As SKColor = surfaceTheme.Material.Mid
            Dim fadeColor As SKColor = ColorMath.WithAlpha(baseColor, 170)

            If scrollY > 0.5F Then
                Dim topFade As New SKRect(
                    viewport.Left,
                    viewport.Top,
                    viewport.Right,
                    Math.Min(viewport.Bottom, viewport.Top + fadeH))

                Using sh As SKShader = SKShader.CreateLinearGradient(
                    New SKPoint(topFade.Left, topFade.Top),
                    New SKPoint(topFade.Left, topFade.Bottom),
                    New SKColor() {fadeColor, SKColors.Transparent},
                    New Single() {0.0F, 1.0F},
                    SKShaderTileMode.Clamp)

                    Using p As New SKPaint With {
                        .IsAntialias = True,
                        .IsDither = True,
                        .Style = SKPaintStyle.Fill,
                        .Shader = sh
                    }
                        canvas.DrawRect(topFade, p)
                    End Using
                End Using
            End If

            If scrollY < (maxScrollY - 0.5F) Then
                Dim bottomFade As New SKRect(
                    viewport.Left,
                    Math.Max(viewport.Top, viewport.Bottom - fadeH),
                    viewport.Right,
                    viewport.Bottom)

                Using sh As SKShader = SKShader.CreateLinearGradient(
                    New SKPoint(bottomFade.Left, bottomFade.Top),
                    New SKPoint(bottomFade.Left, bottomFade.Bottom),
                    New SKColor() {SKColors.Transparent, fadeColor},
                    New Single() {0.0F, 1.0F},
                    SKShaderTileMode.Clamp)

                    Using p As New SKPaint With {
                        .IsAntialias = True,
                        .IsDither = True,
                        .Style = SKPaintStyle.Fill,
                        .Shader = sh
                    }
                        canvas.DrawRect(bottomFade, p)
                    End Using
                End Using
            End If

        End Sub

#End Region

#Region "CheckBox"

        Private Sub DrawCheckBox(canvas As SKCanvas,
                                 ctx As MASThemeContext,
                                 layout As MessageBoxLayoutResult,
                                 dpi As Single,
                                 request As MessageBoxRequest,
                                 state As MessageBoxInteractionState)

            If _disposed Then Return
            If canvas Is Nothing Then Return
            If ctx Is Nothing Then Return
            If layout Is Nothing Then Return
            If request Is Nothing Then Return
            If Not request.ShowCheckBox Then Return
            If layout.CheckBoxRowRect.IsEmpty Then Return

            _checkBoxPainter.Draw(
                canvas:=canvas,
                ctx:=ctx,
                boundsPx:=layout.CheckBoxRowRect,
                text:=If(request.CheckBoxText, String.Empty),
                dpi:=dpi,
                isHover:=If(state IsNot Nothing, state.CheckBoxHover, False),
                isPressed:=If(state IsNot Nothing, state.CheckBoxPressed, False),
                hasFocus:=False,
                isEnabled:=True,
                isChecked:=request.CheckBoxChecked,
                contract:=_checkBoxContractLazy.Value)

        End Sub

#End Region

#Region "Buttons"

        Private Sub DrawButtons(canvas As SKCanvas,
                                ctx As MASThemeContext,
                                layout As MessageBoxLayoutResult,
                                dpi As Single,
                                request As MessageBoxRequest,
                                state As MessageBoxInteractionState)

            If _disposed Then Return
            If canvas Is Nothing Then Return
            If ctx Is Nothing Then Return
            If layout Is Nothing Then Return
            If ctx.ButtonTheme Is Nothing Then Return

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

            If buttons Is Nothing OrElse buttons.Count = 0 Then Return
            If layout.ButtonRects Is Nothing Then Return
            If layout.ButtonRects.Count <> buttons.Count Then Return

            If state Is Nothing Then Return

            EnsureButtonStatesCapacity(state, buttons)

            Dim drawStates As New List(Of MASButtonState)(buttons.Count)

            For i As Integer = 0 To buttons.Count - 1
                Dim spec As MASButtonSpec = buttons(i)
                Dim buttonState As MASButtonState = state.ButtonStates(i)

                buttonState.IsEnabled = (spec IsNot Nothing AndAlso spec.IsEnabled)
                drawStates.Add(buttonState)
            Next

            _buttonRenderer.RenderMany(
                canvas:=canvas,
                ctx:=ctx,
                rects:=layout.ButtonRects,
                dpi:=dpi,
                specs:=buttons,
                states:=drawStates,
                contract:=_buttonContractLazy.Value)

        End Sub

        Private Shared Sub EnsureButtonStatesCapacity(state As MessageBoxInteractionState,
                                                      buttons As IReadOnlyList(Of MASButtonSpec))

            If state Is Nothing Then Return

            Dim count As Integer = If(buttons Is Nothing, 0, buttons.Count)

            If state.ButtonStates Is Nothing Then
                state.ButtonStates = New List(Of MASButtonState)(count)
            End If

            While state.ButtonStates.Count < count
                Dim spec As MASButtonSpec = buttons(state.ButtonStates.Count)
                Dim isEnabled As Boolean = (spec IsNot Nothing AndAlso spec.IsEnabled)
                state.ButtonStates.Add(MASButtonState.CreateDefault(isEnabled))
            End While

            While state.ButtonStates.Count > count
                state.ButtonStates.RemoveAt(state.ButtonStates.Count - 1)
            End While

        End Sub

#End Region

#Region "IDisposable"

        Public Sub Dispose() Implements IDisposable.Dispose

            If _disposed Then Return
            _disposed = True

            Try
                _buttonRenderer.Dispose()
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException1, "MessageBox.Renderer.ButtonRendererDispose")
            End Try

            Try
                _checkBoxPainter.Dispose()
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException2, "MessageBox.Renderer.CheckBoxPainterDispose")
            End Try

        End Sub

#End Region

    End Class

End Namespace