Option Strict On
Option Explicit On

Imports System
Imports Nexamas.UI.General
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    Friend NotInheritable Class BackgroundPainter
        Implements IDisposable

#Region "Public"

        Friend Property Palette As MASBackgroundPalette = MASBackgroundPalette.Default

#End Region

#Region "Fields"

        Private ReadOnly _effects As New BackgroundEffectsPainter()

        Private _cachedW As Integer = -1
        Private _cachedH As Integer = -1
        Private _cachedDpi As Single = Single.NaN
        Private _cachedPalette As MASBackgroundPalette = CType(-1, MASBackgroundPalette)

        Private _cachedTopLift As Single = Single.NaN
        Private _cachedBottomCool As Single = Single.NaN
        Private _cachedVignetteAlpha As Byte = 0

        Private _cachedNoiseEnabled As Boolean
        Private _cachedNoiseAlpha As Byte = 0
        Private _cachedNoiseFreq As Single = Single.NaN
        Private _cachedNoiseOct As Integer = -1

        Private _cachedOverlayTintStrength As Single = Single.NaN
        Private _cachedOverlayTintMix As Single = Single.NaN
        Private _cachedOverlayGrainAlpha As Byte = 0
        Private _cachedOverlayGrainScale As Single = Single.NaN
        Private _cachedOverlayNoiseFreq As Single = Single.NaN
        Private _cachedOverlayNoiseOct As Integer = -1

        Private _cachedBaseBg As SKColor
        Private _cachedColdTint As SKColor
        Private _cachedWashBase As SKColor
        Private _cachedVignetteBase As SKColor
        Private _cachedTransparent As SKColor

        Private _cachedVignetteRadiusFactor As Single = Single.NaN
        Private _cachedTopGlowHeightBias As Single = Single.NaN
        Private _cachedVignetteCenterLiftMul As Single = Single.NaN
        Private _cachedVignetteInnerStopBase As Single = Single.NaN
        Private _cachedVignetteInnerStopLiftMul As Single = Single.NaN
        Private _cachedVignetteInnerStopMax As Single = Single.NaN

        Private _bgImage As SKImage = Nothing
        Private _disposed As Boolean

#End Region

#Region "Render"

        Friend Sub Render(canvas As SKCanvas,
                          ctx As MASThemeContext,
                          rect As SKRect,
                          dpi As Single)

            If _disposed Then Return
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If rect.Width <= 1.0F OrElse rect.Height <= 1.0F Then Return

            dpi = PixelSnap.SafeDpi(dpi)

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

            If _bgImage Is Nothing OrElse NeedsRebuild(ctx, w, h, dpi) Then
                Rebuild(ctx, w, h, dpi)
            End If

            If _bgImage IsNot Nothing Then
                canvas.DrawImage(_bgImage, rect.Left, rect.Top)
            End If
        End Sub

        Friend Sub InvalidateCache()
            _cachedW = -1
            _cachedH = -1
            _cachedDpi = Single.NaN
            _cachedPalette = CType(-1, MASBackgroundPalette)

            _cachedTopLift = Single.NaN
            _cachedBottomCool = Single.NaN
            _cachedVignetteAlpha = 0

            _cachedNoiseEnabled = False
            _cachedNoiseAlpha = 0
            _cachedNoiseFreq = Single.NaN
            _cachedNoiseOct = -1

            _cachedOverlayTintStrength = Single.NaN
            _cachedOverlayTintMix = Single.NaN
            _cachedOverlayGrainAlpha = 0
            _cachedOverlayGrainScale = Single.NaN
            _cachedOverlayNoiseFreq = Single.NaN
            _cachedOverlayNoiseOct = -1

            _cachedBaseBg = Nothing
            _cachedColdTint = Nothing
            _cachedWashBase = Nothing
            _cachedVignetteBase = Nothing
            _cachedTransparent = Nothing

            _cachedVignetteRadiusFactor = Single.NaN
            _cachedTopGlowHeightBias = Single.NaN
            _cachedVignetteCenterLiftMul = Single.NaN
            _cachedVignetteInnerStopBase = Single.NaN
            _cachedVignetteInnerStopLiftMul = Single.NaN
            _cachedVignetteInnerStopMax = Single.NaN

            If _bgImage IsNot Nothing Then
                _bgImage.Dispose()
                _bgImage = Nothing
            End If
        End Sub

#End Region

