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 MASMasterDetailView

#Region "Friend source projection"

        Private _masterDetailItemsSource As IMASItemsSource
        Private _masterDetailSourceDefinition As MASMasterDetailSourceDefinition
        Private _masterDetailSourceRevision As Integer = -1
        Private _isProjectingMasterDetailSource As Boolean

        Friend ReadOnly Property HasMasterDetailSource As Boolean
            Get
                Return _masterDetailItemsSource IsNot Nothing AndAlso _masterDetailSourceDefinition IsNot Nothing AndAlso _masterDetailSourceDefinition.IsValid
            End Get
        End Property

        Friend ReadOnly Property MasterDetailSourceRevision As Integer
            Get
                Return _masterDetailSourceRevision
            End Get
        End Property

        Friend Function SetMasterDetailSource(source As IMASItemsSource,
                                              definition As MASMasterDetailSourceDefinition,
                                              Optional reason As String = "") As MASMasterDetailView
            If source Is Nothing OrElse definition Is Nothing OrElse Not definition.IsValid Then
                Return ClearMasterDetailSource(reason)
            End If

            DetachMasterDetailSource()
            _masterDetailItemsSource = source
            _masterDetailSourceDefinition = definition
            AddHandler _masterDetailItemsSource.Changed, AddressOf OnMasterDetailSourceChanged
            RebuildMasterDetailFromSource(If(reason, "MASMasterDetailView.SetMasterDetailSource"))
            Return Me
        End Function

        Friend Function ClearMasterDetailSource(Optional reason As String = "") As MASMasterDetailView
            If _masterDetailItemsSource Is Nothing AndAlso _masterDetailSourceDefinition Is Nothing Then Return Me
            DetachMasterDetailSource()
            _masterDetailSourceDefinition = Nothing
            _masterDetailSourceRevision = -1
            InvalidateVisual()
            RaiseMasterDetailChangedSafe()
            Return Me
        End Function

        Friend Function GetProjectedMasterDetailKeys() As IReadOnlyList(Of String)
            Return GetItemKeys()
        End Function

        Private Sub OnMasterDetailSourceChanged(sender As Object,
                                                e As MASItemsSourceChangedEventArgs)
            If Not Object.ReferenceEquals(sender, _masterDetailItemsSource) Then Return
            Dim reason As String = "MASMasterDetailView.SourceChanged"
            If e IsNot Nothing AndAlso e.Change IsNot Nothing AndAlso e.Change.Reason.Length > 0 Then reason = e.Change.Reason
            RebuildMasterDetailFromSource(reason)
        End Sub

        Private Sub RebuildMasterDetailFromSource(reason As String)
            If _masterDetailItemsSource Is Nothing OrElse _masterDetailSourceDefinition Is Nothing OrElse Not _masterDetailSourceDefinition.IsValid Then
                ReplaceMasterDetailProjection(New List(Of MasterDetailItemState)(), -1, reason)
                Return
            End If

            Dim snapshot As MASItemsSourceSnapshot = Nothing
            Try
                snapshot = _masterDetailItemsSource.Snapshot()
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(ex, "MASMasterDetailView.SourceSnapshot")
                SetMasterDetailErrorState("Unable to read the master/detail source snapshot.", "MASMasterDetailView.SourceSnapshot")
            End Try

            If snapshot Is Nothing Then
                If _masterDetailContentState <> MASMasterDetailContentStateKind.Faulted Then
                    SetMasterDetailEmptyState("No master/detail source items are available.", reason)
                End If
                ReplaceMasterDetailProjection(New List(Of MasterDetailItemState)(), _masterDetailItemsSource.Revision, reason)
                Return
            End If

            Dim items As New List(Of MasterDetailItemState)()
            Dim usedKeys As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)

            For i As Integer = 0 To snapshot.Items.Count - 1
                If items.Count >= MasterDetailViewTokens.MaxProjectedSourceItems Then Exit For
                Dim sourceItem As MASItemsSourceItem = snapshot.Items(i)
                If sourceItem Is Nothing Then Continue For

                Dim rawTitle As Object = Nothing
                Dim rawDetailText As Object = Nothing
                Dim hasTitle As Boolean = TryResolveMasterDetailField(sourceItem.Item, _masterDetailSourceDefinition.TitleField, rawTitle)
                Dim hasDetail As Boolean = TryResolveMasterDetailField(sourceItem.Item, _masterDetailSourceDefinition.DetailTextField, rawDetailText)
                If Not hasTitle AndAlso Not hasDetail Then Continue For

                Dim rawKey As Object = Nothing
                Dim requestedKey As String = String.Empty
                If _masterDetailSourceDefinition.KeyField.Length > 0 AndAlso TryResolveMasterDetailField(sourceItem.Item, _masterDetailSourceDefinition.KeyField, rawKey) Then
                    requestedKey = Convert.ToString(rawKey, CultureInfo.CurrentCulture)
                End If
                If requestedKey Is Nothing OrElse requestedKey.Trim().Length = 0 Then requestedKey = sourceItem.StableKey
                Dim itemKey As String = ResolveUniqueMasterDetailKey(requestedKey, "item", items.Count + 1, usedKeys)

                Dim rawSubtitle As Object = Nothing
                Dim rawDetailTitle As Object = Nothing
                Dim rawBadge As Object = Nothing
                Dim subtitle As String = String.Empty
                Dim detailTitle As String = String.Empty
                Dim badge As String = String.Empty
                If _masterDetailSourceDefinition.SubtitleField.Length > 0 AndAlso TryResolveMasterDetailField(sourceItem.Item, _masterDetailSourceDefinition.SubtitleField, rawSubtitle) Then
                    subtitle = Convert.ToString(rawSubtitle, CultureInfo.CurrentCulture)
                End If
                If _masterDetailSourceDefinition.DetailTitleField.Length > 0 AndAlso TryResolveMasterDetailField(sourceItem.Item, _masterDetailSourceDefinition.DetailTitleField, rawDetailTitle) Then
                    detailTitle = Convert.ToString(rawDetailTitle, CultureInfo.CurrentCulture)
                End If
                If _masterDetailSourceDefinition.BadgeTextField.Length > 0 AndAlso TryResolveMasterDetailField(sourceItem.Item, _masterDetailSourceDefinition.BadgeTextField, rawBadge) Then
                    badge = Convert.ToString(rawBadge, CultureInfo.CurrentCulture)
                End If

                Dim title As String = If(hasTitle, Convert.ToString(rawTitle, CultureInfo.CurrentCulture), String.Empty)
                Dim detailText As String = If(hasDetail, Convert.ToString(rawDetailText, CultureInfo.CurrentCulture), String.Empty)
                items.Add(New MasterDetailItemState(itemKey,
                                                    MASMasterDetailViewPolicy.NormalizeItemTitle(title),
                                                    MASMasterDetailViewPolicy.NormalizeSubtitle(subtitle),
                                                    MASMasterDetailViewPolicy.NormalizeDetailTitle(detailTitle, title),
                                                    MASMasterDetailViewPolicy.NormalizeDetailText(detailText),
                                                    MASMasterDetailViewPolicy.NormalizeBadgeText(badge)))
            Next

            ReplaceMasterDetailProjection(items, snapshot.Revision, reason)
        End Sub

        Private Sub ReplaceMasterDetailProjection(items As List(Of MasterDetailItemState),
                                                  revision As Integer,
                                                  reason As String)
            If _isProjectingMasterDetailSource Then Return
            _isProjectingMasterDetailSource = True
            Try
                Dim oldSelectedKey As String = SelectedItemKey
                Dim oldTop As Integer = _topIndex

                _items.Clear()
                _hits.Clear()
                _selectedIndex = -1
                _hoverIndex = -1
                _pressedIndex = -1
                _topIndex = 0

                If items IsNot Nothing Then
                    For Each item As MasterDetailItemState In items
                        If item Is Nothing Then Continue For
                        If _items.Count >= MasterDetailViewTokens.MaxProjectedSourceItems Then Exit For
                        _items.Add(item)
                    Next
                End If

                _masterDetailSourceRevision = revision
                _topIndex = Math.Max(0, Math.Min(oldTop, ResolveMasterViewportMaxTop()))

                If oldSelectedKey.Length > 0 Then
                    Dim selected As Integer = FindItemIndex(oldSelectedKey)
                    If selected >= 0 Then _selectedIndex = selected
                End If
                If _selectedIndex < 0 AndAlso _items.Count > 0 Then _selectedIndex = 0
                NormalizeViewState()
                If _items.Count > 0 Then
                    ClearMasterDetailStatusState("MASMasterDetailView.SourceProjectionLoaded")
                ElseIf _masterDetailContentState <> MASMasterDetailContentStateKind.Faulted Then
                    SetMasterDetailEmptyState("No master/detail items match the current source definition.", reason)
                End If

                RequestSizeLayoutRefreshForSizeAffectingChange("MASMasterDetailView.ReplaceSourceProjection")
                InvalidateVisual()
                RaiseMasterDetailChangedSafe()
                RaiseSelectedItemChangedSafe()
            Finally
                _isProjectingMasterDetailSource = False
            End Try
        End Sub

        Private Sub DetachMasterDetailSource()
            If _masterDetailItemsSource IsNot Nothing Then
                RemoveHandler _masterDetailItemsSource.Changed, AddressOf OnMasterDetailSourceChanged
            End If
            _masterDetailItemsSource = Nothing
        End Sub

        Private Sub ClearMasterDetailSourceLinkForManualMutation()
            If _isProjectingMasterDetailSource OrElse _masterDetailItemsSource Is Nothing Then Return
            DetachMasterDetailSource()
            _masterDetailSourceDefinition = Nothing
            _masterDetailSourceRevision = -1
        End Sub

        Private Shared Function ResolveUniqueMasterDetailKey(rawKey As String,
                                                             fallbackPrefix As String,
                                                             index As Integer,
                                                             usedKeys As HashSet(Of String)) As String
            Dim normalized As String = MASMasterDetailViewPolicy.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 MASMasterDetailViewPolicy.NormalizeKey(fallbackPrefix, fallbackPrefix, index)
        End Function

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