Imports System.ComponentModel
Imports System.Threading

Namespace Data

    ''' <summary>
    ''' A virtualizing collection that exposes the item container class.
    ''' </summary>
    ''' <typeparam name="T">The type of the items referenced by the collection.</typeparam>
    ''' <remarks></remarks>
    Public Class VirtualContainerCollection(Of T)
        Implements IList(Of DataContainer(Of T)), IList, INotifyPropertyChanged

        ''' <summary>
        ''' Initializes a new instance of the VirtualContainerCollection(Of T) 
        ''' class with the specified item provider.
        ''' </summary>
        ''' <param name="itemProvider">The provider interface for retrieving virtualized items.</param>
        Public Sub New(ByVal itemProvider As IVirtualizingItemProvider(Of T))
            Me.New(itemProvider, 100)
        End Sub

        ''' <summary>
        ''' Initializes a new instance of the VirtualContainerCollection(Of T) 
        ''' class with the specified item provider and page size.
        ''' </summary>
        ''' <param name="itemProvider">The provider interface for retrieving virtualized items.</param>
        ''' <param name="pageSize">The number of elements in a page.</param>
        Public Sub New(ByVal itemProvider As IVirtualizingItemProvider(Of T), ByVal pageSize As Integer)
            Me.New(itemProvider, pageSize, 10000)
        End Sub

        ''' <summary>
        ''' Initializes a new instance of the VirtualContainerCollection(Of T) 
        ''' class with the specified item provider, page size, and timeout.
        ''' </summary>
        ''' <param name="itemProvider">The provider interface for retrieving virtualized items.</param>
        ''' <param name="pageSize">The number of elements in a page.</param>
        ''' <param name="pageTimeout">The number of milliseconds before the cached page invalidates.</param>
        ''' <remarks></remarks>
        Public Sub New(ByVal itemProvider As IVirtualizingItemProvider(Of T), ByVal pageSize As Integer, ByVal pageTimeout As Long)
            Me._itemProvider = itemProvider
            Me.PageSize = pageSize
            Me.PageTimeout = pageTimeout
        End Sub

