package com.github.davidmoten.rx.internal.operators;

import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import com.github.davidmoten.rx.buffertofile.DataSerializer;
import com.github.davidmoten.rx.buffertofile.Options;
import com.github.davidmoten.util.Preconditions;

import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observable.Operator;
import rx.Producer;
import rx.Scheduler;
import rx.Scheduler.Worker;
import rx.Subscriber;
import rx.exceptions.Exceptions;
import rx.functions.Action0;
import rx.functions.Func0;
import rx.internal.operators.BackpressureUtils;
import rx.observers.Subscribers;

public final class OperatorBufferToFile<T> implements Operator<T, T> {

    private final DataSerializer<T> dataSerializer;
    private final Scheduler scheduler;
    private final Options options;

    public OperatorBufferToFile(DataSerializer<T> dataSerializer, Scheduler scheduler,
            Options options) {
        Preconditions.checkNotNull(dataSerializer);
        Preconditions.checkNotNull(scheduler);
        Preconditions.checkNotNull(options);
        this.scheduler = scheduler;
        this.dataSerializer = dataSerializer;
        this.options = options;
    }

    @Override
    public Subscriber<? super T> call(Subscriber<? super T> child) {

        // create the file based queue
        final QueueWithSubscription<T> queue = createFileBasedQueue(dataSerializer, options);

        // hold a reference to the queueProducer which will be set on
        // subscription to `source`
        final AtomicReference<QueueProducer<T>> queueProducer = new AtomicReference<QueueProducer<T>>();

        // emissions will propagate to downstream via this worker
        final Worker worker = scheduler.createWorker();

        // set up the observable to read from the file based queue
        Observable<T> source = Observable
                .create(new OnSubscribeFromQueue<T>(queueProducer, queue, worker, options));

        // create the parent subscriber
        Subscriber<T> parentSubscriber = new ParentSubscriber<T>(queueProducer);

        // link unsubscription
        child.add(parentSubscriber);

        // close and delete file based queues in RollingQueue on unsubscription
        child.add(queue);

        // ensure onStart not called twice
        Subscriber<T> wrappedChild = Subscribers.wrap(child);

        // ensure worker gets unsubscribed (last)
        child.add(worker);

        // subscribe to queue
        source.unsafeSubscribe(wrappedChild);

        return parentSubscriber;
    }

    private static final boolean MEMORY_MAPPED = "true"
            .equals(System.getProperty("memory.mappped"));

    private static <T> QueueWithSubscription<T> createFileBasedQueue(
            final DataSerializer<T> dataSerializer, final Options options) {
        if (MEMORY_MAPPED) {
            // warning: still in development!
            final int size;
            if (options.rolloverSizeBytes() > Integer.MAX_VALUE) {
                size = 20 * 1024 * 1024;// 20MB
            } else {
                size = (int) options.rolloverSizeBytes();
            }
            return new FileBasedSPSCQueueMemoryMapped<T>(options.fileFactory(), size,
                    dataSerializer);
        }
        if (options.rolloverEvery() == Long.MAX_VALUE
                && options.rolloverSizeBytes() == Long.MAX_VALUE) {
            // skip the Rollover version
            return new QueueWithResourcesNonBlockingUnsubscribe<T>(new FileBasedSPSCQueue<T>(
                    options.bufferSizeBytes(), options.fileFactory().call(), dataSerializer));
        } else {
            final Func0<QueueWithResources<T>> queueFactory = new Func0<QueueWithResources<T>>() {
                @Override
                public QueueWithResources<T> call() {
                    // create the file to be used for queue storage (and whose
                    // file name will determine the names of other files used
                    // for storage if multiple are required per queue)
                    File file = options.fileFactory().call();

                    return new FileBasedSPSCQueue<T>(options.bufferSizeBytes(), file,
                            dataSerializer);
                }
            };
            // the wrapping class ensures that unsubscribe happens in the same
            // thread as the offer or poll which avoids the unsubscribe action
            // not getting a time-slice so that the open file limit is not
            // exceeded (new files are opened in the offer() call).
            return new QueueWithResourcesNonBlockingUnsubscribe<T>(new RollingSPSCQueue<T>(
                    queueFactory, options.rolloverSizeBytes(), options.rolloverEvery()));
        }
    }

    private static final class OnSubscribeFromQueue<T> implements OnSubscribe<T> {

        private final AtomicReference<QueueProducer<T>> queueProducer;
        private final QueueWithSubscription<T> queue;
        private final Worker worker;
        private final Options options;

        OnSubscribeFromQueue(AtomicReference<QueueProducer<T>> queueProducer,
                QueueWithSubscription<T> queue, Worker worker, Options options) {
            this.queueProducer = queueProducer;
            this.queue = queue;
            this.worker = worker;
            this.options = options;
        }

        @Override
        public void call(Subscriber<? super T> child) {
            QueueProducer<T> qp = new QueueProducer<T>(queue, child, worker, options.delayError());
            queueProducer.set(qp);
            child.setProducer(qp);
        }
    }

    private static final class ParentSubscriber<T> extends Subscriber<T> {

