/*******************************************************************************
 * Copyright  2015 rzorzorzo@users.sf.net
 *
 * 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.rzo.netty.ahessian.io;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;

import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.caucho.hessian4.io.FlushableOutput;

/**
 * A buffer for storing outgoing bytes. Bytes are sent upstream if The number of
 * written bytes is > than a watermark, or if flush is called
 */
public class OutputStreamBuffer extends OutputStream implements FlushableOutput
{

	/** The context of the channel on which to send the bytes downstream */
	private volatile ChannelHandlerContext _ctx;

	/** Indicates if the stream has been closed */
	private volatile boolean _closed = false;
	private Lock _lock = new ReentrantLock();

	/** If written bytes > watermark, the bytes are sent downstream */
	int _watermark = 1024 * 1024;
	int _initialBuffSize = 1024;

	/**
	 * The buffer for storing outgoing bytes. Once the bytes have been sent
	 * downstream a new buffer is created
	 */
	private volatile ByteBuf _buf = null;
	ExecutorService _executor = Executors.newSingleThreadExecutor();

	private boolean _immediateFlush = true;

	/**
	 * Instantiates a new output stream buffer.
	 * 
	 * @param ctx
	 *            the context in which bytes are sent downstream
	 */
	OutputStreamBuffer(ChannelHandlerContext ctx)
	{
		super();
		_ctx = ctx;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.io.OutputStream#write(int)
	 */
	@Override
	public void write(int b) throws IOException
	{
		if (_closed)
			throw new IOException("stream closed");
		_lock.lock();
		try
		{
			// System.out.println("write "+_buf.readableBytes());
			checkBuf();
			_buf.writeByte((byte) b);
			if (_buf.writerIndex() >= _watermark)
				sendDownstream(null);
		}
		finally
		{
			_lock.unlock();
		}
	}

	private void checkBuf()
	{
		if (_buf == null)
		{
			_buf = _ctx.alloc().buffer(1024);
			_buf.retain();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.io.OutputStream#write(byte[], int, int)
	 */
	@Override
	public void write(byte b[], int off, int len) throws IOException
	{
		if (_closed)
			throw new IOException("stream closed");
		_lock.lock();
		try
		{
			checkBuf();

			_buf.writeBytes(b, off, len);
			// System.out.println("write "+len+" "+_buf.readableBytes());
			if (_buf.writerIndex() >= _watermark)
				sendDownstream(null);
		}
		finally
		{
			_lock.unlock();
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.io.OutputStream#flush()
	 */
	@Override
	public void flush() throws IOException
	{
		flush(null);
	}

	public void flush(ChannelPromise future) throws IOException
	{
		// System.out.println(System.currentTimeMillis()+" +flush");
		_lock.lock();
		try
		{
			super.flush();
			if (_buf == null || _buf.readableBytes() == 0)
			{
				if (future != null)
					future.setSuccess();
			}
			else
				/*
				 * if (future == null) { sendDownstream(null); _ctx.flush();
				 * //f.await(20000); //if (f != null && !f.await(10000)) //throw
				 * new IOException("write longer than 10 secs"); //
				 * System.out.println("write took longer than 10 2"); } else {
				 * sendDownstream(future); _ctx.flush(); }
				 */
				sendDownstream(future);
			if (_immediateFlush)
				_ctx.flush();
		}
		catch (Exception e)
		{
			throw new IOException(e);
		}
		finally
		{
			_lock.unlock();
		}
		// System.out.println(System.currentTimeMillis()+" -flush");
	}

	private void sendDownstream(ChannelPromise future) throws IOException
	{
		sendDownstream(future, true);
	}

	private void sendDownstream(final ChannelPromise future, boolean wait)
			throws IOException
	{
		if (future != null && future.isDone())
			return;
		if (!_ctx.channel().isActive())
			throw new IOException("channel disconnected");
		_lock.lock();
		ChannelFuture result = null;
		try
		{
			final ByteBuf toSend = _buf;
			_buf = null;
			if (toSend.refCnt() > 1)
				toSend.release(toSend.refCnt() - 1);
			// System.out.println(System.currentTimeMillis()+" outputstream send downstream +");
			if (future == null || future.isDone())
				result = _ctx.write(toSend);
			else
				_ctx.write(toSend, future);
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
		finally
		{
			_lock.unlock();
		}
		// System.out.println(System.currentTimeMillis()+" outputstream send downstream -");

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.io.OutputStream#close()
	 */
	@Override
	public void close() throws IOException
	{
		_lock.lock();
		_closed = true;
		_executor.shutdownNow();
		_lock.unlock();
		// System.out.println("buffer closed");
	}

	public void setContext(ChannelHandlerContext ctx)
	{
		_ctx = ctx;
		reset();
	}

	public ChannelHandlerContext getContext()
	{
		return _ctx;
	}

	public void reset()
	{
		_lock.lock();
		_buf = Unpooled.buffer(1024);
		_closed = false;
		_lock.unlock();
	}

}
