Imports System.Data
Imports System.Drawing
Imports System.Collections
Imports System.Windows.Forms
Imports System.ComponentModel

Namespace Primitives

    ''' <summary>
    ''' DrawItem event delegate.
    ''' </summary>
    ''' <remarks></remarks>
    Public Delegate Sub DrawItemEventHandler(ByVal sender As Object, ByVal e As DrawItemEventArgs)

    ''' <summary>
    ''' Custom base class for extensible, owner-drawn listbox.
    ''' </summary>
    ''' <remarks></remarks>
    Public MustInherit Class OwnerDrawnList
        Inherits Control

        ''' <summary>
        ''' DrawItem event.
        ''' </summary>
        ''' <remarks></remarks>
        Public Event DrawItem As DrawItemEventHandler

        ''' <summary>
        ''' SelectedIndexChanged event.
        ''' </summary>
        ''' <remarks></remarks>
        Public Event SelectedIndexChanged As EventHandler

        ''' <summary>
        ''' ItemData changed
        ''' </summary>
        ''' <remarks></remarks>
        Public Event ItemDataChanged As EventHandler

        ' Data event handlers
        Public Event ListChanged As ListChangedEventHandler
        Public Event PositionChanged As EventHandler

        ' Data
        Private _dataSource As Object
        Private _dataMember As String = String.Empty
        Private _displayMember As String = String.Empty
        Private WithEvents _currencyManager As CurrencyManager

        ' Items
        Private sourceTable As ObjectTable
        Private _minimumItemHeight = 1
        Private _itemHeight As Integer = 16
        Private _itemWidth As Integer
        Private _topIndex As Integer

        ' Minimum height
        Private _minHeight As Integer = 2

        ' Selected index
        Private _selectedIndex As Integer = -1

        ' Scrolling
        Private ReadOnly SCROLL_WIDTH As Integer = 20
        Private ReadOnly SCROLL_HEIGHT As Integer = 20
        Private WithEvents vScroll As ScrollBar

        ' Offscreen buffer for
        ' double buffering.
        Private backBuffer As Bitmap

        ' Border color
        Private _borderColor As Color
        ' Show border boolean
        Private _showBorder As Boolean

#Region "Constructor(s)"

        ''' <summary>
        ''' Default constructor.
        ''' </summary>
        ''' <remarks></remarks>
        Public Sub New()
            ' Instantiate ArrayList
            Me.sourceTable = New ObjectTable()

            ' Create a ScrollBar instance
            Me.vScroll = New VScrollBar
            ' Hide scroll bars
            Me.vScroll.Hide()

            ' Add scroll bars
            Me.Controls.Add(vScroll)

            ' Set data members
            Me._dataSource = Nothing
            Me._dataMember = String.Empty

            ' Set default border
            Me._borderColor = Color.FromArgb(0, 0, 0)
            ' Set default show border
            Me._showBorder = False
        End Sub

#End Region

