/* 
 * E-XML Library:  For XML, XML-RPC, HTTP, and related.
 * Copyright (C) 2002-2008  Elias Ross
 * 
 * genman@noderunner.net
 * http://noderunner.net/~genman
 * 
 * 1025 NE 73RD ST
 * SEATTLE WA 98115
 * USA
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 * 
 * $Id$
 */

package net.noderunner.http;

import java.io.IOException;
import java.io.OutputStream;

/**
 * An <code>OutputStream</code> wrapper that supports the chunked transfer encoding.
 *
 * @author Elias Ross
 *
 * @see ChunkedInputStream
 */
public class ChunkedOutputStream
	extends OutputStream
{

	/**
	 * The underlying output stream to which we will write data.
	 */
	private OutputStream stream;

	/**
	 * True if done chunking.
	 */
	private boolean doneChunking;

	/**
	 * Re-useable length encoding array.
	 */
	private byte lenEnc[] = {'0', '0', '0', '0', 13, 10};

	/**
	 * Conversion symbols.
	 */
	private static final byte hex[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

	/**
	 * Constructs an output stream wrapping the given stream.
	 *
	 * @param stream wrapped output stream. Must be non-null.
	 */
	public ChunkedOutputStream(OutputStream stream) {
		if (stream == null)
			throw new IllegalArgumentException("Stream parameter is null");
		this.stream = stream;
		this.doneChunking = false;
	}

	/**
	 * Writes the specified byte to the output stream.
	 * Note:  Of course, this isn't very efficient, as we have to send
	 * seven bytes of header data as well.
	 *
	 * @param b The byte to be written
	 * @exception IOException if an input/output error occurs
	 */
	public void write(int b) throws IOException {
		if (doneChunking)
			throw new IllegalHttpStateException("Already wrote last chunk");
		writeChunkLen(1);
		stream.write(b);
		writeEndChunk();
	}

	/**
	 * Writes the specified byte array.
	 */
	public void write(byte[] b)
		throws IOException
	{
		write(b, 0, b.length);
	}

	/**
	 * Writes the specified byte array.
	 */
	public void write(byte[] b, int off, int len)
		throws IOException
	{
		if (doneChunking)
			throw new IllegalHttpStateException("Already wrote last chunk");
		writeChunkLen(len);
		stream.write(b, off, len);
		writeEndChunk();
	}

	/**
	 * Writes integer length and CR LR to the stream.
	 */
	void writeChunkLen(int len) throws IOException {
		if (len > 0xffff) {
			lenEnc[0] = hex[(len >> 28 ) & 0x0f];
			lenEnc[1] = hex[(len >> 24 ) & 0x0f];
			lenEnc[2] = hex[(len >> 20 ) & 0x0f];
			lenEnc[3] = hex[(len >> 16 ) & 0x0f];
			stream.write(lenEnc, 0, 4);
		}
		lenEnc[0] = hex[(len >> 12) & 0x0f];
		lenEnc[1] = hex[(len >> 8)  & 0x0f];
		lenEnc[2] = hex[(len >> 4 ) & 0x0f];
		lenEnc[3] = hex[len         & 0x0f];
		stream.write(lenEnc, 0, 6);
	}

	/**
	 * Writes CR LF to the stream.
	 */
	private void writeEndChunk()
		throws IOException
	{
		stream.write(lenEnc, 4, 2);
	}

	/**
	 * Flushes this output stream.
	 */
	public void flush()
		throws IOException
	{
		stream.flush();
	}

	/**
	 * This method differs from <code>close</code> as it merely writes the
	 * final chunk and does not close the underlying output stream.
	 * This has no effect if this method was called already.
	 */
	public void doneOutput()
		throws IOException
	{
		if (!doneChunking) {
			// Write the final chunk.
			writeChunkLen(0);
			writeEndChunk(); // required extra CRLF
			doneChunking = true;
			flush();
		}
	}

	/**
	 * Closes this output stream, calling <code>doneOutput</code> once before closing.
	 */
	public void close()
		throws IOException
	{
		doneOutput();
		stream.close();
	}

	public String toString() {
		return "ChunkedOutputStream " +
			"stream=" + stream +
			"doneChunking=" + doneChunking;
	}


}
