using System;

using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;

using BLToolkit.EditableObjects;
using BLToolkit.Reflection;

namespace BLToolkit.ComponentModel
{
	public class BindingListImpl: IBindingListView, ICancelAddNew, INotifyCollectionChanged
	{
		#region Init

		public BindingListImpl(IList list, Type itemType)
		{
			if (list     == null) throw new ArgumentNullException("list");
			if (itemType == null) throw new ArgumentNullException("itemType");

			_list     = list;
			_itemType = itemType;

			AddInternal(_list);
		}

		#endregion

		#region Protected Members

		private readonly IList _list;
		private readonly Type  _itemType;

		private void ApplySort(IComparer comparer)
		{
			if (_list is ISortable)
				((ISortable)_list).Sort(0, _list.Count, comparer);
			else if (_list is ArrayList)
				((ArrayList)_list).Sort(0, _list.Count, comparer);
			else if (_list is Array)
				Array.Sort((Array)_list, comparer);
			else
			{
				object[] items = new object[_list.Count];

				_list.CopyTo(items, 0);
				Array.Sort(items, comparer);

				for (int i = 0; i < _list.Count; i++)
					_list[i] = items[i];
			}

			_isSorted = true;
		}

		#endregion

		#region IBindingList Members

			#region Command

		private int               _newItemIndex = -1;
		private INotifyObjectEdit _newObject;

		public object AddNew()
		{
			if (AllowNew == false)
				throw new NotSupportedException();

			EndNew();

			object   o = TypeAccessor.CreateInstanceEx(_itemType);
			_newObject = o as INotifyObjectEdit;

			if (_newObject != null)
				_newObject.ObjectEdit += NewObject_ObjectEdit;

			_newItemIndex = _list.Add(o);
			OnAddItem(o, _newItemIndex);

			Debug.WriteLine(string.Format("AddNew - ({0})", o.GetType().Name));

			return o;
		}

		void NewObject_ObjectEdit(object sender, ObjectEditEventArgs args)
		{
			if (sender == _newObject)
			{
				switch (args.EditType)
				{
					case ObjectEditType.End:    EndNew();                 break;
					case ObjectEditType.Cancel: CancelNew(_newItemIndex); break;
					default:                    return;
				}
			}
		}

		public bool AllowNew
		{
			get { return !_list.IsFixedSize; }
		}

		public bool AllowEdit
		{
			get { return !_list.IsReadOnly; }
		}

		public bool AllowRemove
		{
			get { return !_list.IsFixedSize; }
		}

			#endregion

			#region Change Notification

		private bool _notifyChanges = true;
		public  bool  NotifyChanges
		{
			get { return _notifyChanges; }
			set { _notifyChanges = value; }
		}

		public bool SupportsChangeNotification
		{
			get { return true; }
		}

		public event ListChangedEventHandler ListChanged;

		private void FireListChangedEvent(object sender, ListChangedEventArgs e)
		{
			if (_notifyChanges && ListChanged != null)
				ListChanged(sender, e);
		}

		protected virtual void OnListChanged(EditableListChangedEventArgs e)
		{
			FireListChangedEvent(this, e);
		}

		protected void OnListChanged(ListChangedType listChangedType, int index)
		{
			OnListChanged(new EditableListChangedEventArgs(listChangedType, index));
		}

		private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (_notifyChanges && sender != null)
			{
				int indexOfSender = _list.IndexOf(sender);

				if (indexOfSender >= 0)
				{
					MemberAccessor ma = TypeAccessor.GetAccessor(sender.GetType())[e.PropertyName];

					if (ma != null)
						OnListChanged(new EditableListChangedEventArgs(indexOfSender, ma.PropertyDescriptor));
					else
						OnListChanged(new EditableListChangedEventArgs(ListChangedType.ItemChanged, indexOfSender));

					// Do not fire an event for OnCollectionChanged here.

					if (_isSorted && _list.Count > 1)
					{
						int newIndex = GetItemSortedPosition(indexOfSender, sender);

						if (newIndex != indexOfSender)
						{
							_list.RemoveAt(indexOfSender);
							_list.Insert(newIndex, sender);

							OnMoveItem(sender, indexOfSender, newIndex);
						}
					}
				}
			}
		}