#Region "Properties"

        ''' <summary>
        ''' Gets/sets the value to determine whether to display the border.
        ''' </summary>
        ''' <value>Boolean.</value>
        ''' <returns>Boolean show border.</returns>
        ''' <remarks></remarks>
        Public Property ShowBorder() As Boolean
            Get
                Return Me._showBorder
            End Get
            Set(ByVal value As Boolean)
                Me._showBorder = value
            End Set
        End Property

        ''' <summary>
        ''' Gets/sets the color of the list border.
        ''' </summary>
        ''' <value>Color.</value>
        ''' <returns>Color.</returns>
        ''' <remarks></remarks>
        Public Property BorderColor() As Color
            Get
                Return Me._borderColor
            End Get
            Set(ByVal value As Color)
                Me._borderColor = value
            End Set
        End Property

        ''' <summary>
        ''' Gets the CurrencyManager for the DataSource object of the control.
        ''' </summary>
        ''' <value>CurrencyManager.</value>
        ''' <returns>CurrencyManager.</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property DataManager() As CurrencyManager
            Get
                Return Me._currencyManager
            End Get
        End Property

        ''' <summary>
        ''' Gets/sets the data source for this control.
        ''' </summary>
        ''' <value>System.Object.</value>
        ''' <returns>System.Object data source.</returns>
        ''' <remarks></remarks>
        Public Property DataSource() As Object
            Get
                Return Me._dataSource
            End Get
            Set(ByVal value As Object)
                Me._dataSource = value
                TryDataBinding()
            End Set
        End Property

        ''' <summary>
        ''' Gets/sets the data member to be used for this control.
        ''' </summary>
        ''' <value>String.</value>
        ''' <returns>String data member name.</returns>
        ''' <remarks></remarks>
        Public Property DataMember() As String
            Get
                Return Me._dataMember
            End Get
            Set(ByVal value As String)
                Me._dataMember = value
                TryDataBinding()
            End Set
        End Property

        ''' <summary>
        ''' Gets/sets the display member for this control.
        ''' </summary>
        ''' <value>String.</value>
        ''' <returns>String property name.</returns>
        ''' <remarks></remarks>
        Public Property DisplayMember() As String
            Get
                Return Me._displayMember
            End Get
            Set(ByVal value As String)
                Me._displayMember = value
            End Set
        End Property

        ''' <summary>
        ''' Calculate how many items we can draw given the height of the control.
        ''' </summary>
        ''' <value>Integer.</value>
        ''' <returns>Integer count.</returns>
        ''' <remarks></remarks>
        Protected ReadOnly Property DrawCount() As Integer
            Get
                If Me.vScroll.Value + Me.vScroll.LargeChange > Me.vScroll.Maximum Then
                    Return Me.vScroll.Maximum - Me.vScroll.Value + 1
                Else
                    Return Me.vScroll.LargeChange
                End If
            End Get
        End Property

        ''' <summary>
        ''' Gets/sets the value indicating whether the horizontal scrollbar is visible.
        ''' </summary>
        ''' <value>Boolean.</value>
        ''' <returns>True if the vertical scrollbar is visible; otherwise, false.  Default is false.</returns>
        ''' <remarks></remarks>
        Public Property ShowVerticalScrollBar()
            Get
                Return Me.vScroll.Visible
            End Get
            Set(ByVal value)
                Me.vScroll.Visible = value
            End Set
        End Property

        ''' <summary>
        ''' Gets the DataSliceCollection of items from the source object table.
        ''' </summary>
        ''' <value>DataSliceCollection.</value>
        ''' <returns>DataSliceCollection of items.</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property Items() As RowCollection
            Get
                Return Me.sourceTable.Rows
            End Get
        End Property

        ''' <summary>
        ''' Gets the object table for the list control.
        ''' </summary>
        ''' <value>MobileObjectTable.</value>
        ''' <returns>The object table for the list control.</returns>
        ''' <remarks></remarks>
        Public ReadOnly Property ObjectTable() As ObjectTable
            Get
                Return Me.sourceTable
            End Get
        End Property


        ''' <summary>
        ''' Gets/sets the item height.
        ''' </summary>
        ''' <value>Integer item height.</value>
        ''' <returns>Integer item height.</returns>
        ''' <remarks></remarks>
        Protected Overridable Property ItemHeight() As Integer
            Get
                Return Me._itemHeight
            End Get
            Set(ByVal value As Integer)
                Me._itemHeight = value
                ' Set item height to minimum if it is less
                ' than the minimum requirement.
                If Me._itemHeight < Me._minimumItemHeight Then
                    Me._itemHeight = Me._minimumItemHeight
                End If
            End Set
        End Property

        ''' <summary>
        ''' Gets/sets the index of the selected item.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property SelectedIndex() As Integer
            Get
                Return Me._selectedIndex
            End Get
            Set(ByVal Value As Integer)
                Me._selectedIndex = Value
                RaiseEvent SelectedIndexChanged(Me, Nothing)
            End Set
        End Property

#End Region

