// 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.Generic;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Subjects;

namespace System.Reactive.Linq.ObservableImpl
{
    internal static class Window<TSource>
    {
        internal sealed class Count : Producer<IObservable<TSource>, Count._>
        {
            private readonly IObservable<TSource> _source;
            private readonly int _count;
            private readonly int _skip;

            public Count(IObservable<TSource> source, int count, int skip)
            {
                _source = source;
                _count = count;
                _skip = skip;
            }

            protected override _ CreateSink(IObserver<IObservable<TSource>> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run(_source);

            internal sealed class _ : Sink<TSource, IObservable<TSource>>
            {
                private readonly Queue<ISubject<TSource>> _queue = new();
                private readonly SingleAssignmentDisposable _m = new();
                private readonly RefCountDisposable _refCountDisposable;

                private readonly int _count;
                private readonly int _skip;

                public _(Count parent, IObserver<IObservable<TSource>> observer)
                    : base(observer)
                {
                    _refCountDisposable = new RefCountDisposable(_m);

                    _count = parent._count;
                    _skip = parent._skip;
                }

                private int _n;

                public override void Run(IObservable<TSource> source)
                {
                    var firstWindow = CreateWindow();
                    ForwardOnNext(firstWindow);

                    _m.Disposable = source.SubscribeSafe(this);

                    SetUpstream(_refCountDisposable);
                }

                private IObservable<TSource> CreateWindow()
                {
                    var s = new Subject<TSource>();
                    _queue.Enqueue(s);
                    return new WindowObservable<TSource>(s, _refCountDisposable);
                }

                public override void OnNext(TSource value)
                {
                    foreach (var s in _queue)
                    {
                        s.OnNext(value);
                    }

                    var c = _n - _count + 1;
                    if (c >= 0 && c % _skip == 0)
                    {
                        var s = _queue.Dequeue();
                        s.OnCompleted();
                    }

                    _n++;
                    if (_n % _skip == 0)
                    {
                        var newWindow = CreateWindow();
                        ForwardOnNext(newWindow);
                    }
                }

                public override void OnError(Exception error)
                {
                    while (_queue.Count > 0)
                    {
                        _queue.Dequeue().OnError(error);
                    }

                    ForwardOnError(error);
                }

                public override void OnCompleted()
                {
                    while (_queue.Count > 0)
                    {
                        _queue.Dequeue().OnCompleted();
                    }

                    ForwardOnCompleted();
                }
            }
        }

        internal sealed class TimeSliding : Producer<IObservable<TSource>, TimeSliding._>
        {
            private readonly IObservable<TSource> _source;
            private readonly TimeSpan _timeSpan;
            private readonly TimeSpan _timeShift;
            private readonly IScheduler _scheduler;

            public TimeSliding(IObservable<TSource> source, TimeSpan timeSpan, TimeSpan timeShift, IScheduler scheduler)
            {
                _source = source;
                _timeSpan = timeSpan;
                _timeShift = timeShift;
                _scheduler = scheduler;
            }

