package rsc.publisher;

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.function.Supplier;

import org.reactivestreams.Processor;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import rsc.documentation.BackpressureMode;
import rsc.documentation.BackpressureSupport;
import rsc.documentation.FusionMode;
import rsc.documentation.FusionSupport;
import rsc.flow.Disposable;
import rsc.flow.MultiProducer;
import rsc.flow.Producer;
import rsc.flow.Receiver;
import rsc.processor.UnicastProcessor;
import rsc.flow.Trackable;
import rsc.subscriber.SubscriptionHelper;
import rsc.util.BackpressureHelper;
import rsc.util.UnsignalledExceptions;

/**
 * Splits the source sequence into possibly overlapping publishers.
 * 
 * @param <T> the value type
 */
@BackpressureSupport(input = BackpressureMode.BOUNDED, innerOutput = BackpressureMode.BOUNDED, output = BackpressureMode.BOUNDED)
@FusionSupport(innerOutput = { FusionMode.ASYNC })
public final class PublisherWindow<T> extends PublisherSource<T, Px<T>> {

    final int size;
    
    final int skip;
    
    final Supplier<? extends Queue<T>> processorQueueSupplier;

    final Supplier<? extends Queue<UnicastProcessor<T>>> overflowQueueSupplier;

    public PublisherWindow(Publisher<? extends T> source, int size, 
            Supplier<? extends Queue<T>> processorQueueSupplier) {
        super(source);
        if (size <= 0) {
            throw new IllegalArgumentException("size > 0 required but it was " + size);
        }
        this.size = size;
        this.skip = size;
        this.processorQueueSupplier = Objects.requireNonNull(processorQueueSupplier, "processorQueueSupplier");
        this.overflowQueueSupplier = null; // won't be needed here
    }

    
    public PublisherWindow(Publisher<? extends T> source, int size, int skip, 
            Supplier<? extends Queue<T>> processorQueueSupplier,
            Supplier<? extends Queue<UnicastProcessor<T>>> overflowQueueSupplier) {
        super(source);
        if (size <= 0) {
            throw new IllegalArgumentException("size > 0 required but it was " + size);
        }
        if (skip <= 0) {
            throw new IllegalArgumentException("skip > 0 required but it was " + skip);
        }
        this.size = size;
        this.skip = skip;
        this.processorQueueSupplier = Objects.requireNonNull(processorQueueSupplier, "processorQueueSupplier");
        this.overflowQueueSupplier = Objects.requireNonNull(overflowQueueSupplier, "overflowQueueSupplier");
    }
    
    @Override
    public void subscribe(Subscriber<? super Px<T>> s) {
        if (skip == size) {
            source.subscribe(new WindowExactSubscriber<>(s, size, processorQueueSupplier));
        } else
        if (skip > size) {
            source.subscribe(new WindowSkipSubscriber<>(s, size, skip, processorQueueSupplier));
        } else {
            Queue<UnicastProcessor<T>> overflowQueue;
            
            try {
                overflowQueue = overflowQueueSupplier.get();
            } catch (Throwable e) {
                SubscriptionHelper.error(s, e);
                return;
            }
            
            if (overflowQueue == null) {
                SubscriptionHelper.error(s, new NullPointerException("The overflowQueueSupplier returned a null queue"));
                return;
            }
            
            source.subscribe(new WindowOverlapSubscriber<>(s, size, skip, processorQueueSupplier, overflowQueue));
        }
    }

