namespace Eto;

/// <summary>
/// Class to help implement collection changed events on a data store
/// </summary>
/// <remarks>
/// This is used for the platform handler of controls that use collections.
/// This class helps detect changes to a collection so that the appropriate action
/// can be taken to update the UI with the changes.
/// 
/// This is a simple helper that is much easier to implement than handling
/// the <see cref="INotifyCollectionChanged.CollectionChanged"/> event directly.
/// </remarks>
/// <typeparam name="TItem">Type of the items in the collection</typeparam>
/// <typeparam name="TCollection">Type of the collection</typeparam>
public abstract class CollectionChangedHandler<TItem, TCollection>
	where TCollection: class
{
	/// <summary>
	/// Gets the collection that this handler is observing
	/// </summary>
	public TCollection Collection { get; protected set; }

	/// <summary>
	/// Called when the object has been registered (attached) to a collection
	/// </summary>
	protected virtual void OnRegisterCollection(EventArgs e)
	{
	}

	/// <summary>
	/// Called when the object has unregistered the collection
	/// </summary>
	protected virtual void OnUnregisterCollection(EventArgs e)
	{
		RemoveAllItems();
	}

	/// <summary>
	/// Registers a specific collection to observe
	/// </summary>
	/// <param name="collection">collection to observe</param>
	/// <returns>true if the collection was registered, false otherwise</returns>
	public virtual bool Register(TCollection collection)
	{
		if (Collection != null)
			Unregister();				
		Collection = collection;
			
		var notify = Collection as INotifyCollectionChanged;
		if (notify != null)
		{
			notify.CollectionChanged += CollectionChanged;
		}
		OnRegisterCollection(EventArgs.Empty);
		return notify != null;
	}

	/// <summary>
	/// Unregisters the current registered collection
	/// </summary>
	public virtual void Unregister()
	{
		if (Collection == null)
			return;
			
		var notify = Collection as INotifyCollectionChanged;
		if (notify != null)
		{
			notify.CollectionChanged -= CollectionChanged;
		}
		Collection = null;
			
		OnUnregisterCollection(EventArgs.Empty);
	}

	/// <summary>
	/// Gets the index of the specified item
	/// </summary>
	/// <param name="item">Item to find the index of</param>
	/// <returns>Index of the item if contained in the collection, otherwise -1</returns>
	public virtual int IndexOf(TItem item)
	{
		var list = Collection as IList<TItem>;
		if (list != null)
			return list.IndexOf(item);
		return InternalIndexOf(item);
	}

	/// <summary>
	/// Gets the element at the specified index of the current registered collection.
	/// </summary>
	/// <returns>The item at the specified index.</returns>
	/// <param name="index">Index of the item to get.</param>
	public virtual TItem ElementAt(int index)
	{
		if (Collection is IList<TItem> listItem)
			return listItem[index];
		if (Collection is IList list)
			return (TItem)list[index];
		return InternalElementAt(index);
	}

	/// <summary>
	/// Gets the count of the items in the current registered collection.
	/// </summary>
	/// <value>The count of items.</value>
	public abstract int Count { get; }

	/// <summary>
	/// Gets the index of the item from the collection
	/// </summary>
	/// <remarks>
	/// Derived classes should implement this to get the index of the item.
	/// </remarks>
	/// <param name="item">Item to find the index</param>
	/// <returns>index of the item in the collection, or -1 if the item is not found</returns>
	protected abstract int InternalIndexOf(TItem item);

	/// <summary>
	/// Gets the element at the specified index.
	/// </summary>
	/// <remarks>
	/// Derived classes should implement this to get the item at the specified index.
	/// </remarks>
	/// <returns>The item at the specified index.</returns>
	/// <param name="index">Index of the item to get.</param>
	protected abstract TItem InternalElementAt(int index);

	/// <summary>
	/// Adds the item to the end of the collection
	/// </summary>
	/// <param name="item">Item to add to the collection</param>
	public abstract void AddItem(TItem item);

	/// <summary>
	/// Inserts an item at the specified index in the collection
	/// </summary>
	/// <param name="index">Index to insert the item to</param>
	/// <param name="item">Item to insert</param>
	public abstract void InsertItem(int index, TItem item);

	/// <summary>
	/// Removes the item at the specified index
	/// </summary>
	/// <param name="index">Index of the item to remove</param>
	public abstract void RemoveItem(int index);

	/// <summary>
	/// Removes all items from the collection
	/// </summary>
	public abstract void RemoveAllItems();

	/// <summary>
	/// Removes the specified item
	/// </summary>
	/// <remarks>
	/// This will remove the item by finding the index and removing based on index.
	/// Implementors should override this method if there is a faster mechanism to do so.
	/// </remarks>
	/// <param name="item">Item to remove from the collection</param>
	public virtual void RemoveItem(TItem item)
	{
		var index = IndexOf(item);
		if (index >= 0)
			RemoveItem(index);
	}

	/// <summary>
	/// Adds multiple items to the end of the collection
	/// </summary>
	/// <remarks>
	/// This simply calls <see cref="AddItem"/> for each item in the list.  If there
	/// is a faster mechanism for doing so, implementors should override this method.
	/// 
	/// For example, sometimes adding a single item will update the UI for each item, this
	/// should be overridden so the UI is updated after all items have been added.
	/// </remarks>
	/// <param name="items">Enumeration of items to add to the end of the collection</param>
	public virtual void AddRange(IEnumerable<TItem> items)
	{
		foreach (var item in items)
			AddItem(item);
	}

	/// <summary>
	/// Inserts multiple items to the specified index in the collection
	/// </summary>
	/// <remarks>
	/// This simply calls <see cref="InsertItem"/> for each item in the list.  If there
	/// is a faster mechanism for doing so, implementors should override this method.
	/// 
	/// For example, sometimes inserting a single item will update the UI for each item, this
	/// should be overridden so the UI is updated after all items have been inserted.
	/// </remarks>
	/// <param name="index">Index to start adding the items</param>
	/// <param name="items">Enumeration of items to add</param>
	public virtual void InsertRange(int index, IEnumerable<TItem> items)
	{
		foreach (var item in items)
			InsertItem(index++, item);
	}

	/// <summary>
	/// Removes a specified count of items from the collection starting at the specified index
	/// </summary>
	/// <remarks>
	/// This simply calls <see cref="RemoveItem(int)"/> for each item to remove.  If there
	/// is a faster mechanism for doing so, implementors should override this method.
	/// 
	/// For example, sometimes removing a single item will update the UI for each item, this
	/// should be overridden so the UI is updated after all items have been removed.
	/// </remarks>
	/// <param name="index">Index to start removing the items from</param>
	/// <param name="count">Number of items to remove</param>
	public virtual void RemoveRange(int index, int count)
	{
		for (int i = 0; i < count; i++)
			RemoveItem(index);
	}

	/// <summary>
	/// Removes the specified items from the collection
	/// </summary>
	/// <remarks>
	/// This simply calls <see cref="RemoveItem(TItem)"/> for each item to remove.  If there
	/// is a faster mechanism for doing so, implementors should override this method.
	/// 
	/// For example, sometimes removing a single item will update the UI for each item, this
	/// should be overridden so the UI is updated after all items have been removed.
	/// </remarks>
	/// <param name="items">List of items to remove</param>
	public virtual void RemoveRange(IEnumerable<TItem> items)
	{
		foreach (var item in items)
			RemoveItem(item);
	}

	/// <summary>
	/// Resets the collection when it is dramatically changed
	/// </summary>
	/// <remarks>
	/// By default this removes all items and adds all items back (if Collection is an IEnumerable{T})
	/// Platform implementations can override this to do something more efficient if needed.
	/// </remarks>
	public virtual void Reset()
	{
		RemoveAllItems();
		var items = Collection as IEnumerable<TItem>;
		if (items != null)
			AddRange(items);
	}
	
	/// <summary>
	/// Called before any methods to change the collection have been called.
	/// </summary>
	/// <remarks>
	/// There may be multiple methods called when the collection changes, such as RemoveRange then AddRange or
	/// RemoveItem then InsertItem.  When this happens, some platforms may need to know all of the changes before
	/// refreshing the view.  After all of the update methods, <see cref="EndUpdates"/> is called to signal that they are done.
	/// </remarks>
	/// <seealso cref="EndUpdates"/>
	protected virtual void BeginUpdates()
	{
	}
	
	/// <summary>
	/// Called after any methods to change the collection have been called.
	/// </summary>
	/// <remarks>
	/// See <see cref="BeginUpdates"/> for details.
	/// </remarks>
	protected virtual void EndUpdates()
	{
	}

	private protected virtual void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
	{
		BeginUpdates();
		switch (e.Action)
		{
			case NotifyCollectionChangedAction.Add:
				if (e.NewStartingIndex != -1)
				{
					if (e.NewItems.Count == 1)
						InsertItem(e.NewStartingIndex, (TItem)e.NewItems[0]);
					else
						InsertRange(e.NewStartingIndex, e.NewItems.Cast<TItem>());
				}
				else
					AddRange(e.NewItems.Cast<TItem>());
				break;
			case NotifyCollectionChangedAction.Move:
				if (e.OldStartingIndex != -1)
					RemoveRange(e.OldStartingIndex, e.OldItems.Count);
				else
					RemoveRange(e.OldItems.Cast<TItem>());
				InsertRange(e.NewStartingIndex, e.NewItems.Cast<TItem>());
				break;
			case NotifyCollectionChangedAction.Remove:
				if (e.OldStartingIndex != -1)
				{
					if (e.OldItems.Count == 1)
						RemoveItem(e.OldStartingIndex);
					else
						RemoveRange(e.OldStartingIndex, e.OldItems.Count);
				}
				else
					RemoveRange(e.OldItems.Cast<TItem>());
				break;
			case NotifyCollectionChangedAction.Replace:
				if (e.OldStartingIndex != -1)
				{
					if (e.OldItems.Count == e.NewItems.Count && e.OldStartingIndex == e.NewStartingIndex)
					{
						ReplaceRange(e.OldStartingIndex, e.NewItems.Cast<TItem>());
					}
					else
					{
						RemoveRange(e.OldStartingIndex, e.OldItems.Count);
						InsertRange(e.NewStartingIndex, e.NewItems.Cast<TItem>());
					}
				}
				else
				{
					for (int i = 0; i < e.OldItems.Count; i++)
					{
						var index = IndexOf((TItem)e.OldItems[i]);
						ReplaceItem(index, (TItem)e.NewItems[i]);
					}
				}
				break;
			case NotifyCollectionChangedAction.Reset:
				Reset();
				break;
		}
		EndUpdates();
	}
	
	/// <summary>
	/// Replaces the item at the specified index
	/// </summary>
	/// <remarks>
	/// This by default will remove then insert the item at the specified index.
	/// </remarks>
	/// <param name="index">Index of the item to replace</param>
	/// <param name="newItem">New item</param>
	public virtual void ReplaceItem(int index, TItem newItem)
	{
		RemoveItem(index);
		InsertItem(index, newItem);
	}
	
	/// <summary>
	/// Replaces a range of items starting at the specified index.
	/// </summary>
	/// <remarks>
	/// By default this calls <see cref="ReplaceItem"/> for each item.
	/// </remarks>
	/// <param name="index">Index of the start of the range</param>
	/// <param name="newItems">New items to replace the existing items at the same indexes</param>
	public virtual void ReplaceRange(int index, IEnumerable<TItem> newItems)
	{
		foreach (var item in newItems)
		{
			ReplaceItem(index++, item);
		}
	}
}

