/* 
 * 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.EOFException;
import java.io.IOException;
import java.io.InputStream;

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

	/**
	 * True if the final chunk was found.
	 */
	private boolean endChunk;

	/**
	 * Current chunk length.
	 */
	private int chunkLength;

	/**
	 * Current chunk buffer position.
	 */
	private int chunkPos;

	/**
	 * The underlying input stream.
	 */
	private InputStream stream;

	/**
	 * Trailer headers.
	 */
	private MessageHeaders entityHeaders;

	/**
	 * Constructs a chunked input stream wrapping input.
	 *
	 * @param stream Must be non-null.
	 *
	 */
	public ChunkedInputStream(InputStream stream) {
		if (stream == null) {
			throw new IllegalArgumentException("InputStream parameter is null");
		}
		endChunk = false;
		chunkLength = 0;
		chunkPos = 0;
		this.stream = stream;
	}

	/**
	 * Closes the underlying input stream.  
	 */
	public void close()
		throws IOException
	{
		stream.close();
	}


	/**
	 * Reads up to <code>len</code> bytes of data from the input stream
	 * into an array of bytes.  An attempt is made to read as many as
	 * <code>len</code> bytes, but a smaller number may be read,
	 * possibly zero.  The number of bytes actually read is returned as
	 * an integer.
	 *
	 * @param b The buffer into which the data is read
	 * @param off The start offset into array <code>b</code> at which
	 * the data is written
	 * @param len The maximum number of bytes to read
	 *
	 * @exception IOException if an input/output error occurs
	 */
	public int read(byte b[], int off, int len)
		throws IOException
	{
		if (endChunk)
			return -1;

		if (chunkLength == chunkPos) {
			if (chunkLength > 0) // finishes previous chunk
				readChunkEnd();
			chunkLength = readLengthFromStream();
			chunkPos = 0;
			if (chunkLength == 0) {
				readChunkTrailer(); // trailer
				endChunk = true;
				return -1;
			}
		}
		int want = Math.min(len, chunkLength - chunkPos);
		int got = stream.read(b, off, want);
		if (got == -1)
			return -1;
		chunkPos += got;
		return got;
	}

	/*
	// Not supported
	public int available() throws IOException {
		return stream.available();
	}

	public boolean markSupported() {
		return stream.markSupported();
	}

	public void reset() throws IOException {
		stream.reset();
	}

	public void mark(int readlimit) {
		stream.mark(readlimit);
	}
	*/

	/**
	 * Reads and return a single byte from this input stream, or -1 if end of
	 * file has been encountered.
	 *
	 * @exception IOException if an input/output error occurs
	 */
	public int read()
		throws IOException
	{
		byte oneChar[] = new byte[1];
		int c = read(oneChar, 0, 1);
		if (c == -1)
			return -1;
		return oneChar[0];

	}

	int readLengthFromStream()
		throws IOException
	{
		int ch;
		int total = 0;
		boolean extension = false;
		while (true) {
			ch = stream.read();
			if (ch == -1)
				throw new EOFException();
			if (ch == '\r')
				break;
			if (ch == ';')
				extension = true;
			if (ch == ' ') // This is not in the spec, but allowed anyway
				continue;
			if (extension)
				continue;
			total <<= 4;
			if (ch >= '0' && ch <= '9')
				total += ch - '0';
			else if (ch >= 'a' && ch <= 'f')
				total += ch - 'a' + 10;
			else if (ch >= 'A' && ch <= 'F')
				total += ch - 'A' + 10;
			else throw new IOException("Bad length character in stream: " + ch);
		}
		ch = stream.read();
		if (ch != '\n')
			throw new IOException("Expected LF character in stream: " + ch);
		return total;
	}

	/**
	 * Returns "trailer" entity headers, which appear at the end of
	 * a chunked encoding request. Returns null if not at the end of input.
	 * These will only appear when {@link #isEndChunk()} returns true.
	 * 
	 * See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
	 */
	public MessageHeaders getEntityHeaders() {
		return entityHeaders;
	}
	
	/**
	 * Returns true if the end chunk was read.
	 */
	public boolean isEndChunk() {
		return endChunk;
	}

	/**
	 * Reads CR NL.
	 */
	void readChunkEnd()
		throws IOException
	{
		int ch = stream.read();
		if (ch != '\r')
			throw new IOException("Expected CR in end chunk " + (char)ch);
		ch = stream.read();
		if (ch != '\n')
			throw new IOException("Expected LN in end chunk");
	}
	
	/**
	 * Reads chunk trailer.
	 */
	void readChunkTrailer()
		throws IOException
	{
		entityHeaders = MessageHeaders.readHeaders(stream);
	}

	/**
	 * Returns a debug string.
	 */
	public String toString() {
		return "ChunkedInputStream " + 
			" stream=" + stream +
			" chunkLength=" + chunkLength + 
			" chunkPos=" + chunkPos;
	}

}
