Option Strict On
Option Explicit On

Imports System
Imports System.Collections
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Globalization
Imports System.Reflection
Imports Nexamas.UI.DataBinding
Imports Nexamas.UI.Values

Namespace Nexamas.UI.Components

    Partial Public NotInheritable Class MASPivotTable

#Region "Friend pivot-source projection"

        Private NotInheritable Class PivotAggregateBucket
            Friend Count As Integer
            Friend Sum As Decimal
            Friend Minimum As Decimal
            Friend Maximum As Decimal
            Friend HasValue As Boolean

            Friend Sub Add(value As Decimal)
                Count += 1
                Sum += value
                If Not HasValue Then
                    Minimum = value
                    Maximum = value
                    HasValue = True
                Else
                    If value < Minimum Then Minimum = value
                    If value > Maximum Then Maximum = value
                End If
            End Sub

            Friend Function Resolve(kind As MASPivotAggregateKind) As Decimal
                Select Case kind
                    Case MASPivotAggregateKind.Count
                        Return Count
                    Case MASPivotAggregateKind.Average
                        If Count <= 0 Then Return 0D
                        Return Sum / Count
                    Case MASPivotAggregateKind.Minimum
                        If Not HasValue Then Return 0D
                        Return Minimum
                    Case MASPivotAggregateKind.Maximum
                        If Not HasValue Then Return 0D
                        Return Maximum
                    Case Else
                        Return Sum
                End Select
            End Function
        End Class

        Private _pivotItemsSource As IMASItemsSource
        Private _pivotSourceDefinition As MASPivotSourceDefinition
        Private _pivotSourceRevision As Integer = -1
        Private _isProjectingPivotSource As Boolean
        Private ReadOnly _pivotDrilldown As New Dictionary(Of String, List(Of MASPivotDrilldownEntry))(StringComparer.Ordinal)

        Friend ReadOnly Property HasPivotSource As Boolean
            Get
                Return _pivotItemsSource IsNot Nothing AndAlso _pivotSourceDefinition IsNot Nothing AndAlso _pivotSourceDefinition.IsValid
            End Get
        End Property

        Friend ReadOnly Property PivotSourceRevision As Integer
            Get
                Return _pivotSourceRevision
            End Get
        End Property

        Friend Function SetPivotSource(source As IMASItemsSource,
                                       definition As MASPivotSourceDefinition,
                                       Optional reason As String = "") As MASPivotTable
            If Object.ReferenceEquals(_pivotItemsSource, source) AndAlso Object.ReferenceEquals(_pivotSourceDefinition, definition) Then Return Me
            DetachPivotSource()
            _pivotItemsSource = source
            _pivotSourceDefinition = definition
            If _pivotItemsSource IsNot Nothing Then AddHandler _pivotItemsSource.Changed, AddressOf OnPivotSourceChanged
            RebuildPivotFromSource(If(reason, "MASPivotTable.SetPivotSource"))
            Return Me
        End Function

        Friend Function ClearPivotSource(Optional reason As String = "") As MASPivotTable
            If _pivotItemsSource Is Nothing AndAlso _pivotSourceDefinition Is Nothing Then Return Me
            DetachPivotSource()
            _pivotSourceRevision = -1
            _pivotDrilldown.Clear()
            InvalidateVisual()
            RaisePivotTableChangedSafe()
            Return Me
        End Function

        Friend Function GetCellDrilldown(rowKey As String,
                                         columnKey As String) As IReadOnlyList(Of MASPivotDrilldownEntry)
            Dim row As PivotRowState = FindRow(rowKey)
            Dim column As PivotColumnState = FindColumn(columnKey)
            If row Is Nothing OrElse column Is Nothing Then Return New ReadOnlyCollection(Of MASPivotDrilldownEntry)(New List(Of MASPivotDrilldownEntry)())
            Dim entries As List(Of MASPivotDrilldownEntry) = Nothing
            If _pivotDrilldown.TryGetValue(BuildCellKey(row.Key, column.Key), entries) AndAlso entries IsNot Nothing Then
                Return New ReadOnlyCollection(Of MASPivotDrilldownEntry)(New List(Of MASPivotDrilldownEntry)(entries))
            End If
            Return New ReadOnlyCollection(Of MASPivotDrilldownEntry)(New List(Of MASPivotDrilldownEntry)())
        End Function

        Friend Function GetSelectedCellDrilldown() As IReadOnlyList(Of MASPivotDrilldownEntry)
            Return GetCellDrilldown(_selectedRowKey, _selectedColumnKey)
        End Function

        Friend Function CreatePivotExportSnapshot() As MASPivotExportSnapshot
            EnsureSummaryCache()
            Dim rowKeys As New List(Of String)()
            Dim columnKeys As New List(Of String)()
            Dim cells As New List(Of MASPivotExportCell)()
            For Each row As PivotRowState In _rows
                rowKeys.Add(row.Key)
            Next
            For Each column As PivotColumnState In _columns
                columnKeys.Add(column.Key)
            Next
            For Each row As PivotRowState In _rows
                For Each column As PivotColumnState In _columns
                    cells.Add(New MASPivotExportCell(row.Key, column.Key, GetCellValue(row.Key, column.Key)))
                Next
            Next
            Return New MASPivotExportSnapshot(rowKeys, columnKeys, cells, _grandTotal)
        End Function

        Private Sub OnPivotSourceChanged(sender As Object,
                                         e As MASItemsSourceChangedEventArgs)
            If Not Object.ReferenceEquals(sender, _pivotItemsSource) Then Return
            Dim reason As String = "MASPivotTable.PivotSourceChanged"
            If e IsNot Nothing AndAlso e.Change IsNot Nothing AndAlso e.Change.Reason.Length > 0 Then reason = e.Change.Reason
            RebuildPivotFromSource(reason)
        End Sub

        Private Sub RebuildPivotFromSource(reason As String)
            If _pivotItemsSource Is Nothing OrElse _pivotSourceDefinition Is Nothing OrElse Not _pivotSourceDefinition.IsValid Then
                ReplacePivotProjection(New List(Of PivotRowState)(), New List(Of PivotColumnState)(), New Dictionary(Of String, Decimal)(StringComparer.Ordinal), New Dictionary(Of String, List(Of MASPivotDrilldownEntry))(StringComparer.Ordinal), -1, reason)
                Return
            End If

            Dim snapshot As MASItemsSourceSnapshot = Nothing
            Try
                snapshot = _pivotItemsSource.Snapshot()
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(ex, "MASPivotTable.PivotSourceSnapshot")
            End Try

            If snapshot Is Nothing Then
                ReplacePivotProjection(New List(Of PivotRowState)(), New List(Of PivotColumnState)(), New Dictionary(Of String, Decimal)(StringComparer.Ordinal), New Dictionary(Of String, List(Of MASPivotDrilldownEntry))(StringComparer.Ordinal), _pivotItemsSource.Revision, reason)
                Return
            End If

            Dim rowStates As New List(Of PivotRowState)()
            Dim columnStates As New List(Of PivotColumnState)()
            Dim rowMap As New Dictionary(Of String, PivotRowState)(StringComparer.Ordinal)
            Dim columnMap As New Dictionary(Of String, PivotColumnState)(StringComparer.Ordinal)
            Dim rowKeysByLabel As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase)
            Dim columnKeysByLabel As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase)
            Dim buckets As New Dictionary(Of String, PivotAggregateBucket)(StringComparer.Ordinal)
            Dim drilldown As New Dictionary(Of String, List(Of MASPivotDrilldownEntry))(StringComparer.Ordinal)

            For i As Integer = 0 To snapshot.Items.Count - 1
                Dim item As MASItemsSourceItem = snapshot.Items(i)
                If item Is Nothing Then Continue For

                Dim rawRow As Object = Nothing
                Dim rawColumn As Object = Nothing
                If Not TryResolvePivotField(item.Item, _pivotSourceDefinition.RowField, rawRow) Then Continue For
                If Not TryResolvePivotField(item.Item, _pivotSourceDefinition.ColumnField, rawColumn) Then Continue For

                Dim rowLabel As String = MASPivotTablePolicy.NormalizeRowLabel(Convert.ToString(rawRow, CultureInfo.CurrentCulture))
                Dim columnLabel As String = MASPivotTablePolicy.NormalizeColumnHeader(Convert.ToString(rawColumn, CultureInfo.CurrentCulture))
                If rowLabel.Length = 0 OrElse columnLabel.Length = 0 Then Continue For

                Dim rowKey As String = String.Empty
                If Not rowKeysByLabel.TryGetValue(rowLabel, rowKey) Then
                    If rowStates.Count >= PivotTableTokens.MaxProjectedSourceRows Then Continue For
                    rowKey = MASPivotTablePolicy.ResolveUniqueKey(rowLabel, "row", rowStates.Count + 1, rowMap.Keys)
                    rowKeysByLabel(rowLabel) = rowKey
                    Dim row As New PivotRowState(rowKey, rowLabel)
                    rowStates.Add(row)
                    rowMap.Add(rowKey, row)
                End If

                Dim columnKey As String = String.Empty
                If Not columnKeysByLabel.TryGetValue(columnLabel, columnKey) Then
                    If columnStates.Count >= PivotTableTokens.MaxProjectedSourceColumns Then Continue For
                    columnKey = MASPivotTablePolicy.ResolveUniqueKey(columnLabel, "column", columnStates.Count + 1, columnMap.Keys)
                    columnKeysByLabel(columnLabel) = columnKey
                    Dim column As New PivotColumnState(columnKey, columnLabel)
                    columnStates.Add(column)
                    columnMap.Add(columnKey, column)
                End If

                Dim numericValue As Decimal = 1D
                If _pivotSourceDefinition.AggregateKind <> MASPivotAggregateKind.Count Then
                    Dim rawValue As Object = Nothing
                    If Not TryResolvePivotField(item.Item, _pivotSourceDefinition.ValueField, rawValue) Then Continue For
                    If Not MASPivotTablePolicy.TryCoerceDecimal(rawValue, numericValue) Then Continue For
                End If

                Dim cellKey As String = BuildCellKey(rowKey, columnKey)
                Dim bucket As PivotAggregateBucket = Nothing
                If Not buckets.TryGetValue(cellKey, bucket) Then
                    bucket = New PivotAggregateBucket()
                    buckets.Add(cellKey, bucket)
                End If
                bucket.Add(numericValue)

                Dim entries As List(Of MASPivotDrilldownEntry) = Nothing
                If Not drilldown.TryGetValue(cellKey, entries) Then
                    entries = New List(Of MASPivotDrilldownEntry)()
                    drilldown.Add(cellKey, entries)
                End If
                entries.Add(New MASPivotDrilldownEntry(item.StableKey, item.Item, rowKey, columnKey, numericValue))
            Next

            Dim values As New Dictionary(Of String, Decimal)(StringComparer.Ordinal)
            For Each pair As KeyValuePair(Of String, PivotAggregateBucket) In buckets
                values(pair.Key) = pair.Value.Resolve(_pivotSourceDefinition.AggregateKind)
            Next
            ReplacePivotProjection(rowStates, columnStates, values, drilldown, snapshot.Revision, reason)
        End Sub

        Private Sub ReplacePivotProjection(rows As List(Of PivotRowState),
                                           columns As List(Of PivotColumnState),
                                           values As Dictionary(Of String, Decimal),
                                           drilldown As Dictionary(Of String, List(Of MASPivotDrilldownEntry)),
                                           revision As Integer,
                                           reason As String)
            If _isProjectingPivotSource Then Return
            _isProjectingPivotSource = True
            Try
                Dim oldRowKey As String = _selectedRowKey
                Dim oldColumnKey As String = _selectedColumnKey
                _rows.Clear()
                _columns.Clear()
                _rowLookup.Clear()
                _columnLookup.Clear()
                _rowIndexByKey.Clear()
                _columnIndexByKey.Clear()
                _values.Clear()
                _rowTotals.Clear()
                _columnTotals.Clear()
                _pivotDrilldown.Clear()

                If rows IsNot Nothing Then
                    For Each row As PivotRowState In rows
                        If row Is Nothing OrElse _rowLookup.ContainsKey(row.Key) Then Continue For
                        _rowIndexByKey(row.Key) = _rows.Count
                        _rows.Add(row)
                        _rowLookup.Add(row.Key, row)
                        _rowTotals(row.Key) = 0D
                    Next
                End If
                If columns IsNot Nothing Then
                    For Each column As PivotColumnState In columns
                        If column Is Nothing OrElse _columnLookup.ContainsKey(column.Key) Then Continue For
                        _columnIndexByKey(column.Key) = _columns.Count
                        _columns.Add(column)
                        _columnLookup.Add(column.Key, column)
                        _columnTotals(column.Key) = 0D
                    Next
                End If
                If values IsNot Nothing Then
                    For Each pair As KeyValuePair(Of String, Decimal) In values
                        If pair.Value <> 0D Then _values(pair.Key) = pair.Value
                    Next
                End If
                If drilldown IsNot Nothing Then
                    For Each pair As KeyValuePair(Of String, List(Of MASPivotDrilldownEntry)) In drilldown
                        _pivotDrilldown(pair.Key) = New List(Of MASPivotDrilldownEntry)(pair.Value)
                    Next
                End If

                _pivotSourceRevision = revision
                InvalidateSummaryCache(If(reason, "MASPivotTable.ReplacePivotProjection"))
                If oldRowKey.Length > 0 AndAlso oldColumnKey.Length > 0 AndAlso _rowLookup.ContainsKey(oldRowKey) AndAlso _columnLookup.ContainsKey(oldColumnKey) Then
                    _selectedRowKey = oldRowKey
                    _selectedColumnKey = oldColumnKey
                Else
                    _selectedRowKey = String.Empty
                    _selectedColumnKey = String.Empty
                End If
                _hoverRowKey = String.Empty
                _hoverColumnKey = String.Empty
                _pressedRowKey = String.Empty
                _pressedColumnKey = String.Empty
                NormalizeViewState()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASPivotTable.PivotSource")
                InvalidateVisual()
                RaisePivotTableChangedSafe()
                If Not String.Equals(oldRowKey, _selectedRowKey, StringComparison.Ordinal) OrElse Not String.Equals(oldColumnKey, _selectedColumnKey, StringComparison.Ordinal) Then RaiseSelectedCellChangedSafe()
            Finally
                _isProjectingPivotSource = False
            End Try
        End Sub

        Private Sub DetachPivotSourceIfManualMutation(reason As String)
            If _isProjectingPivotSource Then Return
            If _pivotItemsSource Is Nothing AndAlso _pivotSourceDefinition Is Nothing Then Return
            DetachPivotSource()
            _pivotSourceRevision = -1
            _pivotDrilldown.Clear()
        End Sub

        Private Sub DetachPivotSource()
            If _pivotItemsSource IsNot Nothing Then
                Try
                    RemoveHandler _pivotItemsSource.Changed, AddressOf OnPivotSourceChanged
                Catch ex As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(ex, "MASPivotTable.DetachPivotSource")
                End Try
            End If
            _pivotItemsSource = Nothing
            _pivotSourceDefinition = Nothing
        End Sub

        Private Shared Function TryResolvePivotField(item As Object,
                                                     fieldName As String,
                                                     ByRef value As Object) As Boolean
            value = Nothing
            Dim normalizedField As String = If(fieldName, String.Empty).Trim()
            If item Is Nothing OrElse normalizedField.Length = 0 Then Return False

            Dim record As MASPivotSourceRecord = TryCast(item, MASPivotSourceRecord)
            If record IsNot Nothing AndAlso record.TryGetField(normalizedField, value) Then Return True

            Dim genericDictionary As IDictionary(Of String, Object) = TryCast(item, IDictionary(Of String, Object))
            If genericDictionary IsNot Nothing AndAlso genericDictionary.TryGetValue(normalizedField, value) Then Return True

            Dim dictionary As IDictionary = TryCast(item, IDictionary)
            If dictionary IsNot Nothing AndAlso dictionary.Contains(normalizedField) Then
                value = dictionary(normalizedField)
                Return True
            End If

            Dim flags As BindingFlags = BindingFlags.Instance Or BindingFlags.Public Or BindingFlags.IgnoreCase
            Dim itemType As Type = item.GetType()
            Dim prop As PropertyInfo = itemType.GetProperty(normalizedField, flags)
            If prop IsNot Nothing AndAlso prop.GetIndexParameters().Length = 0 Then
                value = prop.GetValue(item, Nothing)
                Return True
            End If
            Dim field As FieldInfo = itemType.GetField(normalizedField, flags)
            If field IsNot Nothing Then
                value = field.GetValue(item)
                Return True
            End If
            Return False
        End Function

#End Region

    End Class

End Namespace