            protected override _ CreateSink(IObserver<IObservable<TSource>> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run(this);

            internal sealed class _ : Sink<TSource, IObservable<TSource>>
            {
                private readonly object _gate = new();
                private readonly Queue<ISubject<TSource>> _q = new();
                private readonly SerialDisposable _timerD = new();

                private readonly IScheduler _scheduler;
                private readonly TimeSpan _timeShift;

                public _(TimeSliding parent, IObserver<IObservable<TSource>> observer)
                    : base(observer)
                {
                    _scheduler = parent._scheduler;
                    _timeShift = parent._timeShift;
                }

                private RefCountDisposable? _refCountDisposable;
                private TimeSpan _totalTime;
                private TimeSpan _nextShift;
                private TimeSpan _nextSpan;

                public void Run(TimeSliding parent)
                {
                    _totalTime = TimeSpan.Zero;
                    _nextShift = parent._timeShift;
                    _nextSpan = parent._timeSpan;

                    var groupDisposable = new CompositeDisposable(2) { _timerD };
                    _refCountDisposable = new RefCountDisposable(groupDisposable);

                    CreateWindow();
                    CreateTimer();

                    groupDisposable.Add(parent._source.SubscribeSafe(this));

                    SetUpstream(_refCountDisposable);
                }

                private void CreateWindow()
                {
                    var s = new Subject<TSource>();
                    _q.Enqueue(s);
                    ForwardOnNext(new WindowObservable<TSource>(s, _refCountDisposable!)); // NB: _refCountDisposable gets assigned in Run.
                }

                private void CreateTimer()
                {
                    var m = new SingleAssignmentDisposable();
                    _timerD.Disposable = m;

                    var isSpan = false;
                    var isShift = false;
                    if (_nextSpan == _nextShift)
                    {
                        isSpan = true;
                        isShift = true;
                    }
                    else if (_nextSpan < _nextShift)
                    {
                        isSpan = true;
                    }
                    else
                    {
                        isShift = true;
                    }

                    var newTotalTime = isSpan ? _nextSpan : _nextShift;
                    var ts = newTotalTime - _totalTime;
                    _totalTime = newTotalTime;

                    if (isSpan)
                    {
                        _nextSpan += _timeShift;
                    }

                    if (isShift)
                    {
                        _nextShift += _timeShift;
                    }

                    m.Disposable = _scheduler.ScheduleAction((@this: this, isSpan, isShift), ts, static tuple => tuple.@this.Tick(tuple.isSpan, tuple.isShift));
                }

                private void Tick(bool isSpan, bool isShift)
                {
                    lock (_gate)
                    {
                        //
                        // BREAKING CHANGE v2 > v1.x - Making behavior of sending OnCompleted to the window
                        //                             before sending out a new window consistent across all
                        //                             overloads of Window and Buffer. Before v2, the two
                        //                             operations below were reversed.
                        //
                        if (isSpan)
                        {
                            var s = _q.Dequeue();
                            s.OnCompleted();
                        }

                        if (isShift)
                        {
                            CreateWindow();
                        }
                    }

                    CreateTimer();
                }

                public override void OnNext(TSource value)
                {
                    lock (_gate)
                    {
                        foreach (var s in _q)
                        {
                            s.OnNext(value);
                        }
                    }
                }

                public override void OnError(Exception error)
                {
                    lock (_gate)
                    {
                        foreach (var s in _q)
                        {
                            s.OnError(error);
                        }

                        ForwardOnError(error);
                    }
                }

                public override void OnCompleted()
                {
                    lock (_gate)
                    {
                        foreach (var s in _q)
                        {
                            s.OnCompleted();
                        }

                        ForwardOnCompleted();
                    }
                }
            }
        }

        internal sealed class TimeHopping : Producer<IObservable<TSource>, TimeHopping._>
        {
            private readonly IObservable<TSource> _source;
            private readonly TimeSpan _timeSpan;
            private readonly IScheduler _scheduler;

            public TimeHopping(IObservable<TSource> source, TimeSpan timeSpan, IScheduler scheduler)
            {
                _source = source;
                _timeSpan = timeSpan;
                _scheduler = scheduler;
            }

            protected override _ CreateSink(IObserver<IObservable<TSource>> observer) => new(observer);

            protected override void Run(_ sink) => sink.Run(this);

            internal sealed class _ : Sink<TSource, IObservable<TSource>>
            {
                private readonly object _gate = new();
                private Subject<TSource> _subject;

                public _(IObserver<IObservable<TSource>> observer)
                    : base(observer)
                {
                    _subject = new Subject<TSource>();
                }

                private RefCountDisposable? _refCountDisposable;

                public void Run(TimeHopping parent)
                {
                    var groupDisposable = new CompositeDisposable(2);
                    _refCountDisposable = new RefCountDisposable(groupDisposable);

                    NextWindow();

                    groupDisposable.Add(parent._scheduler.SchedulePeriodic(this, parent._timeSpan, static @this => @this.Tick()));
                    groupDisposable.Add(parent._source.SubscribeSafe(this));

                    SetUpstream(_refCountDisposable);
                }

                private void Tick()
                {
                    lock (_gate)
                    {
                        _subject.OnCompleted();

                        _subject = new Subject<TSource>();
                        NextWindow();
                    }
                }

                private void NextWindow()
                {
                    ForwardOnNext(new WindowObservable<TSource>(_subject, _refCountDisposable!)); // NB: _refCountDisposable gets assigned in Run.
                }

                public override void OnNext(TSource value)
                {
                    lock (_gate)
                    {
                        _subject.OnNext(value);
                    }
                }

                public override void OnError(Exception error)
                {
                    lock (_gate)
                    {
                        _subject.OnError(error);

                        ForwardOnError(error);
                    }
                }

                public override void OnCompleted()
                {
                    lock (_gate)
                    {
                        _subject.OnCompleted();

                        ForwardOnCompleted();
                    }
                }
            }
        }

        internal sealed class Ferry : Producer<IObservable<TSource>, Ferry._>
        {
            private readonly IObservable<TSource> _source;
            private readonly int _count;
            private readonly TimeSpan _timeSpan;
            private readonly IScheduler _scheduler;

