Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Linq

Namespace Nexamas.UI.Visualization

    ''' <summary>
    ''' Builds deterministic chart render-plan facts for the initial Chart / Visualization System foundation.
    ''' </summary>
    ''' <remarks>
    ''' This builder does not draw, create controls, query databases, virtualize items, execute capture/performance runners,
    ''' or register a public chart API. It converts already-supplied dashboard data into geometry facts for MAS-owned renderers.
    ''' </remarks>
    Friend NotInheritable Class MASChartFoundationBuilder

        Friend Const OfficialSystemName As String = "Nexamas UI Chart / Visualization System Foundation"

        Private Sub New()
        End Sub

        Friend Shared Function BuildRenderPlan(definition As MASChartDefinition,
                                               viewport As MASChartViewport) As MASChartRenderPlan
            If definition Is Nothing Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.InvalidDefinition, "Chart definition is required.")
            If viewport Is Nothing OrElse Not viewport.IsValid Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingViewport, "A valid chart viewport is required.")
            If Not definition.IsSupportedKind Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.UnsupportedChartKind, "Chart kind is not supported by the foundation.")
            If definition.SeriesCount = 0 Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingSeries, "At least one chart series is required.")
            If definition.DataPointCount = 0 Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingData, "At least one data point is required.")
            If Not definition.IsValid Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.InvalidDefinition, "The chart definition contains invalid series or values.")

            Select Case definition.Kind
                Case MASChartKind.Line
                    Return BuildLinePlan(definition, viewport, MASChartVisualPrimitiveKind.LineSegment, MASChartVisualPrimitiveKind.LinePoint)
                Case MASChartKind.Bar
                    Return BuildBarPlan(definition, viewport)
                Case MASChartKind.Pie
                    Return BuildPieOrDonutPlan(definition, viewport, donut:=False)
                Case MASChartKind.Donut
                    Return BuildPieOrDonutPlan(definition, viewport, donut:=True)
                Case MASChartKind.Kpi
                    Return BuildKpiPlan(definition, viewport)
                Case MASChartKind.Sparkline
                    Return BuildLinePlan(definition, viewport, MASChartVisualPrimitiveKind.SparklineSegment, MASChartVisualPrimitiveKind.SparklinePoint)
                Case Else
                    Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.UnsupportedChartKind, "Chart kind is not supported by the foundation.")
            End Select
        End Function

        Friend Shared Function BuildFoundationSnapshot(definitions As IEnumerable(Of MASChartDefinition),
                                                       viewport As MASChartViewport,
                                                       Optional sourceName As String = "Dashboard") As MASChartFoundationSnapshot
            Dim plans As New List(Of MASChartRenderPlan)()
            If definitions IsNot Nothing Then
                For Each definition As MASChartDefinition In definitions
                    plans.Add(BuildRenderPlan(definition, viewport))
                Next
            End If

            Return New MASChartFoundationSnapshot(sourceName, plans.ToArray())
        End Function

        Friend Shared Function BuildBasicDashboardFoundationSnapshot() As MASChartFoundationSnapshot
            Dim revenue As MASChartSeries = MASChartSeries.Create(
                "revenue",
                "Revenue",
                MASChartDataPoint.WithLabel("jan", "Jan", 120.0R),
                MASChartDataPoint.WithLabel("feb", "Feb", 136.0R),
                MASChartDataPoint.WithLabel("mar", "Mar", 142.0R),
                MASChartDataPoint.WithLabel("apr", "Apr", 155.0R))

            Dim tickets As MASChartSeries = MASChartSeries.Create(
                "tickets",
                "Tickets",
                MASChartDataPoint.WithLabel("open", "Open", 18.0R),
                MASChartDataPoint.WithLabel("progress", "In progress", 11.0R),
                MASChartDataPoint.WithLabel("closed", "Closed", 41.0R))

            Dim mix As MASChartSeries = MASChartSeries.Create(
                "mix",
                "Revenue mix",
                MASChartDataPoint.WithLabel("services", "Services", 52.0R),
                MASChartDataPoint.WithLabel("licenses", "Licenses", 31.0R),
                MASChartDataPoint.WithLabel("support", "Support", 17.0R))

            Dim kpi As MASChartSeries = MASChartSeries.Create(
                "kpi",
                "Conversion",
                MASChartDataPoint.WithTrend("conversion", "Conversion", 7.8R, 0.6R))

            Dim definitions As MASChartDefinition() = {
                MASChartDefinition.Line("Monthly revenue", revenue),
                MASChartDefinition.Bar("Ticket status", tickets),
                MASChartDefinition.Pie("Revenue share", mix),
                MASChartDefinition.Donut("Revenue donut", mix),
                MASChartDefinition.Kpi("Conversion KPI", kpi),
                MASChartDefinition.Sparkline("Revenue sparkline", revenue)
            }

            Return BuildFoundationSnapshot(definitions, MASChartViewport.Create(320.0R, 180.0R), "DashboardFoundationHarness")
        End Function

        Private Shared Function BuildBarPlan(definition As MASChartDefinition,
                                             viewport As MASChartViewport) As MASChartRenderPlan
            Dim allPoints As New List(Of Tuple(Of MASChartSeries, MASChartDataPoint))()
            For Each seriesItem As MASChartSeries In definition.Series
                For Each point As MASChartDataPoint In seriesItem.Points
                    allPoints.Add(Tuple.Create(seriesItem, point))
                Next
            Next

            If allPoints.Count = 0 Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingData, "Bar chart requires at least one point.")

            Dim minimum As Double = Math.Min(0.0R, allPoints.Min(Function(item) item.Item2.Value))
            Dim maximum As Double = Math.Max(0.0R, allPoints.Max(Function(item) item.Item2.Value))
            Dim range As Double = NormalizeRange(maximum - minimum)
            Dim baselineY As Double = viewport.Bottom - (((0.0R - minimum) / range) * viewport.Height)
            Dim stepWidth As Double = viewport.Width / CDbl(allPoints.Count)
            Dim barWidth As Double = Math.Max(1.0R, stepWidth * 0.64R)
            Dim primitives As New List(Of MASChartVisualPrimitive)()

            For index As Integer = 0 To allPoints.Count - 1
                Dim seriesItem As MASChartSeries = allPoints(index).Item1
                Dim point As MASChartDataPoint = allPoints(index).Item2
                Dim valueY As Double = viewport.Bottom - (((point.Value - minimum) / range) * viewport.Height)
                Dim barX As Double = viewport.X + (CDbl(index) * stepWidth) + ((stepWidth - barWidth) / 2.0R)
                Dim barY As Double = Math.Min(valueY, baselineY)
                Dim barHeight As Double = Math.Max(1.0R, Math.Abs(baselineY - valueY))

                primitives.Add(New MASChartVisualPrimitive(
                    MASChartVisualPrimitiveKind.Bar,
                    seriesItem.SeriesKey,
                    point.Key,
                    point.Label,
                    point.Value,
                    barX,
                    barY,
                    barWidth,
                    barHeight,
                    barX + barWidth,
                    barY + barHeight,
                    0.0R,
                    0.0R,
                    0.0R,
                    0.0R,
                    isPrimary:=True))
            Next

            Return New MASChartRenderPlan(definition, viewport, primitives.ToArray(), MASChartFoundationStatus.Ready, "Bar chart render plan is ready.")
        End Function

        Private Shared Function BuildLinePlan(definition As MASChartDefinition,
                                              viewport As MASChartViewport,
                                              segmentKind As MASChartVisualPrimitiveKind,
                                              pointKind As MASChartVisualPrimitiveKind) As MASChartRenderPlan
            Dim primitives As New List(Of MASChartVisualPrimitive)()
            Dim allValues As Double() = definition.Series.SelectMany(Function(seriesItem) seriesItem.Points.Select(Function(point) point.Value)).ToArray()
            If allValues.Length = 0 Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingData, "Line chart requires at least one point.")

            Dim minimum As Double = allValues.Min()
            Dim maximum As Double = allValues.Max()
            Dim range As Double = NormalizeRange(maximum - minimum)

            For Each seriesItem As MASChartSeries In definition.Series
                Dim points As MASChartDataPoint() = seriesItem.Points.ToArray()
                Dim previousX As Double = 0.0R
                Dim previousY As Double = 0.0R
                Dim hasPrevious As Boolean = False
                Dim denominator As Double = Math.Max(1.0R, CDbl(points.Length - 1))

                For index As Integer = 0 To points.Length - 1
                    Dim point As MASChartDataPoint = points(index)
                    Dim x As Double = If(points.Length = 1, viewport.X + (viewport.Width / 2.0R), viewport.X + ((CDbl(index) / denominator) * viewport.Width))
                    Dim y As Double = viewport.Bottom - (((point.Value - minimum) / range) * viewport.Height)

                    If hasPrevious Then
                        primitives.Add(New MASChartVisualPrimitive(
                            segmentKind,
                            seriesItem.SeriesKey,
                            point.Key,
                            point.Label,
                            point.Value,
                            previousX,
                            previousY,
                            x - previousX,
                            y - previousY,
                            x,
                            y,
                            0.0R,
                            0.0R,
                            0.0R,
                            0.0R,
                            isPrimary:=False))
                    End If

                    primitives.Add(New MASChartVisualPrimitive(
                        pointKind,
                        seriesItem.SeriesKey,
                        point.Key,
                        point.Label,
                        point.Value,
                        x,
                        y,
                        0.0R,
                        0.0R,
                        x,
                        y,
                        0.0R,
                        0.0R,
                        0.0R,
                        0.0R,
                        isPrimary:=index = points.Length - 1))

                    previousX = x
                    previousY = y
                    hasPrevious = True
                Next
            Next

            If primitives.Count = 0 Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingData, "Line chart produced no primitives.")
            Return New MASChartRenderPlan(definition, viewport, primitives.ToArray(), MASChartFoundationStatus.Ready, "Line chart render plan is ready.")
        End Function

        Private Shared Function BuildPieOrDonutPlan(definition As MASChartDefinition,
                                                    viewport As MASChartViewport,
                                                    donut As Boolean) As MASChartRenderPlan
            Dim seriesItem As MASChartSeries = definition.Series.FirstOrDefault()
            If seriesItem Is Nothing Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingSeries, "Pie and donut charts require one series.")

            Dim positivePoints As MASChartDataPoint() = seriesItem.Points.Where(Function(point) point.Value > 0.0R).ToArray()
            Dim total As Double = positivePoints.Sum(Function(point) point.Value)
            If total <= 0.0R Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingData, "Pie and donut charts require positive values.")

            Dim radius As Double = Math.Max(1.0R, Math.Min(viewport.Width, viewport.Height) / 2.0R)
            Dim innerRadius As Double = If(donut, radius * definition.DonutHoleRatio, 0.0R)
            Dim centerX As Double = viewport.X + (viewport.Width / 2.0R)
            Dim centerY As Double = viewport.Y + (viewport.Height / 2.0R)
            Dim currentAngle As Double = -90.0R
            Dim primitives As New List(Of MASChartVisualPrimitive)()
            Dim sliceKind As MASChartVisualPrimitiveKind = If(donut, MASChartVisualPrimitiveKind.DonutSlice, MASChartVisualPrimitiveKind.PieSlice)

            For Each point As MASChartDataPoint In positivePoints
                Dim sweep As Double = (point.Value / total) * 360.0R
                primitives.Add(New MASChartVisualPrimitive(
                    sliceKind,
                    seriesItem.SeriesKey,
                    point.Key,
                    point.Label,
                    point.Value,
                    centerX,
                    centerY,
                    viewport.Width,
                    viewport.Height,
                    centerX,
                    centerY,
                    currentAngle,
                    sweep,
                    innerRadius,
                    radius,
                    isPrimary:=primitives.Count = 0))
                currentAngle += sweep
            Next

            Return New MASChartRenderPlan(definition, viewport, primitives.ToArray(), MASChartFoundationStatus.Ready, If(donut, "Donut chart render plan is ready.", "Pie chart render plan is ready."))
        End Function

        Private Shared Function BuildKpiPlan(definition As MASChartDefinition,
                                             viewport As MASChartViewport) As MASChartRenderPlan
            Dim seriesItem As MASChartSeries = definition.Series.FirstOrDefault()
            If seriesItem Is Nothing OrElse seriesItem.PointCount = 0 Then Return MASChartRenderPlan.Failed(definition, viewport, MASChartFoundationStatus.MissingData, "KPI chart requires one value.")

            Dim point As MASChartDataPoint = seriesItem.Points.First()
            Dim primitives As New List(Of MASChartVisualPrimitive)()
            primitives.Add(New MASChartVisualPrimitive(
                MASChartVisualPrimitiveKind.KpiValue,
                seriesItem.SeriesKey,
                point.Key,
                point.Label,
                point.Value,
                viewport.X,
                viewport.Y,
                viewport.Width,
                viewport.Height,
                viewport.Right,
                viewport.Bottom,
                0.0R,
                0.0R,
                0.0R,
                0.0R,
                isPrimary:=True))

            If point.HasSecondaryValue Then
                primitives.Add(New MASChartVisualPrimitive(
                    MASChartVisualPrimitiveKind.KpiTrend,
                    seriesItem.SeriesKey,
                    point.Key,
                    point.Label,
                    point.SecondaryValue,
                    viewport.X,
                    viewport.Y,
                    viewport.Width,
                    viewport.Height,
                    viewport.Right,
                    viewport.Bottom,
                    0.0R,
                    0.0R,
                    0.0R,
                    0.0R,
                    isPrimary:=False))
            End If

            Return New MASChartRenderPlan(definition, viewport, primitives.ToArray(), MASChartFoundationStatus.Ready, "KPI render plan is ready.")
        End Function

        Private Shared Function NormalizeRange(value As Double) As Double
            If Math.Abs(value) < 0.000001R Then Return 1.0R
            Return value
        End Function

    End Class

End Namespace
