Option Strict On
Option Explicit On

Imports System
Imports System.Globalization
Imports System.Linq
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports Nexamas.UI.Visualization
Imports SkiaSharp
Imports Nexamas.UI.General

Namespace Nexamas.UI.Rendering

    Friend NotInheritable Class MASChartRenderer

        Friend Sub Draw(canvas As SKCanvas,
                        ctx As MASThemeContext,
                        boundsPx As SKRect,
                        plotRectPx As SKRect,
                        plan As MASChartRenderPlan,
                        contract As MASResolvedComponentContract)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If contract Is Nothing Then Throw New ArgumentNullException(NameOf(contract))
            If boundsPx.Width <= 1.0F OrElse boundsPx.Height <= 1.0F Then Return

            Dim scope As MASContractRenderScope =
                MASArchitectureRuntime.CreateRenderScopeForComponent(Of Nexamas.UI.Components.MASChart)(
                    NameOf(MASChartRenderer),
                    contract,
                    MASRenderLayer.Surface,
                    MASRenderLayer.Border)

            scope.RequireAnyLayer(MASRenderLayer.Surface, MASRenderLayer.Border)

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            Dim surfaceRect As SKRect = MASChartControlTokens.ResolveSurfaceRect(boundsPx, dpi)
            DrawSurface(canvas, ctx, surfaceRect, dpi)

            If plan IsNot Nothing AndAlso plan.Definition IsNot Nothing Then
                DrawTitle(canvas, ctx, surfaceRect, plan.Definition.Title, dpi)
            Else
                DrawTitle(canvas, ctx, surfaceRect, "Chart", dpi)
            End If

            If plan Is Nothing OrElse Not plan.ReadyForDashboardChart Then
                DrawEmptyState(canvas, ctx, plotRectPx, dpi)
                Return
            End If

            Select Case plan.ChartKind
                Case MASChartKind.Line
                    DrawScaffold(canvas, ctx, plotRectPx, dpi, drawAxis:=True)
                    DrawLineOrSparkline(canvas, ctx, plan, dpi, sparkline:=False)
                Case MASChartKind.Sparkline
                    DrawLineOrSparkline(canvas, ctx, plan, dpi, sparkline:=True)
                Case MASChartKind.Bar
                    DrawScaffold(canvas, ctx, plotRectPx, dpi, drawAxis:=True)
                    DrawBars(canvas, ctx, plan, dpi)
                Case MASChartKind.Pie
                    DrawPie(canvas, ctx, plan, dpi, donut:=False)
                Case MASChartKind.Donut
                    DrawPie(canvas, ctx, plan, dpi, donut:=True)
                Case MASChartKind.Kpi
                    DrawKpi(canvas, ctx, plan, plotRectPx, dpi)
                Case Else
                    DrawEmptyState(canvas, ctx, plotRectPx, dpi)
            End Select
        End Sub

        Private Shared Sub DrawSurface(canvas As SKCanvas, ctx As MASThemeContext, rect As SKRect, dpi As Single)
            If rect.Width <= 1.0F OrElse rect.Height <= 1.0F Then Return

            Dim frame As IMASFrameSurfaceTheme = ResolveFrameTheme(ctx)
            Dim fillTop As SKColor = If(frame IsNot Nothing, frame.SurfaceTop, New SKColor(248, 250, 252, 255))
            Dim fillBottom As SKColor = If(frame IsNot Nothing, frame.SurfaceBot, New SKColor(235, 240, 248, 255))
            Dim border As SKColor = If(frame IsNot Nothing, frame.BorderHairline, New SKColor(190, 200, 214, 160))
            Dim radius As Single = Math.Max(1.0F, MASChartControlTokens.ToPx(MASChartControlTokens.SurfaceRadiusDip, dpi))
            Dim snapped As SKRect = PixelSnap.SnapRectHalf(rect)

            Using shader As SKShader = SKShader.CreateLinearGradient(
                New SKPoint(snapped.Left, snapped.Top),
                New SKPoint(snapped.Left, snapped.Bottom),
                New SKColor() {fillTop, fillBottom},
                Nothing,
                SKShaderTileMode.Clamp)

                Using fillPaint As New SKPaint() With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Shader = shader},
                    borderPaint As New SKPaint() With {.IsAntialias = True, .Style = SKPaintStyle.Stroke, .Color = border, .StrokeWidth = Math.Max(1.0F, dpi)}

                    canvas.DrawRoundRect(snapped, radius, radius, fillPaint)
                    canvas.DrawRoundRect(snapped, radius, radius, borderPaint)
                End Using
            End Using
        End Sub

        Private Shared Sub DrawTitle(canvas As SKCanvas, ctx As MASThemeContext, surfaceRect As SKRect, title As String, dpi As Single)
            If ctx.Typography Is Nothing Then Return

            Dim text As String = If(title, String.Empty).Trim()
            If text.Length = 0 Then text = "Chart"

            Dim paint As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Title, ResolvePrimaryText(ctx))
            If paint Is Nothing Then Return

            Dim x As Single = surfaceRect.Left + MASChartControlTokens.ToPx(MASChartControlTokens.ContentInsetDip, dpi)
            Dim baseline As Single = surfaceRect.Top + MASChartControlTokens.ToPx(23.0F, dpi)
            MASShapedText.Draw(canvas, text, x, baseline, paint)
        End Sub

        Private Shared Sub DrawEmptyState(canvas As SKCanvas, ctx As MASThemeContext, plotRect As SKRect, dpi As Single)
            If ctx.Typography Is Nothing OrElse plotRect.Width <= 1.0F OrElse plotRect.Height <= 1.0F Then Return

            Dim paint As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Body, ResolveMutedText(ctx))
            If paint Is Nothing Then Return

            Dim text As String = "No chart data"
            Dim width As Single = MASShapedText.MeasureWidth(text, paint)
            Dim x As Single = plotRect.Left + Math.Max(0.0F, (plotRect.Width - width) / 2.0F)
            Dim y As Single = plotRect.Top + (plotRect.Height / 2.0F)
            MASShapedText.Draw(canvas, text, x, y, paint)
        End Sub

        Private Shared Sub DrawScaffold(canvas As SKCanvas, ctx As MASThemeContext, plotRect As SKRect, dpi As Single, drawAxis As Boolean)
            If plotRect.Width <= 1.0F OrElse plotRect.Height <= 1.0F Then Return

            Dim frame As IMASFrameSurfaceTheme = ResolveFrameTheme(ctx)
            Dim gridColor As SKColor = If(frame IsNot Nothing, frame.SeparatorHairline, New SKColor(174, 184, 198, 120))
            Dim axisColor As SKColor = If(frame IsNot Nothing, frame.BorderHairline, New SKColor(160, 170, 184, 180))
            Dim gridStroke As Single = Math.Max(1.0F, dpi)

            Using gridPaint As New SKPaint() With {.IsAntialias = False, .Style = SKPaintStyle.Stroke, .Color = ColorMath.WithAlpha(gridColor, CByte(110)), .StrokeWidth = gridStroke},
                axisPaint As New SKPaint() With {.IsAntialias = False, .Style = SKPaintStyle.Stroke, .Color = axisColor, .StrokeWidth = gridStroke}

                For index As Integer = 1 To MASChartControlTokens.GridLineCount
                    Dim y As Single = plotRect.Top + ((plotRect.Height / CSng(MASChartControlTokens.GridLineCount + 1)) * CSng(index))
                    y = CSng(Math.Round(y)) + 0.5F
                    canvas.DrawLine(plotRect.Left, y, plotRect.Right, y, gridPaint)
                Next

                If drawAxis Then
                    canvas.DrawLine(plotRect.Left, plotRect.Bottom, plotRect.Right, plotRect.Bottom, axisPaint)
                    canvas.DrawLine(plotRect.Left, plotRect.Top, plotRect.Left, plotRect.Bottom, axisPaint)
                End If
            End Using
        End Sub

        Private Shared Sub DrawBars(canvas As SKCanvas, ctx As MASThemeContext, plan As MASChartRenderPlan, dpi As Single)
            Dim primitives As MASChartVisualPrimitive() = plan.Primitives
            Dim barIndex As Integer = 0
            For Each primitive As MASChartVisualPrimitive In primitives
                If primitive.Kind <> MASChartVisualPrimitiveKind.Bar Then Continue For

                Dim rect As New SKRect(CSng(primitive.X), CSng(primitive.Y), CSng(primitive.X + primitive.Width), CSng(primitive.Y + primitive.Height))
                Dim radius As Single = Math.Max(1.0F, MASChartControlTokens.ToPx(MASChartControlTokens.BarRadiusDip, dpi))
                Using paint As New SKPaint() With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = ResolveSeriesColor(ctx, barIndex)}
                    canvas.DrawRoundRect(PixelSnap.SnapRectHalf(rect), radius, radius, paint)
                End Using
                barIndex += 1
            Next
        End Sub

        Private Shared Sub DrawLineOrSparkline(canvas As SKCanvas, ctx As MASThemeContext, plan As MASChartRenderPlan, dpi As Single, sparkline As Boolean)
            Dim strokeDip As Single = If(sparkline, MASChartControlTokens.SparklineStrokeDip, MASChartControlTokens.LineStrokeDip)
            Dim strokePx As Single = Math.Max(1.0F, MASChartControlTokens.ToPx(strokeDip, dpi))
            Dim pointRadius As Single = MASChartControlTokens.ToPx(MASChartControlTokens.SeriesPointRadiusDip, dpi)
            Dim primitives As MASChartVisualPrimitive() = plan.Primitives
            Dim color As SKColor = ResolveSeriesColor(ctx, 0)

            Using linePaint As New SKPaint() With {.IsAntialias = True, .Style = SKPaintStyle.Stroke, .Color = color, .StrokeWidth = strokePx, .StrokeCap = SKStrokeCap.Round},
                pointPaint As New SKPaint() With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = color}

                For Each primitive As MASChartVisualPrimitive In primitives
                    If primitive.Kind = MASChartVisualPrimitiveKind.LineSegment OrElse primitive.Kind = MASChartVisualPrimitiveKind.SparklineSegment Then
                        canvas.DrawLine(CSng(primitive.X), CSng(primitive.Y), CSng(primitive.X2), CSng(primitive.Y2), linePaint)
                    End If
                Next

                If Not sparkline Then
                    For Each primitive As MASChartVisualPrimitive In primitives
                        If primitive.Kind = MASChartVisualPrimitiveKind.LinePoint Then
                            canvas.DrawCircle(CSng(primitive.X), CSng(primitive.Y), pointRadius, pointPaint)
                        End If
                    Next
                End If
            End Using
        End Sub

        Private Shared Sub DrawPie(canvas As SKCanvas, ctx As MASThemeContext, plan As MASChartRenderPlan, dpi As Single, donut As Boolean)
            Dim primitives As MASChartVisualPrimitive() = plan.Primitives
            Dim sliceIndex As Integer = 0
            For Each primitive As MASChartVisualPrimitive In primitives
                If donut AndAlso primitive.Kind <> MASChartVisualPrimitiveKind.DonutSlice Then Continue For
                If Not donut AndAlso primitive.Kind <> MASChartVisualPrimitiveKind.PieSlice Then Continue For

                Dim cx As Single = CSng(primitive.X)
                Dim cy As Single = CSng(primitive.Y)
                Dim radius As Single = CSng(Math.Max(1.0R, primitive.OuterRadius))
                Dim oval As New SKRect(cx - radius, cy - radius, cx + radius, cy + radius)
                Dim color As SKColor = ResolveSeriesColor(ctx, sliceIndex)

                If donut Then
                    Dim inner As Single = CSng(Math.Max(0.0R, primitive.InnerRadius))
                    Dim strokeWidth As Single = Math.Max(1.0F, radius - inner)
                    Dim strokeRadius As Single = inner + (strokeWidth / 2.0F)
                    Dim strokeOval As New SKRect(cx - strokeRadius, cy - strokeRadius, cx + strokeRadius, cy + strokeRadius)
                    Using paint As New SKPaint() With {.IsAntialias = True, .Style = SKPaintStyle.Stroke, .Color = color, .StrokeWidth = strokeWidth, .StrokeCap = SKStrokeCap.Butt}
                        canvas.DrawArc(strokeOval, CSng(primitive.StartAngleDegrees), CSng(primitive.SweepAngleDegrees), False, paint)
                    End Using
                Else
                    Using path As New SKPath(), paint As New SKPaint() With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = color}
                        path.MoveTo(cx, cy)
                        path.ArcTo(oval, CSng(primitive.StartAngleDegrees), CSng(primitive.SweepAngleDegrees), False)
                        path.Close()
                        canvas.DrawPath(path, paint)
                    End Using
                End If

                sliceIndex += 1
            Next
        End Sub

        Private Shared Sub DrawKpi(canvas As SKCanvas, ctx As MASThemeContext, plan As MASChartRenderPlan, plotRect As SKRect, dpi As Single)
            If ctx.Typography Is Nothing Then Return

            Dim primitives As MASChartVisualPrimitive() = plan.Primitives
            Dim valuePrimitive As MASChartVisualPrimitive = primitives.FirstOrDefault(Function(item) item.Kind = MASChartVisualPrimitiveKind.KpiValue)
            If valuePrimitive Is Nothing Then Return

            Dim titlePaint As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Body, ResolveMutedText(ctx))
            Dim valuePaint As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Title, ResolveSeriesColor(ctx, 0))
            Dim trendPaint As SKPaint = ctx.Typography.GetPaint(MASTypography.MASTextStyle.Small, ResolvePrimaryText(ctx))
            If titlePaint Is Nothing OrElse valuePaint Is Nothing OrElse trendPaint Is Nothing Then Return

            Dim label As String = If(valuePrimitive.Label, String.Empty)
            Dim valueText As String = FormatValue(valuePrimitive.Value)
            Dim titleWidth As Single = MASShapedText.MeasureWidth(label, titlePaint)
            Dim valueWidth As Single = MASShapedText.MeasureWidth(valueText, valuePaint)
            Dim titleX As Single = plotRect.Left + Math.Max(0.0F, (plotRect.Width - titleWidth) / 2.0F)
            Dim valueX As Single = plotRect.Left + Math.Max(0.0F, (plotRect.Width - valueWidth) / 2.0F)
            Dim centerY As Single = plotRect.Top + (plotRect.Height / 2.0F)

            MASShapedText.Draw(canvas, label, titleX, centerY - MASChartControlTokens.ToPx(18.0F, dpi), titlePaint)
            MASShapedText.Draw(canvas, valueText, valueX, centerY + MASChartControlTokens.ToPx(8.0F, dpi), valuePaint)

            Dim trendPrimitive As MASChartVisualPrimitive = primitives.FirstOrDefault(Function(item) item.Kind = MASChartVisualPrimitiveKind.KpiTrend)
            If trendPrimitive IsNot Nothing Then
                Dim trendText As String = If(trendPrimitive.Value >= 0.0R, "+", String.Empty) & FormatValue(trendPrimitive.Value)
                Dim trendWidth As Single = MASShapedText.MeasureWidth(trendText, trendPaint)
                Dim trendX As Single = plotRect.Left + Math.Max(0.0F, (plotRect.Width - trendWidth) / 2.0F)
                MASShapedText.Draw(canvas, trendText, trendX, centerY + MASChartControlTokens.ToPx(30.0F, dpi), trendPaint)
            End If
        End Sub

        Private Shared Function ResolveFrameTheme(ctx As MASThemeContext) As IMASFrameSurfaceTheme
            If ctx Is Nothing OrElse ctx.Theme Is Nothing Then Return Nothing
            Return MASAppThemeContracts.Families(ctx.Theme).FrameSurface
        End Function

        Private Shared Function ResolvePrimaryText(ctx As MASThemeContext) As SKColor
            If ctx IsNot Nothing AndAlso ctx.TextColorTheme IsNot Nothing Then Return ctx.TextColorTheme.PrimaryText
            Return New SKColor(38, 46, 60, 255)
        End Function

        Private Shared Function ResolveMutedText(ctx As MASThemeContext) As SKColor
            If ctx IsNot Nothing AndAlso ctx.TextColorTheme IsNot Nothing Then Return ctx.TextColorTheme.MutedText
            Return New SKColor(88, 98, 114, 255)
        End Function

        Private Shared Function ResolveSeriesColor(ctx As MASThemeContext, index As Integer) As SKColor
            Dim brand As IMASBrandTheme = If(ctx IsNot Nothing, ctx.BrandTheme, Nothing)
            Dim baseColors As SKColor() = If(brand IsNot Nothing,
                New SKColor() {brand.BrandPrimary, brand.BrandSecondary, brand.BrandHighlight, brand.BrandPrimaryStrong, brand.BrandSecondarySoft, brand.BrandSilver},
                New SKColor() {New SKColor(70, 128, 220, 255), New SKColor(65, 180, 155, 255), New SKColor(245, 165, 65, 255), New SKColor(145, 105, 220, 255), New SKColor(220, 95, 110, 255), New SKColor(100, 110, 125, 255)})

            Dim safeIndex As Integer = Math.Abs(index Mod baseColors.Length)
            Dim color As SKColor = baseColors(safeIndex)
            If color.Alpha = 0 Then Return New SKColor(70, 128, 220, 255)
            Return color
        End Function

        Private Shared Function FormatValue(value As Double) As String
            Return value.ToString("0.##", CultureInfo.InvariantCulture)
        End Function

    End Class

End Namespace