#Region "Paging"

        Private _pageSize As Integer

        ''' <summary>
        ''' Gets or sets the number of elements in a data page.
        ''' </summary>
        ''' <returns>The number of elements in a data page.</returns>
        ''' <remarks></remarks>
        Public Property PageSize() As Integer
            Get
                Return Me._pageSize
            End Get
            Set(ByVal value As Integer)
                If Me._pageSize <> value Then
                    Me._pageSize = value
                    Me.OnPropertyChanged("PageSize")
                End If
            End Set
        End Property

        Private _pageTimeout As Long

        ''' <summary>
        ''' Gets or sets the number of milliseconds before the cached page invalidates.
        ''' </summary>
        ''' <returns>The number of milliseconds before the cached page invalidates.</returns>
        ''' <remarks></remarks>
        Public Property PageTimeout() As Long
            Get
                Return Me._pageTimeout
            End Get
            Set(ByVal value As Long)
                If Me._pageTimeout <> value Then
                    Me._pageTimeout = value
                    Me.OnPropertyChanged("PageTimeout")
                End If
            End Set
        End Property

        Private _pages As Dictionary(Of Integer, DataPage(Of T)) = New Dictionary(Of Integer, DataPage(Of T))()

        ''' <summary>
        ''' Removes stale pages that have not been accessed in a period 
        ''' of time or are still referenced by UI elements.
        ''' </summary>
        ''' <remarks></remarks>
        Protected Overridable Sub CleanupPages()
            Dim keys = Me._pages.Keys.ToArray()
            Dim page As DataPage(Of T) = Nothing
            Dim removePage As Boolean
            For Each key As Integer In keys
                Me._pages.TryGetValue(key, page)
                ' Page 0 is a special case, since a WPF items control accesses it frequently.
                If key <> 0 AndAlso page IsNot Nothing AndAlso page.HasExpired(Me.PageTimeout) Then
                    removePage = Not (page.IsInUse)
                    If Me.VirtualizeExplicit Then _
                        removePage = page.CanVirtualize
                    If removePage Then _
                        Me._pages.Remove(key)
                End If
            Next
        End Sub

        Private Function HasPage(ByVal pageIndex As Integer) As Boolean
            Return Me._pages.ContainsKey(pageIndex)
        End Function

        ''' <summary>
        ''' Populates the page cache at the specified index with the specified values.
        ''' </summary>
        ''' <param name="pageIndex">The page index to populate.</param>
        ''' <param name="dataItems">The data items to fill.</param>
        ''' <remarks></remarks>
        Protected Overridable Sub PopulatePage(ByVal pageIndex As Integer, ByVal dataItems As IList(Of T))
            Dim page As DataPage(Of T) = Nothing
            If (Me._pages.TryGetValue(pageIndex, page)) Then
                page.Populate(dataItems)
            End If
        End Sub

        ''' <summary>
        ''' Requests the page at the specified index.
        ''' </summary>
        ''' <param name="pageIndex">The index of the page to find.</param>
        ''' <remarks></remarks>
        Protected Overridable Sub RequestPage(ByVal pageIndex As Integer)
            Dim page As DataPage(Of T) = Nothing
            Dim hasPage As Boolean = Me._pages.TryGetValue(pageIndex, page)
            If Not (hasPage) Then
                Dim pageLength = Math.Min(Me.PageSize, Me.Count - pageIndex * Me.PageSize)
                page = New DataPage(Of T)(pageIndex * Me.PageSize, pageLength, Me.VirtualizeExplicit)
                Me._pages.Add(pageIndex, page)
                Me.LoadPage(pageIndex, pageLength)
            Else
                page.TouchTime = GlobalDateTime.Now
            End If
        End Sub

        ''' <summary>
        ''' Clears the page caches.
        ''' </summary>
        ''' <remarks></remarks>
        Protected Sub ResetPageCache()
            Me._pages.Clear()
        End Sub

        ''' <summary>
        ''' Loads the virtual item count.
        ''' </summary>
        ''' <remarks></remarks>
        Protected Overridable Sub LoadCount()
            Me.SetCachedCount(Me.FetchCount())
        End Sub

        ''' <summary>
        ''' Loads the page at the specified index.
        ''' </summary>
        ''' <param name="pageIndex"></param>
        ''' <remarks></remarks>
        Protected Overridable Sub LoadPage(ByVal pageIndex As Integer, ByVal pageLength As Integer)
            Dim count As Integer = 0
            Me.PopulatePage(pageIndex, Me.FetchPage(pageIndex, pageLength, count))
            Me.SetCachedCount(count)
        End Sub

        ''' <summary>
        ''' Returns the index of the page at the specified item index.
        ''' </summary>
        ''' <param name="index">The item index to locate.</param>
        ''' <returns>The index of the page at the specified item index.</returns>
        ''' <remarks></remarks>
        Protected Function GetPageIndex(ByVal index As Integer) As Integer
            Return CInt(Math.Floor(index / Me.PageSize))
        End Function

        ''' <summary>
        ''' Returns the offset on a page for the specified index.
        ''' </summary>
        ''' <param name="index">The item index to locate.</param>
        ''' <returns>The offset on a page for the specified index.</returns>
        ''' <remarks></remarks>
        Protected Function GetPageOffset(ByVal index As Integer) As Integer
            Return (index Mod Me.PageSize)
        End Function

        Protected Function CanGetNextPage(ByVal atIndex As Integer) As Boolean
            Dim maxPageIndex As Integer = Me.GetPageIndex(Me.Count - 1)
            Return (atIndex < maxPageIndex)
        End Function

#End Region

#Region "Item Provider"

        Private _itemProvider As IVirtualizingItemProvider(Of T)

        ''' <summary>
        ''' Gets the provider interface for retrieving virtualized items.
        ''' </summary>
        ''' <returns>A Mercury.Mobile.Data.IVirtualizingItemProvider(of T) object representing 
        ''' the provider interface for retrieving virtualized items.</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property ItemProvider() As IVirtualizingItemProvider(Of T)
            Get
                Return Me._itemProvider
            End Get
        End Property

        ''' <summary>
        ''' Invalidates the page cache, allowing virtualized items to be refreshed.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub Invalidate()
            Me._cachedCount = 0
            Interlocked.Exchange(Me.countIsSet, 0)
            Me.ResetPageCache()
        End Sub

        ''' <summary>
        ''' Attempts to suggest a requery of the virtual collection by raising the collection reset event.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub SuggestRequery()
            Me.OnCollectionReset(EventArgs.Empty)
        End Sub

        ''' <summary>
        ''' Applies the specified sort and filter to the virtual items.
        ''' </summary>
        ''' <param name="sortExpression">A list of sort field items defining the sort expression to apply.</param>
        ''' <param name="info">The filter to apply.</param>
        ''' <remarks></remarks>
        Public Overridable Sub ApplyView(ByVal sortExpression As IList(Of SortField), ByVal info As FilterInfo)
            If Me.ItemProvider IsNot Nothing Then _
                Me.ItemProvider.Apply(info, sortExpression)
            Me.Invalidate()
            Me.LoadPage(0, Me.PageSize)
        End Sub

        ''' <summary>
        ''' Removes all filters and sort expressions from the virtual items.
        ''' </summary>
        ''' <remarks></remarks>
        Public Overridable Sub ClearViews()
            If Me.ItemProvider IsNot Nothing Then _
                Me.ItemProvider.ClearFilters()
            Me.Invalidate()
            Me.LoadPage(0, Me.PageSize)
        End Sub

        ''' <summary>
        ''' Returns the number of virtual items.
        ''' </summary>
        ''' <returns>The number of virtual items.</returns>
        ''' <remarks></remarks>
        Protected Overridable Function FetchCount() As Integer
            If Me.ItemProvider IsNot Nothing Then _
                Return Me.ItemProvider.FetchCount()
            Return -1
        End Function

        ''' <summary>
        ''' Returns a list of virtual items for the specified page index.
        ''' </summary>
        ''' <param name="pageIndex">The index of the page to find.</param>
        ''' <param name="pageLength">The length of the page to get.</param>
        ''' <param name="count">Out. The total number of elements referenced by the item provider.</param>
        ''' <returns>A list of virtual items for the specified page index.</returns>
        ''' <remarks></remarks>
        Protected Overridable Function FetchPage(ByVal pageIndex As Integer, ByVal pageLength As Integer, ByRef count As Integer) As IList(Of T)
            If Me.ItemProvider IsNot Nothing Then _
                Return Me.ItemProvider.FetchRange(pageIndex * Me.PageSize, pageLength, count)
            Return Nothing
        End Function

        Protected Overridable Function GetAll() As IList(Of T)
            If Me.ItemProvider IsNot Nothing Then _
                Return Me.ItemProvider.FetchAll()
            Return Nothing
        End Function

#End Region

#Region "Private interface methods"

        Protected Overridable Sub UntypedCopyTo(ByVal array As System.Array, ByVal index As Integer) Implements System.Collections.ICollection.CopyTo
            Throw New NotSupportedException()
        End Sub

        Private ReadOnly Property ListCount() As Integer Implements System.Collections.ICollection.Count
            Get
                Return Me.Count
            End Get
        End Property

        Private Function ListAdd(ByVal value As Object) As Integer Implements System.Collections.IList.Add
            Me.Add(CType(value, DataContainer(Of T)))
            Return -1
        End Function

        Private Sub ListClear() Implements System.Collections.IList.Clear
            Me.Clear()
        End Sub

        Private Function ListContains(ByVal value As Object) As Boolean Implements System.Collections.IList.Contains
            Return Me.Contains(value)
        End Function

        Private Function ListIndexOf(ByVal value As Object) As Integer Implements System.Collections.IList.IndexOf
            Return Me.IndexOf(value)
        End Function

        Private Sub ListInsert(ByVal index As Integer, ByVal value As Object) Implements System.Collections.IList.Insert
            Me.Insert(index, value)
        End Sub

        Private ReadOnly Property IsFixedSize() As Boolean Implements System.Collections.IList.IsFixedSize
            Get
                Return False
            End Get
        End Property

        Private ReadOnly Property ListIsReadOnly() As Boolean Implements System.Collections.IList.IsReadOnly
            Get
                Return True
            End Get
        End Property

        Private Property ListItem(ByVal index As Integer) As Object Implements System.Collections.IList.Item
            Get
                Return Me.Item(index)
            End Get
            Set(ByVal value As Object)
                Me.Item(index) = value
            End Set
        End Property

        Private Sub ListRemove(ByVal value As Object) Implements System.Collections.IList.Remove
            Me.Remove(value)
        End Sub

        Private Sub ListRemoveAt(ByVal index As Integer) Implements System.Collections.IList.RemoveAt
            Me.RemoveAt(index)
        End Sub

#End Region

#Region "INotifyPropertyChanged"

        ''' <summary>
        ''' Occurs when a property value changes.
        ''' </summary>
        Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

        ''' <summary>
        ''' Raises the property changed event.
        ''' </summary>
        ''' <param name="propertyName">The name of the property that changed.</param>
        ''' <remarks></remarks>
        Protected Overridable Sub OnPropertyChanged(ByVal propertyName As String)
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
        End Sub

#End Region

#Region "CollectionReset"

        ''' <summary>
        ''' Occurs when the collection is reset.
        ''' </summary>
        Public Event CollectionReset(ByVal sender As Object, ByVal e As EventArgs)

        ''' <summary>
        ''' Raises the collection reset event.
        ''' </summary>
        Protected Overridable Sub OnCollectionReset(ByVal e As EventArgs)
            RaiseEvent CollectionReset(Me, e)
        End Sub

        Friend Sub FireCollectionReset()
            Me.OnCollectionReset(EventArgs.Empty)
        End Sub

#End Region

#Region "ICollection<T>"

        ''' <summary>
        ''' Not supported.
        ''' </summary>
        ''' <param name="item">Unused.</param>
        ''' <remarks></remarks>
        Public Sub Add(ByVal item As DataContainer(Of T)) Implements System.Collections.Generic.ICollection(Of DataContainer(Of T)).Add
            Throw New NotSupportedException()
        End Sub

        ''' <summary>
        ''' Not supported.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub Clear() Implements System.Collections.Generic.ICollection(Of DataContainer(Of T)).Clear
            Throw New NotSupportedException()
        End Sub

        ''' <summary>
        ''' Returns a value indicating whether the specified item is in the collection.
        ''' </summary>
        ''' <param name="item">The item to search for.</param>
        ''' <returns>True if the specified item is in the collection; otherwise, false.</returns>
        ''' <remarks></remarks>
        Public Function Contains(ByVal item As DataContainer(Of T)) As Boolean Implements System.Collections.Generic.ICollection(Of DataContainer(Of T)).Contains
            Dim index = Me.IndexOf(item)
            If index > -1 Then _
                Return True
            Return False
        End Function

        ''' <summary>
        ''' Copies the items in the collection to an array, starting at the specified index.
        ''' </summary>
        ''' <param name="array">The array into which the elements will be copied.</param>
        ''' <param name="arrayIndex">The index in the array at which copying begins.</param>
        ''' <remarks></remarks>
        Public Sub CopyTo(ByVal array() As DataContainer(Of T), ByVal arrayIndex As Integer) Implements System.Collections.Generic.ICollection(Of DataContainer(Of T)).CopyTo
            Throw New NotSupportedException()
        End Sub

        Private countIsSet As Integer = 0
        Private _cachedCount As Integer

        ''' <summary>
        ''' Gets the number of elements in the Mercury.Mobile.Data.VirtualContainerCollection(of T).
        ''' </summary>
        ''' <returns>The number of elements in the Mercury.Mobile.Data.VirtualContainerCollection(of T).</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property Count() As Integer Implements System.Collections.Generic.ICollection(Of DataContainer(Of T)).Count
            Get
                If Interlocked.CompareExchange(Me.countIsSet, 1, 0) = 0 Then
                    Me._cachedCount = 0
                    Me.LoadCount()
                End If
                Return Me._cachedCount
            End Get
        End Property

        Protected Sub SetCachedCount(ByVal value As Integer)
            Me._cachedCount = value
            Me.OnPropertyChanged("Count")
        End Sub

        ''' <summary>
        ''' Gets a value indicating whether this collection is read-only.
        ''' </summary>
        ''' <returns>True if the collection is read-only; otherwise, false.</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property IsReadOnly() As Boolean Implements System.Collections.Generic.ICollection(Of DataContainer(Of T)).IsReadOnly
            Get
                Return True
            End Get
        End Property

        ''' <summary>
        ''' Not supported.
        ''' </summary>
        ''' <param name="item">Unused.</param>
        ''' <returns>Nothing.</returns>
        ''' <remarks></remarks>
        Public Function Remove(ByVal item As DataContainer(Of T)) As Boolean Implements System.Collections.Generic.ICollection(Of DataContainer(Of T)).Remove
            Throw New NotSupportedException()
        End Function