    static final class WindowExactSubscriber<T> implements Subscriber<T>, Subscription,
                                                           Disposable, Producer, Receiver,
                                                           MultiProducer, Trackable {
        
        final Subscriber<? super Px<T>> actual;

        final Supplier<? extends Queue<T>> processorQueueSupplier;
        
        final int size;

        volatile int wip;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowExactSubscriber> WIP =
                AtomicIntegerFieldUpdater.newUpdater(WindowExactSubscriber.class, "wip");

        volatile int once;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowExactSubscriber> ONCE =
                AtomicIntegerFieldUpdater.newUpdater(WindowExactSubscriber.class, "once");

        int index;
        
        Subscription s;
        
        UnicastProcessor<T> window;
        
        boolean done;
        
        public WindowExactSubscriber(Subscriber<? super Px<T>> actual, int size,
                Supplier<? extends Queue<T>> processorQueueSupplier) {
            this.actual = actual;
            this.size = size;
            this.processorQueueSupplier = processorQueueSupplier;
            this.wip = 1;
        }
        
        @Override
        public void onSubscribe(Subscription s) {
            if (SubscriptionHelper.validate(this.s, s)) {
                this.s = s;
                actual.onSubscribe(this);
            }
        }
        
        @Override
        public void onNext(T t) {
            if (done) {
                UnsignalledExceptions.onNextDropped(t);
                return;
            }
            
            int i = index;
            
            UnicastProcessor<T> w = window;
            if (i == 0) {
                WIP.getAndIncrement(this);
                
                
                Queue<T> q;
                
                try {
                    q = processorQueueSupplier.get();
                } catch (Throwable ex) {
                    done = true;
                    cancel();
                    
                    actual.onError(ex);
                    return;
                }
                
                if (q == null) {
                    done = true;
                    cancel();
                    
                    actual.onError(new NullPointerException("The processorQueueSupplier returned a null queue"));
                    return;
                }
                
                w = new UnicastProcessor<>(q, this);
                window = w;
                
                actual.onNext(w);
            }
            
            i++;
            
            w.onNext(t);
            
            if (i == size) {
                index = 0;
                window = null;
                w.onComplete();
            } else {
                index = i;
            }
        }
        
        @Override
        public void onError(Throwable t) {
            if (done) {
                UnsignalledExceptions.onErrorDropped(t);
                return;
            }
            Processor<T, T> w = window;
            if (w != null) {
                window = null;
                w.onError(t);
            }
            
            actual.onError(t);
        }
        
        @Override
        public void onComplete() {
            if (done) {
                return;
            }

            Processor<T, T> w = window;
            if (w != null) {
                window = null;
                w.onComplete();
            }
            
            actual.onComplete();
        }
        
        @Override
        public void request(long n) {
            if (SubscriptionHelper.validate(n)) {
                long u = BackpressureHelper.multiplyCap(size, n);
                s.request(u);
            }
        }
        
        @Override
        public void cancel() {
            if (ONCE.compareAndSet(this, 0, 1)) {
                dispose();
            }
        }

        @Override
        public void dispose() {
            if (WIP.decrementAndGet(this) == 0) {
                s.cancel();
            }
        }

        @Override
        public Object downstream() {
            return actual;
        }

        @Override
        public boolean isStarted() {
            return s != null && !done;
        }

        @Override
        public boolean isTerminated() {
            return done;
        }

        @Override
        public Object upstream() {
            return s;
        }

        @Override
        public Iterator<?> downstreams() {
            return Arrays.asList(window).iterator();
        }

        @Override
        public long downstreamCount() {
            return window != null ? 1L : 0L;
        }

        @Override
        public long expectedFromUpstream() {
            return size - index;
        }

        @Override
        public long limit() {
            return size;
        }
    }
    
    static final class WindowSkipSubscriber<T> implements Subscriber<T>, Subscription,
                                                          Disposable, Receiver,
                                                          MultiProducer, Producer,
                                                          Trackable {
        
        final Subscriber<? super Px<T>> actual;

        final Supplier<? extends Queue<T>> processorQueueSupplier;
        
        final int size;
        
        final int skip;

        volatile int wip;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowSkipSubscriber> WIP =
                AtomicIntegerFieldUpdater.newUpdater(WindowSkipSubscriber.class, "wip");

        volatile int once;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowSkipSubscriber> ONCE =
                AtomicIntegerFieldUpdater.newUpdater(WindowSkipSubscriber.class, "once");

        volatile int firstRequest;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowSkipSubscriber> FIRST_REQUEST =
                AtomicIntegerFieldUpdater.newUpdater(WindowSkipSubscriber.class, "firstRequest");

        int index;
        
        Subscription s;
        
        UnicastProcessor<T> window;
        
        boolean done;
        
        public WindowSkipSubscriber(Subscriber<? super Px<T>> actual, int size, int skip,
                Supplier<? extends Queue<T>> processorQueueSupplier) {
            this.actual = actual;
            this.size = size;
            this.skip = skip;
            this.processorQueueSupplier = processorQueueSupplier;
            this.wip = 1;
        }
        
        @Override
        public void onSubscribe(Subscription s) {
            if (SubscriptionHelper.validate(this.s, s)) {
                this.s = s;
                actual.onSubscribe(this);
            }
        }
        
        @Override
        public void onNext(T t) {
            if (done) {
                UnsignalledExceptions.onNextDropped(t);
                return;
            }
            
            int i = index;
            
            UnicastProcessor<T> w = window;
            if (i == 0) {
                WIP.getAndIncrement(this);
                
                
                Queue<T> q;
                
                try {
                    q = processorQueueSupplier.get();
                } catch (Throwable ex) {
                    done = true;
                    cancel();
                    
                    actual.onError(ex);
                    return;
                }
                
                if (q == null) {
                    done = true;
                    cancel();
                    
                    actual.onError(new NullPointerException("The processorQueueSupplier returned a null queue"));
                    return;
                }
                
                w = new UnicastProcessor<>(q, this);
                window = w;
                
                actual.onNext(w);
            }
            
            i++;
            
            if (w != null) {
                w.onNext(t);
            }
            
            if (i == size) {
                window = null;
                w.onComplete();
            }
            
            if (i == skip) {
                index = 0;
            } else {
                index = i;
            }
        }
        
        @Override
        public void onError(Throwable t) {
            if (done) {
                UnsignalledExceptions.onErrorDropped(t);
                return;
            }
            Processor<T, T> w = window;
            if (w != null) {
                window = null;
                w.onError(t);
            }
            
            actual.onError(t);
        }
        
        @Override
        public void onComplete() {
            if (done) {
                return;
            }

            Processor<T, T> w = window;
            if (w != null) {
                window = null;
                w.onComplete();
            }
            
            actual.onComplete();
        }
        
        @Override
        public void request(long n) {
            if (SubscriptionHelper.validate(n)) {
                if (firstRequest == 0 && FIRST_REQUEST.compareAndSet(this, 0, 1)) {
                    long u = BackpressureHelper.multiplyCap(size, n);
                    long v = BackpressureHelper.multiplyCap(skip - size, n - 1);
                    long w = BackpressureHelper.addCap(u, v);
                    s.request(w);
                } else {
                    long u = BackpressureHelper.multiplyCap(skip, n);
                    s.request(u);
                }
            }
        }
        
        @Override
        public void cancel() {
            if (ONCE.compareAndSet(this, 0, 1)) {
                dispose();
            }
        }

        @Override
        public void dispose() {
            if (WIP.decrementAndGet(this) == 0) {
                s.cancel();
            }
        }

        @Override
        public Object downstream() {
            return actual;
        }

        @Override
        public boolean isStarted() {
            return s != null && !done;
        }

        @Override
        public boolean isTerminated() {
            return done;
        }

        @Override
        public Object upstream() {
            return s;
        }

        @Override
        public Iterator<?> downstreams() {
            return Arrays.asList(window).iterator();
        }

        @Override
        public long downstreamCount() {
            return window != null ? 1L : 0L;
        }

        @Override
        public long getCapacity() {
            return size;
        }

        @Override
        public long getPending() {
            return skip + size - index;
        }
    }