/// <summary>
/// Helper class to handle collection change events of an <see cref="IEnumerable"/>
/// </summary>
/// <remarks>
/// This is used for the platform handler of controls that use collections.
/// This class helps detect changes to a collection so that the appropriate action
/// can be taken to update the UI with the changes.
/// 
/// Use this class as a base when you only have an <see cref="IEnumerable"/>.  If the object
/// also implements <see cref="INotifyCollectionChanged"/> it will get changed events
/// otherwise you must register a new collection each time.
/// </remarks>
/// <typeparam name="TItem">Type of each item in the enumerable</typeparam>
public abstract class EnumerableChangedHandler<TItem> : EnumerableChangedHandler<TItem, IEnumerable<TItem>>
{
}

/// <summary>
/// Helper class to handle collection change events of an <see cref="IEnumerable"/>
/// </summary>
/// <remarks>
/// This is used for the platform handler of controls that use collections.
/// This class helps detect changes to a collection so that the appropriate action
/// can be taken to update the UI with the changes.
/// 
/// Use this class as a base when you only have an <see cref="IEnumerable"/>.  If the object
/// also implements <see cref="INotifyCollectionChanged"/> it will get changed events
/// otherwise you must register a new collection each time.
/// </remarks>
/// <typeparam name="TItem">Type of each item in the enumerable</typeparam>
/// <typeparam name="TCollection">Type of the collection to handle the change events for</typeparam>
public abstract class EnumerableChangedHandler<TItem, TCollection> : CollectionChangedHandler<TItem, TCollection>
	where TCollection: class, IEnumerable<TItem>
{
	IList<TItem> _lookup;
		
	IList<TItem> Lookup => _lookup ?? (_lookup = Collection?.ToList() ?? new List<TItem>());

	/// <summary>
	/// Implements the mechanism for finding the index of an item (the slow way)
	/// </summary>
	/// <remarks>
	/// If the collection object implements <see cref="IList"/>, this will not get called
	/// as it will call it's method of getting the index.  This is used as a fallback.
	/// </remarks>
	/// <param name="item">Item to find in the collection</param>
	/// <returns>Index of the item, or -1 if not found</returns>
	protected override int InternalIndexOf(TItem item)
	{
		if (Collection is IList<TItem> list)
			return list.IndexOf(item);
		if (Collection is IList list2)
			return list2.IndexOf(item);

		return Lookup.IndexOf(item);
	}

	/// <summary>
	/// Gets the count of the items in the current registered collection.
	/// </summary>
	/// <value>The count of items.</value>
	public override int Count
	{
		get
		{
			// mono doesn't test for ICollection, causing performance issues
			if (Collection is ICollection<TItem> coll)
				return coll.Count;
			if (Collection is ICollection coll2)
				return coll2.Count;
			return Lookup.Count;
		}
	}

	/// <summary>
	/// Gets the element at the specified index.
	/// </summary>
	/// <remarks>Derived classes should implement this to get the item at the specified index.</remarks>
	/// <returns>The item at the specified index.</returns>
	/// <param name="index">Index of the item to get.</param>
	protected override TItem InternalElementAt(int index)
	{
		if (Collection is IList<TItem> list)
			return list[index];
		if (Collection is IList list2)
			return (TItem)list2[index];
		return Lookup[index];
	}

	/// <summary>
	/// Called when the collection is registered
	/// </summary>
	protected override void OnRegisterCollection(EventArgs e)
	{
		base.OnRegisterCollection(e);
		InitializeCollection();
	}

	/// <summary>
	/// Initializes the collection, usually by adding the collection to the underlying handler.
	/// </summary>
	protected virtual void InitializeCollection()
	{
		if (Collection != null)
			AddRange(Collection);
		_lookup = null;
	}

	private protected override void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
	{
		_lookup = null;
		base.CollectionChanged(sender, e);
	}
}