#Region "Data Binding Methods"

        Private Function TryDataBinding() As Boolean
            ' If not null valued data source or binding context
            If Me._dataSource IsNot Nothing AndAlso MyBase.BindingContext IsNot Nothing Then
                Dim cManager As CurrencyManager

                Try
                    cManager = CType(MyBase.BindingContext(Me._dataSource, Me._dataMember), CurrencyManager)
                Catch ex As System.ArgumentException
                    Return False
                End Try

                ' If currency manager is set
                If Me._currencyManager IsNot Nothing Then
                    ' If CurrencyManager is not equal
                    If Not (Me._currencyManager.Equals(cManager)) Then
                        ' Unwire old CurrencyManager
                        If Me._currencyManager IsNot Nothing Then
                            ' Remove handlers
                            RemoveHandler Me._currencyManager.ListChanged, AddressOf Me._currencyManager_ListChanged
                            RemoveHandler Me._currencyManager.PositionChanged, _
                                            AddressOf Me._currencyManager_PositionChanged
                        End If

                        ' Set new CurrencyManager
                        Me._currencyManager = cManager

                        ' Wire new CurrencyManager
                        If Me._currencyManager IsNot Nothing Then
                            AddHandler Me._currencyManager.ListChanged, AddressOf Me._currencyManager_ListChanged
                            AddHandler Me._currencyManager.PositionChanged, _
                                        AddressOf Me._currencyManager_PositionChanged

                        End If

                        ' Update data
                        CalculateRequiredColumns()
                        UpdateAllItemData()
                    End If
                Else
                    ' Set currency manager
                    Me._currencyManager = cManager
                    ' Update data
                    CalculateRequiredColumns()
                    UpdateAllItemData()
                End If
            Else
                Me.sourceTable.Columns.Clear()
                Me.sourceTable.Rows.Clear()
            End If
        End Function

        Private Sub CalculateRequiredColumns()
            ' Calculate display column
            CalculateDisplayColumn()
            ' Add all columns to source table
            For Each managerProperty As PropertyDescriptor In Me._currencyManager.GetItemProperties()
                Me.sourceTable.Columns.Add(New Column(managerProperty.Name))
            Next
        End Sub

        Private Sub CalculateDisplayColumn()
            ' Validate display member
            For Each managerProperty As PropertyDescriptor In Me._currencyManager.GetItemProperties()
                ' If valid name
                If managerProperty.Name = Me._displayMember Then
                    Return
                End If
            Next

            ' Invalid display member name.  Set 
            ' display member to first property.
            If Me._currencyManager.GetItemProperties() IsNot Nothing AndAlso _
                Me._currencyManager.GetItemProperties().Count > 0 Then
                ' Get first property
                Me._displayMember = Me._currencyManager.GetItemProperties().Item(0).Name
            End If
        End Sub

        Private Sub UpdateAllItemData()
            ' Clear current data
            Me.sourceTable.Rows.Clear()
            ' Cycle through all data
            For i As Integer = 0 To (Me._currencyManager.Count - 1)
                AddItemData(i)
            Next
        End Sub

        Private Sub AddItemData(ByVal index As Integer)
            Dim insertData As Row = GetDataSliceFromBinding(index)
            ' If insert data exists
            If insertData IsNot Nothing Then
                Me.sourceTable.Rows.InsertAt(insertData, index)
            End If
            ' Raise item data changed event
            OnItemDataChanged(EventArgs.Empty)
        End Sub

        Private Sub UpdateItemData(ByVal index As Integer)
            ' If within acceptable index range
            If index > -1 AndAlso index < Me.sourceTable.Rows.Count Then
                Dim updateData As Row = GetDataSliceFromBinding(index)
                ' If update data exists
                Me.sourceTable.Rows.Item(index) = updateData
                ' Raise item data changed event
                OnItemDataChanged(EventArgs.Empty)
            End If
        End Sub

        Private Sub DeleteItemData(ByVal index As Integer)
            ' If within acceptable index range
            If index > -1 AndAlso index < Me.sourceTable.Rows.Count Then
                Me.sourceTable.Rows.RemoveAt(index)
                ' Raise item data changed event
                OnItemDataChanged(EventArgs.Empty)
            End If
        End Sub

        Private Function GetDataSliceFromBinding(ByVal index As Integer) As Row
            ' Create new slice
            Dim slice As Row
            slice = Me.sourceTable.NewRow()
            ' Get row
            Dim row As Object = Me._currencyManager.List(index)

            ' Fill data slice
            For Each managerProperty As PropertyDescriptor In Me._currencyManager.GetItemProperties()
                slice.Add(managerProperty.GetValue(row))
            Next

            ' Return data slice
            Return slice
        End Function

#End Region

