package com.github.davidmoten.rx2.internal.flowable;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import com.github.davidmoten.guavamini.Preconditions;

import io.reactivex.Flowable;
import io.reactivex.FlowableSubscriber;
import io.reactivex.internal.fuseable.SimplePlainQueue;
import io.reactivex.internal.queue.SpscLinkedArrayQueue;
import io.reactivex.internal.subscriptions.SubscriptionHelper;
import io.reactivex.internal.util.BackpressureHelper;

public final class FlowableMinRequest<T> extends Flowable<T> {

    private final Flowable<T> source;
    private final int[] minRequest;

    public FlowableMinRequest(Flowable<T> source, int[] minRequests) {
        Preconditions.checkArgument(minRequests.length > 0, "minRequests length must be > 0");
        for (int i = 0; i < minRequests.length; i++) {
            Preconditions.checkArgument(minRequests[i] > 0, "each item in minRequests must be > 0");
        }
        this.source = source;
        this.minRequest = minRequests;
    }

    @Override
    protected void subscribeActual(Subscriber<? super T> child) {
        source.subscribe(new MinRequestSubscriber<T>(minRequest, child));
    }

    @SuppressWarnings("serial")
    private static final class MinRequestSubscriber<T> extends AtomicInteger implements FlowableSubscriber<T>, Subscription {

        private final int[] minRequests;
        private int requestNum;
        private final Subscriber<? super T> child;
        private final AtomicLong requested = new AtomicLong();
        private final SimplePlainQueue<T> queue = new SpscLinkedArrayQueue<T>(16);

        private Subscription parent;
        private volatile boolean done;
        private Throwable error;
        private volatile boolean cancelled;
        private long count;

        MinRequestSubscriber(int[] minRequests, Subscriber<? super T> child) {
            this.minRequests = minRequests;
            this.child = child;
        }

        @Override
        public void onSubscribe(Subscription parent) {
            if (SubscriptionHelper.validate(this.parent, parent)) {
                this.parent = parent;
                child.onSubscribe(this);
            }
        }

        @Override
        public void request(long n) {
            if (SubscriptionHelper.validate(n)) {
                BackpressureHelper.add(requested, n);
                drain();
            }
        }

        @Override
        public void cancel() {
            cancelled = true;
            parent.cancel();
        }

        @Override
        public void onNext(T t) {
            queue.offer(t);
            drain();
        }

        @Override
        public void onError(Throwable e) {
            error = e;
            done = true;
            drain();
        }

        @Override
        public void onComplete() {
            done = true;
            drain();
        }

        private void drain() {
            if (getAndIncrement() == 0) {
                int missed = 1;
                while (true) {
                    long r = requested.get();
                    long e = 0;
                    boolean d = done;
                    while (e != r) {
                        if (cancelled) {
                            queue.clear();
                            return;
                        }
                        T t = queue.poll();
                        if (t == null) {
                            if (d) {
                                terminate();
                                return;
                            } else {
                                break;
                            }
                        } else {
                            child.onNext(t);
                            e++;
                            if (count != Long.MAX_VALUE) {
                                count--;
                            }
                        }
                        d = done;
                    }
                    if (d && queue.isEmpty()) {
                        terminate();
                        return;
                    }
                    if (e != 0 && r != Long.MAX_VALUE) {
                        r = requested.addAndGet(-e);
                    }
                    if (r != 0 && count == 0) {
                        // requests from parent have arrived so request some
                        // more
                        int min = minRequests[requestNum];
                        if (requestNum != minRequests.length - 1) {
                            requestNum++;
                        }
                        count = Math.max(r, min);
                        parent.request(count);
                    }
                    missed = addAndGet(-missed);
                    if (missed == 0) {
                        return;
                    }
                }
            }
        }

        private void terminate() {
            parent.cancel();
            Throwable err = error;
            if (err != null) {
                error = null;
                child.onError(err);
            } else {
                child.onComplete();
            }
        }
    }

}