package com.srbenoit.buffer;

import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SocketChannel;
import com.srbenoit.pool.AbstractPoolObject;

/**
 * A buffer that holds byte data and that can be stored in a <code>Pool</code>. Each buffer
 * contains a pointer to a subsequent buffer (null for the last buffer in a chain), allowing
 * buffers to be assembled into a linked list.
 */
public class PooledByteBuffer extends AbstractPooledBuffer {

    /** the next buffer in a linked list */
    private transient PooledByteBuffer next;

    /**
     * Constructs a new <code>PooledByteBuffer</code>.
     *
     * @param  capacity  the capacity of a buffer, in bytes
     */
    public PooledByteBuffer(final int capacity) {

        super(ByteBuffer.allocate(capacity));

        this.next = null;
    }

    /**
     * Gets the buffer.
     *
     * @return  the buffer
     */
    @Override public ByteBuffer getBuffer() {

        return (ByteBuffer) super.getBuffer();
    }

    /**
     * Allocates a new buffer of a specified capacity;
     *
     * @param  capacity  the capacity of the new buffer
     */
    @Override public void newBuffer(final int capacity) {

        setBuffer(ByteBuffer.allocate(capacity));
    }

    /**
     * Sets the next buffer in the linked list.
     *
     * @param  nextBuffer  the next buffer
     */
    public void setNext(final PooledByteBuffer nextBuffer) {

        synchronized (this.synch) {
            this.next = nextBuffer;
        }
    }

    /**
     * Gets the next buffer in the linked list.
     *
     * @return  the next buffer
     */
    public PooledByteBuffer getNext() {

        synchronized (this.synch) {
            return this.next;
        }
    }

    /**
     * Reads the byte at the backing buffer's current position, and then increments that position.
     *
     * @return  the byte at the backing buffer's current position
     * @throws  BufferUnderflowException  if the backing buffer's current position is not smaller
     *                                    than its limit
     */
    public byte get() throws BufferUnderflowException {

        synchronized (this.synch) {
            return getBuffer().get();
        }
    }

    /**
     * Writes the given byte into the backing buffer at the current position, and then increments
     * that position.
     *
     * @param   data  the byte to be written
     * @throws  BufferOverflowException  if the backing buffer's current position is not smaller
     *                                   than its limit
     * @throws  ReadOnlyBufferException  if the backing buffer is read-only
     */
    public void put(final byte data) throws BufferOverflowException, ReadOnlyBufferException {

        synchronized (this.synch) {
            getBuffer().put(data);
        }
    }

    /**
     * Reads the byte from the backing buffer at the given index.
     *
     * @param   index  the index from which the byte will be read
     * @return  the byte at the given index
     * @throws  IndexOutOfBoundsException  if <code>index</code> is negative or not smaller than
     *                                     the backing buffer's limit
     */
    public byte get(final int index) {

        synchronized (this.synch) {
            return getBuffer().get(index);
        }
    }

    /**
     * Writes the given byte into the backing buffer at the given index.
     *
     * @param   index  the index at which the byte will be written
     * @param   data   the byte value to be written
     * @throws  IndexOutOfBoundsException  if <code>index</code> is negative or not smaller than
     *                                     the buffer's limit
     * @throws  ReadOnlyBufferException    if this buffer is read-only
     */
    public void put(final int index, final byte data) throws IndexOutOfBoundsException,
        ReadOnlyBufferException {

        synchronized (this.synch) {
            getBuffer().put(index, data);
        }
    }

    /**
     * Transfers bytes from the backing buffer into the given destination array. If there are fewer
     * bytes remaining in the buffer than are required to satisfy the request, that is, if <code>
     * length &gt; remaining()</code>, then no bytes are transferred and a <code>
     * BufferUnderflowException</code> is thrown.
     *
     * <p>Otherwise, this method copies <code>length</code> bytes from this buffer into the given
     * array, starting at the current position of this buffer and at the given offset in the array.
     * The position of this buffer is then incremented by <code>length</code>.
     *
     * @param   dst     the array into which bytes are to be written
     * @param   offset  the offset within the array of the first byte to be written; must be
     *                  non-negative and no larger than <code>dst.length</code>
     * @param   length  the maximum number of bytes to be written to the given array; must be
     *                  non-negative and no larger than <code>dst.length - offset</code>
     * @throws  BufferUnderflowException   if there are fewer than <code>length</code> bytes
     *                                     remaining in the backing buffer
     * @throws  IndexOutOfBoundsException  if the preconditions on the <code>offset</code> and
     *                                     <code>length</code> parameters do not hold
     */
    public void get(final byte[] dst, final int offset, final int length) {

        synchronized (this.synch) {
            getBuffer().get(dst, offset, length);
        }
    }

