Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.Rendering

    Friend NotInheritable Class MASGroupBoxRenderer
        Implements IDisposable

        Friend Enum GroupBoxRenderSurfaceSource
            Acrylic = 0
            Plain = 1
            TransparentFrame = 2
        End Enum

        Friend Enum GroupBoxRenderBorderRecipe
            ListSurface = 0
            AcrylicSurface = 1
        End Enum

        Friend Enum GroupBoxRenderTitlePlateStrength
            Soft = 0
            Premium = 1
            Strong = 2
        End Enum

        Friend Structure RenderSpec
            Friend Title As String
            Friend SurfaceSource As GroupBoxRenderSurfaceSource
            Friend BorderRecipe As GroupBoxRenderBorderRecipe
            Friend BorderStrength As MASSurfaceFrameStrength
            Friend SurfaceMaterialKey As String
            Friend DrawShadow As Boolean
            Friend DrawSeparator As Boolean
            Friend DrawPremiumBorder As Boolean
            Friend Enabled As Boolean

            Friend TitleNotchEnabled As Boolean
            Friend TitlePlateStrength As GroupBoxRenderTitlePlateStrength
            Friend TitleLift As Single
        End Structure

        Private Structure TitleLayout
            Friend HasValue As Boolean
            Friend PlateRect As SKRect
            Friend TextRect As SKRect
            Friend CutRect As SKRect
            Friend PlateRadiusPx As Single
        End Structure

        Private ReadOnly _surface As New MASVisualPrimitivesPainter()
        Private ReadOnly _shadow As New MASShadowPainter()
        Private ReadOnly _frameBorder As New MASSurfaceFrameBorderPainter()

        Private _disposed As Boolean

        Friend Sub Render(canvas As SKCanvas,
                  ctx As MASThemeContext,
                  contract As MASResolvedComponentContract,
                  boundsPx As SKRect,
                  spec As RenderSpec,
                  Optional titlePlateContract As MASResolvedComponentContract = Nothing)

            If _disposed Then Return
            If canvas Is Nothing OrElse ctx Is Nothing OrElse contract Is Nothing Then Return
            If boundsPx.Width <= 2.0F OrElse boundsPx.Height <= 2.0F Then Return

            MASArchitectureRuntime.CreateRenderScopeForComponent(Of MASGroupBox)(
        NameOf(MASGroupBoxRenderer),
        contract,
        BuildRequiredLayers(spec))

            If titlePlateContract Is Nothing Then
                titlePlateContract = contract
            ElseIf HasTitle(spec.Title) Then
                Dim titleScope As MASContractRenderScope =
                    MASArchitectureRuntime.CreateRenderScopeForComponent(Of MASGroupBox)(
        NameOf(MASGroupBoxRenderer),
        titlePlateContract,
        MASRenderLayer.Surface,
        MASRenderLayer.Text)

                titleScope.RequireAnyLayer(
    MASRenderLayer.Surface,
    MASRenderLayer.Text)
            End If

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

            Dim m As MASSurfaceInteractionRing.Metrics
            Dim baseRect As SKRect

            If Not MASVisualEffectGeometry.TryResolveBaseRect(boundsPx, dpi, m, baseRect) Then Return

            Dim radius As Single =
    MASRadiusResolver.ResolveSurfacePx(
        contract:=contract,
        rPx:=baseRect,
        dpi:=dpi)
            radius = PixelSnap.SafeRadius(radius)

            If spec.DrawShadow AndAlso spec.SurfaceSource <> GroupBoxRenderSurfaceSource.TransparentFrame Then
                _shadow.DrawLayeredShadow(
    canvas:=canvas,
    r:=boundsPx,
    contract:=contract,
    dpi:=dpi,
    surfaceTheme:=ctx.SurfaceTheme)
            End If

            Dim surfaceMaterialDrawn As Boolean = False

            If spec.SurfaceSource <> GroupBoxRenderSurfaceSource.TransparentFrame Then
                surfaceMaterialDrawn = MASSurfaceMaterialComponentGateway.TryDrawGroupBoxSurface(
                    canvas:=canvas,
                    ctx:=ctx,
                    boundsPx:=baseRect,
                    materialKey:=spec.SurfaceMaterialKey,
                    dpi:=dpi,
                    cornerRadiusPx:=radius,
                    surfaceStrengthLevel:=4,
                    borderStrengthLevel:=ResolveBorderStrengthLevel(spec.BorderStrength),
                    frameStrength:=spec.BorderStrength)
            End If

            If Not surfaceMaterialDrawn Then
                Select Case spec.SurfaceSource
                    Case GroupBoxRenderSurfaceSource.Acrylic
                        _surface.DrawAcrylicSurface(
    canvas:=canvas,
    ctx:=ctx,
    r:=boundsPx,
    dpi:=dpi,
    contract:=contract)

                    Case GroupBoxRenderSurfaceSource.Plain
                        DrawPlainSurface(
                            canvas:=canvas,
                            ctx:=ctx,
                            r:=baseRect,
                            radius:=radius,
                            dpi:=dpi,
                            enabled:=spec.Enabled)

                    Case GroupBoxRenderSurfaceSource.TransparentFrame
                        ' Frame only.
                End Select
            End If

            Dim titleLayout As TitleLayout =
    BuildTitleLayout(
        ctx:=ctx,
        baseRect:=baseRect,
        dpi:=dpi,
        title:=spec.Title,
        lift:=spec.TitleLift,
        titlePlateContract:=titlePlateContract)

            If spec.DrawPremiumBorder Then
                DrawFrameBorder(
    canvas:=canvas,
    ctx:=ctx,
    baseRect:=baseRect,
    radius:=radius,
    dpi:=dpi,
    spec:=spec,
    titleLayout:=titleLayout,
    titlePlateContract:=titlePlateContract)
            End If

            DrawFloatingTitle(
                canvas:=canvas,
                ctx:=ctx,
                dpi:=dpi,
                spec:=spec,
                layout:=titleLayout)

            If spec.DrawSeparator Then
                DrawSeparator(
                    canvas:=canvas,
                    ctx:=ctx,
                    baseRect:=baseRect,
                    dpi:=dpi,
                    enabled:=spec.Enabled)
            End If
        End Sub

        Private Shared Function ResolveBorderStrengthLevel(value As MASSurfaceFrameStrength) As Integer
            Select Case value
                Case MASSurfaceFrameStrength.Level1
                    Return 1
                Case MASSurfaceFrameStrength.Level2
                    Return 2
                Case MASSurfaceFrameStrength.Level3
                    Return 3
                Case MASSurfaceFrameStrength.Level4
                    Return 4
                Case MASSurfaceFrameStrength.Level5
                    Return 5
                Case Else
                    Return 6
            End Select
        End Function

        Private Shared Function BuildRequiredLayers(spec As RenderSpec) As MASRenderLayer()
            Dim layers As New List(Of MASRenderLayer) From {
                MASRenderLayer.Surface
            }

            If spec.DrawShadow AndAlso spec.SurfaceSource <> GroupBoxRenderSurfaceSource.TransparentFrame Then
                layers.Add(MASRenderLayer.Shadow)
            End If

            If HasTitle(spec.Title) Then
                layers.Add(MASRenderLayer.Text)
            End If

            Return layers.ToArray()
        End Function

        Private Shared Function HasTitle(title As String) As Boolean
            Return Not String.IsNullOrWhiteSpace(title)
        End Function

        Private Sub DrawFrameBorder(canvas As SKCanvas,
                            ctx As MASThemeContext,
                            baseRect As SKRect,
                            radius As Single,
                            dpi As Single,
                            spec As RenderSpec,
                            titleLayout As TitleLayout,
                            titlePlateContract As MASResolvedComponentContract)

            If canvas Is Nothing OrElse ctx Is Nothing Then Return

            Dim borderSpec As MASSurfaceFrameBorderSpec =
                BuildFrameBorderSpec(
                    ctx:=ctx,
                    recipe:=spec.BorderRecipe,
                    strength:=spec.BorderStrength,
                    enabled:=spec.Enabled)

            borderSpec = ScaleBorderSpec(borderSpec, dpi)

            If Not spec.TitleNotchEnabled OrElse Not titleLayout.HasValue Then
                _frameBorder.Draw(
                    canvas:=canvas,
                    baseRect:=baseRect,
                    baseRadiusPx:=radius,
                    dpi:=dpi,
                    spec:=borderSpec)

                Return
            End If

            Using cutPath As SKPath = BuildTitleCutPath(titleLayout, dpi, titlePlateContract)
                Dim saveCount As Integer = canvas.Save()

                Try
                    canvas.ClipPath(cutPath, SKClipOperation.Difference, True)

                    _frameBorder.Draw(
                        canvas:=canvas,
                        baseRect:=baseRect,
                        baseRadiusPx:=radius,
                        dpi:=dpi,
                        spec:=borderSpec)

                Finally
                    canvas.RestoreToCount(saveCount)
                End Try
            End Using
        End Sub

        Private Shared Function BuildFrameBorderSpec(ctx As MASThemeContext,
                                                     recipe As GroupBoxRenderBorderRecipe,
                                                     strength As MASSurfaceFrameStrength,
                                                     enabled As Boolean) As MASSurfaceFrameBorderSpec

            If ctx Is Nothing OrElse ctx.Theme Is Nothing OrElse MASAppThemeContracts.Families(ctx.Theme).FrameSurface Is Nothing Then
                Return New MASSurfaceFrameBorderSpec()
            End If

            Dim frameSpec As MASSurfaceFrameSpec =
                MASSurfaceFrameFactory.Build(
                    surfaceTheme:=MASAppThemeContracts.Families(ctx.Theme).FrameSurface,
                    surfaceStrength:=MASSurfaceFrameStrength.Level5,
                    borderStrength:=strength)

            Dim border As MASSurfaceFrameBorderSpec = frameSpec.Border

            If Not enabled Then
                border.OuterColor = ColorMath.ScaleAlpha(border.OuterColor, 0.55F)
                border.InnerColor = ColorMath.ScaleAlpha(border.InnerColor, 0.45F)
                border.HairlineColor = ColorMath.ScaleAlpha(border.HairlineColor, 0.5F)
                border.GlowColor = ColorMath.ScaleAlpha(border.GlowColor, 0.35F)
            End If

            Return border
        End Function

        Private Shared Function ScaleBorderSpec(spec As MASSurfaceFrameBorderSpec,
                                                dpi As Single) As MASSurfaceFrameBorderSpec

            dpi = PixelSnap.SafeDpi(dpi)

            spec.OuterStrokePx = Math.Max(0.55F, spec.OuterStrokePx * dpi)
            spec.InnerStrokePx = Math.Max(0.5F, spec.InnerStrokePx * dpi)
            spec.HairlineStrokePx = Math.Max(0.6F, spec.HairlineStrokePx * dpi)
            spec.GlowStrokePx = Math.Max(0.55F, spec.GlowStrokePx * dpi)
            spec.InnerInsetPx = Math.Max(0.5F, spec.InnerInsetPx * dpi)
            spec.GlowBlurSigmaPx = Math.Max(0.0F, spec.GlowBlurSigmaPx * dpi)

            Return spec
        End Function

        Private Shared Sub DrawFloatingTitle(canvas As SKCanvas,
                                             ctx As MASThemeContext,
                                             dpi As Single,
                                             spec As RenderSpec,
                                             layout As TitleLayout)

            If Not layout.HasValue Then Return
            If ctx Is Nothing OrElse ctx.Typography Is Nothing OrElse ctx.GroupBoxTheme Is Nothing Then Return

            Dim title As String = If(spec.Title, String.Empty).Trim()
            If title.Length = 0 Then Return

            DrawFloatingTitlePlate(
                canvas:=canvas,
                ctx:=ctx,
                r:=layout.PlateRect,
                radius:=layout.PlateRadiusPx,
                dpi:=dpi,
                enabled:=spec.Enabled,
                strength:=spec.TitlePlateStrength)

            Dim textColor As SKColor =
                If(spec.Enabled,
                   ctx.GroupBoxTheme.Text.Title,
                   ctx.GroupBoxTheme.Text.TitleDisabled)

            TypographyTextPrimitives.DrawSingleLineText(
                canvas:=canvas,
                ctx:=ctx,
                text:=title,
                bounds:=layout.TextRect,
                dpi:=dpi,
                style:=MASTypography.MASTextStyle.Button,
                color:=textColor,
                align:=TypographyTextPrimitives.TextAlign.Left,
                drawShadow:=False)
        End Sub

        Private Shared Function BuildTitleLayout(ctx As MASThemeContext,
                                         baseRect As SKRect,
                                         dpi As Single,
                                         title As String,
                                         lift As Single,
                                         titlePlateContract As MASResolvedComponentContract) As TitleLayout

            Dim result As New TitleLayout With {
                .HasValue = False,
                .PlateRect = SKRect.Empty,
                .TextRect = SKRect.Empty,
                .CutRect = SKRect.Empty,
                .PlateRadiusPx = 0.0F
            }

            Dim t As String = If(title, String.Empty).Trim()
            If t.Length = 0 Then Return result
            If ctx Is Nothing OrElse ctx.Typography Is Nothing Then Return result

            lift = Clamp01(lift)
            If lift <= 0.0F Then lift = GroupBoxTokens.FloatingTitleDefaultLift

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

            If measurePaint Is Nothing Then Return result

            Dim titleW As Single = MASShapedText.MeasureWidth(t, measurePaint)

            Dim padX As Single = GroupBoxTokens.FloatingTitlePadX * dpi
            Dim badgeH As Single = GroupBoxTokens.FloatingTitleHeight * dpi
            Dim badgeW As Single = titleW + (padX * 2.0F)

            Dim x As Single = baseRect.Left + (GroupBoxTokens.FloatingTitleInsetX * dpi)
            Dim y As Single = baseRect.Top - (badgeH * lift)

            Dim plateRight As Single =
                Math.Min(
                    x + badgeW,
                    baseRect.Right - (GroupBoxTokens.FloatingTitleRightInset * dpi))

            Dim plateRect As SKRect =
                PixelSnap.SnapRectHalf(
                    New SKRect(
                        x,
                        y,
                        plateRight,
                        y + badgeH))

            If plateRect.Width <= 2.0F OrElse plateRect.Height <= 2.0F Then Return result

            Dim textRect As New SKRect(
                plateRect.Left + padX,
                plateRect.Top,
                plateRect.Right - padX,
                plateRect.Bottom)

            Dim cutPadX As Single = GroupBoxTokens.FloatingTitleCutPadX * dpi
            Dim cutPadTop As Single = GroupBoxTokens.FloatingTitleCutPadTop * dpi
            Dim cutPadBottom As Single = GroupBoxTokens.FloatingTitleCutPadBottom * dpi

            Dim cutRect As SKRect =
                PixelSnap.SnapRectHalf(
                    New SKRect(
                        plateRect.Left - cutPadX,
                        plateRect.Top - cutPadTop,
                        plateRect.Right + cutPadX,
                        plateRect.Bottom + cutPadBottom))

            result.HasValue = True
            result.PlateRect = plateRect
            result.TextRect = textRect
            result.CutRect = cutRect
            result.PlateRadiusPx =
    MASRadiusResolver.ResolveSurfacePx(
        contract:=titlePlateContract,
        rPx:=plateRect,
        dpi:=dpi)

            Return result
        End Function

        Private Shared Function BuildTitleCutPath(layout As TitleLayout,
                                          dpi As Single,
                                          titlePlateContract As MASResolvedComponentContract) As SKPath

            Dim path As New SKPath()

            If Not layout.HasValue Then Return path

            Dim radius As Single =
    MASRadiusResolver.ResolveSurfacePx(
        contract:=titlePlateContract,
        rPx:=layout.CutRect,
        dpi:=dpi)

            path.AddRoundRect(layout.CutRect, radius, radius)

            Return path
        End Function

        Private Shared Sub DrawFloatingTitlePlate(canvas As SKCanvas,
                                                  ctx As MASThemeContext,
                                                  r As SKRect,
                                                  radius As Single,
                                                  dpi As Single,
                                                  enabled As Boolean,
                                                  strength As GroupBoxRenderTitlePlateStrength)

            If canvas Is Nothing OrElse ctx Is Nothing OrElse ctx.GroupBoxTheme Is Nothing Then Return
            If r.Width <= 1.0F OrElse r.Height <= 1.0F Then Return

            Dim visual As GroupBoxTitlePlateVisualTheme =
                ResolveTitlePlateVisual(ctx.GroupBoxTheme.TitlePlate, strength)

            Dim shadowColor As SKColor = If(enabled, visual.Shadow, visual.DisabledShadow)
            Dim topCol As SKColor = If(enabled, visual.Top, visual.DisabledTop)
            Dim botCol As SKColor = If(enabled, visual.Bottom, visual.DisabledBottom)
            Dim outerCol As SKColor = If(enabled, visual.OuterStroke, visual.DisabledOuterStroke)
            Dim innerCol As SKColor = If(enabled, visual.InnerStroke, visual.DisabledInnerStroke)

            If shadowColor.Alpha > 0 Then
                Dim sr As New SKRect(
                    r.Left,
                    r.Top - (GroupBoxTokens.TitlePlateHighlightOffsetY * dpi),
                    r.Right,
                    r.Bottom - (GroupBoxTokens.TitlePlateShadowLiftY * dpi))

                MASShadowPainter.DrawImmediateBlurredRoundRectShadow(
                    canvas:=canvas,
                    rectPx:=sr,
                    radiusPx:=radius,
                    sigmaPx:=GroupBoxTokens.TitlePlateBlurSigma * dpi,
                    color:=shadowColor)
            End If

            Using sh As SKShader =
                SKShader.CreateLinearGradient(
                    New SKPoint(r.Left, r.Top),
                    New SKPoint(r.Left, r.Bottom),
                    New SKColor() {topCol, botCol},
                    New Single() {0.0F, 1.0F},
                    SKShaderTileMode.Clamp)

                Using p As New SKPaint With {
                    .IsAntialias = True,
                    .IsDither = True,
                    .Style = SKPaintStyle.Fill,
                    .Shader = sh
                }
                    canvas.DrawRoundRect(r, radius, radius, p)
                End Using
            End Using

            If outerCol.Alpha > 0 Then
                Using p As New SKPaint With {
                    .IsAntialias = True,
                    .IsDither = True,
                    .Style = SKPaintStyle.Stroke,
                    .StrokeWidth = Math.Max(1.0F, GroupBoxTokens.PlainSurfaceOuterStrokeDip * dpi),
                    .Color = outerCol
                }
                    canvas.DrawRoundRect(r, radius, radius, p)
                End Using
            End If

            Dim innerR As SKRect =
                PixelSnap.SnapRectHalf(
                    PixelSnap.InsetAll(r, GroupBoxTokens.PlainSurfaceInnerInsetDip * dpi))

            If innerCol.Alpha > 0 AndAlso innerR.Width > 2.0F AndAlso innerR.Height > 2.0F Then
                Using p As New SKPaint With {
                    .IsAntialias = True,
                    .IsDither = True,
                    .Style = SKPaintStyle.Stroke,
                    .StrokeWidth = Math.Max(0.75F, GroupBoxTokens.PlainSurfaceInnerStrokeDip * dpi),
                    .Color = innerCol
                }
                    Dim innerRadius As Single =
                        CornerRadii.ResolveInsetRadiusPx(
                            radius,
                            GroupBoxTokens.PlainSurfaceInnerInsetDip * dpi)

                    canvas.DrawRoundRect(innerR, innerRadius, innerRadius, p)
                End Using
            End If
        End Sub

        Private Shared Function ResolveTitlePlateVisual(theme As MASGroupBoxTitlePlateTheme,
                                                        strength As GroupBoxRenderTitlePlateStrength) As GroupBoxTitlePlateVisualTheme

            Select Case strength
                Case GroupBoxRenderTitlePlateStrength.Strong
                    Return theme.Strong

                Case GroupBoxRenderTitlePlateStrength.Soft
                    Return theme.Soft

                Case Else
                    Return theme.Premium
            End Select
        End Function

        Private Shared Sub DrawPlainSurface(canvas As SKCanvas,
                                            ctx As MASThemeContext,
                                            r As SKRect,
                                            radius As Single,
                                            dpi As Single,
                                            enabled As Boolean)

            If canvas Is Nothing OrElse ctx Is Nothing OrElse ctx.GroupBoxTheme Is Nothing Then Return
            If r.Width <= 1.0F OrElse r.Height <= 1.0F Then Return

            Dim s As MASGroupBoxSurfaceTheme = ctx.GroupBoxTheme.Surface

            Dim topCol As SKColor = s.PlainTop
            Dim midCol As SKColor = s.PlainMid
            Dim botCol As SKColor = s.PlainBottom

            If Not enabled Then
                Dim scale As Single = Clamp01(s.PlainDisabledAlphaScale)

                topCol = ColorMath.ScaleAlpha(topCol, scale)
                midCol = ColorMath.ScaleAlpha(midCol, scale)
                botCol = ColorMath.ScaleAlpha(botCol, scale)
            End If

            Using sh As SKShader =
                SKShader.CreateLinearGradient(
                    New SKPoint(r.Left, r.Top),
                    New SKPoint(r.Left, r.Bottom),
                    New SKColor() {topCol, midCol, botCol},
                    New Single() {0.0F, 0.56F, 1.0F},
                    SKShaderTileMode.Clamp)

                Using p As New SKPaint With {
                    .IsAntialias = True,
                    .IsDither = True,
                    .Style = SKPaintStyle.Fill,
                    .Shader = sh
                }
                    canvas.DrawRoundRect(r, radius, radius, p)
                End Using
            End Using
        End Sub

        Private Shared Sub DrawSeparator(canvas As SKCanvas,
                                         ctx As MASThemeContext,
                                         baseRect As SKRect,
                                         dpi As Single,
                                         enabled As Boolean)

            If canvas Is Nothing OrElse ctx Is Nothing OrElse ctx.GroupBoxTheme Is Nothing Then Return
            If baseRect.Width <= 1.0F Then Return

            Dim y As Single =
                PixelSnap.SnapHalf(baseRect.Top + (GroupBoxTokens.HeaderHeight * dpi))

            Dim col As SKColor =
                If(enabled,
                   ctx.GroupBoxTheme.Separator.Color,
                   ctx.GroupBoxTheme.Separator.DisabledColor)

            If col.Alpha = 0 Then Return

            Using p As New SKPaint With {
                .IsAntialias = True,
                .IsDither = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = Math.Max(1.0F, GroupBoxTokens.PlainSurfaceOuterStrokeDip * dpi),
                .Color = col
            }
                canvas.DrawLine(
                    baseRect.Left + (GroupBoxTokens.SeparatorInsetX * dpi),
                    y,
                    baseRect.Right - (GroupBoxTokens.SeparatorInsetX * dpi),
                    y,
                    p)
            End Using
        End Sub

        Private Shared Function Clamp01(value As Single) As Single
            If Single.IsNaN(value) OrElse Single.IsInfinity(value) Then Return 0.0F
            If value < 0.0F Then Return 0.0F
            If value > 1.0F Then Return 1.0F
            Return value
        End Function

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

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

            Try
                _shadow.Dispose()
            Catch masCaughtException2 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException2)
            End Try

            Try
                _frameBorder.Dispose()
            Catch masCaughtException3 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDisposeCleanup(masCaughtException3)
            End Try
        End Sub

    End Class

End Namespace
