Public Class DrillDownCategoryBindingList
    Implements System.ComponentModel.IBindingListView

#Region " Variables "

    Private Shared _syncRoot As Object

    Private WithEvents collection As DrillDownCategoryCollection = BusinessLogicManager.Instance.Categories
    Private items As DrillDownCategory()

    Private _isSorted As Boolean = False
    Private _sortDescriptions As ListSortDescription() = New ListSortDescription() {}
    Private _filter As String = String.Empty

    Private includeNullButtonIndices As TriState
    Private departmentID As Nullable(Of Integer)

#End Region 'Variables

#Region " Constructors "

    Public Sub New()
        Me.New(TriState.UseDefault, Nothing)
    End Sub

    Public Sub New(ByVal includeNullButtonIndices As TriState, ByVal departmentID As Nullable(Of Integer))
        Me._sortDescriptions = New ListSortDescription() {}
        Me.includeNullButtonIndices = includeNullButtonIndices
        Me.departmentID = departmentID
        Me.SetInitialFilter()
    End Sub

    Public Sub New(ByVal sortColumn As String)
        Me.New(TriState.UseDefault, _
               Nothing, _
               sortColumn, _
               ListSortDirection.Ascending)
    End Sub

    Public Sub New(ByVal sortColumn As String, ByVal sortOrder As ListSortDirection)
        Me.New(TriState.UseDefault, _
               Nothing, _
               sortColumn, _
               sortOrder)
    End Sub

    Public Sub New(ByVal includeNullButtonIndices As TriState, _
                   ByVal departmentID As Nullable(Of Integer), _
                   ByVal sortColumn As String)
        Me.New(includeNullButtonIndices, _
               departmentID, _
               sortColumn, _
               ListSortDirection.Ascending)
    End Sub

    Public Sub New(ByVal includeNullButtonIndices As TriState, _
                   ByVal departmentID As Nullable(Of Integer), _
                   ByVal sortColumn As String, _
                   ByVal sortOrder As ListSortDirection)
        Dim sortProperty As PropertyDescriptor = TypeDescriptor.GetProperties(GetType(DrillDownCategory)).Find(sortColumn, False)

        Me._sortDescriptions = New ListSortDescription() {New ListSortDescription(sortProperty, sortOrder)}
        Me.includeNullButtonIndices = includeNullButtonIndices
        Me.departmentID = departmentID
        Me.SetInitialFilter()
    End Sub

#End Region 'Constructors

#Region " Event Handlers "

    Private Sub collection_ItemButtonIndexChanged(ByVal sender As Object, ByVal e As DrillDownButtonIndexChangedEventArgs) Handles collection.ItemButtonIndexChanged
        Dim item As DrillDownCategory = DirectCast(e.Item, DrillDownCategory)
        Dim oldIndex As Integer = Array.IndexOf(Me.items, item)
        Dim wasInList As Boolean = (oldIndex <> -1)
        Dim isInList As Boolean = Me.ValidateAgainstFilter(item)

        If wasInList AndAlso isInList Then
            Me.FilterAndSortCore()
            Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemMoved, _
                                                      Array.IndexOf(Me.items, item), _
                                                      oldIndex))
        ElseIf wasInList AndAlso Not isInList Then
            Me.FilterAndSortCore()
            Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemDeleted, oldIndex))
        ElseIf Not wasInList AndAlso isInList Then
            Me.FilterAndSortCore()
            Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemAdded, Array.IndexOf(Me.items, item)))
        End If
    End Sub

#End Region 'Event Handlers

