File: ListProxy.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Dispatching;
 
namespace Microsoft.Maui.Controls
{
	internal sealed class ListProxy : IReadOnlyList<object>, IListProxy, INotifyCollectionChanged
	{
		readonly IDispatcher _dispatcher;
		readonly ICollection _collection;
		readonly IList _list;
		readonly int _windowSize;
		readonly WeakNotifyCollectionChangedProxy _proxy;
		readonly NotifyCollectionChangedEventHandler _collectionChangedDelegate;
 
		IEnumerator _enumerator;
		int _enumeratorIndex;
 
		bool _finished;
		HashSet<int> _indexesCounted;
 
		Dictionary<int, object> _items;
		int _version;
 
		int _windowIndex;
 
		internal ListProxy(IEnumerable enumerable, int windowSize = int.MaxValue, IDispatcher dispatcher = null)
		{
			_dispatcher = dispatcher;
			_windowSize = windowSize;
 
			ProxiedEnumerable = enumerable;
			_collection = enumerable as ICollection;
 
			if (_collection == null && enumerable is IReadOnlyCollection<object> coll)
				_collection = new ReadOnlyListAdapter(coll);
 
			_list = enumerable as IList;
			if (_list == null && enumerable is IReadOnlyList<object>)
				_list = new ReadOnlyListAdapter((IReadOnlyList<object>)enumerable);
 
			if (enumerable is INotifyCollectionChanged changed)
			{
				_collectionChangedDelegate = OnCollectionChanged;
				_proxy = new WeakNotifyCollectionChangedProxy(changed, _collectionChangedDelegate);
			}
		}
 
		~ListProxy() => _proxy?.Unsubscribe();
 
		public IEnumerable ProxiedEnumerable { get; }
 
		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}
 
		public IEnumerator<object> GetEnumerator()
		{
			return new ProxyEnumerator(this);
		}
 
		/// <summary>
		///     Gets whether or not the current window contains the <paramref name="item" />.
		/// </summary>
		/// <param name="item">The item to search for.</param>
		/// <returns><c>true</c> if the item was found in a list or the current window, <c>false</c> otherwise.</returns>
		public bool Contains(object item)
		{
			if (_list != null)
				return _list.Contains(item);
 
			EnsureWindowCreated();
 
			if (_items != null)
				return _items.Values.Contains(item);
 
			return false;
		}
 
		/// <summary>
		///     Gets the index for the <paramref name="item" /> if in a list or the current window.
		/// </summary>
		/// <param name="item">The item to search for.</param>
		/// <returns>The index of the item if in a list or the current window, -1 otherwise.</returns>
		public int IndexOf(object item)
		{
			if (_list != null)
				return _list.IndexOf(item);
 
			EnsureWindowCreated();
 
			if (_items != null)
			{
				foreach (KeyValuePair<int, object> kvp in _items)
				{
					if (Equals(kvp.Value, item))
						return kvp.Key;
				}
			}
 
			return -1;
		}
 
		public event NotifyCollectionChangedEventHandler CollectionChanged;
 
		public int Count
		{
			get
			{
				if (_collection != null)
					return _collection.Count;
 
				EnsureWindowCreated();
 
				if (_indexesCounted != null)
					return _indexesCounted.Count;
 
				return 0;
			}
		}
 
		public object this[int index]
		{
			get
			{
				object value;
				if (!TryGetValue(index, out value))
					throw new ArgumentOutOfRangeException(nameof(index));
 
				return value;
			}
		}
 
		public void Clear()
		{
			_version++;
			_finished = false;
			_windowIndex = 0;
			_enumeratorIndex = 0;
 
			if (_enumerator != null)
			{
				var dispose = _enumerator as IDisposable;
				if (dispose != null)
					dispose.Dispose();
 
				_enumerator = null;
			}
 
			if (_items != null)
				_items.Clear();
			if (_indexesCounted != null)
				_indexesCounted.Clear();
 
			OnCountChanged();
			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
		}
 
		public event EventHandler CountChanged;
 
		void ClearRange(int index, int clearCount)
		{
			if (_items == null)
				return;
 
			for (int i = index; i < index + clearCount; i++)
				_items.Remove(i);
		}
 
		bool CountIndex(int index)
		{
			if (_collection != null)
				return false;
 
			// A collection is used in case TryGetValue is called out of order.
			if (_indexesCounted == null)
				_indexesCounted = new HashSet<int>();
 
			if (_indexesCounted.Contains(index))
				return false;
 
			_indexesCounted.Add(index);
			return true;
		}
 
		void EnsureWindowCreated()
		{
			if (_items != null && _items.Count > 0)
				return;
 
			object value;
			TryGetValue(0, out value);
		}
 
		void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			Action action;
			if (_list == null)
			{
				action = Clear;
			}
			else
			{
				action = () =>
				{
					_version++;
					OnCollectionChanged(e);
				};
			}
 
