// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information. 
using System.Collections;
using System.Collections.Generic;
namespace System.Linq
{
    public static partial class EnumerableEx
    {
        /// 
        /// Creates a buffer with a view over the source sequence, causing each enumerator to obtain access to the remainder of
        /// the sequence from the current index in the buffer.
        /// 
        /// Source sequence element type.
        /// Source sequence.
        /// 
        /// Buffer enabling each enumerator to retrieve elements from the shared source sequence, starting from the index
        /// at the point of obtaining the enumerator.
        /// 
        /// 
        /// var rng = Enumerable.Range(0, 10).Publish();
        /// var e1 = rng.GetEnumerator();    // e1 has a view on the source starting from element 0
        /// Assert.IsTrue(e1.MoveNext());
        /// Assert.AreEqual(0, e1.Current);
        /// Assert.IsTrue(e1.MoveNext());
        /// Assert.AreEqual(1, e1.Current);
        /// var e2 = rng.GetEnumerator();
        /// Assert.IsTrue(e2.MoveNext());    // e2 has a view on the source starting from element 2
        /// Assert.AreEqual(2, e2.Current);
        /// Assert.IsTrue(e1.MoveNext());    // e1 continues to enumerate over its view
        /// Assert.AreEqual(2, e1.Current);
        /// 
        public static IBuffer Publish(this IEnumerable source)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            return new PublishedBuffer(source.GetEnumerator());
        }
        /// 
        /// Publishes the source sequence within a selector function where each enumerator can obtain a view over a tail of the
        /// source sequence.
        /// 
        /// Source sequence element type.
        /// Result sequence element type.
        /// Source sequence.
        /// Selector function with published access to the source sequence for each enumerator.
        /// Sequence resulting from applying the selector function to the published view over the source sequence.
        public static IEnumerable Publish(this IEnumerable source, Func, IEnumerable> selector)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (selector == null)
                throw new ArgumentNullException(nameof(selector));
            return Create(() => selector(source.Publish()).GetEnumerator());
        }
        private sealed class PublishedBuffer : IBuffer
        {
            private readonly object _gate = new object();
            private readonly RefCountList _buffer;
            private readonly IEnumerator _source;
            private bool _disposed;
            private Exception? _error;
            private bool _stopped;
            public PublishedBuffer(IEnumerator source)
            {
                _buffer = new RefCountList(0);
                _source = source;
            }
            public IEnumerator GetEnumerator()
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException("");
                }
                var i = default(int);
                lock (_gate)
                {
                    i = _buffer.Count;
                    _buffer.ReaderCount++;
                }
                return GetEnumeratorCore(i);
            }
            IEnumerator IEnumerable.GetEnumerator()
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException("");
                }
                return GetEnumerator();
            }
            public void Dispose()
            {
                lock (_gate)
                {
                    if (!_disposed)
                    {
                        _source.Dispose();
                        _buffer.Clear();
                    }
                    _disposed = true;
                }
            }
            private IEnumerator GetEnumeratorCore(int i)
            {
                try
                {
                    while (true)
                    {
                        if (_disposed)
                        {
                            throw new ObjectDisposedException("");
                        }
                        var hasValue = default(bool);
                        var current = default(T)!;
                        lock (_gate)
                        {
                            if (i >= _buffer.Count)
                            {
                                if (!_stopped)
                                {
                                    try
                                    {
                                        hasValue = _source.MoveNext();
                                        if (hasValue)
                                        {
                                            current = _source.Current;
                                        }
                                    }
                                    catch (Exception ex)
                                    {
                                        _stopped = true;
                                        _error = ex;
                                        _source.Dispose();
                                    }
                                }
                                if (_stopped)
                                {
                                    if (_error != null)
                                    {
                                        throw _error;
                                    }
                                    else
                                    {
                                        break;
                                    }
                                }
                                if (hasValue)
                                {
                                    _buffer.Add(current);
                                }
                            }
                            else
                            {
                                hasValue = true;
                            }
                        }
                        if (hasValue)
                        {
                            yield return _buffer[i];
                        }
                        else
                        {
                            break;
                        }
                        i++;
                    }
                }
                finally
                {
                    _buffer?.Done(i + 1);
                }
            }
        }
    }
}