#Region " Methods "

    Private Sub SetInitialFilter()
        Dim filter As New StringBuilder

        Select Case Me.includeNullButtonIndices
            Case TriState.True
                filter.Append("ButtonIndex IS Null")
            Case TriState.False
                filter.Append("ButtonIndex IS NOT Null")
        End Select

        If Me.departmentID.HasValue Then
            If filter.Length > 0 Then
                filter.Append(" AND ")
            End If

            filter.Append("DepartmentID = ")
            filter.Append(Me.departmentID.Value)
        End If

        If filter.Length = 0 Then
            Me.FilterAndSortCore()
        Else
            Me.Filter = filter.ToString()
        End If
    End Sub

    Protected Overridable Sub OnListChanged(ByVal e As ListChangedEventArgs)
        RaiseEvent ListChanged(Me, e)
    End Sub

    Private Sub FilterAndSortCore()
        If Me.items IsNot Nothing Then
            Array.Clear(Me.items, 0, Me.items.Length)
        End If

        Dim items As New List(Of DrillDownCategory)

        For Each item As DrillDownCategory In Me.collection
            If Me.ValidateAgainstFilter(item) Then
                items.Add(item)
            End If
        Next item

        Me.items = items.ToArray()
        Array.Sort(Me.items, New Comparison(Of DrillDownCategory)(AddressOf CompareCategories))
        Me._isSorted = (Me._sortDescriptions.Length > 0)
    End Sub

    Private Function CompareCategories(ByVal category1 As DrillDownCategory, ByVal category2 As DrillDownCategory) As Integer
        Dim result As Integer = 0

        If Me.SortProperty IsNot Nothing Then
            Select Case Me.SortProperty.Name
                Case "ButtonIndex"
                    If Not category1.ButtonIndex.HasValue AndAlso category2.ButtonIndex.HasValue Then
                        result = -1
                    ElseIf Not category1.ButtonIndex.HasValue AndAlso Not category2.ButtonIndex.HasValue Then
                        result = 0
                    ElseIf category1.ButtonIndex.HasValue AndAlso Not category2.ButtonIndex.HasValue Then
                        result = 1
                    Else
                        result = category1.ButtonIndex.Value - category2.ButtonIndex.Value
                    End If
                Case "Name"
                    result = String.Compare(category1.Name, category2.Name)
            End Select
        End If

        If Me.SortDirection = ListSortDirection.Descending Then
            result = -result
        End If

        Return result
    End Function

    Private Function ValidateAgainstFilter(ByVal category As DrillDownCategory) As Boolean
        Dim result As Boolean

        Select Case Me.includeNullButtonIndices
            Case TriState.UseDefault
                result = True
            Case TriState.True
                result = Not category.ButtonIndex.HasValue
            Case TriState.False
                result = category.ButtonIndex.HasValue
        End Select

        If Me.departmentID.HasValue Then
            result = result AndAlso _
                     category.DepartmentID.HasValue AndAlso _
                     category.DepartmentID.Value = Me.departmentID.Value
        End If

        Return result
    End Function

    Public Function GetMatrixArray() As DrillDownCategory()(,)
        If Not Me.Filter.ToUpper().Contains("BUTTONINDEX IS NOT NULL") Then
            Throw New InvalidOperationException("Items with no ButtonIndex must be excluded in order to create a cubic array.")
        End If

        Dim configuration As Configuration = BusinessLogicManager.Instance.Configuration
        Dim buttonCount As Integer = Me.Count
        Dim columnCount As Integer = configuration.DeptDrillDownDeptCatColumnCount
        Dim rowCount As Integer = configuration.DeptDrillDownDeptCatRowCount
        Dim buttonCountPerPage As Integer = columnCount * rowCount
        Dim pageCount As Integer = buttonCount \ buttonCountPerPage

        If buttonCount Mod buttonCountPerPage > 0 Then
            pageCount += 1
        End If

        Dim array(pageCount - 1)(,) As DrillDownCategory
        Dim matrix As DrillDownCategory(,)
        Dim categoryIndex As Integer = 0

        For pageIndex As Integer = 0 To pageCount - 1 Step 1
            matrix = New DrillDownCategory(columnCount - 1, rowCount - 1) {}

            For rowIndex As Integer = 0 To rowCount - 1 Step 1
                For columnIndex As Integer = 0 To columnCount - 1 Step 1
                    matrix(columnIndex, rowIndex) = Me.items(categoryIndex)
                    categoryIndex += 1

                    If categoryIndex = buttonCount Then
                        Exit For
                    End If
                Next columnIndex

                If categoryIndex = buttonCount Then
                    Exit For
                End If
            Next rowIndex

            array(pageIndex) = matrix
        Next pageIndex

        Return array
    End Function

    Private Function ParseButtonIndexFilter(ByVal filter As String) As TriState
        Dim result As TriState

        If String.Compare(filter, "ButtonIndex IS Null", True) = 0 Then
            result = TriState.True
        ElseIf String.Compare(filter, "ButtonIndex IS NOT Null", True) = 0 Then
            result = TriState.False
        Else
            Throw New ArgumentException("Invalid filter string.")
        End If

        Return result
    End Function

    Private Function ParseDepartmentIDFilter(ByVal filter As String) As Integer
        Dim result As Integer

        If Not filter.StartsWith("DepartmentID = ", StringComparison.CurrentCultureIgnoreCase) OrElse _
           Not Integer.TryParse(filter.Substring("DepartmentID = ".Length), result) Then
            Throw New ArgumentException("Invalid filter string.")
        End If

        Return result
    End Function