		#endregion

			#region Sorting

		public bool SupportsSorting
		{
			get { return true; }
		}

		[NonSerialized]
		private bool _isSorted;
		public  bool  IsSorted
		{
			get { return _isSorted; }
		}

		[NonSerialized]
		private PropertyDescriptor _sortProperty;
		public  PropertyDescriptor  SortProperty
		{
			get { return _sortProperty; }
		}

		[NonSerialized]
		private ListSortDirection _sortDirection;
		public  ListSortDirection  SortDirection
		{
			get { return _sortDirection; }
		}

		public void ApplySort(PropertyDescriptor property, ListSortDirection direction)
		{
			Debug.WriteLine(string.Format("Begin ApplySort(\"{0}\", {1})", property.Name, direction));

			_sortProperty  = property;
			_sortDirection = direction;
			_sortDescriptions = null;

			ApplySort(GetSortComparer(_sortProperty, _sortDirection));

			if (_list.Count > 0)
				OnReset();

			Debug.WriteLine(string.Format("End   ApplySort(\"{0}\", {1})", property.Name, direction));
		}

		public void RemoveSort()
		{
			_isSorted     = false;
			_sortProperty = null;
			_sortDescriptions = null;

			OnReset();
		}

			#endregion

			#region Searching

		public bool SupportsSearching
		{
			get { return true; }
		}

		public int Find(PropertyDescriptor property, object key)
		{
			if (property == null) throw new ArgumentException("property");

			if (key != null)
				for (int i = 0; i < _list.Count; i++)
					if (key.Equals(property.GetValue(_list[i])))
						return i;

			return -1;
		}

			#endregion

			#region Indexes

		public void AddIndex(PropertyDescriptor property)
		{
		}

		public void RemoveIndex(PropertyDescriptor property)
		{
		}

			#endregion

		#endregion

		#region ICancelAddNew Members

		public void CancelNew(int itemIndex)
		{
			if (itemIndex >= 0 && itemIndex == _newItemIndex)
			{
				_list.RemoveAt(itemIndex);
				OnRemoveItem(_newObject, itemIndex);
				EndNew();
			}
		}

		public void EndNew(int itemIndex)
		{
			if (itemIndex == _newItemIndex)
				EndNew();
		}

		public void EndNew()
		{
			_newItemIndex = -1;

			if (_newObject != null)
				_newObject.ObjectEdit -= NewObject_ObjectEdit;

			_newObject = null;
		}

		#endregion

		#region IList Members

		public int Add(object value)
		{
			int index;
			
			if (!_isSorted)
				index = _list.Add(value);
			else
			{
				index = GetSortedInsertIndex(value);
				_list.Insert(index, value);
			}

			AddInternal(value);
			OnAddItem(value, index);

			return index;
		}

		public void Clear()
		{
			if (_list.Count > 0)
			{
				RemoveInternal(_list);

				_list.Clear();
				OnReset();
			}
		}

		public bool Contains(object value)
		{
			return _list.Contains(value);
		}

		public int IndexOf(object value)
		{
			return _list.IndexOf(value);
		}

		public void Insert(int index, object value)
		{
			if (_isSorted)
				index = GetSortedInsertIndex(value);
			
			_list.Insert(index, value);
			AddInternal(value);

			OnAddItem(value, index);
		}

		public bool IsFixedSize
		{
			get { return _list.IsFixedSize; }
		}

		public bool IsReadOnly
		{
			get { return _list.IsReadOnly; }
		}

		public void Remove(object value)
		{
			int index = IndexOf(value);
			
			if (index >= 0)
				RemoveInternal(value);

			_list.Remove(value);

			if (index >= 0)
				OnRemoveItem(value, index);
		}

