Imports System.Collections.Generic

''' <summary>
''' An implementation of a circular cache, in which overflow values write over previous values.
''' </summary>
''' <remarks></remarks>
Public Class CircularCache(Of TKey, TValue)
    Implements IDictionary(Of TKey, TValue)

    ''' <summary>
    ''' Initializes a new instance of the CircularCache(Of TKey, TValue) class with the specified buffer size.
    ''' </summary>
    ''' <param name="bufferSize">The size of the cache buffer.</param>
    ''' <remarks></remarks>
    Public Sub New(ByVal bufferSize As Integer)
        Me._bufferSize = bufferSize
        Me._buffer = New CacheItem(Of TKey, TValue)(bufferSize - 1) {}
        Me._cache = New Dictionary(Of TKey, CacheItem(Of TKey, TValue))(bufferSize)
    End Sub

#Region "CacheItem"

    ''' <summary>
    ''' An object representing an item in the cache.
    ''' </summary>
    ''' <typeparam name="TCacheKey">The type of the item key.</typeparam>
    ''' <typeparam name="TCacheValue">The type of the item value.</typeparam>
    ''' <remarks></remarks>
    Private Class CacheItem(Of TCacheKey, TCacheValue)

        Public Sub New(ByVal key As TCacheKey, ByVal value As TCacheValue)
            Me.Key = key
            Me.Value = value
        End Sub

        Public Key As TCacheKey
        Public Value As TCacheValue

    End Class

#End Region

    Private _cache As Dictionary(Of TKey, CacheItem(Of TKey, TValue))
    Private _buffer As CacheItem(Of TKey, TValue)()
    Private insertionHead As Integer = 0

    Private _bufferSize As Integer

    ''' <summary>
    ''' Gets the size of the buffer.
    ''' </summary>
    ''' <returns>The size of the buffer.</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property BufferSize() As Integer
        Get
            Return Me._bufferSize
        End Get
    End Property

    ''' <summary>
    ''' Gets the number of elements in the cache.
    ''' </summary>
    ''' <returns>The number of elements in the cache.</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Count() As Integer Implements System.Collections.Generic.ICollection(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)).Count
        Get
            Return Me._cache.Count
        End Get
    End Property

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

    ''' <summary>
    ''' Gets or sets the element with the specified key.
    ''' </summary>
    ''' <param name="key">The key of the item to get or set.</param>
    ''' <returns>The element with the specified key.</returns>
    ''' <remarks></remarks>
    Default Public Property Item(ByVal key As TKey) As TValue Implements System.Collections.Generic.IDictionary(Of TKey, TValue).Item
        Get
            Dim current As TValue = Nothing
            Me.TryGetValue(key, current)
            Return current
        End Get
        Set(ByVal value As TValue)
            If Me.ContainsKey(key) Then
                Dim current = Me._cache(key)
                If current IsNot Nothing Then
                    current.Value = value
                End If
            End If
        End Set
    End Property

#Region "Get"

    ''' <summary>
    ''' Gets an ICollection(of T) containing the keys of the Mercury.Mobile.CircularCache(of TKey, TValue).
    ''' </summary>
    ''' <returns>An ICollection(of T) containing the keys of the Mercury.Mobile.CircularCache(of TKey, TValue).</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Keys() As System.Collections.Generic.ICollection(Of TKey) Implements System.Collections.Generic.IDictionary(Of TKey, TValue).Keys
        Get
            Return Me._cache.Keys
        End Get
    End Property

    ''' <summary>
    ''' Gets an ICollection(of T) containing the values of the Mercury.Mobile.CircularCache(of TKey, TValue).
    ''' </summary>
    ''' <returns>An ICollection(of T) containing the values of the Mercury.Mobile.CircularCache(of TKey, TValue).</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Values() As System.Collections.Generic.ICollection(Of TValue) Implements System.Collections.Generic.IDictionary(Of TKey, TValue).Values
        Get
            Return Me.GetValuesCollection()
        End Get
    End Property

    Private Function GetValuesCollection() As ICollection(Of TValue)
        Dim collection = New List(Of TValue)()
        For Each ci As CacheItem(Of TKey, TValue) In Me._cache.Values
            collection.Add(ci.Value)
        Next
        Return collection
    End Function

    ''' <summary>
    ''' Gets the value associated with the specified key.
    ''' </summary>
    ''' <param name="key">The key of the value to retrieve.</param>
    ''' <returns>The value associated with the specified key.</returns>
    ''' <remarks></remarks>
    Public Function GetValue(ByVal key As TKey) As TValue
        Return Me._cache(key).Value
    End Function

    ''' <summary>
    ''' Attempts to get the value associated with the specified key.
    ''' </summary>
    ''' <param name="key">The key of the value to retrieve.</param>
    ''' <param name="value">Out. The value associated with the specified key.</param>
    ''' <returns>True if the key was found and the value retrieved; otherwise, false.</returns>
    ''' <remarks></remarks>
    Public Function TryGetValue(ByVal key As TKey, ByRef value As TValue) As Boolean Implements IDictionary(Of TKey, TValue).TryGetValue
        Dim current As CacheItem(Of TKey, TValue) = Nothing
        value = Nothing
        If Me._cache.TryGetValue(key, current) Then
            If current IsNot Nothing Then
                value = current.Value
                Return True
            End If
        End If
        Return False
    End Function