#End Region 'Methods

#Region " IEnumerable Support "

    Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
        Return Me.items.GetEnumerator()
    End Function

#End Region 'IEnumerable Support

#Region " ICollection Support "

    Public ReadOnly Property Count() As Integer Implements System.Collections.ICollection.Count
        Get
            Return Me.items.Length
        End Get
    End Property

    Public ReadOnly Property IsSynchronized() As Boolean Implements System.Collections.ICollection.IsSynchronized
        Get
            Return False
        End Get
    End Property

    Public ReadOnly Property SyncRoot() As Object Implements System.Collections.ICollection.SyncRoot
        Get
            If _syncRoot Is Nothing Then
                _syncRoot = New Object
            End If

            Return _syncRoot
        End Get
    End Property

    Public Sub CopyTo(ByVal array As System.Array, ByVal index As Integer) Implements System.Collections.ICollection.CopyTo
        'TODO: Test whether the correct exceptions are thrown implicitly.
        If array Is Nothing Then
            Throw New ArgumentNullException("array")
        End If

        If index < 0 Then
            Throw New ArgumentOutOfRangeException("index", _
                                                  index, _
                                                  "Value must be greater than or equal to zero.")
        End If

        If array.Rank > 1 Then
            Throw New ArgumentException("Array cannot be multidimensional.", "array")
        End If

        If index >= array.Length Then
            Throw New ArgumentException("Value must be less than the length of the array.", "index")
        End If

        If Me.Count > array.Length - index Then
            Throw New ArgumentException("The collection contains too many items to copy to the specified section of the array.")
        End If

        For offset As Integer = 0 To Me.items.GetUpperBound(0) Step 1
            array.SetValue(Me.items(index), index + offset)
        Next offset
    End Sub

#End Region 'ICollection Support

#Region " IList Support "

    Public ReadOnly Property IsFixedSize() As Boolean Implements System.Collections.IList.IsFixedSize
        Get
            Return True
        End Get
    End Property

    Public ReadOnly Property IsReadOnly() As Boolean Implements System.Collections.IList.IsReadOnly
        Get
            Return False
        End Get
    End Property

    Default Public Property Item(ByVal index As Integer) As DrillDownCategory
        Get
            Return Me.items(index)
        End Get
        Set(ByVal value As DrillDownCategory)
            If Me.items(index) IsNot value Then
                Me.items(index) = value
                Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemChanged, index))
            End If
        End Set
    End Property

    Private Property ItemInternal(ByVal index As Integer) As Object Implements System.Collections.IList.Item
        Get
            Return Me.items(index)
        End Get
        Set(ByVal value As Object)
            If value IsNot Nothing AndAlso Not TypeOf value Is DrillDownCategory Then
                Throw New ArgumentException("Object must be of type DrillDownCategory.")
            End If

            If Me.items(index) IsNot value Then
                Me.items(index) = DirectCast(value, DrillDownCategory)
                Me.OnListChanged(New ListChangedEventArgs(ListChangedType.ItemChanged, index))
            End If
        End Set
    End Property

    Public Function Add(ByVal value As Object) As Integer Implements System.Collections.IList.Add
        Throw New NotSupportedException("The size of the list cannot be changed.")
    End Function

    Public Sub Clear() Implements System.Collections.IList.Clear
        Throw New NotSupportedException("The size of the list cannot be changed.")
    End Sub

    Public Function Contains(ByVal value As Object) As Boolean Implements System.Collections.IList.Contains
        Dim result As Boolean = False

        For Each item As DrillDownCategory In Me.items
            If item Is value Then
                result = True
                Exit For
            End If
        Next item

        Return result
    End Function

    Public Function IndexOf(ByVal value As Object) As Integer Implements System.Collections.IList.IndexOf
        Dim result As Integer = -1

        For index As Integer = 0 To Me.items.GetUpperBound(0) Step 1
            If Me.items(index) Is value Then
                result = index
                Exit For
            End If
        Next index

        Return result
    End Function

    Public Sub Insert(ByVal index As Integer, ByVal value As Object) Implements System.Collections.IList.Insert
        Throw New NotSupportedException("The size of the list cannot be changed.")
    End Sub

    Public Sub Remove(ByVal value As Object) Implements System.Collections.IList.Remove
        Throw New NotSupportedException("The size of the list cannot be changed.")
    End Sub

    Public Sub RemoveAt(ByVal index As Integer) Implements System.Collections.IList.RemoveAt
        Throw New NotSupportedException("The size of the list cannot be changed.")
    End Sub