            public Ferry(IObservable<TSource> source, TimeSpan timeSpan, int count, IScheduler scheduler)
            {
                _source = source;
                _timeSpan = timeSpan;
                _count = count;
                _scheduler = scheduler;
            }

            protected override _ CreateSink(IObserver<IObservable<TSource>> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run(_source);

            internal sealed class _ : Sink<TSource, IObservable<TSource>>
            {
                private readonly object _gate = new();
                private readonly SerialDisposable _timerD = new();

                private readonly int _count;
                private readonly TimeSpan _timeSpan;
                private readonly IScheduler _scheduler;

                private Subject<TSource> _s;

                public _(Ferry parent, IObserver<IObservable<TSource>> observer)
                    : base(observer)
                {
                    _count = parent._count;
                    _timeSpan = parent._timeSpan;
                    _scheduler = parent._scheduler;

                    _s = new Subject<TSource>();
                }

                private int _n;

                private RefCountDisposable? _refCountDisposable;

                public override void Run(IObservable<TSource> source)
                {
                    var groupDisposable = new CompositeDisposable(2) { _timerD };
                    _refCountDisposable = new RefCountDisposable(groupDisposable);

                    NextWindow();
                    CreateTimer(_s);

                    groupDisposable.Add(source.SubscribeSafe(this));

                    SetUpstream(_refCountDisposable);
                }

                private void CreateTimer(Subject<TSource> window)
                {
                    var m = new SingleAssignmentDisposable();
                    _timerD.Disposable = m;

                    m.Disposable = _scheduler.ScheduleAction((@this: this, window), _timeSpan, static tuple => tuple.@this.Tick(tuple.window));
                }

                private void NextWindow()
                {
                    ForwardOnNext(new WindowObservable<TSource>(_s, _refCountDisposable!)); // NB: _refCountDisposable gets assigned in Run.
                }

                private void Tick(Subject<TSource> window)
                {
                    Subject<TSource> newWindow;

                    lock (_gate)
                    {
                        if (window != _s)
                        {
                            return;
                        }

                        _n = 0;
                        newWindow = new Subject<TSource>();

                        _s.OnCompleted();
                        _s = newWindow;
                        NextWindow();
                    }

                    CreateTimer(newWindow);
                }

                public override void OnNext(TSource value)
                {
                    Subject<TSource>? newWindow = null;

                    lock (_gate)
                    {
                        _s.OnNext(value);

                        _n++;
                        if (_n == _count)
                        {
                            _n = 0;
                            newWindow = new Subject<TSource>();

                            _s.OnCompleted();
                            _s = newWindow;
                            NextWindow();
                        }
                    }

                    if (newWindow != null)
                    {
                        CreateTimer(newWindow);
                    }
                }

                public override void OnError(Exception error)
                {
                    lock (_gate)
                    {
                        _s.OnError(error);
                        ForwardOnError(error);
                    }
                }

                public override void OnCompleted()
                {
                    lock (_gate)
                    {
                        _s.OnCompleted();
                        ForwardOnCompleted();
                    }
                }
            }
        }
    }

    internal static class Window<TSource, TWindowClosing>
    {
        internal sealed class Selector : Producer<IObservable<TSource>, Selector._>
        {
            private readonly IObservable<TSource> _source;
            private readonly Func<IObservable<TWindowClosing>> _windowClosingSelector;

            public Selector(IObservable<TSource> source, Func<IObservable<TWindowClosing>> windowClosingSelector)
            {
                _source = source;
                _windowClosingSelector = windowClosingSelector;
            }