    /**
     * Transfers bytes from the backing buffer into the given destination array.
     *
     * @param   dst  the array into which bytes are to be written
     * @throws  BufferUnderflowException  if there are fewer than <code>length</code> bytes
     *                                    remaining in the backing buffer
     */
    public void get(final byte[] dst) {

        synchronized (this.synch) {
            getBuffer().get(dst);
        }
    }

    /**
     * Transfers bytes into the backing buffer from the given source array. If there are more bytes
     * to be copied from the array than remain in this buffer, that is, if <code>length &gt;
     * remaining()</code>, then no bytes are transferred and a <code>BufferOverflowException</code>
     * is thrown.
     *
     * <p>Otherwise, this method copies <code>length</code> bytes from the given array into the
     * backing buffer, starting at the given offset in the array and at the current position of the
     * backing buffer. The position of this buffer is then incremented by <code>length</code>.
     *
     * @param   src     the array from which bytes are to be read
     * @param   offset  the offset within the array of the first byte to be read; must be
     *                  non-negative and no larger than <code>array.length</code>
     * @param   length  the number of bytes to be read from the given array; must be non-negative
     *                  and no larger than <code>array.length - offset</code>
     * @throws  BufferOverflowException    if there is insufficient space in this buffer
     * @throws  IndexOutOfBoundsException  if the preconditions on the <code>offset</code> and
     *                                     <code>length</code> parameters do not hold
     * @throws  ReadOnlyBufferException    if this buffer is read-only
     */
    public void put(final byte[] src, final int offset, final int length)
        throws BufferOverflowException, IndexOutOfBoundsException, ReadOnlyBufferException {

        synchronized (this.synch) {
            getBuffer().put(src, offset, length);
        }
    }

    /**
     * This method transfers the entire content of the given source byte array into the backing
     * buffer.
     *
     * @param   src  the array from which bytes are to be read
     * @throws  BufferOverflowException  if there is insufficient space in the backing buffer
     * @throws  ReadOnlyBufferException  if this buffer is read-only
     */
    public void put(final byte[] src) {

        synchronized (this.synch) {
            getBuffer().put(src);
        }
    }

    /**
     * Reads data into the byte buffer from a socket channel.
     *
     * @param   channel  the channel from which to read
     * @return  the number of bytes read, possibly zero, or -1 if the channel has reached
     *          end-of-stream
     * @throws  IOException               if there was an I/O error while reading data
     * @throws  NotYetConnectedException  if the channel is not yet connected
     */
    public int readFrom(final SocketChannel channel) throws IOException, NotYetConnectedException {

        synchronized (this.synch) {
            return channel.read(getBuffer());
        }
    }

    /**
     * Writes data from the byte buffer to a socket channel.
     *
     * @param   channel  the channel from which to read
     * @return  the number of bytes written, possibly zero
     * @throws  IOException               if there was an I/O error while reading data
     * @throws  NotYetConnectedException  if the channel is not yet connected
     */
    public int writeTo(final SocketChannel channel) throws IOException, NotYetConnectedException {

        synchronized (this.synch) {
            return channel.write(getBuffer());
        }
    }

    /**
     * Creates a copy of the object, but with an independent backing buffer - used to create new
     * objects when the pool is empty. This is more efficient than creating a new object.
     *
     * @return  the copy
     */
    @Override public AbstractPoolObject copy() {

        PooledByteBuffer copy;

        synchronized (this.synch) {

            try {
                copy = (PooledByteBuffer) super.clone();
                copy.newBuffer(capacity());
            } catch (CloneNotSupportedException e) {
                copy = new PooledByteBuffer(capacity());
            }
        }

        return copy;
    }

    /**
     * Resets this buffer to a virgin state (used before the buffer is returned to a pool).
     */
    @Override public void toVirginState() {

        synchronized (this.synch) {
            this.next = null;
            super.toVirginState();
        }
    }

    /**
     * Informs the pool object that it is being released for garbage collection and should free any
     * internal resources. The object may not be reused after this method is called.
     */
    @Override public void die() {

        synchronized (this.synch) {
            this.next = null;
            super.die();
        }
    }
}