        private final AtomicReference<QueueProducer<T>> queueProducer;

        ParentSubscriber(AtomicReference<QueueProducer<T>> queueProducer) {
            this.queueProducer = queueProducer;
        }

        @Override
        public void onStart() {
            request(Long.MAX_VALUE);
        }

        @Override
        public void onCompleted() {
            queueProducer.get().onCompleted();
        }

        @Override
        public void onError(Throwable e) {
            queueProducer.get().onError(e);
        }

        @Override
        public void onNext(T t) {
            queueProducer.get().onNext(t);
        }

    }

    private static final class QueueProducer<T> extends AtomicLong implements Producer, Action0 {

        // inherits from AtomicLong to represent the oustanding requests count

        private static final long serialVersionUID = 2521533710633950102L;

        private final QueueWithSubscription<T> queue;
        private final AtomicInteger drainRequested = new AtomicInteger(0);
        private final Subscriber<? super T> child;
        private final Worker worker;
        private final boolean delayError;
        private volatile boolean done;

        // Is set just before the volatile `done` is set and read just after
        // `done` is read. Thus doesn't need to be volatile.
        private Throwable error = null;

        QueueProducer(QueueWithSubscription<T> queue, Subscriber<? super T> child, Worker worker,
                boolean delayError) {
            super();
            this.queue = queue;
            this.child = child;
            this.worker = worker;
            this.delayError = delayError;
            this.done = false;
        }

        void onNext(T t) {
            try {
                if (!queue.offer(t)) {
                    onError(new RuntimeException(
                            "could not place item on queue (queue.offer(item) returned false), item= "
                                    + t));
                    return;
                } else {
                    drain();
                }
            } catch (Throwable e) {
                Exceptions.throwIfFatal(e);
                onError(e);
            }
        }

        void onError(Throwable e) {
            // must assign error before assign done = true to avoid race
            // condition in finished() and also so appropriate memory barrier in
            // place given error is non-volatile
            error = e;
            done = true;
            drain();
        }

        void onCompleted() {
            done = true;
            drain();
        }

        @Override
        public void request(long n) {
            if (n > 0) {
                BackpressureUtils.getAndAddRequest(this, n);
                drain();
            }
        }

        private void drain() {
            // only schedule a drain if current drain has finished
            // otherwise the drainRequested counter will be incremented
            // and the drain loop will ensure that another drain cycle occurs if
            // required
            if (!child.isUnsubscribed() && drainRequested.getAndIncrement() == 0) {
                worker.schedule(this);
            }
        }

        // this method executed from drain() only
        @Override
        public void call() {
            // catch exceptions related to file based queue in drainNow()
            try {
                drainNow();
            } catch (Throwable e) {
                child.onError(e);
            }
        }

        private void drainNow() {
            if (child.isUnsubscribed()) {
                // leave drainRequested > 0 to prevent more
                // scheduling of drains
                return;
            }
            // get the number of unsatisfied requests
            long requests = get();

            for (;;) {
                // reset drainRequested counter
                drainRequested.set(1);
                long emitted = 0;
                while (emitted < requests) {
                    if (child.isUnsubscribed()) {
                        // leave drainRequested > 0 to prevent more
                        // scheduling of drains
                        return;
                    }
                    T item = queue.poll();
                    if (item == null) {
                        // queue is empty
                        if (finished()) {
                            return;
                        } else {
                            // another drain was requested so go
                            // round again but break out of this
                            // while loop to the outer loop so we
                            // can update requests and reset drainRequested
                            break;
                        }
                    } else {
                        // there was an item on the queue
                        if (NullSentinel.isNullSentinel(item)) {
                            child.onNext(null);
                        } else {
                            child.onNext(item);
                        }
                        emitted++;
                    }
                }
                // update requests with emitted value and any new requests
                requests = BackpressureUtils.produced(this, emitted);
                if (child.isUnsubscribed() || (requests == 0L && finished())) {
                    return;
                }
            }
        }

        private boolean finished() {
        	//cannot pass queueKnownToBeEmpty flag to this method because 
        	//to avoid a race condition we must do an actual check on queue.isEmpty()
        	//after finding done is true
            if (done) {
                Throwable t = error;
                if (queue.isEmpty()) {
                    // first close the queue (which in this case though
                    // empty also disposes of its resources)
                    queue.unsubscribe();

                    if (t != null) {
                        child.onError(t);
                    } else {
                        child.onCompleted();
                    }
                    // leave drainRequested > 0 so that further drain
                    // requests are ignored
                    return true;
                } else if (t != null && !delayError) {
                    // queue is not empty but we are going to shortcut
                    // that because delayError is false

                    // first close the queue (which in this case also
                    // disposes of its resources)
                    queue.unsubscribe();

                    // now report the error
                    child.onError(t);

                    // leave drainRequested > 0 so that further drain
                    // requests are ignored
                    return true;
                } else {
                    // otherwise we need to wait for all items waiting
                    // on the queue to be requested and delivered
                    // (delayError=true)
                    return drainRequested.compareAndSet(1, 0);
                }
            } else {
                return drainRequested.compareAndSet(1, 0);
            }
        }
    }
}