		public void RemoveAt(int index)
		{
			object value = this[index];

			RemoveInternal(value);

			_list.RemoveAt(index);

			OnRemoveItem(value, index);
		}

		public object this[int index]
		{
			get { return _list[index]; }
			set 
			{
				object o = _list[index];

				if (o != value)
				{
					RemoveInternal(o);
					
					_list[index] = value;

					AddInternal(value);

					OnChangeItem(o, value, index);

					if (_isSorted)
					{
						int newIndex = GetItemSortedPosition(index, value);
						
						if (newIndex != index)
						{
							_list.RemoveAt(index);
							_list.Insert(newIndex, value);
						}

						OnMoveItem(value, index, newIndex);
					}
				}
			}
		}

		#endregion

		#region ICollection Members

		public void CopyTo(Array array, int index)
		{
			_list.CopyTo(array, index);
		}

		public int Count
		{
			get { return _list.Count; }
		}

		public bool IsSynchronized
		{
			get { return _list.IsSynchronized; }
		}

		public object SyncRoot
		{
			get { return _list.SyncRoot; }
		}

		#endregion

		#region IEnumerable Members

		public IEnumerator GetEnumerator()
		{
			return _list.GetEnumerator();
		}

		#endregion

		#region SortPropertyComparer

		class SortPropertyComparer : IComparer
		{
			readonly PropertyDescriptor _property;
			readonly ListSortDirection  _direction;

			public SortPropertyComparer(PropertyDescriptor property, ListSortDirection direction)
			{
				_property  = property;
				_direction = direction;
			}

			public int Compare(object x, object y)
			{
				object a = _property.GetValue(x);
				object b = _property.GetValue(y);

				int n = Comparer.Default.Compare(a, b);

				return _direction == ListSortDirection.Ascending? n: -n;
			}
		}

		#endregion

		#region IComparer Accessor
		
		public IComparer GetSortComparer()
		{
			if (_isSorted)
			{
				if (_sortDescriptions != null)
					return GetSortComparer(_sortDescriptions);

				return GetSortComparer(_sortProperty, _sortDirection);
			}

			return null;
		}
		
		private IComparer GetSortComparer(PropertyDescriptor sortProperty, ListSortDirection sortDirection)
		{
			if (_sortSubstitutions.ContainsKey(sortProperty.Name))
				sortProperty = ((SortSubstitutionPair)_sortSubstitutions[sortProperty.Name]).Substitute;

			return new SortPropertyComparer(sortProperty, sortDirection);
		}

		private IComparer GetSortComparer(ListSortDescriptionCollection sortDescriptions)
		{
			bool needSubstitution = false;

			if (_sortSubstitutions.Count > 0)
			{
				foreach (ListSortDescription sortDescription in sortDescriptions)
				{
					if (_sortSubstitutions.ContainsKey(sortDescription.PropertyDescriptor.Name))
					{
						needSubstitution = true;
						break;
					}
				}

				if (needSubstitution)
				{
					ListSortDescription[] sorts = new ListSortDescription[sortDescriptions.Count];
					sortDescriptions.CopyTo(sorts, 0);

					for (int i = 0; i < sorts.Length; i++)
						if (_sortSubstitutions.ContainsKey(sorts[i].PropertyDescriptor.Name))
							sorts[i] = new ListSortDescription(((SortSubstitutionPair)_sortSubstitutions[sorts[i].PropertyDescriptor.Name]).Substitute, 
								                               sorts[i].SortDirection);

					sortDescriptions = new ListSortDescriptionCollection(sorts);
				}
			}

			return new SortListPropertyComparer(sortDescriptions);
		}
		
		#endregion

		#region IBindingListView Members

		public bool SupportsAdvancedSorting
		{
			get { return true; }
		}

