/*
 * cc.redberry.pipe: java library for implementation of concurrent pipelines
 * Copyright (c) 2010-2012
 * Bolotin Dmitriy <bolotin.dmitriy@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package cc.redberry.pipe.blocks;

import cc.redberry.pipe.InputPort;
import cc.redberry.pipe.OutputPort;
import cc.redberry.pipe.PInterruptedException;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Fixed size FIFO buffer that concatenates data streams from several created (by {@link #createInputPort()} method)
 * input ports and acting as a single output port.
 *
 * <p>Code is adapted from {@link java.util.concurrent.ArrayBlockingQueue}.</p>
 *
 * @param <E> type of objects to route
 * @author Bolotin Dmitriy
 * @author Mikhail Shugay
 */
public final class Buffer<E> implements OutputPort<E> {
    public static final int DEFAULT_SIZE = 512;
    /**
     * The queued items
     */
    final Object[] items;
    /**
     * Collection to store created input ports
     */
    final List<BufferInputPort> inputPorts = new ArrayList<>();
    /**
     * Main lock guarding all access
     */
    final ReentrantLock lock;
    /**
     * Condition for waiting - take
     */
    private final Condition notEmpty;
    /**
     * Condition for waiting - put
     */
    private final Condition notFull;
    /**
     * Condition for waitClosed
     */
    private final Condition closedCondition;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */
    /**
     * Is this port closed
     */
    boolean closed;
    /**
     * Index of item for next take, pull, peek or remove
     */
    int takeIndex;
    /**
     * Index of item for next put, offer, or add
     */
    int putIndex;
    /**
     * Number of elements in the queue
     */
    int count;


    /**
     * Creates a {@code Buffer} with default size.
     */
    public Buffer() {
        this(DEFAULT_SIZE);
    }

    /**
     * Creates a {@code Buffer} with the given (fixed) size.
     *
     * @param size the size of this queue
     * @throws IllegalArgumentException if {@code size < 1}
     */
    public Buffer(int size) {
        this(size, false);
    }

    /**
     * Creates a {@code Buffer} with the given (fixed) size and the specified access policy.
     *
     * @param size the size of this queue
     * @param fair if {@code true} then queue accesses for threads blocked on insertion or removal are processed in FIFO
     *             order; the access order is unspecified otherwise
     * @throws IllegalArgumentException if {@code size < 1}
     */
    public Buffer(int size, boolean fair) {
        if (size <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[size];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull = lock.newCondition();
        closedCondition = lock.newCondition();
    }

    // Internal helper methods

    @SuppressWarnings("unchecked")
    static <E> E cast(Object item) {
        return (E) item;
    }

    /**
     * Inserts the specified element at the tail of this queue, waiting for space to become available if the queue is
     * full. Invoked only through {@link Buffer.BufferInputPort} instances.
     */
    private void put(E e) {
        final ReentrantLock lock = this.lock;
        try {
            lock.lockInterruptibly();
            try {
                while (count == items.length && !closed)
                    notFull.await();
                if (closed)
                    return; //If buffer is closed drop input object
                insert(e);
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException ie) {
            throw new PInterruptedException(ie);
        }
    }

    /**
     * Take first object that was put through some of input ports owned by this buffer.
     *
     * @return {@inheritDoc}
     * @throws InterruptedException
     */
    @Override
    public E take() {
        if (inputPorts.isEmpty())
            throw new IllegalStateException("No input ports created for this Buffer.");

        final ReentrantLock lock = this.lock;
        try {
            lock.lockInterruptibly();
            try {
                while (count == 0 && !closed)
                    notEmpty.await();
                if (closed && count == 0)
                    return null;
                return extract();
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException ie) {
            throw new PInterruptedException(ie);
        }
    }

    /**
     * Closes current buffer.
     *
     * <p>Any objects putted to this buffer through {@link InputPort#put(Object)} method of owned input ports will be
     * dropped and will never reach the output.</p>
     */
    public void close() {
        lock.lock();
        try {
            closed = true;
            notFull.signalAll();
            notEmpty.signalAll();
            closedCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Creates new input port to put elements to this buffer. Buffer will be closed after all created input ports are
     * closed.
     *
     * @return new InputPort for this buffer
     */
    public InputPort<E> createInputPort() {
        BufferInputPort port = new BufferInputPort();
        inputPorts.add(port);
        return port;
    }

    /**
     * Drop all elements in the buffer.
     */
    //public void clear() {
    //    lock.lock();
    //    try {
    //        count = 0;
    //    } finally {
    //        lock.unlock();
    //    }
    //}

    /**
     * This method will return after this buffer is closed.
     *
     * @throws InterruptedException
     */
    public void waitClosed() throws InterruptedException {
        lock.lock();
        try {
            while (!closed)
                closedCondition.await();
        } finally {
            lock.unlock();
        }
    }

    //Helper methods

    /**
     * Invoked on each input port closing event. Tests whether each input port is closed, and if so closes the buffer.
     */
    private void testClosed() {
        lock.lock();
        try {
            if (closed)
                return;
            for (BufferInputPort port : inputPorts)
                if (!port.currentPortClosed.get())
                    return;
            closed = true;
            notEmpty.signalAll();
            closedCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    /**
     * Circularly increment i.
     */
    final int inc(int i) {
        return (++i == items.length) ? 0 : i;
    }

    /**
     * Inserts element at current put position, advances, and signals. Call only when holding lock.
     */
    private void insert(E x) {
        items[putIndex] = x;
        putIndex = inc(putIndex);
        ++count;
        notEmpty.signal();
    }

    /**
     * Extracts element at current take position, advances, and signals. Call only when holding lock.
     */
    private E extract() {
        final Object[] items = this.items;
        E x = this.<E>cast(items[takeIndex]);
        items[takeIndex] = null;
        takeIndex = inc(takeIndex);
        --count;
        notFull.signal();
        return x;
    }

    /**
     * Implementation of InputPort for buffer.
     */
    private class BufferInputPort implements InputPort<E> {
        AtomicBoolean currentPortClosed = new AtomicBoolean(false);

        public void put(E object) {
            if (object == null) {
                if (!currentPortClosed.compareAndSet(false, true))
                    throw new IllegalArgumentException("Worker is already closed.");
                testClosed();
                return;
            }
            Buffer.this.put(object);
        }
    }
}