/// <summary>
/// Class to help implement change handling for a <see cref="IDataStore{T}"/>
/// </summary>
/// <remarks>
/// This is used for the platform handler of controls that use collections.
/// This class helps detect changes to a collection so that the appropriate action
/// can be taken to update the UI with the changes.
/// 
/// Use this class as a base when you are detecting changes for an <see cref="IDataStore{T}"/>.
/// If the object also implements <see cref="INotifyCollectionChanged"/>, it will get changed events.
/// Otherwise, you must register a new collection each time.
/// </remarks>
/// <typeparam name="TItem">Type of items in the data store</typeparam>
/// <typeparam name="TCollection">Type of the data store to detect changes on</typeparam>
public abstract class DataStoreChangedHandler<TItem, TCollection> : CollectionChangedHandler<TItem, TCollection>, IEnumerable<TItem>
	where TCollection: class, IDataStore<TItem>
{
	/// <summary>
	/// Gets the enumerator.
	/// </summary>
	/// <returns>The enumerator.</returns>
	public IEnumerator<TItem> GetEnumerator()
	{
		return new DataStoreVirtualCollection<TItem>(Collection).GetEnumerator();
	}

	IEnumerator IEnumerable.GetEnumerator()
	{
		return GetEnumerator();
	}

	/// <summary>
	/// Called when the collection is registered
	/// </summary>
	protected override void OnRegisterCollection(EventArgs e)
	{
		base.OnRegisterCollection(e);
		AddRange(this);
	}

	/// <summary>
	/// Implements the mechanism for finding the index of an item (the slow way)
	/// </summary>
	/// <remarks>
	/// If the collection object implements <see cref="IList"/>, this will not get called
	/// as it will call it's method of getting the index.  This is used as a fallback.
	/// </remarks>
	/// <param name="item">Item to find in the collection</param>
	/// <returns>Index of the item, or -1 if not found</returns>
	protected override int InternalIndexOf(TItem item)
	{
		for (int i = 0; i < Collection.Count; i++)
		{
			if (object.ReferenceEquals(item, Collection[i]))
				return i;
		}
		return -1;
	}

	/// <summary>
	/// Gets the count of the items in the current registered collection.
	/// </summary>
	/// <value>The count of items.</value>
	public override int Count
	{
		get { return Collection.Count; }
	}

	/// <summary>
	/// Gets the element at the specified index.
	/// </summary>
	/// <remarks>Derived classes should implement this to get the item at the specified index.</remarks>
	/// <returns>The item at the specified index.</returns>
	/// <param name="index">Index of the item to get.</param>
	protected override TItem InternalElementAt(int index)
	{
		return Collection[index];
	}
}