		public void ApplySort(ListSortDescriptionCollection sorts)
		{
			_sortDescriptions = sorts;

			_isSorted = true;
			_sortProperty = null;
			
			ApplySort(GetSortComparer(sorts));

			if (_list.Count > 0)
				OnReset();
		}

		[NonSerialized]
		private ListSortDescriptionCollection _sortDescriptions;
		public  ListSortDescriptionCollection  SortDescriptions
		{
			get { return _sortDescriptions; }
		}

		public bool SupportsFiltering
		{
			get { return false; }
		}

		public string Filter
		{
			get { throw new NotImplementedException("The method 'BindingListImpl.get_Filter' is not implemented."); }
			set { throw new NotImplementedException("The method 'BindingListImpl.set_Filter' is not implemented."); }
		}

		public void RemoveFilter()
		{
			throw new NotImplementedException("The method 'BindingListImpl.RemoveFilter()' is not implemented.");
		}

		#endregion

		#region SortListPropertyComparer

		class SortListPropertyComparer : IComparer
		{
			readonly ListSortDescriptionCollection _sorts;

			public SortListPropertyComparer(ListSortDescriptionCollection sorts)
			{
				_sorts = sorts;
			}

			public int Compare(object x, object y)
			{
				for (int i = 0; i < _sorts.Count; i++)
				{
					PropertyDescriptor property = _sorts[i].PropertyDescriptor;

					object a = property.GetValue(x);
					object b = property.GetValue(y);

					int n = Comparer.Default.Compare(a, b);

					if (n != 0)
						return _sorts[i].SortDirection == ListSortDirection.Ascending? n: -n;
				}

				return 0;
			}
		}

		#endregion

		#region Sorting enhancement

		private readonly Hashtable _sortSubstitutions = new Hashtable();

		private class SortSubstitutionPair
		{
			public SortSubstitutionPair(PropertyDescriptor original, PropertyDescriptor substitute)
			{
				Original   = original;
				Substitute = substitute;
			}

			public readonly PropertyDescriptor Original;
			public readonly PropertyDescriptor Substitute;
		}

		public void CreateSortSubstitution(string originalProperty, string substituteProperty)
		{
			TypeAccessor typeAccessor = TypeAccessor.GetAccessor(_itemType);

			PropertyDescriptor originalDescriptor = typeAccessor.PropertyDescriptors[originalProperty];
			PropertyDescriptor substituteDescriptor = typeAccessor.PropertyDescriptors[substituteProperty];

			if (originalDescriptor == null)   throw new InvalidOperationException("Can not retrieve PropertyDescriptor for original property: " + originalProperty);
			if (substituteDescriptor == null) throw new InvalidOperationException("Can not retrieve PropertyDescriptor for substitute property: " + substituteProperty);

			_sortSubstitutions[originalProperty] = new SortSubstitutionPair(originalDescriptor, substituteDescriptor);
		}

		public void RemoveSortSubstitution(string originalProperty)
		{
			_sortSubstitutions.Remove(originalProperty);
		}

		#endregion

		#region Sort enforcement

		public int GetItemSortedPosition(int index, object sender)
		{
			IComparer comparer = GetSortComparer();

			if (comparer == null)
				return index;

			if ((index > 0 && comparer.Compare(_list[index - 1], sender) > 0) ||
				(index < _list.Count - 1 && comparer.Compare(_list[index + 1], sender) < 0))
			{
				for (int i = 0; i < _list.Count; i++)
				{
					if (i != index && comparer.Compare(_list[i], sender) > 0)
					{
						if (i > index)
							return i - 1;

						return i;
					}
				}

				return _list.Count - 1;
			}

			return index;
		}

		public int GetSortedInsertIndex(object value)
		{
			IComparer comparer = GetSortComparer();

			if (comparer == null)
				return -1;

			for (int i = 0; i < _list.Count; i++)
				if (comparer.Compare(_list[i], value) > 0)
					return i;

			return _list.Count;
		}

		#endregion

		#region Misc/Range Operations
		
