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 MASKanbanBoard

#Region "Friend source projection"

        Private _kanbanItemsSource As IMASItemsSource
        Private _kanbanSourceDefinition As MASKanbanSourceDefinition
        Private _kanbanSourceRevision As Integer = -1
        Private _isProjectingKanbanSource As Boolean

        Friend ReadOnly Property HasKanbanSource As Boolean
            Get
                Return _kanbanItemsSource IsNot Nothing AndAlso _kanbanSourceDefinition IsNot Nothing AndAlso _kanbanSourceDefinition.IsValid
            End Get
        End Property

        Friend ReadOnly Property KanbanSourceRevision As Integer
            Get
                Return _kanbanSourceRevision
            End Get
        End Property

        Friend Function SetKanbanSource(source As IMASItemsSource,
                                        definition As MASKanbanSourceDefinition,
                                        Optional reason As String = "") As MASKanbanBoard
            If source Is Nothing OrElse definition Is Nothing OrElse Not definition.IsValid Then
                Return ClearKanbanSource(reason)
            End If

            DetachKanbanSource()
            _kanbanItemsSource = source
            _kanbanSourceDefinition = definition
            AddHandler _kanbanItemsSource.Changed, AddressOf OnKanbanSourceChanged
            RebuildKanbanFromSource(If(reason, "MASKanbanBoard.SetKanbanSource"))
            Return Me
        End Function

        Friend Function ClearKanbanSource(Optional reason As String = "") As MASKanbanBoard
            If _kanbanItemsSource Is Nothing AndAlso _kanbanSourceDefinition Is Nothing Then Return Me
            DetachKanbanSource()
            _kanbanSourceDefinition = Nothing
            _kanbanSourceRevision = -1
            InvalidateVisual()
            RaiseBoardChangedSafe()
            Return Me
        End Function

        Friend Function GetProjectedCardKeys() As IReadOnlyList(Of String)
            Dim keys As New List(Of String)()
            For Each column As KanbanColumnState In _columns
                If column Is Nothing Then Continue For
                For Each card As KanbanCardState In column.Cards
                    If card IsNot Nothing Then keys.Add(card.Key)
                Next
            Next
            Return New ReadOnlyCollection(Of String)(keys)
        End Function

        Private Sub OnKanbanSourceChanged(sender As Object,
                                          e As MASItemsSourceChangedEventArgs)
            If Not Object.ReferenceEquals(sender, _kanbanItemsSource) Then Return
            Dim reason As String = "MASKanbanBoard.KanbanSourceChanged"
            If e IsNot Nothing AndAlso e.Change IsNot Nothing AndAlso e.Change.Reason.Length > 0 Then reason = e.Change.Reason
            RebuildKanbanFromSource(reason)
        End Sub

        Private Sub RebuildKanbanFromSource(reason As String)
            If _kanbanItemsSource Is Nothing OrElse _kanbanSourceDefinition Is Nothing OrElse Not _kanbanSourceDefinition.IsValid Then
                ReplaceKanbanProjection(New List(Of KanbanColumnState)(), -1, reason)
                Return
            End If

            Dim snapshot As MASItemsSourceSnapshot = Nothing
            Try
                snapshot = _kanbanItemsSource.Snapshot()
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(ex, "MASKanbanBoard.KanbanSourceSnapshot")
            End Try

            If snapshot Is Nothing Then
                ReplaceKanbanProjection(New List(Of KanbanColumnState)(), _kanbanItemsSource.Revision, reason)
                Return
            End If

            Dim columns As New List(Of KanbanColumnState)()
            Dim columnLookup As New Dictionary(Of String, KanbanColumnState)(StringComparer.OrdinalIgnoreCase)
            Dim usedCardKeys As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
            Dim totalCards As Integer = 0

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

                Dim rawColumnKey As Object = Nothing
                Dim rawCardTitle As Object = Nothing
                If Not TryResolveKanbanField(item.Item, _kanbanSourceDefinition.ColumnKeyField, rawColumnKey) Then Continue For
                If Not TryResolveKanbanField(item.Item, _kanbanSourceDefinition.CardTitleField, rawCardTitle) Then Continue For

                Dim columnKey As String = MASKanbanBoardPolicy.NormalizeKey(Convert.ToString(rawColumnKey, CultureInfo.CurrentCulture), "column", columns.Count + 1)
                If columnKey.Length = 0 Then Continue For

                Dim column As KanbanColumnState = Nothing
                If Not columnLookup.TryGetValue(columnKey, column) Then
                    If columns.Count >= KanbanBoardTokens.MaxColumns Then Continue For
                    Dim rawColumnTitle As Object = Nothing
                    Dim columnTitle As String = String.Empty
                    If _kanbanSourceDefinition.ColumnTitleField.Length > 0 AndAlso TryResolveKanbanField(item.Item, _kanbanSourceDefinition.ColumnTitleField, rawColumnTitle) Then
                        columnTitle = Convert.ToString(rawColumnTitle, CultureInfo.CurrentCulture)
                    End If
                    If columnTitle Is Nothing OrElse columnTitle.Trim().Length = 0 Then columnTitle = columnKey
                    column = New KanbanColumnState(columnKey, MASKanbanBoardPolicy.NormalizeColumnTitle(columnTitle))
                    columns.Add(column)
                    columnLookup.Add(column.Key, column)
                End If

                Dim rawCardKey As Object = Nothing
                Dim requestedCardKey As String = String.Empty
                If _kanbanSourceDefinition.CardKeyField.Length > 0 AndAlso TryResolveKanbanField(item.Item, _kanbanSourceDefinition.CardKeyField, rawCardKey) Then
                    requestedCardKey = Convert.ToString(rawCardKey, CultureInfo.CurrentCulture)
                End If
                If requestedCardKey Is Nothing OrElse requestedCardKey.Trim().Length = 0 Then requestedCardKey = item.StableKey
                Dim cardKey As String = ResolveUniqueKanbanKey(requestedCardKey, "card", totalCards + 1, usedCardKeys)

                Dim rawDetail As Object = Nothing
                Dim rawBadge As Object = Nothing
                Dim detail As String = String.Empty
                Dim badge As String = String.Empty
                If _kanbanSourceDefinition.CardDetailField.Length > 0 AndAlso TryResolveKanbanField(item.Item, _kanbanSourceDefinition.CardDetailField, rawDetail) Then
                    detail = Convert.ToString(rawDetail, CultureInfo.CurrentCulture)
                End If
                If _kanbanSourceDefinition.BadgeTextField.Length > 0 AndAlso TryResolveKanbanField(item.Item, _kanbanSourceDefinition.BadgeTextField, rawBadge) Then
                    badge = Convert.ToString(rawBadge, CultureInfo.CurrentCulture)
                End If

                column.Cards.Add(New KanbanCardState(cardKey,
                                                     MASKanbanBoardPolicy.NormalizeCardTitle(Convert.ToString(rawCardTitle, CultureInfo.CurrentCulture)),
                                                     MASKanbanBoardPolicy.NormalizeCardDetail(detail),
                                                     MASKanbanBoardPolicy.NormalizeBadgeText(badge)))
                totalCards += 1
            Next

            ReplaceKanbanProjection(columns, snapshot.Revision, reason)
        End Sub

        Private Sub ReplaceKanbanProjection(columns As List(Of KanbanColumnState),
                                            revision As Integer,
                                            reason As String)
            If _isProjectingKanbanSource Then Return
            _isProjectingKanbanSource = True
            Try
                Dim oldSelectedCardKey As String = SelectedCardKey
                Dim oldSelectedColumnKey As String = SelectedColumnKey
                Dim oldLeftColumnIndex As Integer = _leftColumnIndex
                Dim oldTopByColumnKey As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
                For Each oldColumn As KanbanColumnState In _columns
                    If oldColumn IsNot Nothing AndAlso Not oldTopByColumnKey.ContainsKey(oldColumn.Key) Then oldTopByColumnKey.Add(oldColumn.Key, oldColumn.TopCardIndex)
                Next

                _columns.Clear()
                _columnLookup.Clear()
                _columnIndexByKey.Clear()
                _cardLocationByKey.Clear()
                _cardCount = 0
                _selectedColumnIndex = -1
                _selectedCardIndex = -1

                If columns IsNot Nothing Then
                    For Each column As KanbanColumnState In columns
                        If column Is Nothing OrElse _columnLookup.ContainsKey(column.Key) Then Continue For
                        If _columns.Count >= KanbanBoardTokens.MaxColumns Then Exit For
                        Dim top As Integer = 0
                        If oldTopByColumnKey.TryGetValue(column.Key, top) Then column.TopCardIndex = Math.Max(0, Math.Min(top, ResolveColumnMaxTop(column)))
                        _columnIndexByKey(column.Key) = _columns.Count
                        _columns.Add(column)
                        _columnLookup.Add(column.Key, column)
                        _cardCount += column.Cards.Count
                    Next
                End If

                For columnIndex As Integer = 0 To _columns.Count - 1
                    ReindexCardLocationsForColumn(columnIndex)
                Next

                _kanbanSourceRevision = revision
                _leftColumnIndex = Math.Max(0, Math.Min(oldLeftColumnIndex, Math.Max(0, _columns.Count - 1)))

                If oldSelectedCardKey.Length > 0 Then
                    Dim location As Tuple(Of Integer, Integer) = FindCardLocation(oldSelectedCardKey)
                    If location IsNot Nothing Then
                        _selectedColumnIndex = location.Item1
                        _selectedCardIndex = location.Item2
                    End If
                End If
                If _selectedColumnIndex < 0 AndAlso oldSelectedColumnKey.Length > 0 Then
                    Dim column As KanbanColumnState = FindColumn(oldSelectedColumnKey)
                    Dim columnIndex As Integer = ResolveColumnIndex(column)
                    If columnIndex >= 0 Then
                        _selectedColumnIndex = columnIndex
                        _selectedCardIndex = If(_columns(columnIndex).Cards.Count > 0, 0, -1)
                    End If
                End If
                If _selectedColumnIndex < 0 AndAlso _columns.Count > 0 Then
                    _selectedColumnIndex = 0
                    _selectedCardIndex = If(_columns(0).Cards.Count > 0, 0, -1)
                End If

                NormalizeSelection()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASKanbanBoard.ReplaceKanbanProjection")
                InvalidateVisual()
                RaiseBoardChangedSafe()
                RaiseSelectedCardChangedSafe()
            Finally
                _isProjectingKanbanSource = False
            End Try
        End Sub

        Private Sub DetachKanbanSource()
            If _kanbanItemsSource IsNot Nothing Then
                RemoveHandler _kanbanItemsSource.Changed, AddressOf OnKanbanSourceChanged
            End If
            _kanbanItemsSource = Nothing
        End Sub

        Private Sub ClearKanbanSourceLinkForManualMutation()
            If _isProjectingKanbanSource OrElse _kanbanItemsSource Is Nothing Then Return
            DetachKanbanSource()
            _kanbanSourceDefinition = Nothing
            _kanbanSourceRevision = -1
        End Sub

        Private Shared Function ResolveUniqueKanbanKey(rawKey As String,
                                                       fallbackPrefix As String,
                                                       index As Integer,
                                                       usedKeys As HashSet(Of String)) As String
            Dim normalized As String = MASKanbanBoardPolicy.NormalizeKey(rawKey, fallbackPrefix, index)
            If usedKeys Is Nothing Then Return normalized
            If Not usedKeys.Contains(normalized) Then
                usedKeys.Add(normalized)
                Return normalized
            End If

            Dim suffix As Integer = 2
            Dim baseKey As String = normalized
            If baseKey.Length > 56 Then baseKey = baseKey.Substring(0, 56)
            Do
                Dim candidate As String = baseKey & "-" & suffix.ToString(CultureInfo.InvariantCulture)
                If Not usedKeys.Contains(candidate) Then
                    usedKeys.Add(candidate)
                    Return candidate
                End If
                suffix += 1
            Loop While suffix < 100000

            Return MASKanbanBoardPolicy.NormalizeKey(fallbackPrefix, fallbackPrefix, index)
        End Function

        Private Shared Function TryResolveKanbanField(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 MASKanbanSourceRecord = TryCast(item, MASKanbanSourceRecord)
            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