#Region "Drawing Methods"

        Private cacheBorderPen As Pen

        ''' <summary>
        ''' If the requested index is before the first visible index then set the 
        ''' first item to be the requested index. If it is after the last visible 
        ''' index, then set the last visible index to be the requested index.
        ''' </summary>
        ''' <param name="index">Integer index.</param>
        ''' <remarks></remarks>
        Public Sub EnsureVisible(ByVal index As Integer)
            If index < Me.vScroll.Value Then
                Me.vScroll.Value = index
                Me.Refresh()
            ElseIf index >= Me.vScroll.Value + Me.DrawCount Then
                Me.vScroll.Value = index - Me.DrawCount + 1
                Me.Refresh()
            End If
        End Sub

        Public Sub PaintItem(ByVal gx As Graphics, ByVal index As Integer)
            Dim itemRect As Rectangle = New Rectangle(0, (index - Me._topIndex) * Me._itemHeight, Me._itemWidth, Me._itemHeight)

            ' Set the appropriate selected state
            Dim state As DrawItemState
            If index = Me._selectedIndex Then
                state = DrawItemState.Selected
            Else
                state = DrawItemState.None
            End If

            ' Prepare args
            Dim drawArgs As DrawItemEventArgs = New DrawItemEventArgs(gx, Me.Font, itemRect, index, state)
            ' Raise drawing event
            OnDrawItem(Me, drawArgs)
        End Sub

        Private Sub DrawControlBorder(ByVal gx As Graphics)
            ' Draw border
            gx.DrawRectangle(Me.cacheBorderPen, Me.ClientRectangle.X, Me.ClientRectangle.Y, _
                             Me.ClientRectangle.Width - 1, Me.ClientRectangle.Height - 1)
        End Sub

#End Region

#Region "Events"

        Protected Overridable Sub OnItemDataChanged(ByVal e As System.EventArgs)
            ' Raise item data changed event
            RaiseEvent ItemDataChanged(Me, e)
        End Sub

        Protected Overrides Sub OnBindingContextChanged(ByVal e As System.EventArgs)
            TryDataBinding()
            MyBase.OnBindingContextChanged(e)
        End Sub

        ' Need to set focus to the control when it 
        ' is clicked so that keyboard events occur.
        Protected Overrides Sub OnClick(ByVal e As EventArgs)
            Me.Focus()
            MyBase.OnClick(e)
        End Sub

        Protected Overridable Sub OnSelectedIndexChanged(ByVal e As System.EventArgs)
            RaiseEvent SelectedIndexChanged(Me, e)
        End Sub

        Protected Overridable Sub OnDrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs)
            RaiseEvent DrawItem(sender, e)
        End Sub

        Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
            Dim previousSelection As Integer = Me._selectedIndex
            Me._selectedIndex = Me.vScroll.Value + Fix(e.Y / Me._itemHeight)

            ' Create graphics
            Dim gxTemp As Graphics = Me.CreateGraphics()

            ' If previously -1
            If previousSelection <> -1 Then
                PaintItem(gxTemp, previousSelection)
            End If

            ' Paint selected
            PaintItem(gxTemp, Me._selectedIndex)

            ' Focus
            Me.Focus()

            ' Invalidate the control so we can draw the item as selected.
            Me.Invalidate()
            ' Fire event
            OnSelectedIndexChanged(EventArgs.Empty)
        End Sub

        Protected Overrides Sub OnPaintBackground(ByVal e As System.Windows.Forms.PaintEventArgs)
            ' Do nothing
        End Sub

        Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
            ' Border pen
            If Me.cacheBorderPen Is Nothing Then
                Me.cacheBorderPen = New Pen(Me._borderColor)
            End If

            ' If buffer is nothing
            If Me.backBuffer Is Nothing Then
                Me.backBuffer = New Bitmap(Me.ClientSize.Width, Me.ClientSize.Height)
            End If

            ' If buffer is not empty
            If Me.backBuffer IsNot Nothing Then
                ' Create off-screen graphics
                Dim gxDoubleBuffer As Graphics = Graphics.FromImage(Me.backBuffer)

                ' Create background brush and fill
                Dim bgBrush As Brush = New SolidBrush(Me.BackColor)
                gxDoubleBuffer.FillRectangle(bgBrush, 0, 0, Me.Width, Me.Height)

                ' Clear
                gxDoubleBuffer.Clear(Me.BackColor)

                ' Draw border
                If Me._showBorder Then
                    DrawControlBorder(gxDoubleBuffer)
                End If

                Dim drawCountNum As Integer = 0

                ' Get first visible item
                If Me.vScroll.Visible Then
                    Me._topIndex = Me.vScroll.Value
                Else
                    Me._topIndex = 0
                End If

                ' Get the count of the draw items
                If Me.vScroll.Visible Then
                    drawCountNum = DrawCount
                Else
                    drawCountNum = Me.sourceTable.Rows.Count
                End If

                ' If vertical scrollbar
                If vScroll.Visible Then
                    Me._itemWidth = Me.Width - vScroll.Width
                Else
                    Me._itemWidth = Me.Width
                End If

                For index As Integer = Me._topIndex To (drawCountNum + Me._topIndex)
                    PaintItem(gxDoubleBuffer, index)
                Next

                ' Draw to screen
                e.Graphics.DrawImage(Me.backBuffer, 0, 0)

                ' Dispose
                gxDoubleBuffer.Dispose()
            End If
        End Sub

        ' Private internal resize
        Private internalHeightCheck As Integer

        Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
            Dim setRectWidth As Integer = 1

            ' If height has been changed
            If Me.internalHeightCheck <> MyBase.Height Then
                ' If height adjustment is needed
                If ((MyBase.Height - 2) Mod Me._itemHeight) <> 0 Then
                    Dim t As Decimal = (MyBase.Height + Me._itemHeight - 1)
                    Dim newHeight As Integer = Decimal.ToInt32(t - (t Mod Me._itemHeight)) - (Me._itemHeight)
                    ' Adjust height
                    If newHeight >= (Me._itemHeight) Then
                        MyBase.Height = Me._minHeight + newHeight
                    Else
                        MyBase.Height = Me._minHeight
                    End If
                End If
            End If

            ' Set current internal height
            Me.internalHeightCheck = Me.Height

            ' Number of items visible
            Dim viewableItemCount As Integer = Fix(Me.ClientSize.Height / Me._itemHeight)

            ' Set viewable bounds
            Me.vScroll.Bounds = New Rectangle(Me.ClientSize.Width - Me.SCROLL_WIDTH - 1, 1, SCROLL_WIDTH, _
                                              Me.ClientSize.Height - 2)

            ' Determine if scrollbars are needed
            If Me.sourceTable.Rows.Count > viewableItemCount Then
                Me.vScroll.Visible = True
                Me.vScroll.LargeChange = viewableItemCount
                setRectWidth = Me.ClientSize.Width - Me.SCROLL_WIDTH
            Else
                Me.vScroll.Visible = False
                Me.vScroll.LargeChange = Me.sourceTable.Rows.Count
                setRectWidth = Me.ClientSize.Width
            End If

            ' Set bitmap
            Me.backBuffer = New Bitmap(Me.ClientSize.Width, Me.ClientSize.Height)

            ' Invalidate
            Me.Invalidate()
        End Sub

#End Region

#Region "Scroll Events"

        Private Sub vScroll_ValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles vScroll.ValueChanged
            Me.Refresh()
        End Sub

        Private Sub ItemDataChanged_Handle(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.ItemDataChanged
            ' Set maximum
            Me.vScroll.Maximum = Me.sourceTable.Rows.Count - 1
        End Sub

#End Region

#Region "Data Binding Events"

        Private Sub _currencyManager_ListChanged(ByVal sender As Object, ByVal e As ListChangedEventArgs) Handles _currencyManager.ListChanged, Me.ListChanged
            ' If reset or item moved
            If e.ListChangedType = ListChangedType.Reset OrElse _
                e.ListChangedType = ListChangedType.ItemMoved Then
                ' Update all data
                UpdateAllItemData()
            ElseIf e.ListChangedType = ListChangedType.ItemAdded Then
                ' Add new item
                AddItemData(e.NewIndex)
            ElseIf e.ListChangedType = ListChangedType.ItemChanged Then
                ' Update item
                UpdateItemData(e.NewIndex)
            ElseIf e.ListChangedType = ListChangedType.ItemDeleted Then
                ' Delete item
                DeleteItemData(e.NewIndex)
            Else
                ' Update all data and calculations
                CalculateRequiredColumns()
                UpdateAllItemData()
            End If

            ' Raise event
            RaiseEvent ListChanged(Me, e)
        End Sub

        Private Sub _currencyManager_PositionChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles _currencyManager.PositionChanged, Me.PositionChanged
            ' Raise event
            RaiseEvent PositionChanged(Me, e)
        End Sub

#End Region

    End Class

End Namespace