#End Region 'IList Support

#Region " IBindingList Support "

    Public ReadOnly Property AllowEdit() As Boolean Implements System.ComponentModel.IBindingList.AllowEdit
        Get
            Return True
        End Get
    End Property

    Public ReadOnly Property AllowNew() As Boolean Implements System.ComponentModel.IBindingList.AllowNew
        Get
            Return False
        End Get
    End Property

    Public ReadOnly Property AllowRemove() As Boolean Implements System.ComponentModel.IBindingList.AllowRemove
        Get
            Return False
        End Get
    End Property

    Public ReadOnly Property IsSorted() As Boolean Implements System.ComponentModel.IBindingList.IsSorted
        Get
            Return Me._isSorted
        End Get
    End Property

    Public ReadOnly Property SortDirection() As System.ComponentModel.ListSortDirection Implements System.ComponentModel.IBindingList.SortDirection
        Get
            Dim value As ListSortDirection = ListSortDirection.Ascending

            If Me._sortDescriptions.Length > 0 Then
                value = Me._sortDescriptions(0).SortDirection
            End If

            Return value
        End Get
    End Property

    Public ReadOnly Property SortProperty() As System.ComponentModel.PropertyDescriptor Implements System.ComponentModel.IBindingList.SortProperty
        Get
            Dim value As PropertyDescriptor = Nothing

            If Me._sortDescriptions.Length > 0 Then
                value = Me._sortDescriptions(0).PropertyDescriptor
            End If

            Return value
        End Get
    End Property

    Public ReadOnly Property SupportsChangeNotification() As Boolean Implements System.ComponentModel.IBindingList.SupportsChangeNotification
        Get
            Return True
        End Get
    End Property

    Public ReadOnly Property SupportsSearching() As Boolean Implements System.ComponentModel.IBindingList.SupportsSearching
        Get
            Return False
        End Get
    End Property

    Public ReadOnly Property SupportsSorting() As Boolean Implements System.ComponentModel.IBindingList.SupportsSorting
        Get
            Return True
        End Get
    End Property

    Public Event ListChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ListChangedEventArgs) Implements System.ComponentModel.IBindingList.ListChanged

    Public Sub AddIndex(ByVal [property] As System.ComponentModel.PropertyDescriptor) Implements System.ComponentModel.IBindingList.AddIndex
        'Do nothing.
    End Sub

    Public Function AddNew() As Object Implements System.ComponentModel.IBindingList.AddNew
        Throw New NotSupportedException("The size of the list cannot be changed.")
    End Function

    Public Overloads Sub ApplySort(ByVal [property] As System.ComponentModel.PropertyDescriptor, ByVal direction As System.ComponentModel.ListSortDirection) Implements System.ComponentModel.IBindingList.ApplySort
        Me._sortDescriptions = New ListSortDescription() {New ListSortDescription([property], direction)}
        Me.FilterAndSortCore()
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, DirectCast(Nothing, PropertyDescriptor)))
    End Sub

    Public Function Find(ByVal [property] As System.ComponentModel.PropertyDescriptor, ByVal key As Object) As Integer Implements System.ComponentModel.IBindingList.Find
        Throw New NotSupportedException("Searching is not supported.")
    End Function

    Public Sub RemoveIndex(ByVal [property] As System.ComponentModel.PropertyDescriptor) Implements System.ComponentModel.IBindingList.RemoveIndex
        'Do nothing.
    End Sub

    Public Sub RemoveSort() Implements System.ComponentModel.IBindingList.RemoveSort
        Me._sortDescriptions = New ListSortDescription() {}
        Me.FilterAndSortCore()
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, DirectCast(Nothing, PropertyDescriptor)))
    End Sub