#End Region

#Region "Add"

    ''' <summary>
    ''' Adds the specified key/value pair to the cache.
    ''' </summary>
    ''' <param name="item">The key/value pair to add.</param>
    ''' <remarks></remarks>
    Public Sub Add(ByVal item As System.Collections.Generic.KeyValuePair(Of TKey, TValue)) Implements System.Collections.Generic.ICollection(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)).Add
        Me.Add(item.Key, item.Value)
    End Sub

    ''' <summary>
    ''' Adds the specified key and value to the cache.
    ''' </summary>
    ''' <param name="key">The key of the value to add.</param>
    ''' <param name="value">The value associated with the specified key.</param>
    ''' <remarks></remarks>
    Public Sub Add(ByVal key As TKey, ByVal value As TValue) Implements System.Collections.Generic.IDictionary(Of TKey, TValue).Add
        Me.AddInternal(New CacheItem(Of TKey, TValue)(key, value), Nothing)
    End Sub

    ''' <summary>
    ''' Adds the specified key and value to the cache.
    ''' </summary>
    ''' <param name="key">The key of the value to add.</param>
    ''' <param name="value">The value associated with the specified key.</param>
    ''' <param name="overwriteValue">The value that is being overwritten or null.</param>
    ''' <remarks></remarks>
    Public Sub Add(ByVal key As TKey, ByVal value As TValue, ByRef overwriteValue As TValue)
        Me.AddInternal(New CacheItem(Of TKey, TValue)(key, value), overwriteValue)
    End Sub

    Private Sub AddInternal(ByVal item As CacheItem(Of TKey, TValue), ByRef overwrite As TValue)
        overwrite = Nothing
        If Me._cache.ContainsKey(item.Key) Then _
            Throw New ArgumentException("The specified key already exists in the cache.")

        ' Reset head if overflow.
        If Not (Me.insertionHead < Me.BufferSize) Then _
            Me.ResetInsertionHead()

        ' Clear cached value if buffer is overwriting.
        Dim current = Me._buffer(Me.insertionHead)
        If current IsNot Nothing Then
            overwrite = current.Value
            If Me._cache.ContainsKey(current.Key) Then _
                Me._cache.Remove(current.Key)
        End If

        ' Add to buffer and cache
        Me._buffer(Me.insertionHead) = item
        Me._cache.Add(item.Key, item)
        Me.insertionHead += 1
    End Sub

    Private Sub ResetInsertionHead()
        Me.insertionHead = 0
    End Sub

#End Region