#Region "Cache Logic"

        Private Function NeedsRebuild(ctx As MASThemeContext,
                                      w As Integer,
                                      h As Integer,
                                      dpi As Single) As Boolean

            If w <> _cachedW OrElse h <> _cachedH Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(dpi, _cachedDpi, BackgroundTokens.CacheEpsDpi) Then Return True
            If _cachedPalette <> Palette Then Return True

            Dim bg As IMASBackgroundTheme = BackgroundThemeSupport.ResolveBackgroundTheme(ctx)
            Dim pal As BackgroundThemeSupport.BgPalette = BackgroundThemeSupport.GetPalette(ctx, Palette)

            If Not BackgroundThemeSupport.NearlyEqual(bg.TopLift, _cachedTopLift, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.BottomCool, _cachedBottomCool, BackgroundTokens.CacheEpsFloat) Then Return True
            If bg.VignetteAlpha <> _cachedVignetteAlpha Then Return True

            If bg.NoiseEnabled <> _cachedNoiseEnabled Then Return True
            If bg.NoiseAlpha <> _cachedNoiseAlpha Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.NoiseFrequency, _cachedNoiseFreq, BackgroundTokens.CacheEpsFloat) Then Return True
            If bg.NoiseOctaves <> _cachedNoiseOct Then Return True

            If Not BackgroundThemeSupport.NearlyEqual(bg.OverlayTintStrength, _cachedOverlayTintStrength, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.OverlayTintMix, _cachedOverlayTintMix, BackgroundTokens.CacheEpsFloat) Then Return True
            If bg.OverlayGrainAlpha <> _cachedOverlayGrainAlpha Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.OverlayGrainScale, _cachedOverlayGrainScale, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.OverlayNoiseFrequency, _cachedOverlayNoiseFreq, BackgroundTokens.CacheEpsFloat) Then Return True
            If bg.OverlayNoiseOctaves <> _cachedOverlayNoiseOct Then Return True

            If pal.BaseBg <> _cachedBaseBg Then Return True
            If pal.ColdTint <> _cachedColdTint Then Return True
            If pal.WashBase <> _cachedWashBase Then Return True
            If pal.VignetteBase <> _cachedVignetteBase Then Return True
            If Values.Color.Core.Transparent <> _cachedTransparent Then Return True

            If Not BackgroundThemeSupport.NearlyEqual(bg.VignetteRadiusFactor, _cachedVignetteRadiusFactor, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.TopGlowHeightBias, _cachedTopGlowHeightBias, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.VignetteCenterLiftMul, _cachedVignetteCenterLiftMul, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.VignetteInnerStopBase, _cachedVignetteInnerStopBase, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.VignetteInnerStopLiftMul, _cachedVignetteInnerStopLiftMul, BackgroundTokens.CacheEpsFloat) Then Return True
            If Not BackgroundThemeSupport.NearlyEqual(bg.VignetteInnerStopMax, _cachedVignetteInnerStopMax, BackgroundTokens.CacheEpsFloat) Then Return True

            Return False
        End Function

#End Region