		public void Move(int newIndex, int oldIndex)
		{
			if (oldIndex != newIndex)
			{
				EndNew();

				object o = _list[oldIndex];

				_list.RemoveAt(oldIndex);
				if (!_isSorted)
					_list.Insert(newIndex, o);
				else
					_list.Insert(newIndex = GetSortedInsertIndex(o), o);

				OnMoveItem(o, oldIndex, newIndex);
			}
		}
		
		public void AddRange(ICollection c)
		{
			foreach (object o in c)
			{
				if (!_isSorted)
					_list.Add(o);
				else
					_list.Insert(GetSortedInsertIndex(o), o);
			}

			AddInternal(c);

			OnReset();
		}
		
		public void InsertRange(int index, ICollection c)
		{
			if (c.Count == 0)
				return;
			
			foreach (object o in c)
			{
				if (!_isSorted)
					_list.Insert(index++, o);
				else
					_list.Insert(GetSortedInsertIndex(o), o);
			}

			AddInternal(c);

			OnReset();
		}
		
		public void RemoveRange(int index, int count)
		{
			object[] toRemove = new object[count];

			for (int i = index; i < _list.Count && i < index + count; i++)
				toRemove[i - index] = _list[i];
			
			RemoveInternal(toRemove);

			foreach (object o in toRemove)
				_list.Remove(o);

			OnReset();
		}
		
		public void SetRange(int index, ICollection c)
		{
			int cCount = c.Count;
			
			if (index < 0 || index >= _list.Count - cCount)
				throw new ArgumentOutOfRangeException("index");

			bool oldNotifyChanges = _notifyChanges;
			_notifyChanges = false;

			int i = index;
			foreach (object newObject in c)
			{
				RemoveInternal(_list[i + index]);
				_list[i + index] = newObject;
			}
			
			AddInternal(c);
			
			if (_isSorted)
				ApplySort(GetSortComparer());

			_notifyChanges = oldNotifyChanges;
			OnReset();
		}

		#endregion

		#region Add/Remove Internal

		private void AddInternal(object value)
		{
			EndNew();

			if (value is INotifyPropertyChanged)
				((INotifyPropertyChanged)value).PropertyChanged += 
					ItemPropertyChanged;
		}

		private void RemoveInternal(object value)
		{
			EndNew();

			if (value is INotifyPropertyChanged)
				((INotifyPropertyChanged)value).PropertyChanged -=
					ItemPropertyChanged;
		}

		private void AddInternal(IEnumerable e)
		{
			foreach (object o in e)
				AddInternal(o);
		}

		private void RemoveInternal(IEnumerable e)
		{
			foreach (object o in e)
				RemoveInternal(o);
		}

		private void OnAddItem(object item, int index)
		{
			OnListChanged(new EditableListChangedEventArgs(ListChangedType.ItemAdded, index));
			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
		}

		private void OnRemoveItem(object item, int index)
		{
			OnListChanged(new EditableListChangedEventArgs(ListChangedType.ItemDeleted, index));
			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
		}

		private void OnMoveItem(object item, int oldIndex, int newIndex)
		{
			OnListChanged(new EditableListChangedEventArgs(newIndex, oldIndex));
			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
		}

		private void OnChangeItem(object oldValue, object newValue, int index)
		{
			OnListChanged(new EditableListChangedEventArgs(ListChangedType.ItemChanged, index));
			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, oldValue, newValue, index));
		}

		private void OnReset()
		{
			OnListChanged(new EditableListChangedEventArgs(ListChangedType.Reset));
			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
		}

		#endregion

		#region INotifyCollectionChanged Members

		public event NotifyCollectionChangedEventHandler CollectionChanged;

		private void FireCollectionChangedEvent(object sender, NotifyCollectionChangedEventArgs ea)
		{
			if (_notifyChanges && CollectionChanged != null)
				CollectionChanged(sender, ea);
		}

		protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs ea)
		{
			FireCollectionChangedEvent(this, ea);
		}

		#endregion
	}
}