#End Region 'IBindingList Support

#Region " IBindingListView Support "

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' Valid filter formats include:
    ''' <list>
    ''' <item>ButtonIndex IS [NOT] Null</item>
    ''' <item>DepartmentID = <i>departmentID</i></item>
    ''' </list>
    ''' </remarks>
    Public Property Filter() As String Implements System.ComponentModel.IBindingListView.Filter
        Get
            Return Me._filter
        End Get
        Set(ByVal value As String)
            If value Is Nothing Then
                value = String.Empty
            End If

            If value = String.Empty Then
                Me.includeNullButtonIndices = TriState.UseDefault
                Me.departmentID = Nothing
            Else
                If value.IndexOf("ButtonIndex", StringComparison.CurrentCultureIgnoreCase) <> value.LastIndexOf("ButtonIndex", StringComparison.CurrentCultureIgnoreCase) OrElse _
                   value.IndexOf("DepartmentID", StringComparison.CurrentCultureIgnoreCase) <> value.LastIndexOf("DepartmentID", StringComparison.CurrentCultureIgnoreCase) Then
                    Throw New ArgumentException("Invalid filter string.")
                Else
                    Dim criteria As String() = value.Split(New String() {" AND "}, _
                                                           StringSplitOptions.None)

                    If criteria.Length > 2 Then
                        Throw New ArgumentException("Invalid filter string.")
                    End If

                    Dim includeNullButtonIndices As TriState = TriState.UseDefault
                    Dim departmentID As Nullable(Of Integer) = Nothing

                    For Each criterion As String In criteria
                        If criterion.StartsWith("ButtonIndex", StringComparison.CurrentCultureIgnoreCase) Then
                            includeNullButtonIndices = Me.ParseButtonIndexFilter(criterion)
                        ElseIf criterion.StartsWith("DepartmentID", StringComparison.CurrentCultureIgnoreCase) Then
                            departmentID = Me.ParseDepartmentIDFilter(criterion)
                        Else
                            Throw New ArgumentException("Invalid filter string.")
                        End If
                    Next criterion

                    Me.includeNullButtonIndices = includeNullButtonIndices
                    Me.departmentID = departmentID
                End If
            End If

            If Me._filter <> value Then
                Me._filter = value
                Me.FilterAndSortCore()
                Me.OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, DirectCast(Nothing, PropertyDescriptor)))
            End If
        End Set
    End Property

    Public ReadOnly Property SortDescriptions() As System.ComponentModel.ListSortDescriptionCollection Implements System.ComponentModel.IBindingListView.SortDescriptions
        Get
            Return New ListSortDescriptionCollection(Me._sortDescriptions)
        End Get
    End Property

    Public ReadOnly Property SupportsAdvancedSorting() As Boolean Implements System.ComponentModel.IBindingListView.SupportsAdvancedSorting
        Get
            Return True
        End Get
    End Property

    Public ReadOnly Property SupportsFiltering() As Boolean Implements System.ComponentModel.IBindingListView.SupportsFiltering
        Get
            Return True
        End Get
    End Property

    Public Overloads Sub ApplySort(ByVal sorts As System.ComponentModel.ListSortDescriptionCollection) Implements System.ComponentModel.IBindingListView.ApplySort
        Me._sortDescriptions = New ListSortDescription(sorts.Count - 1) {}
        sorts.CopyTo(Me._sortDescriptions, 0)
        Me.FilterAndSortCore()
        Me.OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, DirectCast(Nothing, PropertyDescriptor)))
    End Sub

    Public Sub RemoveFilter() Implements System.ComponentModel.IBindingListView.RemoveFilter
        Me.Filter = String.Empty
    End Sub

#End Region 'IBindingListView Support

End Class