#End Region

#Region "IEnumerable<T>"

        ''' <summary>
        ''' Returns an enumerator that iterates through the Mercury.Mobile.Data.VirtualContainerCollection(of T).
        ''' </summary>
        ''' <returns>An enumerator that iterates through the Mercury.Mobile.Data.VirtualContainerCollection(of T).</returns>
        ''' <remarks></remarks>
        Public Overridable Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of DataContainer(Of T)) Implements System.Collections.Generic.IEnumerable(Of DataContainer(Of T)).GetEnumerator
            Return New Iterator(Of DataContainer(Of T), Integer)(AddressOf _IterateContainers, -1)
        End Function

        Private Function _IterateContainers(ByRef current As DataContainer(Of T), ByRef state As Integer) As Boolean
            If state < Me.Count - 1 Then
                state += 1
                current = Me.Item(state)
                Return True
            End If
            Return False
        End Function

        Private Function GetListEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
            Return Me.GetEnumerator()
        End Function

#End Region

#Region "IList<T>"

        ''' <summary>
        ''' Returns the index of the specified item.
        ''' </summary>
        ''' <param name="item">The item to locate.</param>
        ''' <returns>The index of the specified item or -1.</returns>
        ''' <remarks></remarks>
        Public Function IndexOf(ByVal item As DataContainer(Of T)) As Integer Implements System.Collections.Generic.IList(Of DataContainer(Of T)).IndexOf
            If item IsNot Nothing Then
                Dim indexWithinPage As Integer = 0
                For Each kv As KeyValuePair(Of Integer, DataPage(Of T)) In Me._pages
                    indexWithinPage = kv.Value.IndexOfData(item.Data)
                    If indexWithinPage > -1 Then _
                        Return Me.PageSize * kv.Key + indexWithinPage
                Next
            End If
            Return -1
        End Function

        ''' <summary>
        ''' Not supported.
        ''' </summary>
        ''' <param name="index">Unused.</param>
        ''' <param name="item">Unused.</param>
        ''' <remarks></remarks>
        Public Sub Insert(ByVal index As Integer, ByVal item As DataContainer(Of T)) Implements System.Collections.Generic.IList(Of DataContainer(Of T)).Insert
            Throw New NotSupportedException()
        End Sub

        ''' <summary>
        ''' Gets the item at the specified index.
        ''' </summary>
        ''' <param name="index">The zero-based index of the item to retrieve.</param>
        ''' <returns>The item at the specified index.</returns>
        ''' <remarks></remarks>
        Default Public Overridable Property Item(ByVal index As Integer) As DataContainer(Of T) Implements System.Collections.Generic.IList(Of DataContainer(Of T)).Item
            Get
                ' Get container
                Return Me.GetContainer(index)
            End Get
            Set(ByVal value As DataContainer(Of T))
                Throw New NotSupportedException()
            End Set
        End Property

        ''' <summary>
        ''' Not supported.
        ''' </summary>
        ''' <param name="index">Unused.</param>
        ''' <remarks></remarks>
        Public Sub RemoveAt(ByVal index As Integer) Implements System.Collections.Generic.IList(Of DataContainer(Of T)).RemoveAt
            Throw New NotSupportedException()
        End Sub

        ''' <summary>
        ''' Gets a value indicating whether this collection is synchronized.
        ''' </summary>
        ''' <returns>True if the collection is synchronized; otherwise, false.</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property IsSynchronized() As Boolean Implements System.Collections.ICollection.IsSynchronized
            Get
                Return False
            End Get
        End Property

        ''' <summary>
        ''' Gets a synchronization primitive to synchronize this collection.
        ''' </summary>
        ''' <returns>A synchronization primitive to synchronize this collection.</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property SyncRoot() As Object Implements System.Collections.ICollection.SyncRoot
            Get
                Return Me
            End Get
        End Property

