/*
 * Copyright 2016 The Simple File Server Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.sfs.filesystem.volume;

import com.google.common.collect.Lists;
import io.vertx.core.Context;
import org.sfs.SfsVertx;
import org.sfs.filesystem.ChecksummedPositional;
import rx.Observable;
import rx.Producer;
import rx.Subscriber;

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

import static com.google.common.base.Preconditions.checkState;
import static java.lang.Long.MAX_VALUE;
import static org.sfs.block.RangeLock.lockedObservable;
import static org.sfs.filesystem.volume.IndexBlockReader.LockType.NONE;
import static org.sfs.filesystem.volume.IndexBlockReader.LockType.READ;
import static org.sfs.filesystem.volume.IndexBlockReader.LockType.WRITE;
import static org.sfs.protobuf.XVolume.XIndexBlock;
import static rx.Observable.create;
import static rx.internal.operators.BackpressureUtils.getAndAddRequest;

public class IndexBlockReader {

    public enum LockType {
        READ,
        WRITE,
        NONE
    }

    private final SfsVertx vertx;
    private long position = 0;
    private final long lastBlockPosition;
    private final int batchSize;
    private final int readSize;
    private IndexFile blockFile;
    private LockType lockType = NONE;
    private long lockWaitTimeout = -1;
    private boolean hasNext = true;

    public IndexBlockReader(SfsVertx vertx, IndexFile blockFile, int blockSize, int batchSize, long lastBlockPosition) {
        this.vertx = vertx;
        this.blockFile = blockFile;
        this.batchSize = batchSize;
        this.readSize = blockSize * batchSize;
        this.lastBlockPosition = lastBlockPosition;
    }

    public IndexBlockReader enableLocking(LockType lockType, long lockWaitTimeout) {
        this.lockType = lockType;
        this.lockWaitTimeout = lockWaitTimeout;
        return this;
    }

    public Observable<Iterable<ChecksummedPositional<XIndexBlock>>> toObservable() {
        return create(new OnSubscribeFromBlockReader<>(this));
    }

    protected boolean hasNext() {
        return hasNext;
    }

    protected Observable<? extends List<ChecksummedPositional<XIndexBlock>>> next() {
        if (READ.equals(lockType) || WRITE.equals(lockType)) {
            checkState(lockWaitTimeout > 0, "Invalid LockWaitTimeout value %s", lockWaitTimeout);
            return lockedObservable(vertx,
                    () -> {
                        if (WRITE.equals(lockType)) {
                            return blockFile.tryWriteLock(position, readSize);
                        } else if (READ.equals(lockType)) {
                            return blockFile.tryReadLock(position, readSize);
                        } else {
                            throw new IllegalStateException("Unsupported Locked Type " + lockType);
                        }
                    },
                    () -> blockFile.getBlocks(vertx, position, batchSize)
                            .map(Lists::newArrayList)
                            .doOnNext(checksummedPositionals -> hasNext = !checksummedPositionals.isEmpty())
                            .doOnNext(checksummedPositionals -> position = position + readSize),
                    lockWaitTimeout);
        } else {
            return blockFile.getBlocks(vertx, position, batchSize)
                    .map(Lists::newArrayList)
                    .doOnNext(checksummedPositionals -> hasNext = !checksummedPositionals.isEmpty())
                    .doOnNext(checksummedPositionals -> position = position + readSize);
        }
    }

    protected static class OnSubscribeFromBlockReader<T extends Iterable<ChecksummedPositional<XIndexBlock>>> implements Observable.OnSubscribe<T> {

        final IndexBlockReader blockReader;

        public OnSubscribeFromBlockReader(IndexBlockReader iterable) {
            if (iterable == null) {
                throw new NullPointerException("iterable must not be null");
            }
            this.blockReader = iterable;
        }

        @Override
        public void call(Subscriber<? super T> o) {
            if (!blockReader.hasNext() && !o.isUnsubscribed()) {
                o.onCompleted();
            } else {
                o.setProducer(new BlockReaderProducer<>(o, blockReader));
            }
        }

        private static final class BlockReaderProducer<T extends Iterable<ChecksummedPositional<XIndexBlock>>> extends AtomicLong implements Producer {
            /** */
            private static final long serialVersionUID = -1L;
            private final Subscriber<? super T> o;
            private final IndexBlockReader blockReader;

            BlockReaderProducer(Subscriber<? super T> o, IndexBlockReader blockReader) {
                this.o = o;
                this.blockReader = blockReader;
            }

            @Override
            public void request(long n) {
                if (get() == MAX_VALUE) {
                    // already started with fast-path
                    return;
                }
                if (n == MAX_VALUE && compareAndSet(0, MAX_VALUE)) {
                    fastpath();
                } else if (n > 0 && getAndAddRequest(this, n) == 0L) {
                    slowpath(n, 0);
                }

            }

            void slowpath(long n, long numberEmitted) {
                if (o.isUnsubscribed()) {
                    // don't recurse
                } else if (numberEmitted < n) {
                    if (blockReader.hasNext()) {
                        blockReader.next()
                                .subscribe(new Subscriber<Iterable<ChecksummedPositional<XIndexBlock>>>() {
                                    @Override
                                    public void onCompleted() {
                                        o.onCompleted();
                                    }

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

                                    @Override
                                    public void onNext(Iterable<ChecksummedPositional<XIndexBlock>> checksummedPositionals) {
                                        o.onNext((T) checksummedPositionals);
                                        blockReader.vertx.runOnContext(event -> slowpath(n, numberEmitted + 1));
                                    }
                                });
                    } else {
                        if (!o.isUnsubscribed()) {
                            o.onCompleted();
                        }
                    }
                } else if (numberEmitted >= n) {
                    if (!o.isUnsubscribed()) {
                        o.onCompleted();
                    }
                }
            }

            void fastpath() {
                // fast-path without backpressure
                final Subscriber<? super T> o = this.o;
                final IndexBlockReader it = this.blockReader;

                if (o.isUnsubscribed()) {
                    return;
                } else if (it.hasNext()) {
                    it.next().subscribe(
                            new Subscriber<Iterable<ChecksummedPositional<XIndexBlock>>>() {
                                @Override
                                public void onCompleted() {
                                    o.onCompleted();
                                }

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

                                @Override
                                public void onNext(Iterable<ChecksummedPositional<XIndexBlock>> checksummedPositionals) {
                                    o.onNext((T) checksummedPositionals);
                                    blockReader.vertx.runOnContext(event -> fastpath());
                                }
                            });
                } else if (!o.isUnsubscribed()) {
                    o.onCompleted();
                    return;
                } else {
                    // is unsubscribed
                    return;
                }
            }
        }

    }

}
