/**
 * 
 */
package net.sf.fmj.media;

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.Buffer;

import net.sf.fmj.utility.LoggerSingleton;

import com.lti.utils.synchronization.ProducerConsumerQueue;

/**
 * Turns a queue of Buffer objects into an InputStream.
 * @author Ken Larson
 *
 */
public class BufferQueueInputStream extends InputStream
{
	private static final Logger logger = LoggerSingleton.logger;

	// TODO: we have to be careful about buffering up too much, on a large file, we can 
	// run out of heap space.
	private final ProducerConsumerQueue q;

	private static final int DEFAULT_QUEUE_SIZE = 20;

	public BufferQueueInputStream()
	{	this(DEFAULT_QUEUE_SIZE);
	}
	
	public BufferQueueInputStream(int qSize)
	{
		this.q = new ProducerConsumerQueue(qSize);
	}
	
	public BufferQueueInputStream(ProducerConsumerQueue q)
	{
		this.q = q;
	}
	
	private boolean trace = false;
	
	public void setTrace(boolean value)
	{	this.trace = value;
	}
	
	public boolean put(Buffer b)
	{	return put(b, true);
	}
	
	public void blockingPut(Buffer b)
	{	blockingPut(b, true);
	}
	
	public boolean put(Buffer b, boolean clone)
	{
		return put(b, false, clone);
	}
	
	public void blockingPut(Buffer b, boolean clone)
	{
		put(b, true, clone);
	}
	
	/** Note: buffers should be cloned before passing in. Returns false if no room. */
	private boolean put(Buffer b, boolean block, boolean clone)
	{
		if (b.getLength() == -1)
			throw new IllegalArgumentException();
		if (!(b.getData() instanceof byte[]))
			throw new IllegalArgumentException("Expected byte array: " + b.getData());
		if (b.isEOM())
		{	logger.fine("putting EOM buffer");
		}
		else
		{
			if (b.getLength() == 0)
			{	if (trace) logger.fine("Skipping zero length buffer, not adding to queue");
				return true;
			}
			if (b.isDiscard())
			{	if (trace) logger.fine("Skipping discard buffer, not adding to queue");
				return true;
			}
		}
		
		if (trace) logger.fine(this + " BufferQueueInputStream.put: Putting buffer, length=" + b.getLength() + " eom=" + b.isEOM());
		try
		{
			synchronized (q)
			{
				if (!block && q.isFull())
					return false;
				available += b.getLength();
				q.put(clone ? (Buffer) b.clone() : b);
				if (trace) logger.fine(this + " put: available=" + available);
				return true;
			}
			
		} catch (InterruptedException e)
		{
			logger.log(Level.WARNING, "" + e, e);
			throw new RuntimeException(e);
		}
	}
	
	private Buffer buffer;
	private int available = 0;	// keep track of # of available bytes, in case available() is called.  Must be synchronized on q to use.
	
	private void fillBuffer() throws IOException
	{
		try
		{

			synchronized (q)
			{
			
				do
				{
					if (buffer != null)
					{
						if (buffer.isEOM())
							return;
						
						if (buffer.getLength() > 0)
							return;	 // still have data in buffer
					}
					// TODO: any fields to set?
	
					buffer = (Buffer) q.get();
					if (trace) logger.fine(this + " Getting buffer: " + buffer.getLength());
					
					if (buffer.getLength() == 0 && !buffer.isDiscard())
						if (trace) logger.fine("Skipping zero-length buffer in queue");
					
				}
				while (buffer.isDiscard() || buffer.getLength() == 0);
			}
		
		} catch (InterruptedException e)
		{
			logger.log(Level.WARNING, "" + e, e);
			throw new InterruptedIOException();
		}
	}
	
	@Override
	public int available()
	{
		synchronized (q)
		{
			if (trace) logger.fine(this + " available: available=" + available);

			return available;
		}
	}
	
	//@Override
	@Override
	public int read() throws IOException
	{
		// TODO: how do we detect IOException? 
		fillBuffer();
		if (buffer.getLength() <= 0 && buffer.isEOM()) // TODO: will always be EOM if length is 0
		{	if (trace) logger.fine(this + " BufferQueueInputStream.read: returning -1");
			return -1;
		}
		final byte[] data = (byte[]) buffer.getData();
		final int result = data[buffer.getOffset()] & 0xff;
		buffer.setOffset(buffer.getOffset() + 1);
		buffer.setLength(buffer.getLength() - 1);
		synchronized (q)
		{
			available -= 1;
			if (trace) logger.fine(this + " read: available=" + available);

		}
		
		if (trace) logger.fine(this + " BufferQueueInputStream.read: returning " + result);
		
		return result;
		
	}

	//@Override
	@Override
	public int read(byte[] b, int off, int len) throws IOException
	{
		// TODO: how do we detect IOException? 
		fillBuffer();
		if (buffer.getLength() <= 0 && buffer.isEOM()) // TODO: will always be EOM if length is 0
		{	if (trace) logger.fine(this + " BufferQueueInputStream.read: returning -1");
			return -1;
			// TODO: how can buffer.getLength() == -1?
		}
		final byte[] data = (byte[]) buffer.getData();
		if (data == null)
			throw new NullPointerException("Buffer has null data.  length=" + buffer.getLength() + " offset=" + buffer.getOffset());

		int lengthToCopy = buffer.getLength() < len ? buffer.getLength() : len;
		if (trace) logger.fine(this + " BufferQueueInputStream.read: lengthToCopy=" + lengthToCopy + " buffer.getLength()=" + buffer.getLength() + " buffer.getOffset()=" + buffer.getOffset() + " b.length=" + b.length + " len=" + len + " off=" + off);
		System.arraycopy(data, buffer.getOffset(), b, off, lengthToCopy);
		buffer.setOffset(buffer.getOffset() + lengthToCopy);
		buffer.setLength(buffer.getLength() - lengthToCopy);

		synchronized (q)
		{
			available -= lengthToCopy;
			if (trace) logger.fine(this + " read: available=" + available);

		}
		
		if (trace) logger.fine(this + " BufferQueueInputStream.read[]: returning " + lengthToCopy);

		
		return lengthToCopy;
	}


}