Imports System.Collections.Specialized

Public Class TwoListSynchronizer
   Implements IWeakEventListener

   Private Shared ReadOnly DefaultConverter As IListItemConverter = New DoNothingListItemConverter()
   Private ReadOnly _masterList As IList
   Private ReadOnly _masterTargetConverter As IListItemConverter
   Private ReadOnly _targetList As IList

   Public Sub New(ByVal masterList As IList, ByVal targetList As IList, ByVal masterTargetConverter As IListItemConverter)
      _masterList = masterList
      _targetList = targetList
      _masterTargetConverter = masterTargetConverter
   End Sub

   Public Sub New(ByVal masterList As IList, ByVal targetList As IList)
      Me.New(masterList, targetList, DefaultConverter)
   End Sub

   Private Delegate Sub ChangeListAction(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))

   Public Sub StartSynchronizing()
      ListenForChangeEvents(_masterList)
      ListenForChangeEvents(_targetList)
      SetListValuesFromSource(_masterList, _targetList, New Converter(Of Object, Object)(AddressOf ConvertFromMasterToTarget))

      If Not TargetAndMasterCollectionsAreEqual() Then
         SetListValuesFromSource(_targetList, _masterList, New Converter(Of Object, Object)(AddressOf ConvertFromTargetToMaster))
      End If
   End Sub

   Public Sub StopSynchronizing()
      StopListeningForChangeEvents(_masterList)
      StopListeningForChangeEvents(_targetList)
   End Sub

   Public Function ReceiveWeakEvent(ByVal managerType As Type, ByVal sender As Object, ByVal e As EventArgs) As Boolean Implements IWeakEventListener.ReceiveWeakEvent
      HandleCollectionChanged(TryCast(sender, IList), TryCast(e, NotifyCollectionChangedEventArgs))
      Return True
   End Function

   Protected Sub ListenForChangeEvents(ByVal list As IList)
      If TypeOf list Is INotifyCollectionChanged Then
         CollectionChangedEventManager.AddListener(TryCast(list, INotifyCollectionChanged), Me)
      End If
   End Sub

   Protected Sub StopListeningForChangeEvents(ByVal list As IList)
      If TypeOf list Is INotifyCollectionChanged Then
         CollectionChangedEventManager.RemoveListener(TryCast(list, INotifyCollectionChanged), Me)
      End If
   End Sub

   Private Sub AddItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
      Dim itemCount As Integer = e.NewItems.Count

      For i As Integer = 0 To itemCount - 1
         Dim insertionPoint As Integer = e.NewStartingIndex + i

         If insertionPoint > list.Count Then
            list.Add(converter(e.NewItems(i)))
         Else
            list.Insert(insertionPoint, converter(e.NewItems(i)))
         End If
      Next
   End Sub

   Private Function ConvertFromMasterToTarget(ByVal masterListItem As Object) As Object
      Return If(_masterTargetConverter Is Nothing, masterListItem, _masterTargetConverter.Convert(masterListItem))
   End Function

   Private Function ConvertFromTargetToMaster(ByVal targetListItem As Object) As Object
      Return If(_masterTargetConverter Is Nothing, targetListItem, _masterTargetConverter.ConvertBack(targetListItem))
   End Function

   Private Sub HandleCollectionChanged(ByVal sender As Object, ByVal e As NotifyCollectionChangedEventArgs)
      Dim sourceList As IList = TryCast(sender, IList)

      Select Case e.Action
         Case NotifyCollectionChangedAction.Add
            PerformActionOnAllLists(AddressOf AddItems, sourceList, e)
         Case NotifyCollectionChangedAction.Move
            PerformActionOnAllLists(AddressOf MoveItems, sourceList, e)
         Case NotifyCollectionChangedAction.Remove
            PerformActionOnAllLists(AddressOf RemoveItems, sourceList, e)
         Case NotifyCollectionChangedAction.Replace
            PerformActionOnAllLists(AddressOf ReplaceItems, sourceList, e)
         Case NotifyCollectionChangedAction.Reset
            UpdateListsFromSource(TryCast(sender, IList))
         Case Else
      End Select
   End Sub

   Private Sub MoveItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
      RemoveItems(list, e, converter)
      AddItems(list, e, converter)
   End Sub

   Private Sub PerformActionOnAllLists(ByVal action As ChangeListAction, ByVal sourceList As IList, ByVal collectionChangedArgs As NotifyCollectionChangedEventArgs)
      If sourceList Is _masterList Then
         PerformActionOnList(_targetList, action, collectionChangedArgs, New Converter(Of Object, Object)(AddressOf ConvertFromMasterToTarget))
      Else
         PerformActionOnList(_masterList, action, collectionChangedArgs, New Converter(Of Object, Object)(AddressOf ConvertFromTargetToMaster))
      End If
   End Sub

   Private Sub PerformActionOnList(ByVal list As IList, ByVal action As ChangeListAction, ByVal collectionChangedArgs As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
      StopListeningForChangeEvents(list)
      action(list, collectionChangedArgs, converter)
      ListenForChangeEvents(list)
   End Sub

   Private Sub RemoveItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
      If e.OldItems.Count = 1 AndAlso e.OldStartingIndex <= list.Count - 1 Then
         list.RemoveAt(e.OldStartingIndex)
      Else
         For Each Item In e.OldItems
            list.Remove(Item)
         Next
      End If
   End Sub

   Private Sub ReplaceItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
      RemoveItems(list, e, converter)
      AddItems(list, e, converter)
   End Sub

   Private Sub SetListValuesFromSource(ByVal sourceList As IList, ByVal targetList As IList, ByVal converter As Converter(Of Object, Object))
      StopListeningForChangeEvents(targetList)
      targetList.Clear()

      For Each o As Object In sourceList
         targetList.Add(converter(o))
      Next

      ListenForChangeEvents(targetList)
   End Sub

   Private Function TargetAndMasterCollectionsAreEqual() As Boolean
      Return _masterList.Cast(Of Object)().SequenceEqual(_targetList.Cast(Of Object)().[Select](Function(item) ConvertFromTargetToMaster(item)))
   End Function

   Private Sub UpdateListsFromSource(ByVal sourceList As IList)
      If sourceList Is _masterList Then
         SetListValuesFromSource(_masterList, _targetList, New Converter(Of Object, Object)(AddressOf ConvertFromMasterToTarget))
      Else
         SetListValuesFromSource(_targetList, _masterList, New Converter(Of Object, Object)(AddressOf ConvertFromTargetToMaster))
      End If
   End Sub

   Friend Class DoNothingListItemConverter
      Implements IListItemConverter

      Public Function Convert(ByVal masterListItem As Object) As Object Implements IListItemConverter.Convert
         Return masterListItem
      End Function

      Public Function ConvertBack(ByVal targetListItem As Object) As Object Implements IListItemConverter.ConvertBack
         Return targetListItem
      End Function
   End Class
End Class