#End Region

        ''' <summary>
        ''' Returns the data container at the specified index.
        ''' </summary>
        ''' <param name="index">The index of the data container to retrieve.</param>
        ''' <returns>The Mercury.Mobile.Data.DataContainer object at the specified index.</returns>
        ''' <remarks></remarks>
        Public Function GetContainer(ByVal index As Integer) As DataContainer(Of T)
            ' Get index and page offset
            Dim pageIndex As Integer = Me.GetPageIndex(index)
            Dim pageOffset As Integer = Me.GetPageOffset(index)

            ' Remove stale pages
            Me.CleanupPages()

            ' Request
            Me.RequestPage(pageIndex)

            ' If upper 50% accessed, request next page.
            If pageOffset > (Me.PageSize / 2) AndAlso Me.CanGetNextPage(pageIndex) Then _
                Me.RequestPage(pageIndex + 1)

            ' If lower 50% accessed, request previous page.
            If pageOffset < (Me.PageSize / 2) AndAlso pageIndex > 0 Then _
                Me.RequestPage(pageIndex - 1)

            ' Check for async
            If Me._pages(pageIndex) Is Nothing Then _
                Return Nothing

            ' Return requested
            Return Me._pages(pageIndex).Items(pageOffset)
        End Function

        Private _virtualizeExplicit As Boolean

        ''' <summary>
        ''' Gets or sets a value indicating whether containers can be explicitly marked to virtualize.
        ''' </summary>
        ''' <returns>True if containers can be explicitly marked to virtualize; otherwise, false.</returns>
        ''' <remarks></remarks>
        Public Property VirtualizeExplicit() As Boolean
            Get
                Return Me._virtualizeExplicit
            End Get
            Set(ByVal value As Boolean)
                If Me._virtualizeExplicit <> value Then
                    Me._virtualizeExplicit = value
                    OnPropertyChanged("VirtualizeExplicit")
                End If
            End Set
        End Property

    End Class

End Namespace