    static final class WindowOverlapSubscriber<T> implements Subscriber<T>, Subscription,
                                                             Disposable,
                                                             Producer, MultiProducer, Receiver,
                                                             Trackable {
        
        final Subscriber<? super Px<T>> actual;

        final Supplier<? extends Queue<T>> processorQueueSupplier;

        final Queue<UnicastProcessor<T>> queue;
        
        final int size;
        
        final int skip;

        final ArrayDeque<UnicastProcessor<T>> windows;

        volatile int wip;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowOverlapSubscriber> WIP =
                AtomicIntegerFieldUpdater.newUpdater(WindowOverlapSubscriber.class, "wip");

        volatile int once;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowOverlapSubscriber> ONCE =
                AtomicIntegerFieldUpdater.newUpdater(WindowOverlapSubscriber.class, "once");

        volatile int firstRequest;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowOverlapSubscriber> FIRST_REQUEST =
                AtomicIntegerFieldUpdater.newUpdater(WindowOverlapSubscriber.class, "firstRequest");

        volatile long requested;
        @SuppressWarnings("rawtypes")
        static final AtomicLongFieldUpdater<WindowOverlapSubscriber> REQUESTED =
                AtomicLongFieldUpdater.newUpdater(WindowOverlapSubscriber.class, "requested");

        volatile int dw;
        @SuppressWarnings("rawtypes")
        static final AtomicIntegerFieldUpdater<WindowOverlapSubscriber> DW =
                AtomicIntegerFieldUpdater.newUpdater(WindowOverlapSubscriber.class, "dw");

        int index;
        
        int produced;
        
        Subscription s;
        
        volatile boolean done;
        Throwable error;
        
        volatile boolean cancelled;
        
        public WindowOverlapSubscriber(Subscriber<? super Px<T>> actual, int size, int skip,
                Supplier<? extends Queue<T>> processorQueueSupplier,
                Queue<UnicastProcessor<T>> overflowQueue) {
            this.actual = actual;
            this.size = size;
            this.skip = skip;
            this.processorQueueSupplier = processorQueueSupplier;
            this.wip = 1;
            this.queue = overflowQueue;
            this.windows = new ArrayDeque<>();
        }
        
        @Override
        public void onSubscribe(Subscription s) {
            if (SubscriptionHelper.validate(this.s, s)) {
                this.s = s;
                actual.onSubscribe(this);
            }
        }
        
        @Override
        public void onNext(T t) {
            if (done) {
                UnsignalledExceptions.onNextDropped(t);
                return;
            }
            
            int i = index;
            
            if (i == 0) {
                if (!cancelled) {
                    WIP.getAndIncrement(this);
                    
                    
                    Queue<T> q;
                    
                    try {
                        q = processorQueueSupplier.get();
                    } catch (Throwable ex) {
                        done = true;
                        cancel();
                        
                        actual.onError(ex);
                        return;
                    }
                    
                    if (q == null) {
                        done = true;
                        cancel();
                        
                        actual.onError(new NullPointerException("The processorQueueSupplier returned a null queue"));
                        return;
                    }
                    
                    UnicastProcessor<T> w = new UnicastProcessor<>(q, this);
                    
                    windows.offer(w);
                    
                    queue.offer(w);
                    drain();
                }
            }
            
            i++;

            for (Processor<T, T> w : windows) {
                w.onNext(t);
            }
            
            int p = produced + 1;
            if (p == size) {
                produced = p - skip;
                
                Processor<T, T> w = windows.poll();
                if (w != null) {
                    w.onComplete();
                }
            } else {
                produced = p;
            }
            
            if (i == skip) {
                index = 0;
            } else {
                index = i;
            }
        }
        
        @Override
        public void onError(Throwable t) {
            if (done) {
                UnsignalledExceptions.onErrorDropped(t);
                return;
            }

            for (Processor<T, T> w : windows) {
                w.onError(t);
            }
            windows.clear();
            
            error = t;
            done = true;
            drain();
        }
        
        @Override
        public void onComplete() {
            if (done) {
                return;
            }

            for (Processor<T, T> w : windows) {
                w.onComplete();
            }
            windows.clear();
            
            done = true;
            drain();
        }
        
        void drain() {
            if (DW.getAndIncrement(this) != 0) {
                return;
            }
            
            final Subscriber<? super Px<T>> a = actual;
            final Queue<UnicastProcessor<T>> q = queue;
            int missed = 1;
            
            for (;;) {
                
                long r = requested;
                long e = 0;
                
                while (e != r) {
                    boolean d = done;
                    
                    UnicastProcessor<T> t = q.poll();
                    
                    boolean empty = t == null;
                    
                    if (checkTerminated(d, empty, a, q)) {
                        return;
                    }
                    
                    if (empty) {
                        break;
                    }
                    
                    a.onNext(t);
                    
                    e++;
                }
                
                if (e == r) {
                    if (checkTerminated(done, q.isEmpty(), a, q)) {
                        return;
                    }
                }
                
                if (e != 0L && r != Long.MAX_VALUE) {
                    REQUESTED.addAndGet(this, -e);
                }
                
                missed = DW.addAndGet(this, -missed);
                if (missed == 0) {
                    break;
                }
            }
        }
        
        boolean checkTerminated(boolean d, boolean empty, Subscriber<?> a, Queue<?> q) {
            if (cancelled) {
                q.clear();
                return true;
            }
            
            if (d) {
                Throwable e = error;
                
                if (e != null) {
                    q.clear();
                    a.onError(e);
                    return true;
                } else
                if (empty) {
                    a.onComplete();
                    return true;
                }
            }
            
            return false;
        }
        
        @Override
        public void request(long n) {
            if (SubscriptionHelper.validate(n)) {
                
                BackpressureHelper.getAndAddCap(REQUESTED, this, n);
                
                if (firstRequest == 0 && FIRST_REQUEST.compareAndSet(this, 0, 1)) {
                    long u = BackpressureHelper.multiplyCap(skip, n - 1);
                    long v = BackpressureHelper.addCap(size, u);
                    s.request(v);
                } else {
                    long u = BackpressureHelper.multiplyCap(skip, n);
                    s.request(u);
                }
                
                drain();
            }
        }
        
        @Override
        public void cancel() {
            cancelled = true;
            if (ONCE.compareAndSet(this, 0, 1)) {
                dispose();
            }
        }

        @Override
        public void dispose() {
            if (WIP.decrementAndGet(this) == 0) {
                s.cancel();
            }
        }

        @Override
        public Object downstream() {
            return actual;
        }

        @Override
        public boolean isCancelled() {
            return cancelled;
        }

        @Override
        public boolean isStarted() {
            return s != null && !done && !cancelled;
        }

        @Override
        public boolean isTerminated() {
            return done;
        }

        @Override
        public Object upstream() {
            return s;
        }

        @Override
        public Throwable getError() {
            return error;
        }

        @Override
        public long expectedFromUpstream() {
            return (size + skip) - produced;
        }

        @Override
        public long limit() {
            return skip;
        }

        @Override
        public Iterator<?> downstreams() {
            return Arrays.asList(windows.toArray()).iterator();
        }

        @Override
        public long downstreamCount() {
            return windows.size();
        }

        @Override
        public long getCapacity() {
            return size;
        }

        @Override
        public long getPending() {
            return size - produced ;
        }
    }

}