#Region "Remove"

    ''' <summary>
    ''' Attempts to remove the value associated with the specified 
    ''' key from the <see cref="Mercury.Mobile.CircularCache(Of TKey,TValue)"/>.
    ''' </summary>
    ''' <param name="key">The key of the value to remove.</param>
    ''' <param name="value">Out. The value associated with the specified key.</param>
    ''' <returns>True if the remove operation was successful; otherwise, false.</returns>
    ''' <remarks></remarks>
    Public Function TryRemove(ByVal key As TKey, ByRef value As TValue) As Boolean
        Dim current As CacheItem(Of TKey, TValue) = Nothing
        value = Nothing
        If Me._cache.TryGetValue(key, current) Then
            If current IsNot Nothing Then
                value = current.Value
                Me._cache.Remove(key)
            End If
            Me.RemoveFromBuffer(key)
            Return True
        End If
        Return False
    End Function

    ''' <summary>
    ''' Removes the specified key/value pair from the <see cref="Mercury.Mobile.CircularCache(Of TKey,TValue)"/>.
    ''' </summary>
    ''' <param name="item">The key/value pair to remove.</param>
    ''' <returns>True if the remove operation was successful; otherwise, false.</returns>
    ''' <remarks></remarks>
    Public Function Remove(ByVal item As System.Collections.Generic.KeyValuePair(Of TKey, TValue)) As Boolean Implements System.Collections.Generic.ICollection(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)).Remove
        Return Me.TryRemove(item.Key, Nothing)
    End Function

    ''' <summary>
    ''' Removes the element with the specified key from the <see cref="Mercury.Mobile.CircularCache(Of TKey,TValue)"/>.
    ''' </summary>
    ''' <param name="key">The key of the element to remove.</param>
    ''' <returns>True if the remove operation was successful; otherwise, false.</returns>
    ''' <remarks></remarks>
    Public Function Remove(ByVal key As TKey) As Boolean Implements System.Collections.Generic.IDictionary(Of TKey, TValue).Remove
        Return Me.TryRemove(key, Nothing)
    End Function

    Private Sub RemoveFromBuffer(ByVal key As TKey)
        Dim item As CacheItem(Of TKey, TValue) = Nothing
        Dim comparer = EqualityComparer(Of TKey).Default

        For i As Integer = 0 To Me._buffer.Count - 1
            item = Me._buffer(i)
            If item IsNot Nothing AndAlso comparer.Equals(item.Key, key) Then
                Me.DeleteIndexFromBuffer(i)
                Exit For
            End If
        Next
    End Sub

    Private Sub DeleteIndexFromBuffer(ByVal index As Integer)
        If index > -1 AndAlso index < Me.BufferSize Then
            Me._buffer(index) = Nothing

            ' Calculate buffer compaction
            Dim endIndex = Math.Max(Me.insertionHead, Me.BufferSize - 1)
            Dim lastItemIndex = endIndex - 1
            Dim copyLength = lastItemIndex - index

            ' Copy to temp
            Dim temp = New CacheItem(Of TKey, TValue)(copyLength - 1) {}
            Array.Copy(Me._buffer, index + 1, temp, 0, copyLength)

            ' Copy to buffer
            Array.Copy(temp, 0, Me._buffer, index, temp.Length)

            ' Remove content at last item index
            Me._buffer(lastItemIndex) = Nothing

            ' Reposition head if greater than index.
            If Me.insertionHead > index Then _
                Me.insertionHead -= 1
        End If
    End Sub

    ''' <summary>
    ''' Removes all elements from the cache.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Clear() Implements System.Collections.Generic.ICollection(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)).Clear
        Me._cache.Clear()
        Me.ResetInsertionHead()
        For i As Integer = 0 To Me.BufferSize - 1
            Me._buffer(i) = Nothing
        Next
    End Sub

#End Region  
    
#Region "IEnumerable"

    ''' <summary>
    ''' Returns an enumerator that iterates through the collection.
    ''' </summary>
    ''' <returns>An enumerator that iterates through the collection.</returns>
    ''' <remarks></remarks>
    Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)) Implements System.Collections.Generic.IEnumerable(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)).GetEnumerator
        Dim enumerator = New Iterator(Of KeyValuePair(Of TKey, TValue), Integer)(AddressOf _EnumeratorMoveNext, -1)
        Return enumerator
    End Function

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

    Private Function _EnumeratorMoveNext(ByRef current As KeyValuePair(Of TKey, TValue), ByRef state As Integer) As Boolean
        If state < Me.BufferSize - 1 Then
            state += 1
            Dim bufferVal = Me._buffer(state)
            If bufferVal IsNot Nothing Then
                current = New KeyValuePair(Of TKey, TValue)(bufferVal.Key, bufferVal.Value)
                Return True
            End If
        End If
        Return False
    End Function

#End Region

    ''' <summary>
    ''' Determines whether the <see cref="Mercury.Mobile.CircularCache(Of TKey, TValue)"/> contains the specified key/value pair.
    ''' </summary>
    ''' <param name="item">The key value pair to find.</param>
    ''' <returns>True if the cache contains the specified key/value pair; otherwise, false.</returns>
    ''' <remarks></remarks>
    Public Function Contains(ByVal item As System.Collections.Generic.KeyValuePair(Of TKey, TValue)) As Boolean Implements System.Collections.Generic.ICollection(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)).Contains
        Return Me._cache.ContainsKey(item.Key)
    End Function

    Public Sub CopyTo(ByVal array() As System.Collections.Generic.KeyValuePair(Of TKey, TValue), ByVal arrayIndex As Integer) Implements System.Collections.Generic.ICollection(Of System.Collections.Generic.KeyValuePair(Of TKey, TValue)).CopyTo
        Throw New NotImplementedException()
    End Sub

    ''' <summary>
    ''' Determines whether the <see cref="Mercury.Mobile.CircularCache(Of TKey, TValue)"/> 
    ''' contains an element with the specified key.
    ''' </summary>
    ''' <param name="key">The key of the element to find.</param>
    ''' <returns>True if the cache contains an element with the specified key; otherwise, false.</returns>
    ''' <remarks></remarks>
    Public Function ContainsKey(ByVal key As TKey) As Boolean Implements System.Collections.Generic.IDictionary(Of TKey, TValue).ContainsKey
        Return Me._cache.ContainsKey(key)
    End Function

End Class