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 MASAgendaView

#Region "Friend source projection"

        Private _agendaItemsSource As IMASItemsSource
        Private _agendaSourceDefinition As MASAgendaSourceDefinition
        Private _agendaSourceRevision As Integer = -1
        Private _isProjectingAgendaSource As Boolean

        Friend ReadOnly Property HasAgendaSource As Boolean
            Get
                Return _agendaItemsSource IsNot Nothing AndAlso _agendaSourceDefinition IsNot Nothing AndAlso _agendaSourceDefinition.IsValid
            End Get
        End Property

        Friend ReadOnly Property AgendaSourceRevision As Integer
            Get
                Return _agendaSourceRevision
            End Get
        End Property

        Friend Function SetAgendaSource(source As IMASItemsSource,
                                        definition As MASAgendaSourceDefinition,
                                        Optional reason As String = "") As MASAgendaView
            If source Is Nothing OrElse definition Is Nothing OrElse Not definition.IsValid Then
                Return ClearAgendaSource(reason)
            End If

            DetachAgendaSource()
            _agendaItemsSource = source
            _agendaSourceDefinition = definition
            AddHandler _agendaItemsSource.Changed, AddressOf OnAgendaSourceChanged
            RebuildAgendaFromSource(If(reason, "MASAgendaView.SetAgendaSource"))
            Return Me
        End Function

        Friend Function ClearAgendaSource(Optional reason As String = "") As MASAgendaView
            If _agendaItemsSource Is Nothing AndAlso _agendaSourceDefinition Is Nothing Then Return Me
            DetachAgendaSource()
            _agendaSourceDefinition = Nothing
            _agendaSourceRevision = -1
            InvalidateVisual()
            RaiseAgendaChangedSafe()
            Return Me
        End Function

        Friend Function GetProjectedAgendaEventKeys() As IReadOnlyList(Of String)
            Dim keys As New List(Of String)()
            For Each item As AgendaEventState In _events
                If item IsNot Nothing Then keys.Add(item.Key)
            Next
            Return New ReadOnlyCollection(Of String)(keys)
        End Function

        Private Sub OnAgendaSourceChanged(sender As Object,
                                          e As MASItemsSourceChangedEventArgs)
            If Not Object.ReferenceEquals(sender, _agendaItemsSource) Then Return
            Dim reason As String = "MASAgendaView.AgendaSourceChanged"
            If e IsNot Nothing AndAlso e.Change IsNot Nothing AndAlso e.Change.Reason.Length > 0 Then reason = e.Change.Reason
            RebuildAgendaFromSource(reason)
        End Sub

        Private Sub RebuildAgendaFromSource(reason As String)
            If _agendaItemsSource Is Nothing OrElse _agendaSourceDefinition Is Nothing OrElse Not _agendaSourceDefinition.IsValid Then
                ReplaceAgendaProjection(New List(Of AgendaEventState)(), -1, reason)
                Return
            End If

            Dim snapshot As MASItemsSourceSnapshot = Nothing
            Try
                snapshot = _agendaItemsSource.Snapshot()
            Catch ex As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(ex, "MASAgendaView.AgendaSourceSnapshot")
            End Try

            If snapshot Is Nothing Then
                ReplaceAgendaProjection(New List(Of AgendaEventState)(), _agendaItemsSource.Revision, reason)
                Return
            End If

            Dim projected As New List(Of AgendaEventState)()
            Dim usedKeys As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)

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

                Dim rawTitle As Object = Nothing
                Dim rawStart As Object = Nothing
                Dim rawEnd As Object = Nothing
                If Not TryResolveAgendaField(sourceItem.Item, _agendaSourceDefinition.TitleField, rawTitle) Then Continue For
                If Not TryResolveAgendaField(sourceItem.Item, _agendaSourceDefinition.StartField, rawStart) Then Continue For
                If Not TryResolveAgendaField(sourceItem.Item, _agendaSourceDefinition.EndField, rawEnd) Then Continue For

                Dim startValue As DateTime
                Dim endValue As DateTime
                If Not TryCoerceAgendaDateTime(rawStart, startValue) Then Continue For
                If Not TryCoerceAgendaDateTime(rawEnd, endValue) Then Continue For
                If endValue <= startValue Then Continue For

                Dim rawKey As Object = Nothing
                Dim requestedKey As String = String.Empty
                If _agendaSourceDefinition.KeyField.Length > 0 AndAlso TryResolveAgendaField(sourceItem.Item, _agendaSourceDefinition.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 eventKey As String = ResolveUniqueAgendaKey(requestedKey, "event", projected.Count + 1, usedKeys)

                Dim rawDetail As Object = Nothing
                Dim detail As String = String.Empty
                If _agendaSourceDefinition.DetailField.Length > 0 AndAlso TryResolveAgendaField(sourceItem.Item, _agendaSourceDefinition.DetailField, rawDetail) Then
                    detail = Convert.ToString(rawDetail, CultureInfo.CurrentCulture)
                End If

                Dim eventDay As Date = MASAgendaViewPolicy.NormalizeEventDay(startValue, _day)
                Dim startMinute As Integer = MASAgendaViewPolicy.ClampMinuteToRange(MASAgendaViewPolicy.MinutesOfDay(startValue), _startHour, _endHour)
                Dim endMinute As Integer = MASAgendaViewPolicy.ClampMinuteToRange(MASAgendaViewPolicy.MinutesOfDay(endValue), _startHour, _endHour)
                If endMinute <= startMinute Then endMinute = Math.Min(_endHour * 60, startMinute + 30)
                If endMinute <= startMinute Then Continue For

                projected.Add(New AgendaEventState(eventKey,
                                                   MASAgendaViewPolicy.NormalizeEventTitle(Convert.ToString(rawTitle, CultureInfo.CurrentCulture)),
                                                   MASAgendaViewPolicy.NormalizeEventDetail(detail),
                                                   eventDay,
                                                   startMinute,
                                                   endMinute))
            Next

            ReplaceAgendaProjection(projected, snapshot.Revision, reason)
        End Sub

        Private Sub ReplaceAgendaProjection(events As List(Of AgendaEventState),
                                            revision As Integer,
                                            reason As String)
            If _isProjectingAgendaSource Then Return
            _isProjectingAgendaSource = True
            Try
                Dim oldSelectedKey As String = SelectedEventKey
                _events.Clear()
                _selectedEventIndex = -1
                _hoverEventIndex = -1
                _pressedEventIndex = -1
                _topEventIndex = 0

                If events IsNot Nothing Then
                    For Each item As AgendaEventState In events
                        If item IsNot Nothing Then _events.Add(item)
                    Next
                End If

                _agendaSourceRevision = revision
                If oldSelectedKey.Length > 0 Then _selectedEventIndex = FindEventIndex(oldSelectedKey)
                NormalizeEventOrderAndSelection()
                ResetPointerState()
                RequestSizeLayoutRefreshForSizeAffectingChange("MASAgendaView.AgendaSource")
                InvalidateVisual()
                RaiseAgendaChangedSafe()
                If Not String.Equals(oldSelectedKey, SelectedEventKey, StringComparison.Ordinal) Then RaiseSelectedEventChangedSafe()
            Finally
                _isProjectingAgendaSource = False
            End Try
        End Sub

        Private Sub DetachAgendaSource()
            If _agendaItemsSource IsNot Nothing Then
                Try
                    RemoveHandler _agendaItemsSource.Changed, AddressOf OnAgendaSourceChanged
                Catch ex As Exception
                    Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowDiagnosticOnly(ex, "MASAgendaView.DetachAgendaSource")
                End Try
            End If
            _agendaItemsSource = Nothing
        End Sub

        Private Sub ClearAgendaSourceLinkForManualMutation()
            If _isProjectingAgendaSource OrElse _agendaItemsSource Is Nothing Then Return
            DetachAgendaSource()
            _agendaSourceDefinition = Nothing
            _agendaSourceRevision = -1
        End Sub

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

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

        Private Shared Function TryCoerceAgendaDateTime(value As Object,
                                                        ByRef result As DateTime) As Boolean
            result = DateTime.MinValue
            If value Is Nothing Then Return False
            If TypeOf value Is DateTime Then
                result = CType(value, DateTime)
                Return True
            End If
            Dim text As String = Convert.ToString(value, CultureInfo.CurrentCulture)
            If DateTime.TryParse(text, CultureInfo.CurrentCulture, DateTimeStyles.None, result) Then Return True
            If DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.None, result) Then Return True
            Return False
        End Function

#End Region

    End Class

End Namespace