            protected override _ CreateSink(IObserver<IObservable<TSource>> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run(_source);

            internal sealed class _ : Sink<TSource, IObservable<TSource>>
            {
                private readonly object _gate = new();
                private readonly AsyncLock _windowGate = new();
                private readonly SerialDisposable _m = new();
                private readonly Func<IObservable<TWindowClosing>> _windowClosingSelector;

                private Subject<TSource> _window;

                public _(Selector parent, IObserver<IObservable<TSource>> observer)
                    : base(observer)
                {
                    _windowClosingSelector = parent._windowClosingSelector;

                    _window = new Subject<TSource>();
                }

                private RefCountDisposable? _refCountDisposable;

                public override void Run(IObservable<TSource> source)
                {
                    var groupDisposable = new CompositeDisposable(2) { _m };
                    _refCountDisposable = new RefCountDisposable(groupDisposable);

                    NextWindow();

                    groupDisposable.Add(source.SubscribeSafe(this));

                    _windowGate.Wait(this, static @this => @this.CreateWindowClose());

                    SetUpstream(_refCountDisposable);
                }

                private void NextWindow()
                {
                    var window = new WindowObservable<TSource>(_window, _refCountDisposable!); // NB: _refCountDisposable gets assigned in Run.
                    ForwardOnNext(window);
                }

                private void CreateWindowClose()
                {
                    IObservable<TWindowClosing> windowClose;
                    try
                    {
                        windowClose = _windowClosingSelector();
                    }
                    catch (Exception exception)
                    {
                        lock (_gate)
                        {
                            ForwardOnError(exception);
                        }
                        return;
                    }

                    var observer = new WindowClosingObserver(this);
                    _m.Disposable = observer;
                    observer.SetResource(windowClose.SubscribeSafe(observer));
                }

                private void CloseWindow(IDisposable closingSubscription)
                {
                    closingSubscription.Dispose();

                    lock (_gate)
                    {
                        _window.OnCompleted();
                        _window = new Subject<TSource>();

                        NextWindow();
                    }

                    _windowGate.Wait(this, static @this => @this.CreateWindowClose());
                }

                private sealed class WindowClosingObserver : SafeObserver<TWindowClosing>
                {
                    private readonly _ _parent;

                    public WindowClosingObserver(_ parent)
                    {
                        _parent = parent;
                    }

                    public override void OnNext(TWindowClosing value)
                    {
                        _parent.CloseWindow(this);
                    }

                    public override void OnError(Exception error)
                    {
                        _parent.OnError(error);
                    }

                    public override void OnCompleted()
                    {
                        _parent.CloseWindow(this);
                    }
                }

                public override void OnNext(TSource value)
                {
                    lock (_gate)
                    {
                        _window.OnNext(value);
                    }
                }

                public override void OnError(Exception error)
                {
                    lock (_gate)
                    {
                        _window.OnError(error);
                        ForwardOnError(error);
                    }
                }

                public override void OnCompleted()
                {
                    lock (_gate)
                    {
                        _window.OnCompleted();
                        ForwardOnCompleted();
                    }
                }
            }
        }

        internal sealed class Boundaries : Producer<IObservable<TSource>, Boundaries._>
        {
            private readonly IObservable<TSource> _source;
            private readonly IObservable<TWindowClosing> _windowBoundaries;

            public Boundaries(IObservable<TSource> source, IObservable<TWindowClosing> windowBoundaries)
            {
                _source = source;
                _windowBoundaries = windowBoundaries;
            }

            protected override _ CreateSink(IObserver<IObservable<TSource>> observer) => new(observer);

            protected override void Run(_ sink) => sink.Run(this);

            internal sealed class _ : Sink<TSource, IObservable<TSource>>
            {
                private readonly object _gate = new();

                private Subject<TSource> _window;

                public _(IObserver<IObservable<TSource>> observer)
                    : base(observer)
                {
                    _window = new Subject<TSource>();
                }

                private RefCountDisposable? _refCountDisposable;

                public void Run(Boundaries parent)
                {
                    var d = new CompositeDisposable(2);
                    _refCountDisposable = new RefCountDisposable(d);

                    NextWindow();

                    d.Add(parent._source.SubscribeSafe(this));
                    d.Add(parent._windowBoundaries.SubscribeSafe(new WindowClosingObserver(this)));

                    SetUpstream(_refCountDisposable);
                }

                private void NextWindow()
                {
                    var window = new WindowObservable<TSource>(_window, _refCountDisposable!); // NB: _refCountDisposable gets assigned in Run.
                    ForwardOnNext(window);
                }

                private sealed class WindowClosingObserver : IObserver<TWindowClosing>
                {
                    private readonly _ _parent;

                    public WindowClosingObserver(_ parent)
                    {
                        _parent = parent;
                    }

                    public void OnNext(TWindowClosing value)
                    {
                        lock (_parent._gate)
                        {
                            _parent._window.OnCompleted();
                            _parent._window = new Subject<TSource>();

                            _parent.NextWindow();
                        }
                    }

                    public void OnError(Exception error)
                    {
                        _parent.OnError(error);
                    }

                    public void OnCompleted()
                    {
                        _parent.OnCompleted();
                    }
                }

                public override void OnNext(TSource value)
                {
                    lock (_gate)
                    {
                        _window.OnNext(value);
                    }
                }

                public override void OnError(Exception error)
                {
                    lock (_gate)
                    {
                        _window.OnError(error);
                        ForwardOnError(error);
                    }
                }

                public override void OnCompleted()
                {
                    lock (_gate)
                    {
                        _window.OnCompleted();
                        ForwardOnCompleted();
                    }
                }
            }
        }
    }

    internal sealed class WindowObservable<TSource> : AddRef<TSource>
    {
        public WindowObservable(IObservable<TSource> source, RefCountDisposable refCount)
            : base(source, refCount)
        {
        }
    }
}