#Region "Rebuild"

        Private Sub Rebuild(ctx As MASThemeContext,
                            w As Integer,
                            h As Integer,
                            dpi As Single)

            _bgImage?.Dispose()
            _bgImage = Nothing

            Dim bg As IMASBackgroundTheme = BackgroundThemeSupport.ResolveBackgroundTheme(ctx)
            Dim pal As BackgroundThemeSupport.BgPalette = BackgroundThemeSupport.GetPalette(ctx, Palette)

            _cachedW = w
            _cachedH = h
            _cachedDpi = dpi
            _cachedPalette = Palette

            _cachedTopLift = bg.TopLift
            _cachedBottomCool = bg.BottomCool
            _cachedVignetteAlpha = bg.VignetteAlpha

            _cachedNoiseEnabled = bg.NoiseEnabled
            _cachedNoiseAlpha = bg.NoiseAlpha
            _cachedNoiseFreq = bg.NoiseFrequency
            _cachedNoiseOct = bg.NoiseOctaves

            _cachedOverlayTintStrength = bg.OverlayTintStrength
            _cachedOverlayTintMix = bg.OverlayTintMix
            _cachedOverlayGrainAlpha = bg.OverlayGrainAlpha
            _cachedOverlayGrainScale = bg.OverlayGrainScale
            _cachedOverlayNoiseFreq = bg.OverlayNoiseFrequency
            _cachedOverlayNoiseOct = bg.OverlayNoiseOctaves

            _cachedBaseBg = pal.BaseBg
            _cachedColdTint = pal.ColdTint
            _cachedWashBase = pal.WashBase
            _cachedVignetteBase = pal.VignetteBase
            _cachedTransparent = Values.Color.Core.Transparent

            _cachedVignetteRadiusFactor = bg.VignetteRadiusFactor
            _cachedTopGlowHeightBias = bg.TopGlowHeightBias
            _cachedVignetteCenterLiftMul = bg.VignetteCenterLiftMul
            _cachedVignetteInnerStopBase = bg.VignetteInnerStopBase
            _cachedVignetteInnerStopLiftMul = bg.VignetteInnerStopLiftMul
            _cachedVignetteInnerStopMax = bg.VignetteInnerStopMax

            _effects.Palette = Palette

            Dim info As New SKImageInfo(w, h, SKColorType.Bgra8888, SKAlphaType.Premul)

            Using surface As SKSurface = SKSurface.Create(info)
                Dim c As SKCanvas = surface.Canvas
                Dim r As New SKRect(0, 0, w, h)

                c.Clear(_cachedBaseBg)

                DrawBaseGradient(c, r)
                DrawTopGlow(c, r, bg)
                DrawVignette(c, r, bg)
                DrawBaseNoise(c, r)
                _effects.Render(c, ctx, r, dpi)

                _bgImage = surface.Snapshot()
            End Using
        End Sub

        Private Sub DrawBaseGradient(canvas As SKCanvas, r As SKRect)
            Dim topCol As SKColor = ColorMath.Mix(_cachedBaseBg, _cachedWashBase, _cachedTopLift)
            Dim bottomCol As SKColor = ColorMath.Mix(_cachedBaseBg, _cachedColdTint, _cachedBottomCool)

            Using fill As New SKPaint With {
                .IsAntialias = True,
                .IsDither = True
            }
                Using grad As SKShader = SKShader.CreateLinearGradient(
                    New SKPoint(r.MidX, r.Top),
                    New SKPoint(r.MidX, r.Bottom),
                    New SKColor() {topCol, bottomCol},
                    New Single() {0.0F, 1.0F},
                    SKShaderTileMode.Clamp)

                    fill.Shader = grad
                    canvas.DrawRect(r, fill)
                    fill.Shader = Nothing
                End Using
            End Using
        End Sub

        Private Sub DrawTopGlow(canvas As SKCanvas,
                                r As SKRect,
                                bg As IMASBackgroundTheme)

            Using glowPaint As New SKPaint With {
                .IsAntialias = True,
                .IsDither = True,
                .BlendMode = SKBlendMode.SoftLight
            }

                Dim glowH As Single =
                    r.Height * (1.0F - _cachedVignetteRadiusFactor + _cachedTopGlowHeightBias)

                Dim glowRect As New SKRect(r.Left, r.Top, r.Right, r.Top + glowH)

                Dim glowMix As Single = BackgroundThemeSupport.Clamp01(bg.OverlayTintMix)
                Dim glowAlpha As Byte =
                    CByte(Math.Max(0, Math.Min(255, CInt(bg.OverlayTintStrength * 255.0F))))

                Dim glowCol As SKColor =
                    ColorMath.Mix(_cachedWashBase, _cachedColdTint, glowMix).WithAlpha(glowAlpha)

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

                    glowPaint.Shader = glowShader
                    canvas.DrawRect(glowRect, glowPaint)
                    glowPaint.Shader = Nothing
                End Using
            End Using
        End Sub

        Private Sub DrawVignette(canvas As SKCanvas,
                                 r As SKRect,
                                 bg As IMASBackgroundTheme)

            If _cachedVignetteAlpha <= 0 Then Return

            Using vPaint As New SKPaint With {
                .IsAntialias = True,
                .BlendMode = SKBlendMode.SrcOver
            }

                Dim center As New SKPoint(
                    r.MidX,
                    r.MidY - (r.Height * (_cachedTopLift * _cachedVignetteCenterLiftMul))
                )

                Dim vrad As Single = r.Width * _cachedVignetteRadiusFactor

                Dim innerStop As Single =
                    _cachedVignetteInnerStopBase +
                    (_cachedTopLift * _cachedVignetteInnerStopLiftMul)

                innerStop = Math.Max(0.0F, Math.Min(_cachedVignetteInnerStopMax, innerStop))

                Using vShader As SKShader = SKShader.CreateRadialGradient(
                    center,
                    vrad,
                    New SKColor() {
                        _cachedTransparent,
                        _cachedVignetteBase.WithAlpha(_cachedVignetteAlpha)
                    },
                    New Single() {innerStop, 1.0F},
                    SKShaderTileMode.Clamp)

                    vPaint.Shader = vShader
                    canvas.DrawRect(r, vPaint)
                    vPaint.Shader = Nothing
                End Using
            End Using
        End Sub

        Private Sub DrawBaseNoise(canvas As SKCanvas, r As SKRect)
            If Not _cachedNoiseEnabled Then Return
            If _cachedNoiseAlpha <= 0 Then Return

            Using noisePaint As New SKPaint With {
                .IsAntialias = True,
                .IsDither = True,
                .BlendMode = SKBlendMode.SoftLight,
                .Color = _cachedWashBase.WithAlpha(_cachedNoiseAlpha)
            }

                Dim f As Single = Math.Max(BackgroundTokens.NoiseFreqMin, _cachedNoiseFreq)

                Using noiseShader As SKShader = SKShader.CreatePerlinNoiseFractalNoise(
                    f, f,
                    Math.Max(1, _cachedNoiseOct),
                    0.0F)

                    noisePaint.Shader = noiseShader
                    canvas.DrawRect(r, noisePaint)
                    noisePaint.Shader = Nothing
                End Using
            End Using
        End Sub

#End Region

#Region "Dispose"

        Public Sub Dispose() Implements IDisposable.Dispose
            If _disposed Then Return
            _disposed = True

            _bgImage?.Dispose()
            _bgImage = Nothing

            Try
                _effects.Dispose()
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException1)
            End Try
        End Sub

#End Region

    End Class

End Namespace