			CollectionSynchronizationContext sync;
			if (BindingBase.TryGetSynchronizedCollection(ProxiedEnumerable, out sync))
			{
				sync.Callback(ProxiedEnumerable, sync.Context, () =>
				{
					e = e.WithCount(Count);
					_dispatcher.DispatchIfRequired(action);
				}, false);
			}
			else
			{
				e = e.WithCount(Count);
				_dispatcher.DispatchIfRequired(action);
			}
		}
 
		void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
		{
			NotifyCollectionChangedEventHandler changed = CollectionChanged;
			if (changed != null)
				changed(this, e);
		}
 
		void OnCountChanged()
		{
			EventHandler changed = CountChanged;
			if (changed != null)
				changed(this, EventArgs.Empty);
		}
 
		bool TryGetValue(int index, out object value)
		{
			value = null;
 
			CollectionSynchronizationContext syncContext;
			BindingBase.TryGetSynchronizedCollection(ProxiedEnumerable, out syncContext);
 
			if (_list != null)
			{
				object indexedValue = null;
				var inRange = false;
				Action getFromList = () =>
				{
					if (index >= _list.Count)
						return;
 
					indexedValue = _list[index];
					inRange = true;
				};
 
				if (syncContext != null)
					syncContext.Callback(ProxiedEnumerable, syncContext.Context, getFromList, false);
				else
					getFromList();
 
				value = indexedValue;
				return inRange;
			}
 
			if (_collection != null && index >= _collection.Count)
				return false;
			if (_items != null)
			{
				bool found = _items.TryGetValue(index, out value);
				if (found || _finished)
					return found;
			}
 
			if (index >= _windowIndex + _windowSize)
			{
				int newIndex = index - _windowSize / 2;
				ClearRange(_windowIndex, newIndex - _windowIndex);
				_windowIndex = newIndex;
			}
			else if (index < _windowIndex)
			{
				int clearIndex = _windowIndex;
				int clearSize = _windowSize;
				if (clearIndex <= index + clearSize)
				{
					int diff = index + clearSize - clearIndex;
					clearIndex += diff + 1;
					clearSize -= diff;
				}
 
				ClearRange(clearIndex, clearSize);
				_windowIndex = 0;
 
				var dispose = _enumerator as IDisposable;
				if (dispose != null)
					dispose.Dispose();
 
				_enumerator = null;
				_enumeratorIndex = 0;
			}
 
			if (_enumerator == null)
				_enumerator = ProxiedEnumerable.GetEnumerator();
			if (_items == null)
				_items = new Dictionary<int, object>();
 
			var countChanged = false;
			int end = _windowIndex + _windowSize;
 
			for (; _enumeratorIndex < end; _enumeratorIndex++)
			{
				var moved = false;
				Action move = () =>
				{
					try
					{
						moved = _enumerator.MoveNext();
					}
					catch (InvalidOperationException ioex)
					{
						throw new InvalidOperationException("You must call UpdateNonNotifyingList() after updating a list that does not implement INotifyCollectionChanged", ioex);
					}
 
					if (!moved)
					{
						var dispose = _enumerator as IDisposable;
						if (dispose != null)
							dispose.Dispose();
 
						_enumerator = null;
						_enumeratorIndex = 0;
						_finished = true;
					}
				};
 
				if (syncContext == null)
					move();
				else
					syncContext.Callback(ProxiedEnumerable, syncContext.Context, move, false);
 
				if (!moved)
					break;
 
				if (CountIndex(_enumeratorIndex))
					countChanged = true;
 
				if (_enumeratorIndex >= _windowIndex)
					_items.Add(_enumeratorIndex, _enumerator.Current);
			}
 
			if (countChanged)
				OnCountChanged();
 
			return _items.TryGetValue(index, out value);
		}
 
		class ProxyEnumerator : IEnumerator<object>
		{
			readonly ListProxy _proxy;
			readonly int _version;
 
			int _index;
 
			public ProxyEnumerator(ListProxy proxy)
			{
				_proxy = proxy;
				_version = proxy._version;
			}
 
			public void Dispose()
			{
			}
 
			public bool MoveNext()
			{
				if (_proxy._version != _version)
					throw new InvalidOperationException();
 
				object value;
				bool next = _proxy.TryGetValue(_index++, out value);
				if (next)
					Current = value;
 
				return next;
			}
 
			public void Reset()
			{
				_index = 0;
				Current = null;
			}
 
			public object Current { get; private set; }
		}
 
		#region IList
 
		bool IListProxy.TryGetValue(int index, out object value)
			=> TryGetValue(index, out value);
 
		object IList.this[int index]
		{
			get { return this[index]; }
			set { throw new NotSupportedException(); }
		}
 
		bool IList.IsReadOnly
		{
			get { return true; }
		}
 
		bool IList.IsFixedSize
		{
			get { return false; }
		}
 
		bool ICollection.IsSynchronized
		{
			get { return false; }
		}
 
		object ICollection.SyncRoot
		{
			get { throw new NotSupportedException(); }
		}
 
		void ICollection.CopyTo(Array array, int index)
		{
			throw new NotSupportedException();
		}
 
		int IList.Add(object item)
		{
			throw new NotSupportedException();
		}
 
		void IList.Remove(object item)
		{
			throw new NotSupportedException();
		}
 
		void IList.Insert(int index, object item)
		{
			throw new NotSupportedException();
		}
 
		void IList.RemoveAt(int index)
		{
			throw new NotSupportedException();
		}
 
		void IList.Clear()
		{
			throw new NotSupportedException();
		}
 
		#endregion
	}
}